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 { existsSync, readdirSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { getMainSessionID } from "../features/claude-code-session-state"
import { import {
findNearestMessageWithFields, findNearestMessageWithFields,
MESSAGE_STORAGE, MESSAGE_STORAGE,
@@ -112,6 +113,12 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
log(`[${HOOK_NAME}] session.idle received`, { sessionID }) 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) const existingCountdown = pendingCountdowns.get(sessionID)
if (existingCountdown) { if (existingCountdown) {
clearInterval(existingCountdown.intervalId) clearInterval(existingCountdown.intervalId)
@@ -119,21 +126,6 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
log(`[${HOOK_NAME}] Cancelled existing countdown`, { sessionID }) log(`[${HOOK_NAME}] Cancelled existing countdown`, { sessionID })
} }
const showCountdownToast = async (seconds: number): Promise<void> => {
await ctx.client.tui.showToast({
body: {
title: "Todo Continuation",
message: `Resuming in ${seconds}s...`,
variant: "warning" as const,
duration: TOAST_DURATION_MS,
},
}).catch(() => {})
}
const executeAfterCountdown = async (): Promise<void> => {
pendingCountdowns.delete(sessionID)
log(`[${HOOK_NAME}] Countdown finished, checking conditions`, { sessionID })
// Check if session is in recovery mode - if so, skip entirely without clearing state // Check if session is in recovery mode - if so, skip entirely without clearing state
if (recoveringSessions.has(sessionID)) { if (recoveringSessions.has(sessionID)) {
log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID }) log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID })
@@ -142,10 +134,9 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID) const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID)
if (shouldBypass) {
interruptedSessions.delete(sessionID) interruptedSessions.delete(sessionID)
errorSessions.delete(sessionID) errorSessions.delete(sessionID)
if (shouldBypass) {
log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID }) log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID })
return return
} }
@@ -155,6 +146,7 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
return return
} }
// Check for incomplete todos BEFORE starting countdown
let todos: Todo[] = [] let todos: Todo[] = []
try { try {
log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID }) log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID })
@@ -182,16 +174,38 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
return return
} }
log(`[${HOOK_NAME}] Found incomplete todos`, { sessionID, incomplete: incomplete.length, total: todos.length }) log(`[${HOOK_NAME}] Found incomplete todos, starting countdown`, { sessionID, incomplete: incomplete.length, total: todos.length })
remindedSessions.add(sessionID)
// Re-check if abort occurred during the delay/fetch const showCountdownToast = async (seconds: number): Promise<void> => {
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID) || recoveringSessions.has(sessionID)) { await ctx.client.tui.showToast({
log(`[${HOOK_NAME}] Abort occurred during delay/fetch`, { sessionID }) body: {
remindedSessions.delete(sessionID) title: "Todo Continuation",
message: `Resuming in ${seconds}s... (${incomplete.length} tasks remaining)`,
variant: "warning" as const,
duration: TOAST_DURATION_MS,
},
}).catch(() => {})
}
const executeAfterCountdown = async (): Promise<void> => {
pendingCountdowns.delete(sessionID)
log(`[${HOOK_NAME}] Countdown finished, executing continuation`, { sessionID })
// Re-check conditions after countdown
if (recoveringSessions.has(sessionID)) {
log(`[${HOOK_NAME}] Abort: session entered recovery mode during countdown`, { sessionID })
return return
} }
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
log(`[${HOOK_NAME}] Abort: error/interrupt occurred during countdown`, { sessionID })
interruptedSessions.delete(sessionID)
errorSessions.delete(sessionID)
return
}
remindedSessions.add(sessionID)
try { try {
// Get previous message's agent info to respect agent mode // Get previous message's agent info to respect agent mode
const messageDir = getMessageDir(sessionID) const messageDir = getMessageDir(sessionID)