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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 72 additions & 10 deletions src/components/TodoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,23 @@ const TodoContent = styled.div`
min-width: 0;
`;

const TodoText = styled.div<{ status: TodoItem["status"] }>`
/**
* Calculate opacity fade for items distant from the center (exponential decay).
* @param distance - How far from the center (higher = more fade)
* @param minOpacity - Minimum opacity floor
* @returns Opacity value between minOpacity and 1.0
*/
function calculateFadeOpacity(distance: number, minOpacity: number): number {
return Math.max(minOpacity, 1 - distance * 0.15);
}

const TodoText = styled.div<{
status: TodoItem["status"];
completedIndex?: number;
totalCompleted?: number;
pendingIndex?: number;
totalPending?: number;
}>`
color: ${(props) => {
switch (props.status) {
case "completed":
Expand All @@ -68,7 +84,34 @@ const TodoText = styled.div<{ status: TodoItem["status"] }>`
}
}};
text-decoration: ${(props) => (props.status === "completed" ? "line-through" : "none")};
opacity: ${(props) => (props.status === "completed" ? "0.7" : "1")};
opacity: ${(props) => {
if (props.status === "completed") {
// Apply gradient fade for old completed items (distant past)
if (
props.completedIndex !== undefined &&
props.totalCompleted !== undefined &&
props.totalCompleted > 2 &&
props.completedIndex < props.totalCompleted - 2
) {
const distance = props.totalCompleted - props.completedIndex;
return calculateFadeOpacity(distance, 0.35);
}
return "0.7";
}
if (props.status === "pending") {
// Apply gradient fade for far future pending items (distant future)
if (
props.pendingIndex !== undefined &&
props.totalPending !== undefined &&
props.totalPending > 2 &&
props.pendingIndex > 1
) {
const distance = props.pendingIndex - 1;
return calculateFadeOpacity(distance, 0.5);
}
}
return "1";
}};
font-weight: ${(props) => (props.status === "in_progress" ? "500" : "normal")};
white-space: nowrap;

Expand Down Expand Up @@ -121,16 +164,35 @@ function getStatusIcon(status: TodoItem["status"]): string {
* - PinnedTodoList (pinned at bottom of chat)
*/
export const TodoList: React.FC<TodoListProps> = ({ todos }) => {
// Count completed and pending items for fade effects
const completedCount = todos.filter((t) => t.status === "completed").length;
const pendingCount = todos.filter((t) => t.status === "pending").length;
let completedIndex = 0;
let pendingIndex = 0;

return (
<TodoListContainer>
{todos.map((todo, index) => (
<TodoItemContainer key={index} status={todo.status}>
<TodoIcon>{getStatusIcon(todo.status)}</TodoIcon>
<TodoContent>
<TodoText status={todo.status}>{todo.content}</TodoText>
</TodoContent>
</TodoItemContainer>
))}
{todos.map((todo, index) => {
const currentCompletedIndex = todo.status === "completed" ? completedIndex++ : undefined;
const currentPendingIndex = todo.status === "pending" ? pendingIndex++ : undefined;

return (
<TodoItemContainer key={index} status={todo.status}>
<TodoIcon>{getStatusIcon(todo.status)}</TodoIcon>
<TodoContent>
<TodoText
status={todo.status}
completedIndex={currentCompletedIndex}
totalCompleted={completedCount}
pendingIndex={currentPendingIndex}
totalPending={pendingCount}
>
{todo.content}
</TodoText>
</TodoContent>
</TodoItemContainer>
);
})}
</TodoListContainer>
);
};
2 changes: 2 additions & 0 deletions src/constants/toolLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export const BASH_DEFAULT_MAX_LINES = 300;
export const BASH_HARD_MAX_LINES = 300;
export const BASH_MAX_LINE_BYTES = 1024; // 1KB per line
export const BASH_MAX_TOTAL_BYTES = 16 * 1024; // 16KB total output

export const MAX_TODOS = 7; // Maximum number of TODO items in a list
36 changes: 36 additions & 0 deletions src/services/tools/todo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,42 @@ describe("Todo Storage", () => {
expect(storedTodos).toEqual([]);
});

it("should reject when exceeding MAX_TODOS limit", async () => {
// Create a list with 8 items (exceeds MAX_TODOS = 7)
const tooManyTodos: TodoItem[] = [
{ content: "Task 1", status: "completed" },
{ content: "Task 2", status: "completed" },
{ content: "Task 3", status: "completed" },
{ content: "Task 4", status: "completed" },
{ content: "Task 5", status: "in_progress" },
{ content: "Task 6", status: "pending" },
{ content: "Task 7", status: "pending" },
{ content: "Task 8", status: "pending" },
];

await expect(setTodosForTempDir(tempDir, tooManyTodos)).rejects.toThrow(
/Too many TODOs \(8\/7\)/i
);
await expect(setTodosForTempDir(tempDir, tooManyTodos)).rejects.toThrow(
/Keep high precision at the center/i
);
});

it("should accept exactly MAX_TODOS items", async () => {
const maxTodos: TodoItem[] = [
{ content: "Old work (2 tasks)", status: "completed" },
{ content: "Recent task", status: "completed" },
{ content: "Current work", status: "in_progress" },
{ content: "Next step 1", status: "pending" },
{ content: "Next step 2", status: "pending" },
{ content: "Next step 3", status: "pending" },
{ content: "Future work (5 items)", status: "pending" },
];

await setTodosForTempDir(tempDir, maxTodos);
expect(await getTodosForTempDir(tempDir)).toEqual(maxTodos);
});

it("should reject multiple in_progress tasks", async () => {
const validTodos: TodoItem[] = [
{
Expand Down
15 changes: 15 additions & 0 deletions src/services/tools/todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as path from "path";
import type { ToolFactory } from "@/utils/tools/tools";
import { TOOL_DEFINITIONS } from "@/utils/tools/toolDefinitions";
import type { TodoItem } from "@/types/tools";
import { MAX_TODOS } from "@/constants/toolLimits";

/**
* Get path to todos.json file in the stream's temporary directory
Expand All @@ -29,6 +30,7 @@ async function readTodos(tempDir: string): Promise<TodoItem[]> {
/**
* Validate todo sequencing rules before persisting.
* Enforces order: completed β†’ in_progress β†’ pending (top to bottom)
* Enforces maximum count to encourage summarization.
*/
function validateTodos(todos: TodoItem[]): void {
if (!Array.isArray(todos)) {
Expand All @@ -39,6 +41,19 @@ function validateTodos(todos: TodoItem[]): void {
return;
}

// Enforce maximum TODO count
if (todos.length > MAX_TODOS) {
throw new Error(
`Too many TODOs (${todos.length}/${MAX_TODOS}). ` +
`Keep high precision at the center: ` +
`summarize old completed work (e.g., 'Setup phase (3 tasks)'), ` +
`keep recent completions detailed (1-2), ` +
`one in_progress, ` +
`immediate pending detailed (2-3), ` +
`and summarize far future work (e.g., 'Testing phase (4 items)').`
);
}

let phase: "completed" | "in_progress" | "pending" = "completed";
let inProgressCount = 0;

Expand Down
15 changes: 13 additions & 2 deletions src/utils/tools/toolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,23 @@ export const TOOL_DEFINITIONS = {
},
todo_write: {
description:
"Create or update the todo list for tracking multi-step tasks. " +
"Create or update the todo list for tracking multi-step tasks (limit: 7 items). " +
"Use this for ALL complex, multi-step plans to keep the user informed of progress. " +
"Replace the entire list on each call - the AI should track which tasks are completed. " +
"\n\n" +
"Structure the list with high precision at the center:\n" +
"- Old completed work: Summarize into 1 overview item (e.g., 'Set up project infrastructure (4 tasks)')\n" +
"- Recent completions: Keep detailed (last 1-2 items)\n" +
"- Current work: One in_progress item with clear description\n" +
"- Immediate next steps: Detailed pending items (next 2-3 actions)\n" +
"- Far future work: Summarize into phase items (e.g., 'Testing and polish (3 items)')\n" +
"\n" +
"Update frequently as work progresses. As tasks complete, older completions should be " +
"condensed to make room. Similarly, summarized future work expands into detailed items " +
"as it becomes immediate. " +
"\n\n" +
"Mark ONE task as in_progress at a time. " +
"Order tasks as: completed first, then in_progress (max 1), then pending last. " +
"Update frequently as work progresses to provide visibility into ongoing operations. " +
"Before finishing your response, ensure all todos are marked as completed. " +
"Use appropriate tense in content: past tense for completed (e.g., 'Added tests'), " +
"present progressive for in_progress (e.g., 'Adding tests'), " +
Expand Down