From 03876d03605dcd6ecd0412969f8d90b144113e30 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 11 Jan 2026 23:33:27 +0100 Subject: [PATCH] feat(coding-agent): added todo_write tool, persistent todo panel, grouped Read display, and refactored prompts - Added todo_write tool for creating and managing structured task lists during coding sessions. - Added persistent todo panel above the editor that displays task progress with Ctrl+T toggle. - Added grouped display for consecutive Read tool calls in a compact tree view. - Changed system prompt to use more direct, field-oriented language with emphasis on verification and assumptions. - Changed Ctrl+Enter to insert newline when not streaming, and Ctrl+T from toggling thinking blocks to toggling todo list. - Replaced yaml package dependency with Bun's built-in YAML parser and extracted frontmatter parsing to dedicated module. --- packages/ai/CHANGELOG.md | 4 + packages/ai/src/providers/cursor.ts | 86 ++++- packages/coding-agent/CHANGELOG.md | 14 +- packages/coding-agent/package.json | 1 - packages/coding-agent/src/core/frontmatter.ts | 45 +++ packages/coding-agent/src/core/keybindings.ts | 2 +- .../coding-agent/src/core/prompt-templates.ts | 39 +-- packages/coding-agent/src/core/skills.ts | 2 +- .../coding-agent/src/core/slash-commands.ts | 2 +- .../coding-agent/src/core/tools/index.test.ts | 2 + packages/coding-agent/src/core/tools/index.ts | 3 + .../coding-agent/src/core/tools/lsp/config.ts | 4 +- .../coding-agent/src/core/tools/renderers.ts | 2 + .../src/core/tools/task/agents.ts | 3 +- .../src/core/tools/task/commands.ts | 2 +- .../src/core/tools/task/discovery.ts | 3 +- .../coding-agent/src/core/tools/todo-write.ts | 256 +++++++++++++++ .../core/tools/web-scrapers/choosealicense.ts | 2 +- .../coding-agent/src/discovery/builtin.ts | 2 +- packages/coding-agent/src/discovery/cline.ts | 3 +- packages/coding-agent/src/discovery/codex.ts | 2 +- packages/coding-agent/src/discovery/cursor.ts | 2 +- packages/coding-agent/src/discovery/github.ts | 3 +- .../src/discovery/helpers.test.ts | 2 +- .../coding-agent/src/discovery/helpers.ts | 39 +-- .../coding-agent/src/discovery/windsurf.ts | 2 +- .../interactive/components/todo-display.ts | 112 +++++++ .../controllers/command-controller.ts | 2 +- .../controllers/event-controller.ts | 9 +- .../controllers/input-controller.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 102 +++++- .../src/modes/interactive/theme/theme.ts | 4 + .../src/modes/interactive/types.ts | 12 + .../src/prompts/system/system-prompt.md | 303 ++++++++++-------- .../src/prompts/tools/todo-write.md | 187 +++++++++++ 35 files changed, 1020 insertions(+), 240 deletions(-) create mode 100644 packages/coding-agent/src/core/frontmatter.ts create mode 100644 packages/coding-agent/src/core/tools/todo-write.ts create mode 100644 packages/coding-agent/src/modes/interactive/components/todo-display.ts create mode 100644 packages/coding-agent/src/prompts/tools/todo-write.md diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index aa1faf747..09ae34b06 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Cursor todo updates to bridge update_todos tool calls to the local todo_write tool + ## [4.3.2] - 2026-01-11 ## [4.3.1] - 2026-01-11 diff --git a/packages/ai/src/providers/cursor.ts b/packages/ai/src/providers/cursor.ts index 61a11115a..1908cb093 100644 --- a/packages/ai/src/providers/cursor.ts +++ b/packages/ai/src/providers/cursor.ts @@ -356,7 +356,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = ( let endStreamError: Error | null = null; let currentTextBlock: (TextContent & { index: number }) | null = null; let currentThinkingBlock: (ThinkingContent & { index: number }) | null = null; - let currentToolCall: (ToolCall & { index: number; partialJson: string }) | null = null; + let currentToolCall: ToolCallState | null = null; const usageState: UsageState = { sawTokenDelta: false }; const state: BlockState = { @@ -526,13 +526,15 @@ export const streamCursor: StreamFunction<"cursor-agent"> = ( return stream; }; +type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo_write" }; + interface BlockState { currentTextBlock: (TextContent & { index: number }) | null; currentThinkingBlock: (ThinkingContent & { index: number }) | null; - currentToolCall: (ToolCall & { index: number; partialJson: string }) | null; + currentToolCall: ToolCallState | null; setTextBlock: (b: (TextContent & { index: number }) | null) => void; setThinkingBlock: (b: (ThinkingContent & { index: number }) | null) => void; - setToolCall: (t: (ToolCall & { index: number; partialJson: string }) | null) => void; + setToolCall: (t: ToolCallState | null) => void; } interface UsageState { @@ -1542,6 +1544,42 @@ function decodeMcpCall(args: { }; } +function mapTodoStatusValue(status?: number): "pending" | "in_progress" | "completed" { + switch (status) { + case 2: + return "in_progress"; + case 3: + return "completed"; + default: + return "pending"; + } +} + +interface CursorTodoItem { + id?: string; + content?: string; + status?: number; +} + +interface CursorUpdateTodosToolCall { + updateTodosToolCall?: { args?: { todos?: CursorTodoItem[] } }; +} + +function buildTodoWriteArgs(toolCall: CursorUpdateTodosToolCall): { + todos: Array<{ id?: string; content: string; activeForm: string; status: "pending" | "in_progress" | "completed" }>; +} | null { + const todos = toolCall.updateTodosToolCall?.args?.todos; + if (!todos) return null; + return { + todos: todos.map((todo) => ({ + id: typeof todo.id === "string" && todo.id.length > 0 ? todo.id : undefined, + content: typeof todo.content === "string" ? todo.content : "", + activeForm: typeof todo.content === "string" ? todo.content : "", + status: mapTodoStatusValue(typeof todo.status === "number" ? todo.status : undefined), + })), + }; +} + function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolResultMessage) { if (toolResult.isError) { return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed"); @@ -1654,13 +1692,31 @@ function processInteractionUpdate( const mcpCall = toolCall.mcpToolCall; if (mcpCall) { const args = mcpCall.args || {}; - const block: ToolCall & { index: number; partialJson: string } = { + const block: ToolCallState = { type: "toolCall", id: args.toolCallId || crypto.randomUUID(), name: args.name || args.toolName || "", arguments: {}, index: output.content.length, partialJson: "", + kind: "mcp", + }; + output.content.push(block); + state.setToolCall(block); + stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output }); + return; + } + + const todoArgs = buildTodoWriteArgs(toolCall); + if (todoArgs) { + const callId = update.message.value.callId || crypto.randomUUID(); + const block: ToolCallState = { + type: "toolCall", + id: callId, + name: "todo_write", + arguments: todoArgs, + index: output.content.length, + kind: "todo_write", }; output.content.push(block); state.setToolCall(block); @@ -1668,23 +1724,31 @@ function processInteractionUpdate( } } } else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") { - if (state.currentToolCall) { + if (state.currentToolCall?.kind === "mcp") { const delta = update.message.value.argsTextDelta || ""; - state.currentToolCall.partialJson += delta; - state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson); + state.currentToolCall.partialJson = `${state.currentToolCall.partialJson ?? ""}${delta}`; + state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson ?? ""); const idx = output.content.indexOf(state.currentToolCall); stream.push({ type: "toolcall_delta", contentIndex: idx, delta, partial: output }); } } else if (updateCase === "toolCallCompleted") { if (state.currentToolCall) { const toolCall = update.message.value.toolCall; - const decodedArgs = decodeMcpArgsMap(toolCall?.mcpToolCall?.args?.args); - if (decodedArgs) { - state.currentToolCall.arguments = decodedArgs; + if (state.currentToolCall.kind === "mcp") { + const decodedArgs = decodeMcpArgsMap(toolCall?.mcpToolCall?.args?.args); + if (decodedArgs) { + state.currentToolCall.arguments = decodedArgs; + } + } else if (state.currentToolCall.kind === "todo_write" && toolCall) { + const todoArgs = buildTodoWriteArgs(toolCall); + if (todoArgs) { + state.currentToolCall.arguments = todoArgs; + } } const idx = output.content.indexOf(state.currentToolCall); delete (state.currentToolCall as any).partialJson; delete (state.currentToolCall as any).index; + delete (state.currentToolCall as any).kind; stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: state.currentToolCall, partial: output }); state.setToolCall(null); } @@ -1722,7 +1786,7 @@ function createBlobId(data: Uint8Array): Uint8Array { return new Uint8Array(createHash("sha256").update(data).digest()); } -const CURSOR_NATIVE_TOOL_NAMES = new Set(["bash", "read", "write", "delete", "ls", "grep", "lsp"]); +const CURSOR_NATIVE_TOOL_NAMES = new Set(["bash", "read", "write", "delete", "ls", "grep", "lsp", "todo_write"]); function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] { if (!tools || tools.length === 0) { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8ab401dab..9d67a487f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,22 +1,34 @@ # Changelog ## [Unreleased] - ### Added +- Added `todo_write` tool for creating and managing structured task lists during coding sessions +- Added persistent todo panel above the editor that displays task progress +- Added `Ctrl+T` keybinding to toggle todo list expansion - Added grouped display for consecutive Read tool calls, showing multiple file reads in a compact tree view +- Added `todo_write` tool and persistent todo panel above the editor ### Changed +- Changed `Ctrl+Enter` to insert a newline when not streaming (previously `Alt+Enter`) +- Changed `Ctrl+T` from toggling thinking block visibility to toggling todo list expansion +- Changed system prompt to use more direct, field-oriented language with emphasis on verification and assumptions - Changed temporary model selector keybinding from Ctrl+Y to Alt+P - Changed expand hint text from "Ctrl+O to expand" to "Ctrl+O for more" - Changed Read tool result display to hide content by default, showing only file path and status +- Changed `Ctrl+T` to toggle todo panel expansion + +### Removed + +- Removed `yaml` package dependency in favor of Bun's built-in YAML parser ### Fixed - Fixed Alt+Enter to insert a newline when not streaming, instead of submitting the message - Fixed Alt+Enter inserting a new line when not streaming instead of submitting a message - Fixed Cursor provider to avoid advertising the Edit tool, relying on full-file Write operations instead +- Fixed prompt template loading to strip leading HTML comment metadata blocks ## [4.3.2] - 2026-01-11 ### Changed diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 01c6d2648..b8ab27b9d 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -63,7 +63,6 @@ "strip-ansi": "^7.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0", - "yaml": "^2.8.2", "zod": "^4.3.5" }, "devDependencies": { diff --git a/packages/coding-agent/src/core/frontmatter.ts b/packages/coding-agent/src/core/frontmatter.ts new file mode 100644 index 000000000..80028f6f4 --- /dev/null +++ b/packages/coding-agent/src/core/frontmatter.ts @@ -0,0 +1,45 @@ +import { YAML } from "bun"; +import { logger } from "./logger"; + +function stripHtmlComments(content: string): string { + return content.replace(//g, ""); +} + +/** + * Parse YAML frontmatter from markdown content + * Returns { frontmatter, body } where body has frontmatter stripped + */ +export function parseFrontmatter(content: string): { frontmatter: Record; body: string } { + const normalized = stripHtmlComments(content.replace(/\r\n/g, "\n").replace(/\r/g, "\n")); + + if (!normalized.startsWith("---")) { + return { frontmatter: {}, body: normalized }; + } + + const endIndex = normalized.indexOf("\n---", 3); + if (endIndex === -1) { + return { frontmatter: {}, body: normalized }; + } + + const metadata = normalized.slice(4, endIndex); + const body = normalized.slice(endIndex + 4).trim(); + + try { + // Replace tabs with spaces for YAML compatibility, use failsafe mode for robustness + const frontmatter = YAML.parse(metadata.replaceAll("\t", " ")) as Record | null; + return { frontmatter: frontmatter ?? {}, body: body }; + } catch (error) { + logger.warn("Failed to parse YAML frontmatter", { error: String(error) }); + + // Simple YAML parsing - just key: value pairs + const frontmatter: Record = {}; + for (const line of metadata.split("\n")) { + const match = line.match(/^(\w+):\s*(.*)$/); + if (match) { + frontmatter[match[1]] = match[2].trim(); + } + } + + return { frontmatter, body: body }; + } +} diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index 4a2c52f84..92f3ba447 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -59,7 +59,7 @@ export const DEFAULT_APP_KEYBINDINGS: Record = { expandTools: "ctrl+o", toggleThinking: "ctrl+t", externalEditor: "ctrl+g", - followUp: "alt+enter", + followUp: "ctrl+enter", dequeue: "alt+up", }; diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index 63f1e3218..5ca6ad07e 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -1,6 +1,7 @@ import { join, resolve } from "node:path"; import Handlebars from "handlebars"; import { CONFIG_DIR_NAME, getPromptsDir } from "../config"; +import { parseFrontmatter } from "./frontmatter"; import { logger } from "./logger"; /** @@ -280,36 +281,6 @@ function optimizePromptLayout(input: string): string { return s.trim(); } -/** - * Parse YAML frontmatter from markdown content - * Returns { frontmatter, content } where content has frontmatter stripped - */ -function parseFrontmatter(content: string): { frontmatter: Record; content: string } { - const frontmatter: Record = {}; - - if (!content.startsWith("---")) { - return { frontmatter, content }; - } - - const endIndex = content.indexOf("\n---", 3); - if (endIndex === -1) { - return { frontmatter, content }; - } - - const frontmatterBlock = content.slice(4, endIndex); - const remainingContent = content.slice(endIndex + 4).trim(); - - // Simple YAML parsing - just key: value pairs - for (const line of frontmatterBlock.split("\n")) { - const match = line.match(/^(\w+):\s*(.*)$/); - if (match) { - frontmatter[match[1]] = match[2].trim(); - } - } - - return { frontmatter, content: remainingContent }; -} - /** * Parse command arguments respecting quoted strings (bash-style) * Returns array of arguments @@ -413,7 +384,7 @@ async function loadTemplatesFromDir( if (entry.endsWith(".md")) { const rawContent = await file.text(); - const { frontmatter, content } = parseFrontmatter(rawContent); + const { frontmatter, body } = parseFrontmatter(rawContent); const name = entry.split("/").pop()!.slice(0, -3); // Remove .md extension @@ -429,9 +400,9 @@ async function loadTemplatesFromDir( } // Get description from frontmatter or first non-empty line - let description = frontmatter.description || ""; + let description = String(frontmatter.description || ""); if (!description) { - const firstLine = content.split("\n").find((line) => line.trim()); + const firstLine = body.split("\n").find((line) => line.trim()); if (firstLine) { // Truncate if too long description = firstLine.slice(0, 60); @@ -445,7 +416,7 @@ async function loadTemplatesFromDir( templates.push({ name, description, - content, + content: body, source: sourceStr, }); } diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 73abc4a28..378957fb2 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -6,7 +6,7 @@ import { skillCapability } from "../capability/skill"; import type { SourceMeta } from "../capability/types"; import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery"; import { loadCapability } from "../discovery"; -import { parseFrontmatter } from "../discovery/helpers"; +import { parseFrontmatter } from "./frontmatter"; import { logger } from "./logger"; import type { SkillsSettings } from "./settings-manager"; diff --git a/packages/coding-agent/src/core/slash-commands.ts b/packages/coding-agent/src/core/slash-commands.ts index ea99da56c..137846d2a 100644 --- a/packages/coding-agent/src/core/slash-commands.ts +++ b/packages/coding-agent/src/core/slash-commands.ts @@ -1,7 +1,7 @@ import { slashCommandCapability } from "../capability/slash-command"; import type { SlashCommand } from "../discovery"; import { loadCapability } from "../discovery"; -import { parseFrontmatter } from "../discovery/helpers"; +import { parseFrontmatter } from "./frontmatter"; import { renderPromptTemplate } from "./prompt-templates"; import { EMBEDDED_COMMAND_TEMPLATES } from "./tools/task/commands"; diff --git a/packages/coding-agent/src/core/tools/index.test.ts b/packages/coding-agent/src/core/tools/index.test.ts index fbd6cd61e..df7a18e62 100644 --- a/packages/coding-agent/src/core/tools/index.test.ts +++ b/packages/coding-agent/src/core/tools/index.test.ts @@ -29,6 +29,7 @@ describe("createTools", () => { expect(names).toContain("lsp"); expect(names).toContain("notebook"); expect(names).toContain("task"); + expect(names).toContain("todo_write"); expect(names).toContain("output"); expect(names).toContain("web_fetch"); expect(names).toContain("web_search"); @@ -164,6 +165,7 @@ describe("createTools", () => { "output", "read", "task", + "todo_write", "web_fetch", "web_search", "write", diff --git a/packages/coding-agent/src/core/tools/index.ts b/packages/coding-agent/src/core/tools/index.ts index 98c760ba7..0ac705de0 100644 --- a/packages/coding-agent/src/core/tools/index.ts +++ b/packages/coding-agent/src/core/tools/index.ts @@ -28,6 +28,7 @@ export { createReadTool, type ReadToolDetails } from "./read"; export { reportFindingTool, type SubmitReviewDetails } from "./review"; export { createSshTool, type SSHToolDetails } from "./ssh"; export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index"; +export { createTodoWriteTool, type TodoItem, type TodoWriteToolDetails } from "./todo-write"; export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, @@ -79,6 +80,7 @@ import { createReadTool } from "./read"; import { reportFindingTool } from "./review"; import { createSshTool } from "./ssh"; import { createTaskTool } from "./task/index"; +import { createTodoWriteTool } from "./todo-write"; import { createWebFetchTool } from "./web-fetch"; import { createWebSearchTool } from "./web-search/index"; import { createWriteTool } from "./write"; @@ -145,6 +147,7 @@ export const BUILTIN_TOOLS: Record = { output: createOutputTool, read: createReadTool, task: createTaskTool, + todo_write: createTodoWriteTool, web_fetch: createWebFetchTool, web_search: createWebSearchTool, write: createWriteTool, diff --git a/packages/coding-agent/src/core/tools/lsp/config.ts b/packages/coding-agent/src/core/tools/lsp/config.ts index df848cb96..7d5331abe 100644 --- a/packages/coding-agent/src/core/tools/lsp/config.ts +++ b/packages/coding-agent/src/core/tools/lsp/config.ts @@ -1,7 +1,7 @@ import { homedir } from "node:os"; import { basename, extname, join } from "node:path"; +import { YAML } from "bun"; import { globSync } from "glob"; -import { parse as parseYaml } from "yaml"; import { getConfigDirPaths } from "../../../config"; import { logger } from "../../logger"; import { createBiomeClient } from "./clients/biome-client"; @@ -32,7 +32,7 @@ function isRecord(value: unknown): value is Record { function parseConfigContent(content: string, filePath: string): unknown { const extension = extname(filePath).toLowerCase(); if (extension === ".yaml" || extension === ".yml") { - return parseYaml(content) as unknown; + return YAML.parse(content) as unknown; } return JSON.parse(content) as unknown; } diff --git a/packages/coding-agent/src/core/tools/renderers.ts b/packages/coding-agent/src/core/tools/renderers.ts index ae44a3e5d..0581fdc43 100644 --- a/packages/coding-agent/src/core/tools/renderers.ts +++ b/packages/coding-agent/src/core/tools/renderers.ts @@ -20,6 +20,7 @@ import { outputToolRenderer } from "./output"; import { readToolRenderer } from "./read"; import { sshToolRenderer } from "./ssh"; import { taskToolRenderer } from "./task/render"; +import { todoWriteToolRenderer } from "./todo-write"; import { webFetchToolRenderer } from "./web-fetch"; import { webSearchToolRenderer } from "./web-search/render"; import { writeToolRenderer } from "./write"; @@ -49,6 +50,7 @@ export const toolRenderers: Record = { read: readToolRenderer as ToolRenderer, ssh: sshToolRenderer as ToolRenderer, task: taskToolRenderer as ToolRenderer, + todo_write: todoWriteToolRenderer as ToolRenderer, web_fetch: webFetchToolRenderer as ToolRenderer, web_search: webSearchToolRenderer as ToolRenderer, write: writeToolRenderer as ToolRenderer, diff --git a/packages/coding-agent/src/core/tools/task/agents.ts b/packages/coding-agent/src/core/tools/task/agents.ts index e4485a6d4..7d2cee00d 100644 --- a/packages/coding-agent/src/core/tools/task/agents.ts +++ b/packages/coding-agent/src/core/tools/task/agents.ts @@ -4,7 +4,8 @@ * Agents are embedded at build time via Bun's import with { type: "text" }. */ -import { parseAgentFields, parseFrontmatter } from "../../../discovery/helpers"; +import { parseFrontmatter } from "../../../core/frontmatter"; +import { parseAgentFields } from "../../../discovery/helpers"; import exploreMd from "../../../prompts/agents/explore.md" with { type: "text" }; // Embed agent markdown files at build time import agentFrontmatterTemplate from "../../../prompts/agents/frontmatter.md" with { type: "text" }; diff --git a/packages/coding-agent/src/core/tools/task/commands.ts b/packages/coding-agent/src/core/tools/task/commands.ts index b0e3e14c3..e15205ff1 100644 --- a/packages/coding-agent/src/core/tools/task/commands.ts +++ b/packages/coding-agent/src/core/tools/task/commands.ts @@ -6,8 +6,8 @@ import * as path from "node:path"; import { type SlashCommand, slashCommandCapability } from "../../../capability/slash-command"; +import { parseFrontmatter } from "../../../core/frontmatter"; import { loadCapability } from "../../../discovery"; -import { parseFrontmatter } from "../../../discovery/helpers"; // Embed command markdown files at build time import initMd from "../../../prompts/agents/init.md" with { type: "text" }; diff --git a/packages/coding-agent/src/core/tools/task/discovery.ts b/packages/coding-agent/src/core/tools/task/discovery.ts index 1f7831f45..d6b0d8fc4 100644 --- a/packages/coding-agent/src/core/tools/task/discovery.ts +++ b/packages/coding-agent/src/core/tools/task/discovery.ts @@ -15,7 +15,8 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { findAllNearestProjectConfigDirs, getConfigDirs } from "../../../config"; -import { parseAgentFields, parseFrontmatter } from "../../../discovery/helpers"; +import { parseFrontmatter } from "../../../core/frontmatter"; +import { parseAgentFields } from "../../../discovery/helpers"; import { loadBundledAgents } from "./agents"; import type { AgentDefinition, AgentSource } from "./types"; diff --git a/packages/coding-agent/src/core/tools/todo-write.ts b/packages/coding-agent/src/core/tools/todo-write.ts new file mode 100644 index 000000000..01c8d21a2 --- /dev/null +++ b/packages/coding-agent/src/core/tools/todo-write.ts @@ -0,0 +1,256 @@ +import { randomUUID } from "node:crypto"; +import { mkdirSync } from "node:fs"; +import path from "node:path"; +import type { AgentTool } from "@oh-my-pi/pi-agent-core"; +import type { Component } from "@oh-my-pi/pi-tui"; +import { Text } from "@oh-my-pi/pi-tui"; +import { Type } from "@sinclair/typebox"; +import chalk from "chalk"; +import type { Theme } from "../../modes/interactive/theme/theme"; +import todoWriteDescription from "../../prompts/tools/todo-write.md" with { type: "text" }; +import type { RenderResultOptions } from "../custom-tools/types"; +import { logger } from "../logger"; +import { renderPromptTemplate } from "../prompt-templates"; +import type { ToolSession } from "../sdk"; +import { ensureArtifactsDir, getArtifactsDir } from "./task/artifacts"; + +const todoWriteSchema = Type.Object({ + todos: Type.Array( + Type.Object({ + id: Type.Optional(Type.String({ description: "Stable todo id" })), + content: Type.String({ minLength: 1, description: "Imperative task description (e.g., 'Run tests')" }), + activeForm: Type.String({ minLength: 1, description: "Present continuous form (e.g., 'Running tests')" }), + status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")]), + }), + { description: "The updated todo list" }, + ), +}); + +type TodoStatus = "pending" | "in_progress" | "completed"; + +export interface TodoItem { + id: string; + content: string; + activeForm: string; + status: TodoStatus; +} + +interface TodoFile { + updatedAt: number; + todos: TodoItem[]; +} + +export interface TodoWriteToolDetails { + todos: TodoItem[]; + updatedAt: number; + storage: "session" | "memory"; +} + +const TODO_FILE_NAME = "todos.json"; + +function normalizeTodoStatus(status?: string): TodoStatus { + switch (status) { + case "in_progress": + return "in_progress"; + case "completed": + case "done": + case "complete": + return "completed"; + default: + return "pending"; + } +} + +function normalizeTodos( + items: Array<{ id?: string; content?: string; activeForm?: string; status?: string }>, +): TodoItem[] { + return items.map((item) => { + if (!item.content || !item.activeForm) { + throw new Error("Todo content and activeForm are required."); + } + const content = item.content.trim(); + const activeForm = item.activeForm.trim(); + if (!content) { + throw new Error("Todo content cannot be empty."); + } + if (!activeForm) { + throw new Error("Todo activeForm cannot be empty."); + } + return { + id: item.id && item.id.trim().length > 0 ? item.id : randomUUID(), + content, + activeForm, + status: normalizeTodoStatus(item.status), + }; + }); +} + +function validateSequentialTodos(todos: TodoItem[]): { valid: boolean; error?: string } { + if (todos.length === 0) return { valid: true }; + + const firstIncompleteIndex = todos.findIndex((todo) => todo.status !== "completed"); + if (firstIncompleteIndex >= 0) { + for (let i = firstIncompleteIndex + 1; i < todos.length; i++) { + if (todos[i].status === "completed") { + return { + valid: false, + error: `Error: Cannot complete "${todos[i].content}" before completing "${todos[firstIncompleteIndex].content}". Todos must be completed sequentially.`, + }; + } + } + } + + const inProgressIndices = todos.reduce((acc, todo, index) => { + if (todo.status === "in_progress") acc.push(index); + return acc; + }, []); + + if (inProgressIndices.length > 1) { + return { valid: false, error: "Only one todo can be in progress at a time." }; + } + + if (inProgressIndices.length === 1 && firstIncompleteIndex >= 0) { + if (inProgressIndices[0] !== firstIncompleteIndex) { + return { valid: false, error: "Todo in progress must be the next incomplete item." }; + } + } + + return { valid: true }; +} + +async function loadTodoFile(filePath: string): Promise { + const file = Bun.file(filePath); + if (!(await file.exists())) return null; + try { + const text = await file.text(); + const data = JSON.parse(text) as TodoFile; + if (!data || !Array.isArray(data.todos)) return null; + return data; + } catch (error) { + logger.warn("Failed to read todo file", { path: filePath, error: String(error) }); + return null; + } +} + +async function saveTodoFile(filePath: string, data: TodoFile): Promise { + await Bun.write(filePath, JSON.stringify(data, null, 2)); +} + +function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string { + const checkbox = uiTheme.checkbox; + const displayText = + item.status === "in_progress" && item.activeForm !== item.content ? item.activeForm : item.content; + switch (item.status) { + case "completed": + return uiTheme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(item.content)}`); + case "in_progress": + return uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${displayText}`); + default: + return uiTheme.fg("dim", `${prefix}${checkbox.unchecked} ${item.content}`); + } +} + +function formatTodoSummary(todos: TodoItem[]): string { + if (todos.length === 0) return "Todo list cleared."; + const completed = todos.filter((t) => t.status === "completed").length; + const inProgress = todos.filter((t) => t.status === "in_progress").length; + const pending = todos.filter((t) => t.status === "pending").length; + return `Saved ${todos.length} todos (${pending} pending, ${inProgress} in progress, ${completed} completed).`; +} + +export function createTodoWriteTool(session: ToolSession): AgentTool { + return { + name: "todo_write", + label: "Todo Write", + description: renderPromptTemplate(todoWriteDescription), + parameters: todoWriteSchema, + execute: async ( + _toolCallId: string, + params: { todos: Array<{ id?: string; content?: string; activeForm?: string; status?: string }> }, + ) => { + const todos = normalizeTodos(params.todos ?? []); + const validation = validateSequentialTodos(todos); + if (!validation.valid) { + throw new Error(validation.error ?? "Todos must be completed sequentially."); + } + const updatedAt = Date.now(); + + const sessionFile = session.getSessionFile(); + if (!sessionFile) { + return { + content: [{ type: "text", text: formatTodoSummary(todos) }], + details: { todos, updatedAt, storage: "memory" }, + }; + } + + const artifactsDir = getArtifactsDir(sessionFile); + if (!artifactsDir) { + return { + content: [{ type: "text", text: formatTodoSummary(todos) }], + details: { todos, updatedAt, storage: "memory" }, + }; + } + + ensureArtifactsDir(artifactsDir); + const todoPath = path.join(artifactsDir, TODO_FILE_NAME); + const existing = await loadTodoFile(todoPath); + const storedTodos = existing?.todos ?? []; + const merged = todos.length > 0 ? todos : []; + const fileData: TodoFile = { updatedAt, todos: merged }; + + try { + mkdirSync(artifactsDir, { recursive: true }); + await saveTodoFile(todoPath, fileData); + } catch (error) { + logger.error("Failed to write todo file", { path: todoPath, error: String(error) }); + return { + content: [{ type: "text", text: "Failed to save todos." }], + details: { todos: storedTodos, updatedAt, storage: "session" }, + }; + } + + return { + content: [{ type: "text", text: formatTodoSummary(merged) }], + details: { todos: merged, updatedAt, storage: "session" }, + }; + }, + }; +} + +// ============================================================================= +// TUI Renderer +// ============================================================================= + +interface TodoWriteRenderArgs { + todos?: Array<{ id?: string; content?: string; activeForm?: string; status?: string }>; +} + +export const todoWriteToolRenderer = { + renderCall(args: TodoWriteRenderArgs, uiTheme: Theme): Component { + const count = args.todos?.length ?? 0; + const summary = count > 0 ? uiTheme.fg("accent", `${count} items`) : uiTheme.fg("toolOutput", "empty"); + return new Text(`${uiTheme.fg("toolTitle", uiTheme.bold("Todo Write"))} ${summary}`, 0, 0); + }, + + renderResult( + result: { content: Array<{ type: string; text?: string }>; details?: TodoWriteToolDetails }, + _options: RenderResultOptions, + uiTheme: Theme, + _args?: TodoWriteRenderArgs, + ): Component { + const todos = result.details?.todos ?? []; + const indent = " "; + const hook = uiTheme.tree.hook; + const lines = [indent + uiTheme.bold(uiTheme.fg("accent", "Todos"))]; + + if (todos.length > 0) { + const visibleTodos = todos; + visibleTodos.forEach((todo, index) => { + const prefix = `${indent}${index === 0 ? hook : " "} `; + lines.push(formatTodoLine(todo, uiTheme, prefix)); + }); + } + + return new Text(lines.join("\n"), 0, 0); + }, +}; diff --git a/packages/coding-agent/src/core/tools/web-scrapers/choosealicense.ts b/packages/coding-agent/src/core/tools/web-scrapers/choosealicense.ts index e512b4f8a..2a3b5c2a1 100644 --- a/packages/coding-agent/src/core/tools/web-scrapers/choosealicense.ts +++ b/packages/coding-agent/src/core/tools/web-scrapers/choosealicense.ts @@ -1,4 +1,4 @@ -import { parseFrontmatter } from "../../../discovery/helpers"; +import { parseFrontmatter } from "../../../core/frontmatter"; import type { RenderResult, SpecialHandler } from "./types"; import { finalizeOutput, loadPage } from "./types"; diff --git a/packages/coding-agent/src/discovery/builtin.ts b/packages/coding-agent/src/discovery/builtin.ts index f25789eb7..0fcf438a0 100644 --- a/packages/coding-agent/src/discovery/builtin.ts +++ b/packages/coding-agent/src/discovery/builtin.ts @@ -22,6 +22,7 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt"; import { type CustomTool, toolCapability } from "../capability/tool"; import type { LoadContext, LoadResult } from "../capability/types"; +import { parseFrontmatter } from "../core/frontmatter"; import { createSourceMeta, discoverExtensionModulePaths, @@ -29,7 +30,6 @@ import { getExtensionNameFromPath, loadFilesFromDir, loadSkillsFromDir, - parseFrontmatter, parseJSON, SOURCE_PATHS, } from "./helpers"; diff --git a/packages/coding-agent/src/discovery/cline.ts b/packages/coding-agent/src/discovery/cline.ts index e74befe65..149c10bbd 100644 --- a/packages/coding-agent/src/discovery/cline.ts +++ b/packages/coding-agent/src/discovery/cline.ts @@ -11,7 +11,8 @@ import { registerProvider } from "../capability/index"; import type { Rule } from "../capability/rule"; import { ruleCapability } from "../capability/rule"; import type { LoadContext, LoadResult } from "../capability/types"; -import { createSourceMeta, loadFilesFromDir, parseFrontmatter } from "./helpers"; +import { parseFrontmatter } from "../core/frontmatter"; +import { createSourceMeta, loadFilesFromDir } from "./helpers"; const PROVIDER_ID = "cline"; const DISPLAY_NAME = "Cline"; diff --git a/packages/coding-agent/src/discovery/codex.ts b/packages/coding-agent/src/discovery/codex.ts index dffdacba6..e8a82df8b 100644 --- a/packages/coding-agent/src/discovery/codex.ts +++ b/packages/coding-agent/src/discovery/codex.ts @@ -29,6 +29,7 @@ import { slashCommandCapability } from "../capability/slash-command"; import type { CustomTool } from "../capability/tool"; import { toolCapability } from "../capability/tool"; import type { LoadContext, LoadResult } from "../capability/types"; +import { parseFrontmatter } from "../core/frontmatter"; import { logger } from "../core/logger"; import { createSourceMeta, @@ -36,7 +37,6 @@ import { getExtensionNameFromPath, loadFilesFromDir, loadSkillsFromDir, - parseFrontmatter, SOURCE_PATHS, } from "./helpers"; diff --git a/packages/coding-agent/src/discovery/cursor.ts b/packages/coding-agent/src/discovery/cursor.ts index 8c77d65b4..a1259ba8c 100644 --- a/packages/coding-agent/src/discovery/cursor.ts +++ b/packages/coding-agent/src/discovery/cursor.ts @@ -22,13 +22,13 @@ import { ruleCapability } from "../capability/rule"; import type { Settings } from "../capability/settings"; import { settingsCapability } from "../capability/settings"; import type { LoadContext, LoadResult } from "../capability/types"; +import { parseFrontmatter } from "../core/frontmatter"; import { createSourceMeta, expandEnvVarsDeep, getProjectPath, getUserPath, loadFilesFromDir, - parseFrontmatter, parseJSON, } from "./helpers"; diff --git a/packages/coding-agent/src/discovery/github.ts b/packages/coding-agent/src/discovery/github.ts index f63271022..f129be432 100644 --- a/packages/coding-agent/src/discovery/github.ts +++ b/packages/coding-agent/src/discovery/github.ts @@ -18,7 +18,8 @@ import { readFile } from "../capability/fs"; import { registerProvider } from "../capability/index"; import { type Instruction, instructionCapability } from "../capability/instruction"; import type { LoadContext, LoadResult, SourceMeta } from "../capability/types"; -import { calculateDepth, createSourceMeta, getProjectPath, loadFilesFromDir, parseFrontmatter } from "./helpers"; +import { parseFrontmatter } from "../core/frontmatter"; +import { calculateDepth, createSourceMeta, getProjectPath, loadFilesFromDir } from "./helpers"; const PROVIDER_ID = "github"; const DISPLAY_NAME = "GitHub Copilot"; diff --git a/packages/coding-agent/src/discovery/helpers.test.ts b/packages/coding-agent/src/discovery/helpers.test.ts index 55e56e5e8..8f431a8ba 100644 --- a/packages/coding-agent/src/discovery/helpers.test.ts +++ b/packages/coding-agent/src/discovery/helpers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { parseFrontmatter } from "./helpers"; +import { parseFrontmatter } from "../core/frontmatter"; describe("parseFrontmatter", () => { test("parses simple key-value pairs", () => { diff --git a/packages/coding-agent/src/discovery/helpers.ts b/packages/coding-agent/src/discovery/helpers.ts index 9898587b8..10a4dd8d3 100644 --- a/packages/coding-agent/src/discovery/helpers.ts +++ b/packages/coding-agent/src/discovery/helpers.ts @@ -5,11 +5,10 @@ import { homedir } from "node:os"; import { join, resolve } from "node:path"; import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; -import { parse as parseYAML } from "yaml"; import { readDirEntries, readFile } from "../capability/fs"; import type { Skill, SkillFrontmatter } from "../capability/skill"; import type { LoadContext, LoadResult, SourceMeta } from "../capability/types"; -import { logger } from "../core/logger"; +import { parseFrontmatter } from "../core/frontmatter"; const VALID_THINKING_LEVELS: readonly string[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; @@ -120,42 +119,6 @@ export function createSourceMeta(provider: string, path: string, level: "user" | }; } -/** - * Strip YAML frontmatter from content. - * Returns { frontmatter, body, raw } - */ -export function parseFrontmatter(content: string): { - frontmatter: Record; - body: string; - raw: string; -} { - const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - - if (!normalized.startsWith("---")) { - return { frontmatter: {}, body: normalized, raw: "" }; - } - - const endIndex = normalized.indexOf("\n---", 3); - if (endIndex === -1) { - return { frontmatter: {}, body: normalized, raw: "" }; - } - - const raw = normalized.slice(4, endIndex); - const body = normalized.slice(endIndex + 4).trim(); - - try { - // Replace tabs with spaces for YAML compatibility, use failsafe mode for robustness - const frontmatter = parseYAML(raw.replaceAll("\t", " "), { compat: "failsafe" }) as Record< - string, - unknown - > | null; - return { frontmatter: frontmatter ?? {}, body, raw }; - } catch (error) { - logger.warn("Failed to parse YAML frontmatter", { error: String(error) }); - return { frontmatter: {}, body, raw }; - } -} - /** * Parse thinking level from frontmatter. * Supports keys: thinkingLevel, thinking-level, thinking diff --git a/packages/coding-agent/src/discovery/windsurf.ts b/packages/coding-agent/src/discovery/windsurf.ts index c49a32dff..fcedc4f85 100644 --- a/packages/coding-agent/src/discovery/windsurf.ts +++ b/packages/coding-agent/src/discovery/windsurf.ts @@ -16,13 +16,13 @@ import { registerProvider } from "../capability/index"; import { type MCPServer, mcpCapability } from "../capability/mcp"; import { type Rule, ruleCapability } from "../capability/rule"; import type { LoadContext, LoadResult } from "../capability/types"; +import { parseFrontmatter } from "../core/frontmatter"; import { createSourceMeta, expandEnvVarsDeep, getProjectPath, getUserPath, loadFilesFromDir, - parseFrontmatter, parseJSON, } from "./helpers"; diff --git a/packages/coding-agent/src/modes/interactive/components/todo-display.ts b/packages/coding-agent/src/modes/interactive/components/todo-display.ts new file mode 100644 index 000000000..a6d64fc7a --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/components/todo-display.ts @@ -0,0 +1,112 @@ +import * as path from "node:path"; +import { Text } from "@oh-my-pi/pi-tui"; +import { logger } from "../../../core/logger"; +import { getArtifactsDir } from "../../../core/tools/task/artifacts"; +import { theme } from "../theme/theme"; +import type { TodoItem } from "../types"; + +const TODO_FILE_NAME = "todos.json"; + +interface TodoFile { + updatedAt: number; + todos: TodoItem[]; +} + +async function loadTodoFile(filePath: string): Promise { + const file = Bun.file(filePath); + if (!(await file.exists())) return null; + try { + const text = await file.text(); + const data = JSON.parse(text) as TodoFile; + if (!data || !Array.isArray(data.todos)) return null; + return data; + } catch (error) { + logger.warn("Failed to read todo file", { path: filePath, error: String(error) }); + return null; + } +} + +export class TodoDisplayComponent { + public todos: TodoItem[] = []; + private expanded = false; + private visible = false; + + constructor(private readonly sessionFile: string | null) {} + + async loadTodos(): Promise { + if (!this.sessionFile) { + this.todos = []; + this.visible = false; + return; + } + + const artifactsDir = getArtifactsDir(this.sessionFile); + if (!artifactsDir) { + this.todos = []; + this.visible = false; + return; + } + + const todoPath = path.join(artifactsDir, TODO_FILE_NAME); + const data = await loadTodoFile(todoPath); + this.todos = data?.todos ?? []; + this.visible = this.todos.length > 0; + } + + setTodos(todos: TodoItem[]): void { + this.todos = todos; + this.visible = this.todos.length > 0; + } + + setExpanded(expanded: boolean): void { + this.expanded = expanded; + } + + isVisible(): boolean { + return this.visible; + } + + render(_width: number): string[] { + if (!this.visible || this.todos.length === 0) { + return []; + } + + const lines: string[] = []; + const maxItems = this.expanded ? this.todos.length : Math.min(5, this.todos.length); + const hasMore = !this.expanded && this.todos.length > 5; + + for (let i = 0; i < maxItems; i++) { + const todo = this.todos[i]; + const prefix = i === 0 ? ` ${theme.tree.hook} ` : " "; + + let checkbox: string; + let text: string; + + if (todo.status === "completed") { + checkbox = theme.checkbox.checked; + text = theme.fg("success", `${prefix}${checkbox} ${theme.strikethrough(todo.content)}`); + } else if (todo.status === "in_progress") { + checkbox = theme.checkbox.unchecked; + const displayText = todo.activeForm !== todo.content ? todo.activeForm : todo.content; + text = theme.fg("accent", `${prefix}${checkbox} ${displayText}`); + } else { + checkbox = theme.checkbox.unchecked; + text = theme.fg("dim", `${prefix}${checkbox} ${todo.content}`); + } + + lines.push(text); + } + + if (hasMore) { + lines.push(theme.fg("dim", ` ${theme.tree.hook} +${this.todos.length - 5} more (Ctrl+T to expand)`)); + } + + return lines; + } + + getRenderedComponent(): Text | null { + if (!this.visible) return null; + const lines = this.render(80); + return new Text(lines.join("\n"), 0, 0); + } +} diff --git a/packages/coding-agent/src/modes/interactive/controllers/command-controller.ts b/packages/coding-agent/src/modes/interactive/controllers/command-controller.ts index 54652bf0d..625af1c2b 100644 --- a/packages/coding-agent/src/modes/interactive/controllers/command-controller.ts +++ b/packages/coding-agent/src/modes/interactive/controllers/command-controller.ts @@ -338,7 +338,7 @@ export class CommandController { | \`Ctrl+L\` | Select model (set roles) | | \`Ctrl+R\` | Search prompt history | | \`Ctrl+O\` | Toggle tool output expansion | -| \`Ctrl+T\` | Toggle thinking block visibility | +| \`Ctrl+T\` | Toggle todo list expansion | | \`Ctrl+G\` | Edit message in external editor | | \`/\` | Slash commands | | \`!\` | Run bash command | diff --git a/packages/coding-agent/src/modes/interactive/controllers/event-controller.ts b/packages/coding-agent/src/modes/interactive/controllers/event-controller.ts index 2c6251a2b..b0901ee3e 100644 --- a/packages/coding-agent/src/modes/interactive/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/interactive/controllers/event-controller.ts @@ -6,7 +6,7 @@ import { ReadToolGroupComponent } from "../components/read-tool-group"; import { ToolExecutionComponent } from "../components/tool-execution"; import { TtsrNotificationComponent } from "../components/ttsr-notification"; import { getSymbolTheme, theme } from "../theme/theme"; -import type { InteractiveModeContext } from "../types"; +import type { InteractiveModeContext, TodoItem } from "../types"; export class EventController { private lastReadGroup: ReadToolGroupComponent | undefined = undefined; @@ -243,6 +243,13 @@ export class EventController { this.ctx.pendingTools.delete(event.toolCallId); this.ctx.ui.requestRender(); } + // Update todo display when todo_write tool completes + if (event.toolName === "todo_write" && !event.isError) { + const details = event.result.details as { todos?: TodoItem[] } | undefined; + if (details?.todos) { + this.ctx.setTodos(details.todos); + } + } break; } diff --git a/packages/coding-agent/src/modes/interactive/controllers/input-controller.ts b/packages/coding-agent/src/modes/interactive/controllers/input-controller.ts index be12c7e3e..26c433dbb 100644 --- a/packages/coding-agent/src/modes/interactive/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/interactive/controllers/input-controller.ts @@ -67,7 +67,7 @@ export class InputController { this.ctx.editor.onCtrlL = () => this.ctx.showModelSelector(); this.ctx.editor.onCtrlR = () => this.ctx.showHistorySearch(); this.ctx.editor.onCtrlO = () => this.toggleToolOutputExpansion(); - this.ctx.editor.onCtrlT = () => this.toggleThinkingBlockVisibility(); + this.ctx.editor.onCtrlT = () => this.ctx.toggleTodoExpansion(); this.ctx.editor.onCtrlG = () => this.openExternalEditor(); this.ctx.editor.onQuestionMark = () => this.ctx.handleHotkeysCommand(); this.ctx.editor.onCtrlV = () => this.handleImagePaste(); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 6a537535c..e8ba5962f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -16,6 +16,7 @@ import { Text, TUI, } from "@oh-my-pi/pi-tui"; +import chalk from "chalk"; import type { AgentSession, AgentSessionEvent } from "../../core/agent-session"; import type { ExtensionUIContext } from "../../core/extensions/index"; import { HistoryStorage } from "../../core/history-storage"; @@ -26,6 +27,7 @@ import { getRecentSessions } from "../../core/session-manager"; import type { SettingsManager } from "../../core/settings-manager"; import { loadSlashCommands } from "../../core/slash-commands"; import { setTerminalTitle } from "../../core/title-generator"; +import { getArtifactsDir } from "../../core/tools/task/artifacts"; import { VoiceSupervisor } from "../../core/voice-supervisor"; import { registerAsyncCleanup } from "../cleanup"; import type { AssistantMessageComponent } from "./components/assistant-message"; @@ -45,10 +47,12 @@ import { InputController } from "./controllers/input-controller"; import { SelectorController } from "./controllers/selector-controller"; import type { Theme } from "./theme/theme"; import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/theme"; -import type { CompactionQueuedMessage, InteractiveModeContext } from "./types"; +import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem } from "./types"; import { UiHelpers } from "./utils/ui-helpers"; import { VoiceManager } from "./utils/voice-manager"; +const TODO_FILE_NAME = "todos.json"; + /** Options for creating an InteractiveMode instance (for future API use) */ export interface InteractiveModeOptions { /** Providers that were migrated during startup */ @@ -75,6 +79,7 @@ export class InteractiveMode implements InteractiveModeContext { public chatContainer: Container; public pendingMessagesContainer: Container; public statusContainer: Container; + public todoContainer: Container; public editor: CustomEditor; public editorContainer: Container; public statusLine: StatusLineComponent; @@ -83,6 +88,8 @@ export class InteractiveMode implements InteractiveModeContext { public isBackgrounded = false; public isBashMode = false; public toolOutputExpanded = false; + public todoExpanded = false; + public todoItems: TodoItem[] = []; public hideThinkingBlock = false; public pendingImages: ImageContent[] = []; public compactionQueuedMessages: CompactionQueuedMessage[] = []; @@ -148,6 +155,7 @@ export class InteractiveMode implements InteractiveModeContext { this.chatContainer = new Container(); this.pendingMessagesContainer = new Container(); this.statusContainer = new Container(); + this.todoContainer = new Container(); this.editor = new CustomEditor(getEditorTheme()); this.editor.setUseTerminalCursor(true); this.editor.onAutocompleteCancel = () => { @@ -308,6 +316,7 @@ export class InteractiveMode implements InteractiveModeContext { this.ui.addChild(this.chatContainer); this.ui.addChild(this.pendingMessagesContainer); this.ui.addChild(this.statusContainer); + this.ui.addChild(this.todoContainer); this.ui.addChild(new Spacer(1)); this.ui.addChild(this.editorContainer); this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border) @@ -316,6 +325,9 @@ export class InteractiveMode implements InteractiveModeContext { this.inputController.setupKeyHandlers(); this.inputController.setupEditorSubmitHandler(); + // Load initial todos + await this.loadTodoList(); + // Start the UI this.ui.start(); this.isInitialized = true; @@ -379,6 +391,82 @@ export class InteractiveMode implements InteractiveModeContext { this.renderSessionContext(context); } + private formatTodoLine(todo: TodoItem, prefix: string): string { + const checkbox = theme.checkbox; + const label = todo.status === "in_progress" ? todo.activeForm : todo.content; + switch (todo.status) { + case "completed": + return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(todo.content)}`); + case "in_progress": + return theme.fg("accent", `${prefix}${checkbox.unchecked} ${label}`); + default: + return theme.fg("dim", `${prefix}${checkbox.unchecked} ${label}`); + } + } + + private getCollapsedTodos(todos: TodoItem[]): TodoItem[] { + let startIndex = 0; + for (let i = todos.length - 1; i >= 0; i -= 1) { + if (todos[i].status === "completed") { + startIndex = i; + break; + } + } + return todos.slice(startIndex, startIndex + 5); + } + + private renderTodoList(): void { + this.todoContainer.clear(); + if (this.todoItems.length === 0) { + return; + } + + const visibleTodos = this.todoExpanded ? this.todoItems : this.getCollapsedTodos(this.todoItems); + const indent = " "; + const hook = theme.tree.hook; + const lines = [indent + theme.bold(theme.fg("accent", "Todos"))]; + + visibleTodos.forEach((todo, index) => { + const prefix = `${indent}${index === 0 ? hook : " "} `; + lines.push(this.formatTodoLine(todo, prefix)); + }); + + if (!this.todoExpanded && visibleTodos.length < this.todoItems.length) { + const remaining = this.todoItems.length - visibleTodos.length; + lines.push(theme.fg("muted", `${indent}${hook} +${remaining} more (Ctrl+T to expand)`)); + } + + this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0)); + } + + private async loadTodoList(): Promise { + const sessionFile = this.sessionManager.getSessionFile() ?? null; + if (!sessionFile) { + this.renderTodoList(); + return; + } + const artifactsDir = getArtifactsDir(sessionFile); + if (!artifactsDir) { + this.renderTodoList(); + return; + } + const todoPath = path.join(artifactsDir, TODO_FILE_NAME); + const file = Bun.file(todoPath); + if (!(await file.exists())) { + this.renderTodoList(); + return; + } + try { + const data = (await file.json()) as { todos?: TodoItem[] }; + if (data?.todos && Array.isArray(data.todos)) { + this.todoItems = data.todos; + } + } catch (error) { + logger.warn("Failed to load todos", { path: todoPath, error: String(error) }); + } + this.renderTodoList(); + } + stop(): void { if (this.loadingAnimation) { this.loadingAnimation.stop(); @@ -636,6 +724,18 @@ export class InteractiveMode implements InteractiveModeContext { this.inputController.toggleThinkingBlockVisibility(); } + toggleTodoExpansion(): void { + this.todoExpanded = !this.todoExpanded; + this.renderTodoList(); + this.ui.requestRender(); + } + + setTodos(todos: TodoItem[]): void { + this.todoItems = todos; + this.renderTodoList(); + this.ui.requestRender(); + } + openExternalEditor(): void { this.inputController.openExternalEditor(); } diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 8776e54d3..fbad9609b 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -1325,6 +1325,10 @@ export class Theme { return chalk.underline(text); } + strikethrough(text: string): string { + return chalk.strikethrough(text); + } + inverse(text: string): string { return chalk.inverse(text); } diff --git a/packages/coding-agent/src/modes/interactive/types.ts b/packages/coding-agent/src/modes/interactive/types.ts index 298cdd281..c1ff7083b 100644 --- a/packages/coding-agent/src/modes/interactive/types.ts +++ b/packages/coding-agent/src/modes/interactive/types.ts @@ -23,12 +23,20 @@ export type CompactionQueuedMessage = { mode: "steer" | "followUp"; }; +export type TodoItem = { + id: string; + content: string; + activeForm: string; + status: "pending" | "in_progress" | "completed"; +}; + export interface InteractiveModeContext { // UI access ui: TUI; chatContainer: Container; pendingMessagesContainer: Container; statusContainer: Container; + todoContainer: Container; editor: CustomEditor; editorContainer: Container; statusLine: StatusLineComponent; @@ -46,6 +54,7 @@ export interface InteractiveModeContext { isBackgrounded: boolean; isBashMode: boolean; toolOutputExpanded: boolean; + todoExpanded: boolean; hideThinkingBlock: boolean; pendingImages: ImageContent[]; compactionQueuedMessages: CompactionQueuedMessage[]; @@ -74,6 +83,7 @@ export interface InteractiveModeContext { lastStatusSpacer: Spacer | undefined; lastStatusText: Text | undefined; fileSlashCommands: Set; + todoItems: TodoItem[]; // Lifecycle init(): Promise; @@ -110,6 +120,8 @@ export interface InteractiveModeContext { updateEditorTopBorder(): void; updateEditorBorderColor(): void; rebuildChatFromMessages(): void; + setTodos(todos: TodoItem[]): void; + toggleTodoExpansion(): void; // Command handling handleExportCommand(text: string): Promise; diff --git a/packages/coding-agent/src/prompts/system/system-prompt.md b/packages/coding-agent/src/prompts/system/system-prompt.md index 148abcb6f..bb3935088 100644 --- a/packages/coding-agent/src/prompts/system/system-prompt.md +++ b/packages/coding-agent/src/prompts/system/system-prompt.md @@ -1,149 +1,160 @@ -You are a Distinguished Staff Engineer: high-agency, principled, decisive. -Deep expertise in debugging, refactoring, and system design. You use tools to read/edit code and run commands to finish tasks. +You are a Distinguished Staff Engineer: high-agency, principled, decisive, with deep expertise in debugging, refactoring, and system design. - -- Correctness > politeness. Be direct. -- Be concise and scannable. Use file paths in backticks. -- No filler. No apologies. No "hope this helps". -- Quote only the minimum relevant excerpts (avoid full-file/log dumps). - + +You are entering a code field. - -Get this right. This matters. -- Complete the full user request before ending your turn. -- Use tools for any deterministic fact. If you cannot verify, say so explicitly. -- When results conflict or are incomplete: investigate, iterate, re-run verification. -- When asked for "patches", output *actual* patches (unified diff or SEARCH/REPLACE), not descriptions. - +Code is frozen thought. The bugs live where the thinking stopped too soon. +Tools are extensions of attention. Use them to see, not to assume. -{{#if systemPromptCustomization}} - -{{systemPromptCustomization}} - -{{/if}} +Notice the completion reflex: +- The urge to produce something that runs +- The pattern-match to similar problems you've seen +- The assumption that compiling is correctness +- The satisfaction of "it works" before "it works in all cases" - -{{#list environment prefix="- " join="\n"}}{{label}}: {{value}}{{/list}} - +Before you write: +- What are you assuming about the input? +- What are you assuming about the environment? +- What would break this? +- What would a malicious caller do? +- What would a tired maintainer misunderstand? - -{{#if toolDescriptions.length}} -{{#list toolDescriptions prefix="- " join="\n"}}{{name}}: {{description}}{{/list}} -{{else}} -(none) -{{/if}} - +Do not: +- Write code before stating assumptions +- Claim correctness you haven't verified +- Handle the happy path and gesture at the rest +- Import complexity you don't need +- Solve problems you weren't asked to solve +- Produce code you wouldn't want to debug at 3am + -{{#has tools "bash"}} -{{#ifAny (includes tools "read") (includes tools "grep") (includes tools "find") (includes tools "edit") (includes tools "git")}} -## Tool Usage Rules — MANDATORY + +Correctness over politeness. Brevity over ceremony. +Say what is true. Omit what is filler. +No apologies. No "hope this helps." No comfort where clarity belongs. -### Forbidden Bash Patterns -NEVER use bash for these operations: +Quote only what illuminates. The rest is noise. + -{{#has tools "read"}}- **File reading**: Use `read` instead of cat/head/tail/less/more{{/has}} -{{#has tools "grep"}}- **Content search**: Use `grep` instead of grep/rg/ag/ack{{/has}} -{{#has tools "find"}}- **File finding**: Use `find` instead of find/fd/locate{{/has}} -{{#has tools "ls"}}- **Directory listing**: Use `ls` instead of bash ls{{/has}} -{{#has tools "edit"}}- **File editing**: Use `edit` instead of sed/awk/perl -pi/echo >/cat < +This matters. Get it right. -### Tool Preference (highest → lowest priority) -{{#has tools "lsp"}}1. lsp (go-to-definition, references, type info) — DETERMINISTIC{{/has}} -{{#has tools "grep"}}2. grep (text/regex search){{/has}} -{{#has tools "find"}}3. find (locate files by pattern){{/has}} -{{#has tools "read"}}4. read (view file contents){{/has}} -{{#has tools "edit"}}5. edit (precise text replacement){{/has}} -{{#has tools "git"}}6. git (structured git operations with safety guards){{/has}} -7. bash (ONLY for {{#unless (includes tools "git")}}git, {{/unless}}npm, docker, make, cargo, etc.) +- Complete the full request before yielding control. +- Use tools for any fact that can be verified. If you cannot verify, say so. +- When results conflict: investigate. When incomplete: iterate. When uncertain: re-run. + + + +## The right tool exists. Use it. + +Every tool is a choice. The wrong choice is friction. The right choice is invisible. + +### What bash is not for +Bash is the fallback, not the first reach. + +{{#has tools "read"}}- Reading files: `read` sees. `cat` just runs.{{/has}} +{{#has tools "grep"}}- Searching content: `grep` finds. Shell pipelines guess.{{/has}} +{{#has tools "find"}}- Finding files: `find` knows structure. `ls | grep` hopes.{{/has}} +{{#has tools "ls"}}- Listing directories: `ls` tool, not bash ls.{{/has}} +{{#has tools "edit"}}- Editing files: `edit` is precise. `sed` is brittle.{{/has}} +{{#has tools "git"}}- Git operations: `git` tool has guards. Bash git has none.{{/has}} + +### Hierarchy of trust +The most constrained tool is the most trustworthy. + +{{#has tools "lsp"}}1. **lsp** — semantic truth, deterministic{{/has}} +{{#has tools "grep"}}2. **grep** — pattern truth{{/has}} +{{#has tools "find"}}3. **find** — structural truth{{/has}} +{{#has tools "read"}}4. **read** — content truth{{/has}} +{{#has tools "edit"}}5. **edit** — surgical change{{/has}} +{{#has tools "git"}}6. **git** — versioned change with safety{{/has}} +7. **bash** — everything else ({{#unless (includes tools "git")}}git, {{/unless}}npm, docker, make, cargo) {{#has tools "lsp"}} -### LSP — Preferred for Semantic Queries -Use `lsp` instead of grep/bash when you need: -- **Where is X defined?** → `lsp definition` -- **What calls X?** → `lsp incoming_calls` -- **What does X call?** → `lsp outgoing_calls` -- **What type is X?** → `lsp hover` -- **What symbols are in this file?** → `lsp symbols` -- **Find symbol across codebase** → `lsp workspace_symbols` +### LSP knows what grep guesses +For semantic questions, ask the semantic tool: +- Where is X defined? → `lsp definition` +- What calls X? → `lsp incoming_calls` +- What does X call? → `lsp outgoing_calls` +- What type is X? → `lsp hover` +- What lives in this file? → `lsp symbols` +- Where does this symbol exist? → `lsp workspace_symbols` {{/has}} {{#has tools "git"}} -### Git Tool — Preferred for Git Operations -Use `git` instead of bash git when you need: -- **Status/diff/log**: `git { operation: 'status' }`, `git { operation: 'diff' }`, `git { operation: 'log' }` -- **Commit workflow**: `git { operation: 'add', paths: [...] }` then `git { operation: 'commit', message: '...' }` -- **Branching**: `git { operation: 'branch', action: 'create', name: '...' }` -- **GitHub PRs**: `git { operation: 'pr', action: 'create', title: '...', body: '...' }` -- **GitHub Issues**: `git { operation: 'issue', action: 'list' }` or `{ operation: 'issue', number: 123 }` -The git tool provides typed output, safety guards, and a clean API for all git and GitHub operations. +### Git tool over bash git +The git tool returns structure. Bash git returns strings you must parse. +- Status, diff, log: `git { operation: '...' }` +- Commits: `git { operation: 'add' }` then `git { operation: 'commit' }` +- Branches: `git { operation: 'branch', action: 'create' }` +- PRs: `git { operation: 'pr', action: 'create' }` +- Issues: `git { operation: 'issue', action: 'list' }` {{/has}} {{#has tools "ssh"}} -### SSH Command Execution -**Critical**: Each SSH host runs a specific shell. **You MUST match commands to the host's shell type**. -Check the host list in the ssh tool description. Shell types: -- linux/bash, linux/zsh, macos/bash, macos/zsh: ls, cat, grep, find, ps, df, uname -- windows/bash, windows/sh: ls, cat, grep, find (Windows with WSL/Cygwin — Unix commands) -- windows/cmd: dir, type, findstr, tasklist, systeminfo -- windows/powershell: Get-ChildItem, Get-Content, Select-String, Get-Process +### SSH: Know the shell you're speaking to +Each host has a language. Speak it. -### SSH Filesystems -Mounted at `~/.omp/remote//` — use read/edit/write tools directly. -Windows paths need colon: `~/.omp/remote/host/C:/Users/...` not `C/Users/...` +Check the host list. Match commands to shell type: +- linux/bash, macos/zsh: Unix commands +- windows/bash: Unix commands (WSL/Cygwin) +- windows/cmd: dir, type, findstr, tasklist +- windows/powershell: Get-ChildItem, Get-Content, Select-String + +Remote filesystems mount at `~/.omp/remote//`. +Windows paths need colons: `C:/Users/...` not `C/Users/...` {{/has}} {{#ifAny (includes tools "grep") (includes tools "find")}} -### Search-First Protocol -Before reading any file: -{{#has tools "find"}}1. Unknown structure → `find` to see file layout{{/has}} -{{#has tools "grep"}}2. Known location → `grep` for specific symbol/error{{/has}} -{{#has tools "read"}}3. Use `read offset/limit` for line ranges, not entire large files{{/has}} -4. Never read a large file hoping to find something — search first -{{/ifAny}} -{{/ifAny}} -{{/has}} +### Search before you read +Do not open a file hoping to find something. Know where to look first. - +{{#has tools "find"}}1. Unknown territory → `find` to map it{{/has}} +{{#has tools "grep"}}2. Known territory → `grep` to locate{{/has}} +{{#has tools "read"}}3. Known location → `read` with offset/limit, not the whole file{{/has}} +4. The large file you read in full is the time you wasted +{{/ifAny}} + + + {{#ifAll (includes tools "bash") (not (includes tools "edit")) (not (includes tools "write"))}} -- Use bash only for read-only operations (git log, gh issue view, curl, etc.). Use edit/write for file changes. +- Bash reads. Edit/write changes. {{/ifAll}} {{#ifAll (includes tools "read") (includes tools "edit")}} -- Use read to examine files before editing +- Read before you edit. Know what you're touching. {{/ifAll}} {{#has tools "edit"}} -- Use edit for precise changes (old text must match exactly, fuzzy matching handles whitespace) +- Edit is surgery. The old text must match exactly. {{/has}} {{#has tools "write"}} -- Use write only for new files or complete rewrites +- Write is creation or replacement. Not modification. {{/has}} {{#ifAny (includes tools "edit") (includes tools "write")}} -- When summarizing your actions, output plain text directly; reference file paths instead of reprinting content. +- When summarizing: plain text, file paths. Do not echo content back. {{/ifAny}} -- Be concise in your responses -- Show file paths clearly when working with files - +- Be brief. Show paths clearly. + - -## Workflow -1. If the task is non-trivial, produce a short plan (3–7 bullets). -2. Before each tool call, state intent in **one sentence**. -3. After each tool call, interpret the output and decide next step (don't repeat tool outputs, user can see that). + +## Before action +1. If the task has weight, write a plan. Three to seven bullets. No more. +2. Before each tool call: one sentence of intent. +3. After each tool call: interpret, decide, move. Do not repeat what the tool said. ## Verification -- Prefer external feedback loops: tests, linters, typechecks, repro steps, tool output. -- If you didn't run verification, say what to run and why (and what you expect to see). -- Ask for missing parameters **only when truly required**; otherwise choose the safest default and state it. +The urge to call it done is not the same as done. +- Prefer external proof: tests, linters, type checks, reproduction steps. +- If you did not verify, say what to run and what you expect. +- Ask for parameters only when truly required. Otherwise choose safe defaults and state them. -## Project Integration -- Follow AGENTS.md by scope: nearest file applies, deeper overrides higher. -- Do not search for AGENTS.md during execution; use this list as authoritative. +## Integration +- AGENTS.md files define local law. Nearest file wins. Deeper overrides higher. +- Do not search for them at runtime. This list is authoritative: {{#if agentsMdSearch.files.length}} -Relevant files are: {{#list agentsMdSearch.files join="\n"}}- {{this}}{{/list}} {{/if}} - Resolve blockers before yielding. - + {{#if contextFiles.length}} @@ -156,26 +167,24 @@ Relevant files are: {{/if}} + {{#if git.isRepo}} # Git Status - This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation. Current branch: {{git.currentBranch}} +Main branch: {{git.mainBranch}} -Main branch (you will usually use this for PRs): {{git.mainBranch}} - -Status: +## Status {{git.status}} -Recent commits: +## Recent commits {{git.commits}} {{/if}} + {{#if skills.length}} -The following skills provide specialized instructions for specific tasks. -Use the read tool to load a skill's file when the task matches its description. - - + + Skills are specialized knowledge. Load when the task matches by reading: {{#list skills join="\n"}} {{escapeXml name}} @@ -183,14 +192,13 @@ Use the read tool to load a skill's file when the task matches its description. {{escapeXml filePath}} {{/list}} - + {{/if}} {{#if rules.length}} -The following rules define project-specific guidelines and constraints. -Use the read tool to load a rule's file when working in its applicable context. + Rules are local constraints. Load when working in their domain: {{#list rules join="\n"}} {{escapeXml name}} @@ -208,30 +216,55 @@ Use the read tool to load a rule's file when working in its applicable context. {{/if}} -Current date and time: {{dateTime}} -Current working directory: {{cwd}} +Current time: {{dateTime}} +Current directory: {{cwd}} - -Maximize correctness, usefulness, and faithfulness to reality. -- Style yields to correctness/clarity when they conflict. -- State uncertainty explicitly. Never fabricate tool output or project state. - + +Correctness. Usefulness. Fidelity to what is actually true. - -IMPORTANT: Avoid reward hacking. Always: -- Fix underlying code; use tests/linters to validate correctness. -- Report only actual outputs after running tools. -- Implement breaking changes when required for correctness. - +When style and correctness conflict, correctness wins. +When you are uncertain, say so. Do not invent. + + + +The temptation to appear correct is not correctness. + +Do not: +- Suppress tests to make code pass +- Report outputs you did not observe +- Avoid breaking changes that correctness requires +- Solve the problem you wish you had instead of the one you have + + + +Suppress: + - Tutorial voice + - Explanatory scaffolding + - Name dropping as anchoring + - Summary driven closure + {{#if appendSystemPrompt}} {{appendSystemPrompt}} {{/if}} -Keep going until fully resolved. -- Do not stop early; finish the requested scope. -- If blocked: show evidence, attempted fixes, and ask the *minimum* necessary question(s). -- Quote only what's needed; avoid large logs/files. +Keep going until finished. +- Do not stop early. Do not yield incomplete work. +- If blocked: show evidence, show what you tried, ask the minimum question. +- Quote only what is needed. The rest is noise. +- Do not write code before stating assumptions. +- Do not claim correctness you haven't verified. +- Do not handle only the happy path. + + +Let edge cases surface before you handle them. Let the failure modes exist in your mind before you prevent them. Let the code be smaller than your first instinct. + +The tests you didn't write are the bugs you'll ship. +The assumptions you didn't state are the docs you'll need. +The edge cases you didn't name are the incidents you'll debug. + +The question is not "Does this work?" but "Under what conditions does this work, and what happens outside them?" +Write what you can defend. diff --git a/packages/coding-agent/src/prompts/tools/todo-write.md b/packages/coding-agent/src/prompts/tools/todo-write.md new file mode 100644 index 000000000..2f5af59f5 --- /dev/null +++ b/packages/coding-agent/src/prompts/tools/todo-write.md @@ -0,0 +1,187 @@ +Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user. +It also helps the user understand the progress of the task and overall progress of their requests. + +## When to Use This Tool + +Use this tool proactively in these scenarios: + +1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions +2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations +3. User explicitly requests todo list - When the user directly asks you to use the todo list +4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated) +5. After receiving new instructions - Immediately capture user requirements as todos +6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time +7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation + +## When NOT to Use This Tool + +Skip using this tool when: + +1. There is only a single, straightforward task +2. The task is trivial and tracking it provides no organizational benefit +3. The task can be completed in less than 3 trivial steps +4. The task is purely conversational or informational + +NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly. + +## Examples of When to Use the Todo List + + +User: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done! +Assistant: I'll help add a dark mode toggle to your application settings. Let me create a todo list to track this implementation. +*Creates todo list with the following items:* +1. Creating dark mode toggle component in Settings page +2. Adding dark mode state management (context/store) +3. Implementing CSS-in-JS styles for dark theme +4. Updating existing components to support theme switching +5. Running tests and build process, addressing any failures or errors that occur +*Begins working on the first task* + + +The assistant used the todo list because: +1. Adding dark mode is a multi-step feature requiring UI, state management, and styling changes +2. The user explicitly requested tests and build be run afterward +3. The assistant inferred that tests and build need to pass by adding "Ensure tests and build succeed" as the final task + + + + +User: Help me rename the function getCwd to getCurrentWorkingDirectory across my project +Assistant: Let me first search through your codebase to find all occurrences of 'getCwd'. +*Uses grep or search tools to locate all instances of getCwd in the codebase* +Assistant: I've found 15 instances of 'getCwd' across 8 different files. Let me create a todo list to track these changes. +*Creates todo list with specific items for each file that needs updating* + + +The assistant used the todo list because: +1. First, the assistant searched to understand the scope of the task +2. Upon finding multiple occurrences across different files, it determined this was a complex task with multiple steps +3. The todo list helps ensure every instance is tracked and updated systematically +4. This approach prevents missing any occurrences and maintains code consistency + + + + +User: I need to implement these features for my e-commerce site: user registration, product catalog, shopping cart, and checkout flow. +Assistant: I'll help implement these features. First, let's add all the features to the todo list. +*Creates a todo list breaking down each feature into specific tasks based on the project architecture* +Assistant: Let's start with user registration. This will involve creating database models, API endpoints, and frontend forms that integrate with your existing authentication system. + + +The assistant used the todo list because: +1. The user provided multiple complex features to implement in a comma separated list +2. The todo list helps organize these large features into manageable tasks +3. This approach allows for tracking progress across the entire implementation + + + + +User: Can you help optimize my React application? It's rendering slowly and has performance issues. +Assistant: I'll help optimize your React application. First, let me examine your codebase to identify potential performance bottlenecks. +*Reviews component structure, render patterns, state management, and data fetching* +Assistant: After analyzing your codebase, I've identified several performance issues. Let me create a todo list to track our optimization efforts. +*Creates todo list with items like: 1) Implementing memoization for expensive calculations in ProductList, 2) Adding virtualization for long lists in Dashboard, 3) Optimizing image loading in Gallery component, 4) Fixing state update loops in ShoppingCart, 5) Reviewing bundle size and implementing code splitting* +Let's start by implementing memoization for the expensive calculations in your ProductList component. + + +The assistant used the todo list because: +1. First, the assistant examined the codebase to identify specific performance issues +2. Based on this analysis, it identified multiple optimization opportunities +3. Performance optimization is a non-trivial task requiring multiple steps +4. The todo list helps methodically track improvements across different components +5. This systematic approach ensures all performance bottlenecks are addressed + + + +## Examples of When NOT to Use the Todo List + + +User: How do I print 'Hello World' in Python? +Assistant: In Python, you can print "Hello World" with this simple code: + +python +print("Hello World") + +This will output the text "Hello World" to the console when executed. + + +The assistant did not use the todo list because this is a single, trivial task that can be completed in one step. There's no need to track multiple tasks or steps for such a straightforward request. + + + + +User: What does the git status command do? +Assistant: The git status command shows the current state of your working directory and staging area. It displays which changes have been staged, which haven't, and which files aren't being tracked by Git. This is a fundamental command for understanding the current state of your repository before making commits. + + +The assistant did not use the todo list because this is an informational request with no actual coding task to complete. The user is simply asking for an explanation, not for the assistant to perform multiple steps or tasks. + + + + +User: Can you add a comment to the calculateTotal function to explain what it does? +Assistant: Sure, let me add a comment to the calculateTotal function to explain what it does. +* Uses the ${EDIT_TOOL_NAME} tool to add a comment to the calculateTotal function * + + +The assistant did not use the todo list because this is a single, straightforward task confined to one location in the code. Adding a comment doesn't require tracking multiple steps or systematic organization. + + + + +User: Run npm install for me and tell me what happens. +Assistant: I'll run the npm install command for you. + +_Executes: npm install_ + +The command completed successfully. Here's the output: +[Output of npm install command] + +All dependencies have been installed according to your package.json file. + + +The assistant did not use the todo list because this is a single command execution with immediate results. There are no multiple steps to track or organize, making the todo list unnecessary for this straightforward task. + + + +## Task States and Management + +1. **Task States**: Use these states to track progress: + + - pending: Task not yet started + - in_progress: Currently working on (limit to ONE task at a time) + - completed: Task finished successfully + + **IMPORTANT**: Task descriptions must have two forms: + + - content: The imperative form describing what needs to be done (e.g., "Run tests", "Build the project") + - activeForm: The present continuous form shown during execution (e.g., "Running tests", "Building the project") + +2. **Task Management**: + + - Update task status in real-time as you work + - Mark tasks complete IMMEDIATELY after finishing (don't batch completions) + - Exactly ONE task must be in_progress at any time (not less, not more) + - Complete current tasks before starting new ones + - Remove tasks that are no longer relevant from the list entirely + +3. **Task Completion Requirements**: + + - ONLY mark a task as completed when you have FULLY accomplished it + - If you encounter errors, blockers, or cannot finish, keep the task as in_progress + - When blocked, create a new task describing what needs to be resolved + - Never mark a task as completed if: + - Tests are failing + - Implementation is partial + - You encountered unresolved errors + - You couldn't find necessary files or dependencies + +4. **Task Breakdown**: + - Create specific, actionable items + - Break complex tasks into smaller, manageable steps + - Use clear, descriptive task names + - Always provide both forms: + - content: "Fix authentication bug" + - activeForm: "Fixing authentication bug" + +When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.