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.
This commit is contained in:
YeonGyu-Kim
2025-12-19 19:06:35 +09:00
parent 15d36ab461
commit 2025f7e884

View File

@@ -1,6 +1,7 @@
import { existsSync, readdirSync } from "node:fs"
import { join } from "node:path"
import type { PluginInput } from "@opencode-ai/plugin"
import { getMainSessionID } from "../features/claude-code-session-state"
import {
findNearestMessageWithFields,
MESSAGE_STORAGE,
@@ -112,6 +113,12 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
log(`[${HOOK_NAME}] session.idle received`, { sessionID })
const mainSessionID = getMainSessionID()
if (mainSessionID && sessionID !== mainSessionID) {
log(`[${HOOK_NAME}] Skipped: not main session`, { sessionID, mainSessionID })
return
}
const existingCountdown = pendingCountdowns.get(sessionID)
if (existingCountdown) {
clearInterval(existingCountdown.intervalId)
@@ -119,11 +126,61 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
log(`[${HOOK_NAME}] Cancelled existing countdown`, { sessionID })
}
// Check if session is in recovery mode - if so, skip entirely without clearing state
if (recoveringSessions.has(sessionID)) {
log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID })
return
}
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID)
if (shouldBypass) {
interruptedSessions.delete(sessionID)
errorSessions.delete(sessionID)
log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID })
return
}
if (remindedSessions.has(sessionID)) {
log(`[${HOOK_NAME}] Skipped: already reminded this session`, { sessionID })
return
}
// Check for incomplete todos BEFORE starting countdown
let todos: Todo[] = []
try {
log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID })
const response = await ctx.client.session.todo({
path: { id: sessionID },
})
todos = (response.data ?? response) as Todo[]
log(`[${HOOK_NAME}] Todo API response`, { sessionID, todosCount: todos?.length ?? 0 })
} catch (err) {
log(`[${HOOK_NAME}] Todo API error`, { sessionID, error: String(err) })
return
}
if (!todos || todos.length === 0) {
log(`[${HOOK_NAME}] No todos found`, { sessionID })
return
}
const incomplete = todos.filter(
(t) => t.status !== "completed" && t.status !== "cancelled"
)
if (incomplete.length === 0) {
log(`[${HOOK_NAME}] All todos completed`, { sessionID, total: todos.length })
return
}
log(`[${HOOK_NAME}] Found incomplete todos, starting countdown`, { sessionID, incomplete: incomplete.length, total: todos.length })
const showCountdownToast = async (seconds: number): Promise<void> => {
await ctx.client.tui.showToast({
body: {
title: "Todo Continuation",
message: `Resuming in ${seconds}s...`,
message: `Resuming in ${seconds}s... (${incomplete.length} tasks remaining)`,
variant: "warning" as const,
duration: TOAST_DURATION_MS,
},
@@ -132,66 +189,23 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
const executeAfterCountdown = async (): Promise<void> => {
pendingCountdowns.delete(sessionID)
log(`[${HOOK_NAME}] Countdown finished, checking conditions`, { sessionID })
log(`[${HOOK_NAME}] Countdown finished, executing continuation`, { sessionID })
// Check if session is in recovery mode - if so, skip entirely without clearing state
// Re-check conditions after countdown
if (recoveringSessions.has(sessionID)) {
log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID })
log(`[${HOOK_NAME}] Abort: session entered recovery mode during countdown`, { sessionID })
return
}
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID)
interruptedSessions.delete(sessionID)
errorSessions.delete(sessionID)
if (shouldBypass) {
log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID })
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
log(`[${HOOK_NAME}] Abort: error/interrupt occurred during countdown`, { sessionID })
interruptedSessions.delete(sessionID)
errorSessions.delete(sessionID)
return
}
if (remindedSessions.has(sessionID)) {
log(`[${HOOK_NAME}] Skipped: already reminded this session`, { sessionID })
return
}
let todos: Todo[] = []
try {
log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID })
const response = await ctx.client.session.todo({
path: { id: sessionID },
})
todos = (response.data ?? response) as Todo[]
log(`[${HOOK_NAME}] Todo API response`, { sessionID, todosCount: todos?.length ?? 0 })
} catch (err) {
log(`[${HOOK_NAME}] Todo API error`, { sessionID, error: String(err) })
return
}
if (!todos || todos.length === 0) {
log(`[${HOOK_NAME}] No todos found`, { sessionID })
return
}
const incomplete = todos.filter(
(t) => t.status !== "completed" && t.status !== "cancelled"
)
if (incomplete.length === 0) {
log(`[${HOOK_NAME}] All todos completed`, { sessionID, total: todos.length })
return
}
log(`[${HOOK_NAME}] Found incomplete todos`, { sessionID, incomplete: incomplete.length, total: todos.length })
remindedSessions.add(sessionID)
// Re-check if abort occurred during the delay/fetch
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID) || recoveringSessions.has(sessionID)) {
log(`[${HOOK_NAME}] Abort occurred during delay/fetch`, { sessionID })
remindedSessions.delete(sessionID)
return
}
try {
// Get previous message's agent info to respect agent mode
const messageDir = getMessageDir(sessionID)