Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions docs/todo-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ The LLxprt Code features a sophisticated task management system to help AI agent

This built-in tool allows the AI model to read the current state of the todo list for the active session.

**Purpose**: To retrieve the list of tasks, their statuses, priorities, subtasks, and recent tool calls.
**Returns**: A markdown block that mirrors the Todo panel, including status icons (/○/→), priority badges, subtasks, and the five most recent tool calls per todo.
**Purpose**: To retrieve the list of tasks, their statuses, subtasks, and recent tool calls.
**Returns**: A markdown block that mirrors the Todo panel, including status icons (/○/→), subtasks, and the five most recent tool calls per todo.
**Parameters**: None.

## `todo_write` Tool
Expand All @@ -21,7 +21,6 @@ This built-in tool allows the AI model to create, update, or overwrite the entir
- `id` (string, required): A unique identifier for the todo item.
- `content` (string, required): A clear, descriptive task for the AI to perform.
- `status` (string, enum: "pending", "in_progress", "completed", required): The current status of the task.
- `priority` (string, enum: "high", "medium", "low", required): The priority level of the task.

**Behavior**:
The tool completely replaces the current todo list with the one provided in the `todos` array. In non-interactive sessions it returns a simplified markdown view of the list to the AI. In interactive sessions the CLI renders the Todo panel by default, but if you disable the panel (see below) LLxprt synthesizes the same structured markdown that `todo_read` now emits so the entire list remains visible in scrollback.
Expand All @@ -44,7 +43,7 @@ This built-in tool allows the AI model to pause its automatic workflow continuat
Some users prefer all todo updates to remain in the scrollback instead of a separate Ink panel. Open `/settings` (or edit `.llxprt/settings.json`) and toggle **UI → Show Todo Panel**. When this setting is off:

- The Todo panel is hidden immediately—no restart required.
- `todo_write` tool calls render the full structured todo list inline (status icons, priorities, subtasks, recent tool calls) instead of the ` Todo list updated` placeholder.
- `todo_write` tool calls render the full structured todo list inline (status icons, subtasks, recent tool calls) instead of the ` Todo list updated` placeholder.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix code span with leading space (MD038).

The backticked placeholder has a leading space, which violates markdownlint’s no-space-in-code rule and can render inconsistently. Prefer removing the space or describing it in prose.

📝 Suggested doc fix
-- `todo_write` tool calls render the full structured todo list inline (status icons, subtasks, recent tool calls) instead of the ` Todo list updated` placeholder.
+- `todo_write` tool calls render the full structured todo list inline (status icons, subtasks, recent tool calls) instead of the `Todo list updated` placeholder.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `todo_write` tool calls render the full structured todo list inline (status icons, subtasks, recent tool calls) instead of the ` Todo list updated` placeholder.
- `todo_write` tool calls render the full structured todo list inline (status icons, subtasks, recent tool calls) instead of the `Todo list updated` placeholder.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

46-46: Spaces inside code span elements

(MD038, no-space-in-code)

🤖 Prompt for AI Agents
In `@docs/todo-system.md` at line 46, The backticked placeholder "` Todo list
updated`" contains a leading space which violates MD038; update the markdown in
docs/todo-system.md to remove the leading space (change to "`Todo list
updated`") or convert the placeholder into normal prose, ensuring any reference
to the todo_write tool calls still reads correctly.

- `todo_read` outputs the same formatter, so both tools always share one canonical textual representation.

Re-enable the toggle to restore the rich Ink panel without losing any history.
Expand Down
31 changes: 16 additions & 15 deletions integration-tests/todo-continuation.e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ test('basic todo continuation flow', { skip: skipTodoTests }, async () => {

// First, create a todo list with an active task
const _createResult = await rig.run(
'Create a todo list with these tasks: 1. Implement auth system (in_progress, high), 2. Write tests (pending, medium)',
'Create a todo list with these tasks: 1. Implement auth system (in_progress), 2. Write tests (pending)',
);

// Wait for todo_write tool call
Expand Down Expand Up @@ -161,7 +161,7 @@ test(
});

const _createResult = await rig.run(
'Create a todo list with one item: Build login form (in_progress, high)',
'Create a todo list with one item: Build login form (in_progress)',
);
const writeToolCall = await rig.waitForToolCall('todo_write');
assert.ok(writeToolCall, 'Expected to find a todo_write tool call');
Expand Down Expand Up @@ -393,25 +393,25 @@ test(

/**
* @requirement REQ-006
* @scenario Multiple active todos continuation priority
* @scenario Multiple active todos continuation alphabetical sorting
* @given Multiple in_progress todos
* @when Continuation is triggered
* @then Continuation focuses on highest priority active todo
* @then Continuation focuses on first alphabetically sorted active todo
*/
test(
'multiple active todos continuation priority',
'multiple active todos continuation alphabetical sorting',
{ skip: skipTodoTests },
async () => {
const rig = new TestRig();
await rig.setup('multiple active todos priority', {
await rig.setup('multiple active todos alphabetical sorting', {
settings: {
'todo-continuation': true,
},
});

// Create multiple active todos with different priorities
// Create multiple active todos - alphabetically "Fix critical bug" comes before "Update docs"
const _createResult = await rig.run(
'Create todos: 1. Fix critical bug (in_progress, high), 2. Update docs (in_progress, low), 3. Code review (pending, medium)',
'Create todos: 1. Fix critical bug (in_progress), 2. Update docs (in_progress), 3. Code review (pending)',
);

const writeToolCall = await rig.waitForToolCall('todo_write');
Expand All @@ -422,15 +422,16 @@ test(
'I need to step back and think about priorities',
);

// Should focus on the high priority task
const focusesOnHighPriority =
// Should focus on the first alphabetically sorted task
const focusesOnFirstAlphabetical =
priorityResult.toLowerCase().includes('critical bug') ||
priorityResult.toLowerCase().includes('bug') ||
priorityResult.toLowerCase().includes('critical');
priorityResult.toLowerCase().includes('critical') ||
priorityResult.toLowerCase().includes('fix');

if (!focusesOnHighPriority) {
if (!focusesOnFirstAlphabetical) {
printDebugInfo(rig, priorityResult, {
'Expected high priority focus': true,
'Expected alphabetically first focus': true,
'Mentions critical bug': priorityResult
.toLowerCase()
.includes('critical'),
Expand All @@ -441,11 +442,11 @@ test(
});
}

// The AI should reference the most important active task
// The AI should reference the alphabetically first active task
validateModelOutput(
priorityResult,
['bug', 'critical', 'fix'],
'Priority continuation test',
'Alphabetical sorting continuation test',
);

await rig.cleanup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ describe('Todo Continuation Integration Tests', () => {
id,
content,
status,
priority: 'medium',
});

beforeEach(async () => {
Expand Down Expand Up @@ -619,20 +618,17 @@ describe('Todo Continuation Integration Tests', () => {
id: '1',
content: 'Valid todo',
status: 'pending',
priority: 'medium',
} as Todo,
{
id: '2',
content: 'Todo with special chars: !@#$%^&*()',
status: 'in_progress',
priority: 'high',
} as Todo,
{
id: '3',
content:
'Very long todo content that spans multiple lines and contains various characters',
status: 'completed',
priority: 'low',
} as Todo,
];

Expand All @@ -644,7 +640,7 @@ describe('Todo Continuation Integration Tests', () => {

// Test that TodoStore validates malformed todos (expected to throw)
const malformedTodos = [
{ id: '1', status: 'pending', priority: 'low' } as unknown as Todo, // Missing content
{ id: '1', status: 'pending' } as unknown as Todo, // Missing content
];

// TodoStore should validate and throw for invalid data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,10 @@ describe('TodoContinuationService', () => {
id: string,
content: string,
status: 'pending' | 'in_progress' | 'completed' = 'pending',
priority: 'high' | 'medium' | 'low' = 'medium',
): Todo => ({
id,
content,
status,
priority,
});

const createConfig = (
Expand Down Expand Up @@ -366,22 +364,16 @@ describe('TodoContinuationService', () => {
'1',
'Implement user authentication',
'pending',
'high',
);
const pendingTodo2 = createTodo(
'2',
'Update documentation',
'pending',
'low',
);
const pendingTodo2 = createTodo('2', 'Update documentation', 'pending');

const todos = [pendingTodo2, pendingTodo1]; // Lower priority first
const todos = [pendingTodo2, pendingTodo1]; // Second alphabetically
const context = createContext({ todos, hadToolCalls: false });

const result = service.checkContinuationConditions(context);

expect(result.shouldContinue).toBe(true);
expect(result.activeTodo).toEqual(pendingTodo1); // Should pick higher priority
expect(result.activeTodo).toEqual(pendingTodo1); // Should pick first alphabetically
});

it('formats todo content into readable task description', () => {
Expand All @@ -408,26 +400,16 @@ describe('TodoContinuationService', () => {
expect(result.length).toBeGreaterThan(0); // Should provide fallback
});

it('prioritizes high-priority pending todos over low-priority ones', () => {
const lowPriorityTodo = createTodo(
'1',
'Update README',
'pending',
'low',
);
const highPriorityTodo = createTodo(
'2',
'Fix security vulnerability',
'pending',
'high',
);
it('sorts pending todos alphabetically', () => {
const todoZ = createTodo('1', 'Update README', 'pending');
const todoF = createTodo('2', 'Fix security vulnerability', 'pending');

const todos = [lowPriorityTodo, highPriorityTodo];
const todos = [todoZ, todoF];
const context = createContext({ todos, hadToolCalls: false });

const result = service.checkContinuationConditions(context);

expect(result.activeTodo).toEqual(highPriorityTodo);
expect(result.activeTodo).toEqual(todoF); // 'Fix' comes before 'Update' alphabetically
});
});
});
Expand Down Expand Up @@ -489,7 +471,6 @@ describe('TodoContinuationService', () => {
id: '1',
content: 'Minimal todo item',
status: 'in_progress',
priority: 'medium',
// subtasks and toolCalls are undefined
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,28 +534,19 @@ export class TodoContinuationService {
* @returns Best active todo or undefined
*/
private findBestActiveTodo(todos: readonly Todo[]): Todo | undefined {
// Priority 1: Find in_progress todos (should be max 1)
// First: Find in_progress todos (should be max 1)
const inProgressTodos = todos.filter(
(todo) => todo.status === 'in_progress',
);
if (inProgressTodos.length > 0) {
return inProgressTodos[0];
}

// Priority 2: Find pending todos, prioritize by priority
// Second: Find pending todos, sort alphabetically by content
const pendingTodos = todos.filter((todo) => todo.status === 'pending');
if (pendingTodos.length > 0) {
// Sort by priority: high > medium > low
const priorityOrder: Record<string, number> = {
high: 3,
medium: 2,
low: 1,
};
pendingTodos.sort((a, b) => {
const aPriority = priorityOrder[a.priority || 'medium'] || 2;
const bPriority = priorityOrder[b.priority || 'medium'] || 2;
return bPriority - aPriority; // Descending order (high to low)
});
// Sort alphabetically by content
pendingTodos.sort((a, b) => a.content.localeCompare(b.content));
return pendingTodos[0];
Comment on lines 536 to 550
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard alphabetical sort against missing content.
localeCompare will throw if content is undefined; malformed or user-edited todo files could crash continuation. Consider a null-safe fallback when sorting.

🛠️ Proposed fix
-      pendingTodos.sort((a, b) => a.content.localeCompare(b.content));
+      pendingTodos.sort(
+        (a, b) => (a.content ?? '').localeCompare(b.content ?? ''),
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private findBestActiveTodo(todos: readonly Todo[]): Todo | undefined {
// Priority 1: Find in_progress todos (should be max 1)
// First: Find in_progress todos (should be max 1)
const inProgressTodos = todos.filter(
(todo) => todo.status === 'in_progress',
);
if (inProgressTodos.length > 0) {
return inProgressTodos[0];
}
// Priority 2: Find pending todos, prioritize by priority
// Second: Find pending todos, sort alphabetically by content
const pendingTodos = todos.filter((todo) => todo.status === 'pending');
if (pendingTodos.length > 0) {
// Sort by priority: high > medium > low
const priorityOrder: Record<string, number> = {
high: 3,
medium: 2,
low: 1,
};
pendingTodos.sort((a, b) => {
const aPriority = priorityOrder[a.priority || 'medium'] || 2;
const bPriority = priorityOrder[b.priority || 'medium'] || 2;
return bPriority - aPriority; // Descending order (high to low)
});
// Sort alphabetically by content
pendingTodos.sort((a, b) => a.content.localeCompare(b.content));
return pendingTodos[0];
private findBestActiveTodo(todos: readonly Todo[]): Todo | undefined {
// First: Find in_progress todos (should be max 1)
const inProgressTodos = todos.filter(
(todo) => todo.status === 'in_progress',
);
if (inProgressTodos.length > 0) {
return inProgressTodos[0];
}
// Second: Find pending todos, sort alphabetically by content
const pendingTodos = todos.filter((todo) => todo.status === 'pending');
if (pendingTodos.length > 0) {
// Sort alphabetically by content
pendingTodos.sort(
(a, b) => (a.content ?? '').localeCompare(b.content ?? ''),
);
return pendingTodos[0];
🤖 Prompt for AI Agents
In `@packages/cli/src/services/todo-continuation/todoContinuationService.ts`
around lines 536 - 550, The alphabetical sort in findBestActiveTodo can throw
when todo.content is undefined; update the pendingTodos.sort call to compare
content in a null-safe way (e.g., use a default empty string or other fallback
for a.content and b.content before calling localeCompare) so malformed or
user-edited todos don't crash the continuation; ensure the comparator references
todo.content fallback consistently and keeps the same sort semantics for normal
entries.

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,16 @@ const testTodos: Todo[] = [
content:
'This is a very long todo item that should be truncated at different widths',
status: 'completed',
priority: 'medium',
},
{
id: '2',
content: 'Short task',
status: 'in_progress',
priority: 'high',
},
{
id: '3',
content: 'Another pending task with moderate length content',
status: 'pending',
priority: 'low',
},
];

Expand Down Expand Up @@ -263,7 +260,6 @@ describe('TodoPanel Responsive Behavior', () => {
content:
'This is a very long todo item that should use more width for better readability instead of being truncated too early',
status: 'pending',
priority: 'medium',
},
];

Expand Down Expand Up @@ -306,7 +302,6 @@ describe('TodoPanel Responsive Behavior', () => {
content:
'This extremely long todo item content should demonstrate the improved truncation behavior by showing much more text',
status: 'pending',
priority: 'medium',
},
];

Expand Down
5 changes: 0 additions & 5 deletions packages/cli/src/ui/components/TodoPanel.semantic.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ describe('TodoPanel Semantic Colors', () => {
id: '1',
content: 'Completed task',
status: 'completed',
priority: 'medium',
};

mockTodoContext.todos = [completedTodo];
Expand All @@ -98,7 +97,6 @@ describe('TodoPanel Semantic Colors', () => {
id: '1',
content: 'Current task',
status: 'in_progress',
priority: 'high',
};

mockTodoContext.todos = [inProgressTodo];
Expand All @@ -120,7 +118,6 @@ describe('TodoPanel Semantic Colors', () => {
id: '1',
content: 'Pending task',
status: 'pending',
priority: 'low',
};

mockTodoContext.todos = [pendingTodo];
Expand All @@ -142,7 +139,6 @@ describe('TodoPanel Semantic Colors', () => {
id: '1',
content: 'Test task',
status: 'completed',
priority: 'medium',
};

mockTodoContext.todos = [testTodo];
Expand Down Expand Up @@ -197,7 +193,6 @@ describe('TodoPanel Semantic Colors', () => {
id: '1',
content: 'Main task',
status: 'in_progress',
priority: 'medium',
subtasks: [
{ id: '1-1', content: 'Subtask 1', toolCalls: [] },
{ id: '1-2', content: 'Subtask 2', toolCalls: [] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ describe('<ToolGroupMessage />', () => {
id: 'todo-1',
content: 'Implement role-based access control',
status: 'in_progress',
priority: 'high',
subtasks: [
{
id: 'sub-1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,11 @@ describe('Todo Continuation Integration - useGeminiStream', () => {
id: 'todo-1',
content: 'Implement user auth',
status: 'in_progress' as const,
priority: 'high' as const,
},
{
id: 'todo-2',
content: 'Add validation',
status: 'pending' as const,
priority: 'medium' as const,
},
];

Expand Down Expand Up @@ -692,13 +690,11 @@ describe('Todo Continuation Integration - useGeminiStream', () => {
id: 'todo-1',
content: 'Completed task',
status: 'completed' as const,
priority: 'high' as const,
},
{
id: 'todo-2',
content: 'Active pending task',
status: 'pending' as const,
priority: 'medium' as const,
},
];

Expand Down Expand Up @@ -744,13 +740,11 @@ describe('Todo Continuation Integration - useGeminiStream', () => {
id: 'todo-1',
content: 'Pending task',
status: 'pending' as const,
priority: 'high' as const,
},
{
id: 'todo-2',
content: 'In progress task',
status: 'in_progress' as const,
priority: 'low' as const, // Lower priority but in_progress should win
},
];

Expand Down Expand Up @@ -796,7 +790,6 @@ describe('Todo Continuation Integration - useGeminiStream', () => {
id: 'todo-1',
content: 'Done task',
status: 'completed' as const,
priority: 'high' as const,
},
];

Expand Down
Loading
Loading