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:
@@ -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,11 +126,61 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
|
|||||||
log(`[${HOOK_NAME}] Cancelled existing countdown`, { sessionID })
|
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> => {
|
const showCountdownToast = async (seconds: number): Promise<void> => {
|
||||||
await ctx.client.tui.showToast({
|
await ctx.client.tui.showToast({
|
||||||
body: {
|
body: {
|
||||||
title: "Todo Continuation",
|
title: "Todo Continuation",
|
||||||
message: `Resuming in ${seconds}s...`,
|
message: `Resuming in ${seconds}s... (${incomplete.length} tasks remaining)`,
|
||||||
variant: "warning" as const,
|
variant: "warning" as const,
|
||||||
duration: TOAST_DURATION_MS,
|
duration: TOAST_DURATION_MS,
|
||||||
},
|
},
|
||||||
@@ -132,66 +189,23 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
|
|||||||
|
|
||||||
const executeAfterCountdown = async (): Promise<void> => {
|
const executeAfterCountdown = async (): Promise<void> => {
|
||||||
pendingCountdowns.delete(sessionID)
|
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)) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID)
|
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
|
||||||
|
log(`[${HOOK_NAME}] Abort: error/interrupt occurred during countdown`, { sessionID })
|
||||||
interruptedSessions.delete(sessionID)
|
interruptedSessions.delete(sessionID)
|
||||||
errorSessions.delete(sessionID)
|
errorSessions.delete(sessionID)
|
||||||
|
|
||||||
if (shouldBypass) {
|
|
||||||
log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID })
|
|
||||||
return
|
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)
|
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 {
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user