Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/hooks/todo-continuation-enforcer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment on lines +403 to +406
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test verifies that the todo content is included in the prompt but doesn't verify the format of status indicators (🔄/⏳) or priority labels ([HIGH]/[MED]/[LOW]). Consider adding assertions to verify the complete formatting, such as checking for the presence of "🔄 [MED] Add unit tests" to ensure the formatRemainingTodos function is working correctly.

Copilot uses AI. Check for mistakes.

// 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"
Expand Down
26 changes: 25 additions & 1 deletion src/hooks/todo-continuation-enforcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Comment on lines +88 to +90
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getIncompleteTodos function duplicates the filtering logic from getIncompleteCount. Consider refactoring getIncompleteCount to use getIncompleteTodos to reduce code duplication and maintain a single source of truth for the incomplete filtering logic.

Copilot uses AI. Check for mistakes.

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 = {}
Expand Down Expand Up @@ -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 }
Expand Down