Skip to content

Commit 2025f7e

Browse files
committed
fix(todo-continuation-enforcer): only show countdown when incomplete todos exist in main session
🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) Changes: - Add main session check: skip toast for subagent sessions - Move todo validation BEFORE countdown: only start countdown when incomplete todos actually exist - Improve toast message to show remaining task count This fixes the issue where countdown toast was showing on every idle event, even when no todos existed or in subagent sessions.
1 parent 15d36ab commit 2025f7e

File tree

1 file changed

+65
-51
lines changed

1 file changed

+65
-51
lines changed

src/hooks/todo-continuation-enforcer.ts

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { existsSync, readdirSync } from "node:fs"
22
import { join } from "node:path"
33
import type { PluginInput } from "@opencode-ai/plugin"
4+
import { getMainSessionID } from "../features/claude-code-session-state"
45
import {
56
findNearestMessageWithFields,
67
MESSAGE_STORAGE,
@@ -112,18 +113,74 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
112113

113114
log(`[${HOOK_NAME}] session.idle received`, { sessionID })
114115

116+
const mainSessionID = getMainSessionID()
117+
if (mainSessionID && sessionID !== mainSessionID) {
118+
log(`[${HOOK_NAME}] Skipped: not main session`, { sessionID, mainSessionID })
119+
return
120+
}
121+
115122
const existingCountdown = pendingCountdowns.get(sessionID)
116123
if (existingCountdown) {
117124
clearInterval(existingCountdown.intervalId)
118125
pendingCountdowns.delete(sessionID)
119126
log(`[${HOOK_NAME}] Cancelled existing countdown`, { sessionID })
120127
}
121128

129+
// Check if session is in recovery mode - if so, skip entirely without clearing state
130+
if (recoveringSessions.has(sessionID)) {
131+
log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID })
132+
return
133+
}
134+
135+
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID)
136+
137+
if (shouldBypass) {
138+
interruptedSessions.delete(sessionID)
139+
errorSessions.delete(sessionID)
140+
log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID })
141+
return
142+
}
143+
144+
if (remindedSessions.has(sessionID)) {
145+
log(`[${HOOK_NAME}] Skipped: already reminded this session`, { sessionID })
146+
return
147+
}
148+
149+
// Check for incomplete todos BEFORE starting countdown
150+
let todos: Todo[] = []
151+
try {
152+
log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID })
153+
const response = await ctx.client.session.todo({
154+
path: { id: sessionID },
155+
})
156+
todos = (response.data ?? response) as Todo[]
157+
log(`[${HOOK_NAME}] Todo API response`, { sessionID, todosCount: todos?.length ?? 0 })
158+
} catch (err) {
159+
log(`[${HOOK_NAME}] Todo API error`, { sessionID, error: String(err) })
160+
return
161+
}
162+
163+
if (!todos || todos.length === 0) {
164+
log(`[${HOOK_NAME}] No todos found`, { sessionID })
165+
return
166+
}
167+
168+
const incomplete = todos.filter(
169+
(t) => t.status !== "completed" && t.status !== "cancelled"
170+
)
171+
172+
if (incomplete.length === 0) {
173+
log(`[${HOOK_NAME}] All todos completed`, { sessionID, total: todos.length })
174+
return
175+
}
176+
177+
log(`[${HOOK_NAME}] Found incomplete todos, starting countdown`, { sessionID, incomplete: incomplete.length, total: todos.length })
178+
122179
const showCountdownToast = async (seconds: number): Promise<void> => {
123180
await ctx.client.tui.showToast({
124181
body: {
125182
title: "Todo Continuation",
126-
message: `Resuming in ${seconds}s...`,
183+
message: `Resuming in ${seconds}s... (${incomplete.length} tasks remaining)`,
127184
variant: "warning" as const,
128185
duration: TOAST_DURATION_MS,
129186
},
@@ -132,66 +189,23 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
132189

133190
const executeAfterCountdown = async (): Promise<void> => {
134191
pendingCountdowns.delete(sessionID)
135-
log(`[${HOOK_NAME}] Countdown finished, checking conditions`, { sessionID })
192+
log(`[${HOOK_NAME}] Countdown finished, executing continuation`, { sessionID })
136193

137-
// Check if session is in recovery mode - if so, skip entirely without clearing state
194+
// Re-check conditions after countdown
138195
if (recoveringSessions.has(sessionID)) {
139-
log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID })
140-
return
141-
}
142-
143-
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID)
144-
145-
interruptedSessions.delete(sessionID)
146-
errorSessions.delete(sessionID)
147-
148-
if (shouldBypass) {
149-
log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID })
150-
return
151-
}
152-
153-
if (remindedSessions.has(sessionID)) {
154-
log(`[${HOOK_NAME}] Skipped: already reminded this session`, { sessionID })
155-
return
156-
}
157-
158-
let todos: Todo[] = []
159-
try {
160-
log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID })
161-
const response = await ctx.client.session.todo({
162-
path: { id: sessionID },
163-
})
164-
todos = (response.data ?? response) as Todo[]
165-
log(`[${HOOK_NAME}] Todo API response`, { sessionID, todosCount: todos?.length ?? 0 })
166-
} catch (err) {
167-
log(`[${HOOK_NAME}] Todo API error`, { sessionID, error: String(err) })
196+
log(`[${HOOK_NAME}] Abort: session entered recovery mode during countdown`, { sessionID })
168197
return
169198
}
170199

171-
if (!todos || todos.length === 0) {
172-
log(`[${HOOK_NAME}] No todos found`, { sessionID })
200+
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
201+
log(`[${HOOK_NAME}] Abort: error/interrupt occurred during countdown`, { sessionID })
202+
interruptedSessions.delete(sessionID)
203+
errorSessions.delete(sessionID)
173204
return
174205
}
175206

176-
const incomplete = todos.filter(
177-
(t) => t.status !== "completed" && t.status !== "cancelled"
178-
)
179-
180-
if (incomplete.length === 0) {
181-
log(`[${HOOK_NAME}] All todos completed`, { sessionID, total: todos.length })
182-
return
183-
}
184-
185-
log(`[${HOOK_NAME}] Found incomplete todos`, { sessionID, incomplete: incomplete.length, total: todos.length })
186207
remindedSessions.add(sessionID)
187208

188-
// Re-check if abort occurred during the delay/fetch
189-
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID) || recoveringSessions.has(sessionID)) {
190-
log(`[${HOOK_NAME}] Abort occurred during delay/fetch`, { sessionID })
191-
remindedSessions.delete(sessionID)
192-
return
193-
}
194-
195209
try {
196210
// Get previous message's agent info to respect agent mode
197211
const messageDir = getMessageDir(sessionID)

0 commit comments

Comments
 (0)