diff --git a/packages/agent-core/src/tools/builtin/state/todo-list.md b/packages/agent-core/src/tools/builtin/state/todo-list.md index 3dc3c08dc..aeec3986d 100644 --- a/packages/agent-core/src/tools/builtin/state/todo-list.md +++ b/packages/agent-core/src/tools/builtin/state/todo-list.md @@ -19,9 +19,10 @@ Use this tool to maintain a structured TODO list as you work through a multi-ste - If no available tool can move any task forward, tell the user where you are stuck instead of repeatedly re-ordering the same todos. **How to use:** -- Call with `todos: [...]` to replace the full list. Statuses: pending / in_progress / done. +- Call with `todos: [...]` to replace the full list. Statuses: `pending` / `in_progress` / `done`. - Call with no `todos` argument to retrieve the current list without changing it. - Call with `todos: []` to clear the list. +- **Important:** the status must be exactly `done`, not `completed` or `finished`. - Keep titles short and actionable (e.g. "Read session-control.ts", "Add planMode flag to TurnManager"). - Update statuses as you make progress. - When work is underway, keep exactly one task `in_progress`. diff --git a/packages/agent-core/src/tools/builtin/state/todo-list.ts b/packages/agent-core/src/tools/builtin/state/todo-list.ts index 852042e19..9a5ffdb20 100644 --- a/packages/agent-core/src/tools/builtin/state/todo-list.ts +++ b/packages/agent-core/src/tools/builtin/state/todo-list.ts @@ -28,7 +28,7 @@ export const TODO_STORE_KEY = 'todo'; const TODO_LIST_WRITE_REMINDER = 'Ensure that you continue to use the todo list to track progress. Mark tasks done immediately after finishing them, and keep exactly one task in_progress when work is underway.'; -export type TodoStatus = 'pending' | 'in_progress' | 'done'; +export type TodoStatus = 'pending' | 'in_progress' | 'done' | 'completed'; export interface TodoItem { readonly title: string; @@ -45,7 +45,9 @@ declare module '../../store' { const TodoItemSchema = z.object({ title: z.string().min(1).describe('Short, actionable title for the todo.'), - status: z.enum(['pending', 'in_progress', 'done']).describe('Current status of the todo.'), + status: z + .preprocess((val) => (val === 'completed' ? 'done' : val), z.enum(['pending', 'in_progress', 'done'])) + .describe('Current status of the todo. Must be exactly one of: pending, in_progress, done. Do NOT use completed or finished.'), }); export interface TodoListInput { @@ -81,6 +83,7 @@ function statusMarker(status: TodoStatus): string { case 'in_progress': return '[in_progress]'; case 'done': + case 'completed': return '[done]'; default: { const _exhaustive: never = status; @@ -133,7 +136,10 @@ export class TodoListTool implements BuiltinTool { private setTodos(todos: readonly TodoItem[]): void { this.store.set( TODO_STORE_KEY, - todos.map((todo) => ({ title: todo.title, status: todo.status })), + todos.map((todo) => ({ + title: todo.title, + status: todo.status === 'completed' ? 'done' : todo.status, + })), ); } } diff --git a/packages/agent-core/test/tools/todo-list.test.ts b/packages/agent-core/test/tools/todo-list.test.ts index 003e14a2e..3d8f4aad3 100644 --- a/packages/agent-core/test/tools/todo-list.test.ts +++ b/packages/agent-core/test/tools/todo-list.test.ts @@ -142,6 +142,23 @@ describe('TodoListTool', () => { ]); }); + it('accepts "completed" as a status and maps it to "done"', async () => { + const { tool, getTodos } = makeTool(); + + const result = await executeTool(tool, { + turnId: 't1', + toolCallId: 'call_1', + args: { + todos: [{ title: 'done task', status: 'completed' }], + }, + signal, + }); + + expect(result).toMatchObject({ isError: false }); + expect(result.output).toContain('[done] done task'); + expect(getTodos()).toEqual([{ title: 'done task', status: 'done' }]); + }); + it('renders a done todo with a marker matching the status enum value', async () => { const { tool } = makeTool([{ title: 'shipped', status: 'done' }]); diff --git a/packages/pi-tui/src/tui.ts b/packages/pi-tui/src/tui.ts index c8924d47c..2bd25c32d 100644 --- a/packages/pi-tui/src/tui.ts +++ b/packages/pi-tui/src/tui.ts @@ -319,6 +319,7 @@ export class TUI extends Container { private static readonly MIN_RENDER_INTERVAL_MS = 16; private cursorRow = 0; // Logical cursor row (end of rendered content) private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning) + private hardwareCursorCol = 0; // Actual terminal cursor column (tracked to avoid spurious ANSI writes) private showHardwareCursor = process.env['PI_HARDWARE_CURSOR'] === "1"; private clearOnShrink = process.env['PI_CLEAR_ON_SHRINK'] === "1"; // Clear empty rows when content shrinks (default: off) private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered) @@ -726,6 +727,7 @@ export class TUI extends Container { this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear this.cursorRow = 0; this.hardwareCursorRow = 0; + this.hardwareCursorCol = 0; this.maxLinesRendered = 0; this.previousViewportTop = 0; if (this.renderTimer) { @@ -1653,6 +1655,13 @@ export class TUI extends Container { const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1)); const targetCol = Math.max(0, cursorPos.col); + // Skip repositioning when cursor hasn't moved and hardware cursor is hidden. + // This avoids spurious ANSI sequences that can cause viewport scroll jumps + // on terminals with Kitty keyboard protocol support. + if (!this.showHardwareCursor && targetRow === this.hardwareCursorRow && targetCol === this.hardwareCursorCol) { + return; + } + // Move cursor from current position to target const rowDelta = targetRow - this.hardwareCursorRow; let buffer = ""; @@ -1669,6 +1678,7 @@ export class TUI extends Container { } this.hardwareCursorRow = targetRow; + this.hardwareCursorCol = targetCol; if (this.showHardwareCursor) { this.terminal.showCursor(); } else {