diff --git a/src/hooks/todo-continuation-enforcer.test.ts b/src/hooks/todo-continuation-enforcer.test.ts index 8f6c6f7e4f..43a73062d5 100644 --- a/src/hooks/todo-continuation-enforcer.test.ts +++ b/src/hooks/todo-continuation-enforcer.test.ts @@ -367,6 +367,101 @@ describe("todo-continuation-enforcer", () => { expect(toastCalls[0].message).toContain("2s") }) + test("should include remaining todo list in continuation prompt", async () => { + // #given - session with multiple incomplete todos + const sessionID = "main-todo-list" + setMainSession(sessionID) + + const mockInput = createMockPluginInput() + mockInput.client.session.todo = async () => ({ data: [ + { id: "1", content: "Fix authentication bug", status: "pending", priority: "high" }, + { id: "2", content: "Add unit tests", status: "in_progress", priority: "medium" }, + { id: "3", content: "Update documentation", status: "completed", priority: "low" }, + { id: "4", content: "Review code changes", status: "pending", priority: "high" }, + ]}) + + const hook = createTodoContinuationEnforcer(mockInput, { + backgroundManager: createMockBackgroundManager(false), + }) + + // #when - session goes idle and countdown completes + await hook.handler({ + event: { type: "session.idle", properties: { sessionID } }, + }) + await new Promise(r => setTimeout(r, 2500)) + + // #then - continuation prompt should include the remaining todo items + expect(promptCalls.length).toBe(1) + const promptText = promptCalls[0].text + + // Should contain status summary + expect(promptText).toContain("[Status: 1/4 completed, 3 remaining]") + + // Should contain remaining todos section + expect(promptText).toContain("Remaining Tasks:") + + // Should list pending tasks with content + expect(promptText).toContain("Fix authentication bug") + expect(promptText).toContain("Add unit tests") + expect(promptText).toContain("Review code changes") + + // Should NOT include completed task + expect(promptText).not.toContain("Update documentation") + + // Should include status indicators (🔄 for in_progress, ⏳ for pending) + expect(promptText).toContain("🔄") + expect(promptText).toContain("⏳") + + // Should include priority labels with correct format + expect(promptText).toContain("[HIGH]") + expect(promptText).toContain("[MED]") + + // Should have complete formatted entries + expect(promptText).toContain("🔄 [MED] Add unit tests") + expect(promptText).toContain("⏳ [HIGH] Fix authentication bug") + expect(promptText).toContain("⏳ [HIGH] Review code changes") + }) + + test("should handle undefined priority gracefully", async () => { + // #given - session with todos that have undefined/missing priority + const sessionID = "main-undefined-priority" + setMainSession(sessionID) + + const mockInput = createMockPluginInput() + mockInput.client.session.todo = async () => ({ data: [ + { id: "1", content: "Task with no priority", status: "pending", priority: undefined }, + { id: "2", content: "Task with null priority", status: "pending", priority: null }, + { id: "3", content: "Task with empty priority", status: "in_progress", priority: "" }, + { id: "4", content: "Normal high priority", status: "pending", priority: "high" }, + ]}) + + const hook = createTodoContinuationEnforcer(mockInput, { + backgroundManager: createMockBackgroundManager(false), + }) + + // #when - session goes idle and countdown completes + await hook.handler({ + event: { type: "session.idle", properties: { sessionID } }, + }) + await new Promise(r => setTimeout(r, 2500)) + + // #then - should handle undefined priority with [NONE] label + expect(promptCalls.length).toBe(1) + const promptText = promptCalls[0].text + + // Should contain all tasks + expect(promptText).toContain("Task with no priority") + expect(promptText).toContain("Task with null priority") + expect(promptText).toContain("Task with empty priority") + expect(promptText).toContain("Normal high priority") + + // Should have [NONE] for undefined/null/empty priorities + expect(promptText).toContain("[NONE]") + + // Should still have [HIGH] for the normal priority task + expect(promptText).toContain("[HIGH]") + }) + test("should not have 10s throttle between injections", async () => { // #given - new hook instance (no prior state) const sessionID = "main-no-throttle" diff --git a/src/hooks/todo-continuation-enforcer.ts b/src/hooks/todo-continuation-enforcer.ts index 5e16354d72..6253923d78 100644 --- a/src/hooks/todo-continuation-enforcer.ts +++ b/src/hooks/todo-continuation-enforcer.ts @@ -85,6 +85,30 @@ function isLastAssistantMessageAborted(messages: Array<{ info?: MessageInfo }>): return errorName === "MessageAbortedError" || errorName === "AbortError" } +function getIncompleteTodos(todos: Todo[]): Todo[] { + return todos.filter(t => t.status !== "completed" && t.status !== "cancelled") +} + +function formatRemainingTodos(todos: Todo[]): string { + const incompleteTodos = getIncompleteTodos(todos) + if (incompleteTodos.length === 0) return "" + + const lines = incompleteTodos.map((todo, idx) => { + const statusIndicator = todo.status === "in_progress" ? "🔄" : "⏳" + const priorityLabel = + todo.priority === "high" + ? "[HIGH]" + : todo.priority === "medium" + ? "[MED]" + : todo.priority === "low" + ? "[LOW]" + : "[NONE]" + return ` ${idx + 1}. ${statusIndicator} ${priorityLabel} ${todo.content}` + }) + + return `\n\nRemaining Tasks:\n${lines.join("\n")}` +} + export function createTodoContinuationEnforcer( ctx: PluginInput, options: TodoContinuationEnforcerOptions = {} @@ -198,7 +222,7 @@ export function createTodoContinuationEnforcer( return } - const prompt = `${CONTINUATION_PROMPT}\n\n[Status: ${todos.length - freshIncompleteCount}/${todos.length} completed, ${freshIncompleteCount} remaining]` + const prompt = `${CONTINUATION_PROMPT}\n\n[Status: ${todos.length - freshIncompleteCount}/${todos.length} completed, ${freshIncompleteCount} remaining]${formatRemainingTodos(todos)}` const modelField = prevMessage?.model?.providerID && prevMessage?.model?.modelID ? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID }