From c0f14aa2486fa567b46d376a5b56f0bbeb1cf615 Mon Sep 17 00:00:00 2001 From: luke <16418011+DeprecatedLuke@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:02:02 +0000 Subject: [PATCH] feat(coding-agent): auto-clear completed todo tasks (#435) - Schedule auto-removal of completed/abandoned tasks after ~1 minute delay - Strip already-done tasks when restoring session from branch history - Add todo_auto_clear event to trigger UI refresh on removal - Add blank line before Todos header for visual spacing --- packages/coding-agent/CHANGELOG.md | 8 ++ .../src/config/settings-schema.ts | 11 +++ .../src/modes/components/settings-defs.ts | 11 +++ .../src/modes/controllers/event-controller.ts | 4 + .../src/modes/interactive-mode.ts | 2 +- .../coding-agent/src/session/agent-session.ts | 76 ++++++++++++++++++- 6 files changed, 109 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 49da2f246..acb0fd18d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- Added automatic clearing of completed and abandoned todo tasks after ~1 minute + +### Changed + +- Strip already-completed tasks when restoring session from branch history + ## [13.12.4] - 2026-03-15 ### Added diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 96799753d..1cd814614 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -1292,6 +1292,17 @@ export const SETTINGS_SCHEMA = { default: {} as Record, }, + "tasks.todoClearDelay": { + type: "number", + default: 60, + ui: { + tab: "tasks", + label: "Todo auto-clear delay", + description: "How long to wait before removing completed/abandoned tasks from the list", + submenu: true, + }, + }, + // Skills "skills.enabled": { type: "boolean", default: true }, diff --git a/packages/coding-agent/src/modes/components/settings-defs.ts b/packages/coding-agent/src/modes/components/settings-defs.ts index ab14ccb1e..fd766e263 100644 --- a/packages/coding-agent/src/modes/components/settings-defs.ts +++ b/packages/coding-agent/src/modes/components/settings-defs.ts @@ -248,6 +248,17 @@ const OPTION_PROVIDERS: Partial> = { { value: "1000", label: "1000 lines" }, { value: "5000", label: "5000 lines" }, ], + // Todo auto-clear delay + "tasks.todoClearDelay": [ + { value: "0", label: "Instant" }, + { value: "60", label: "1 minute", description: "Default" }, + { value: "300", label: "5 minutes" }, + { value: "900", label: "15 minutes" }, + { value: "1800", label: "30 minutes" }, + { value: "3600", label: "1 hour" }, + { value: "-1", label: "Never" }, + ], + // Edit fuzzy threshold "edit.fuzzyThreshold": [ { value: "0.85", label: "0.85", description: "Lenient" }, diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index 5121a963c..d7085930c 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -548,6 +548,10 @@ export class EventController { this.ctx.ui.requestRender(); break; } + + case "todo_auto_clear": + await this.ctx.reloadTodos(); + break; } } diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 22f66b8fb..473b9e4fe 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -542,7 +542,7 @@ export class InteractiveMode implements InteractiveModeContext { const indent = " "; const hook = theme.tree.hook; - const lines = [indent + theme.bold(theme.fg("accent", "Todos"))]; + const lines = ["", indent + theme.bold(theme.fg("accent", "Todos"))]; if (!this.todoExpanded) { const activePhase = this.#getActivePhase(phases); diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index bcd3d6ebd..0b0769ccb 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -154,7 +154,8 @@ export type AgentSessionEvent = | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string } | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string } | { type: "ttsr_triggered"; rules: Rule[] } - | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }; + | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number } + | { type: "todo_auto_clear" }; /** Listener function for agent session events */ export type AgentSessionEventListener = (event: AgentSessionEvent) => void; @@ -361,6 +362,7 @@ export class AgentSession { // Todo completion reminder state #todoReminderCount = 0; #todoPhases: TodoPhase[] = []; + #todoClearTimers = new Map(); #nextToolChoiceOverride: ToolChoice | undefined = undefined; // Bash execution state @@ -1534,6 +1536,7 @@ export class AgentSession { logger.warn("Failed to emit session_shutdown event", { error: String(error) }); } this.#cancelPostPromptTasks(); + this.#clearTodoClearTimers(); const drained = await this.#asyncJobManager?.dispose({ timeoutMs: 3_000 }); const deliveryState = this.#asyncJobManager?.getDeliveryState(); if (drained === false && deliveryState) { @@ -2538,10 +2541,17 @@ export class AgentSession { setTodoPhases(phases: TodoPhase[]): void { this.#todoPhases = this.#cloneTodoPhases(phases); + this.#scheduleTodoAutoClear(phases); } #syncTodoPhasesFromBranch(): void { - this.setTodoPhases(getLatestTodoPhasesFromEntries(this.sessionManager.getBranch())); + const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch()); + // Strip completed/abandoned tasks — they were done in a previous run, + // so the auto-clear grace period has already elapsed. + for (const phase of phases) { + phase.tasks = phase.tasks.filter(t => t.status !== "completed" && t.status !== "abandoned"); + } + this.setTodoPhases(phases.filter(p => p.tasks.length > 0)); } #cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] { @@ -2557,6 +2567,68 @@ export class AgentSession { })); } + /** Schedule auto-removal of completed/abandoned tasks after a delay. */ + #scheduleTodoAutoClear(phases: TodoPhase[]): void { + const delaySec = this.settings.get("tasks.todoClearDelay") ?? 60; + if (delaySec < 0) return; // "Never" — no auto-clear + const delayMs = delaySec * 1000; + const doneTaskIds = new Set(); + for (const phase of phases) { + for (const task of phase.tasks) { + if (task.status === "completed" || task.status === "abandoned") { + doneTaskIds.add(task.id); + } + } + } + + // Cancel timers for tasks that are no longer done (e.g. status was reverted) + for (const [id, timer] of this.#todoClearTimers) { + if (!doneTaskIds.has(id)) { + clearTimeout(timer); + this.#todoClearTimers.delete(id); + } + } + + // Schedule new timers for newly-done tasks + for (const id of doneTaskIds) { + if (this.#todoClearTimers.has(id)) continue; + if (delayMs === 0) { + // Instant — run synchronously on next microtask to batch removals + const timer = setTimeout(() => this.#runTodoAutoClear(id), 0); + this.#todoClearTimers.set(id, timer); + } else { + const timer = setTimeout(() => this.#runTodoAutoClear(id), delayMs); + this.#todoClearTimers.set(id, timer); + } + } + } + + /** Remove a single completed task and notify the UI. */ + #runTodoAutoClear(taskId: string): void { + this.#todoClearTimers.delete(taskId); + let removed = false; + for (const phase of this.#todoPhases) { + const idx = phase.tasks.findIndex(t => t.id === taskId); + if (idx !== -1 && (phase.tasks[idx].status === "completed" || phase.tasks[idx].status === "abandoned")) { + phase.tasks.splice(idx, 1); + removed = true; + break; + } + } + if (!removed) return; + + // Remove empty phases + this.#todoPhases = this.#todoPhases.filter(p => p.tasks.length > 0); + this.#emit({ type: "todo_auto_clear" }); + } + + #clearTodoClearTimers(): void { + for (const timer of this.#todoClearTimers.values()) { + clearTimeout(timer); + } + this.#todoClearTimers.clear(); + } + /** * Abort current operation and wait for agent to become idle. */