Unverified 提交 23f9a95e authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Agent TODOs tool (#2136)

<!-- CURSOR_SUMMARY --> > [!NOTE] > Introduces live Agent to-do tracking during local-agent runs. > > - **New tool:** `update_todos` (merge/replace support) added to agent toolset; updates per-turn `todos` in `AgentContext` and broadcasts via `onUpdateTodos` > - **IPC plumbing:** New `agent-tool:todos-update` channel allowlisted in `preload`; `IpcClient` adds `onAgentTodosUpdate` and `onChatStreamStart` hooks and forwards updates; main handler emits updates and initializes `ctx.todos` > - **Types:** Adds `AgentTodo` and `AgentTodosUpdatePayload`; extends `AgentContext` with `todos` and `onUpdateTodos` > - **UI/state:** New `agentTodosByChatIdAtom`; `TodoList` component; `ChatInput` renders live todos; renderer subscribes to updates and clears todos on stream start/end > - **Tests:** E2E snapshot updated with `update_todos` tool schema > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e2bc12f172b2b6ae15c3514e7e9c4d2b693a6e99. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds an Agent TODOs tool and UI to track task progress during local agent runs. Live updates stream to the chat and display in a collapsible todo list, which clears when a new stream starts. - **New Features** - Added update_todos tool with merge/replace support and status updates; broadcasts via onUpdateTodos. - Introduced IPC channel agent-tool:todos-update with IpcClient.onAgentTodosUpdate and preload whitelist. - Defined AgentTodo and AgentTodosUpdatePayload types. - Added agentTodosByChatIdAtom and cleanup on chat stream start. - Implemented TodoList UI and wired into ChatInput to show live progress and counts. <sup>Written for commit e2bc12f172b2b6ae15c3514e7e9c4d2b693a6e99. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
上级 c145a0cd
...@@ -324,6 +324,58 @@ ...@@ -324,6 +324,58 @@
"$schema": "http://json-schema.org/draft-07/schema#" "$schema": "http://json-schema.org/draft-07/schema#"
} }
} }
},
{
"type": "function",
"function": {
"name": "update_todos",
"description": "\n### When to Use This Tool\n\nUse proactively for:\n1. Complex multi-step tasks (3+ distinct steps)\n2. Non-trivial tasks requiring careful planning\n3. User explicitly requests todo list\n4. User provides multiple tasks (numbered/comma-separated)\n5. After completing tasks - mark complete with merge=true and add follow-ups\n6. When starting new tasks - mark as in_progress (ideally only one at a time)\n\n### When NOT to Use\n\nSkip for:\n1. Single, straightforward tasks\n2. Trivial tasks with no organizational benefit\n3. Tasks completable in < 3 trivial steps\n4. Purely conversational/informational requests\n5. Todo items should NOT include operational actions done in service of higher-level tasks.\n\nNEVER INCLUDE THESE IN TODOS: linting; testing; searching or examining the codebase.\n\n### Examples\n\n<example>\nUser: Add dark mode toggle to settings\nAssistant:\n- *Creates todo list:*\n1. Add state management [in_progress]\n2. Implement styles\n3. Create toggle component\n4. Update components\n- [Immediately begins working on todo 1 in the same tool call batch]\n<reasoning>\nMulti-step feature with dependencies.\n</reasoning>\n</example>\n\n<example>\n// User: Implement user registration, product catalog, shopping cart, checkout flow.\nAssistant: *Creates todo list breaking down each feature into specific tasks*\n<reasoning>\nMultiple complex features provided as list requiring organized task management.\n</reasoning>\n</example>\n\n### Task States and Management\n\n1. **Task States:**\n- pending: Not yet started\n- in_progress: Currently working on\n- completed: Finished successfully\n\n2. **Task Management:**\n- Update status in real-time\n- Mark complete IMMEDIATELY after finishing\n- Only ONE task in_progress at a time\n- Complete current tasks before starting new ones\n\n3. **Task Breakdown:**\n- Create specific, actionable items\n- Break complex tasks into manageable steps\n- Use clear, descriptive names\n\n4. **Parallel Todo Writes:**\n- Prefer creating the first todo as in_progress\n- Start working on todos by using tool calls in the same tool call batch as the todo write\n- Batch todo updates with other tool calls for better latency and lower costs for the user\n",
"parameters": {
"type": "object",
"properties": {
"merge": {
"type": "boolean",
"description": "Whether to merge the todos with the existing todos. If true, the todos will be merged into the existing todos based on the id field. You can leave unchanged properties undefined. If false, the new todos will replace the existing todos."
},
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the todo item"
},
"content": {
"type": "string",
"description": "The description/content of the todo item"
},
"status": {
"type": "string",
"enum": [
"pending",
"in_progress",
"completed"
],
"description": "The current status of the todo item"
}
},
"required": [
"id"
],
"additionalProperties": false
},
"description": "Array of todo items. When merge is true, only include todos that need updates. When merge is false, this is the complete list."
}
},
"required": [
"merge",
"todos"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
} }
], ],
"tool_choice": "auto", "tool_choice": "auto",
......
import type { FileAttachment, Message } from "@/ipc/ipc_types"; import type { FileAttachment, Message, AgentTodo } from "@/ipc/ipc_types";
import { atom } from "jotai"; import { atom } from "jotai";
// Per-chat atoms implemented with maps keyed by chatId // Per-chat atoms implemented with maps keyed by chatId
...@@ -28,3 +28,6 @@ export interface PendingAgentConsent { ...@@ -28,3 +28,6 @@ export interface PendingAgentConsent {
} }
export const pendingAgentConsentsAtom = atom<PendingAgentConsent[]>([]); export const pendingAgentConsentsAtom = atom<PendingAgentConsent[]>([]);
// Agent todos per chat
export const agentTodosByChatIdAtom = atom<Map<number, AgentTodo[]>>(new Map());
...@@ -27,6 +27,7 @@ import { ...@@ -27,6 +27,7 @@ import {
chatMessagesByIdAtom, chatMessagesByIdAtom,
selectedChatIdAtom, selectedChatIdAtom,
pendingAgentConsentsAtom, pendingAgentConsentsAtom,
agentTodosByChatIdAtom,
} from "@/atoms/chatAtoms"; } from "@/atoms/chatAtoms";
import { atom, useAtom, useSetAtom, useAtomValue } from "jotai"; import { atom, useAtom, useSetAtom, useAtomValue } from "jotai";
import { useStreamChat } from "@/hooks/useStreamChat"; import { useStreamChat } from "@/hooks/useStreamChat";
...@@ -63,6 +64,7 @@ import { useSummarizeInNewChat } from "./SummarizeInNewChatButton"; ...@@ -63,6 +64,7 @@ import { useSummarizeInNewChat } from "./SummarizeInNewChatButton";
import { ChatInputControls } from "../ChatInputControls"; import { ChatInputControls } from "../ChatInputControls";
import { ChatErrorBox } from "./ChatErrorBox"; import { ChatErrorBox } from "./ChatErrorBox";
import { AgentConsentBanner } from "./AgentConsentBanner"; import { AgentConsentBanner } from "./AgentConsentBanner";
import { TodoList } from "./TodoList";
import { import {
selectedComponentsPreviewAtom, selectedComponentsPreviewAtom,
previewIframeRefAtom, previewIframeRefAtom,
...@@ -121,6 +123,10 @@ export function ChatInput({ chatId }: { chatId?: number }) { ...@@ -121,6 +123,10 @@ export function ChatInput({ chatId }: { chatId?: number }) {
(c) => c.chatId === chatId, (c) => c.chatId === chatId,
); );
const pendingAgentConsent = consentsForThisChat[0] ?? null; const pendingAgentConsent = consentsForThisChat[0] ?? null;
// Get todos for this chat
const agentTodosByChatId = useAtomValue(agentTodosByChatIdAtom);
const chatTodos = chatId ? (agentTodosByChatId.get(chatId) ?? []) : [];
const { checkProblems } = useCheckProblems(appId); const { checkProblems } = useCheckProblems(appId);
const { refreshAppIframe } = useRunApp(); const { refreshAppIframe } = useRunApp();
// Use the attachments hook // Use the attachments hook
...@@ -319,6 +325,8 @@ export function ChatInput({ chatId }: { chatId?: number }) { ...@@ -319,6 +325,8 @@ export function ChatInput({ chatId }: { chatId?: number }) {
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
> >
{/* Show todo list if there are todos for this chat */}
{chatTodos.length > 0 && <TodoList todos={chatTodos} />}
{/* Show agent consent banner if there's a pending consent request */} {/* Show agent consent banner if there's a pending consent request */}
{pendingAgentConsent && ( {pendingAgentConsent && (
<AgentConsentBanner <AgentConsentBanner
......
import React, { useState } from "react";
import type { AgentTodo } from "@/ipc/ipc_types";
import {
CheckCircle2,
Circle,
Loader2,
ChevronDown,
ChevronUp,
ListTodo,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface TodoListProps {
todos: AgentTodo[];
}
function getStatusIcon(status: AgentTodo["status"], size: "sm" | "md" = "sm") {
const sizeClass = size === "sm" ? "w-3.5 h-3.5" : "w-4 h-4";
switch (status) {
case "completed":
return (
<CheckCircle2
className={cn(sizeClass, "text-green-500 flex-shrink-0")}
/>
);
case "in_progress":
return (
<Loader2
className={cn(sizeClass, "text-blue-500 animate-spin flex-shrink-0")}
/>
);
case "pending":
default:
return (
<Circle
className={cn(sizeClass, "text-muted-foreground flex-shrink-0")}
/>
);
}
}
export function TodoList({ todos }: TodoListProps) {
const [isExpanded, setIsExpanded] = useState(false);
if (!todos.length) return null;
const completed = todos.filter((t) => t.status === "completed").length;
const total = todos.length;
const inProgressTask = todos.find((t) => t.status === "in_progress");
return (
<div className="border-b border-border bg-muted/30">
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between px-3 py-2 hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-2.5 min-w-0 flex-1">
{isExpanded ? (
<>
<ListTodo className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<span className="text-sm">
{completed} of {total} To-dos Completed
</span>
</>
) : inProgressTask ? (
<>
{getStatusIcon("in_progress", "md")}
<span className="text-sm truncate">{inProgressTask.content}</span>
<span className="text-xs text-muted-foreground tabular-nums flex-shrink-0">
({completed}/{total})
</span>
</>
) : (
<>
{completed === total ? (
<CheckCircle2 className="w-4 h-4 text-green-500 flex-shrink-0" />
) : (
<Circle className="w-4 h-4 text-muted-foreground flex-shrink-0" />
)}
<span className="text-sm text-muted-foreground">
{completed === total
? "All tasks completed"
: "No task in progress"}
</span>
<span className="text-xs text-muted-foreground tabular-nums flex-shrink-0">
({completed}/{total})
</span>
</>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0 ml-3">
{isExpanded ? (
<ChevronUp className="w-4 h-4 text-muted-foreground" />
) : (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
)}
</div>
</button>
{isExpanded && (
<ul className="px-3 pb-2.5 space-y-1.5">
{todos.map((todo) => (
<li
key={todo.id}
className={cn(
"flex items-center gap-2.5 text-sm py-0.5",
todo.status === "completed" && "text-muted-foreground",
)}
>
{getStatusIcon(todo.status)}
<span
className={cn(todo.status === "completed" && "line-through")}
>
{todo.content}
</span>
</li>
))}
</ul>
)}
</div>
);
}
...@@ -81,6 +81,7 @@ import type { ...@@ -81,6 +81,7 @@ import type {
SetAgentToolConsentParams, SetAgentToolConsentParams,
AgentToolConsentRequestPayload, AgentToolConsentRequestPayload,
AgentToolConsentResponseParams, AgentToolConsentResponseParams,
AgentTodosUpdatePayload,
TelemetryEventPayload, TelemetryEventPayload,
ConsoleEntry, ConsoleEntry,
} from "./ipc_types"; } from "./ipc_types";
...@@ -138,7 +139,10 @@ export class IpcClient { ...@@ -138,7 +139,10 @@ export class IpcClient {
>; >;
private mcpConsentHandlers: Map<string, (payload: any) => void>; private mcpConsentHandlers: Map<string, (payload: any) => void>;
private agentConsentHandlers: Map<string, (payload: any) => void>; private agentConsentHandlers: Map<string, (payload: any) => void>;
private agentTodosHandlers: Set<(payload: AgentTodosUpdatePayload) => void>;
private telemetryEventHandlers: Set<(payload: TelemetryEventPayload) => void>; private telemetryEventHandlers: Set<(payload: TelemetryEventPayload) => void>;
// Global handlers called for any chat stream start (used for cleanup)
private globalChatStreamStartHandlers: Set<(chatId: number) => void>;
// Global handlers called for any chat stream completion (used for cleanup) // Global handlers called for any chat stream completion (used for cleanup)
private globalChatStreamEndHandlers: Set<(chatId: number) => void>; private globalChatStreamEndHandlers: Set<(chatId: number) => void>;
private constructor() { private constructor() {
...@@ -148,7 +152,9 @@ export class IpcClient { ...@@ -148,7 +152,9 @@ export class IpcClient {
this.helpStreams = new Map(); this.helpStreams = new Map();
this.mcpConsentHandlers = new Map(); this.mcpConsentHandlers = new Map();
this.agentConsentHandlers = new Map(); this.agentConsentHandlers = new Map();
this.agentTodosHandlers = new Set();
this.telemetryEventHandlers = new Set(); this.telemetryEventHandlers = new Set();
this.globalChatStreamStartHandlers = new Set();
this.globalChatStreamEndHandlers = new Set(); this.globalChatStreamEndHandlers = new Set();
// Set up listeners for stream events // Set up listeners for stream events
this.ipcRenderer.on("chat:response:chunk", (data) => { this.ipcRenderer.on("chat:response:chunk", (data) => {
...@@ -297,6 +303,13 @@ export class IpcClient { ...@@ -297,6 +303,13 @@ export class IpcClient {
if (handler) handler(payload); if (handler) handler(payload);
}); });
// Agent todos update from main
this.ipcRenderer.on("agent-tool:todos-update", (payload) => {
for (const handler of this.agentTodosHandlers) {
handler(payload as unknown as AgentTodosUpdatePayload);
}
});
// Telemetry events from main to renderer // Telemetry events from main to renderer
this.ipcRenderer.on("telemetry:event", (payload) => { this.ipcRenderer.on("telemetry:event", (payload) => {
if (payload && typeof payload === "object" && "eventName" in payload) { if (payload && typeof payload === "object" && "eventName" in payload) {
...@@ -451,6 +464,11 @@ export class IpcClient { ...@@ -451,6 +464,11 @@ export class IpcClient {
} = options; } = options;
this.chatStreams.set(chatId, { onUpdate, onEnd, onError }); this.chatStreams.set(chatId, { onUpdate, onEnd, onError });
// Notify global stream start handlers
for (const handler of this.globalChatStreamStartHandlers) {
handler(chatId);
}
// Handle file attachments if provided // Handle file attachments if provided
if (attachments && attachments.length > 0) { if (attachments && attachments.length > 0) {
// Process each file attachment and convert to base64 // Process each file attachment and convert to base64
...@@ -975,6 +993,31 @@ export class IpcClient { ...@@ -975,6 +993,31 @@ export class IpcClient {
this.ipcRenderer.invoke("agent-tool:consent-response", params); this.ipcRenderer.invoke("agent-tool:consent-response", params);
} }
/**
* Subscribe to agent todos updates from the local agent.
* Called when the agent updates its todo list during a streaming session.
*/
public onAgentTodosUpdate(
handler: (payload: AgentTodosUpdatePayload) => void,
) {
this.agentTodosHandlers.add(handler);
return () => {
this.agentTodosHandlers.delete(handler);
};
}
/**
* Subscribe to be notified when any chat stream starts.
* Useful for cleanup tasks like clearing pending consent requests.
* @returns Unsubscribe function
*/
public onChatStreamStart(handler: (chatId: number) => void): () => void {
this.globalChatStreamStartHandlers.add(handler);
return () => {
this.globalChatStreamStartHandlers.delete(handler);
};
}
/** /**
* Subscribe to be notified when any chat stream ends (either successfully or with an error). * Subscribe to be notified when any chat stream ends (either successfully or with an error).
* Useful for cleanup tasks like clearing pending consent requests. * Useful for cleanup tasks like clearing pending consent requests.
......
...@@ -704,6 +704,21 @@ export interface AgentToolConsentResponseParams { ...@@ -704,6 +704,21 @@ export interface AgentToolConsentResponseParams {
export type AgentToolConsent = "ask" | "always"; export type AgentToolConsent = "ask" | "always";
// ============================================================================
// Agent Todo Types
// ============================================================================
export interface AgentTodo {
id: string;
content: string;
status: "pending" | "in_progress" | "completed";
}
export interface AgentTodosUpdatePayload {
chatId: number;
todos: AgentTodo[];
}
export interface TelemetryEventPayload { export interface TelemetryEventPayload {
eventName: string; eventName: string;
properties?: Record<string, unknown>; properties?: Record<string, unknown>;
......
...@@ -175,6 +175,8 @@ const validReceiveChannels = [ ...@@ -175,6 +175,8 @@ const validReceiveChannels = [
"mcp:tool-consent-request", "mcp:tool-consent-request",
// Agent tool consent request from main to renderer // Agent tool consent request from main to renderer
"agent-tool:consent-request", "agent-tool:consent-request",
// Agent todos update from main to renderer
"agent-tool:todos-update",
// Telemetry events from main to renderer // Telemetry events from main to renderer
"telemetry:event", "telemetry:event",
] as const; ] as const;
......
...@@ -166,6 +166,7 @@ export async function handleLocalAgentStream( ...@@ -166,6 +166,7 @@ export async function handleLocalAgentStream(
supabaseOrganizationSlug: chat.app.supabaseOrganizationSlug, supabaseOrganizationSlug: chat.app.supabaseOrganizationSlug,
messageId: placeholderMessageId, messageId: placeholderMessageId,
isSharedModulesChanged: false, isSharedModulesChanged: false,
todos: [],
onXmlStream: (accumulatedXml: string) => { onXmlStream: (accumulatedXml: string) => {
// Stream accumulated XML to UI without persisting // Stream accumulated XML to UI without persisting
streamingPreview = accumulatedXml; streamingPreview = accumulatedXml;
...@@ -198,6 +199,12 @@ export async function handleLocalAgentStream( ...@@ -198,6 +199,12 @@ export async function handleLocalAgentStream(
appendUserMessage: (content: UserMessageContentPart[]) => { appendUserMessage: (content: UserMessageContentPart[]) => {
pendingUserMessages.push(content); pendingUserMessages.push(content);
}, },
onUpdateTodos: (todos) => {
safeSend(event.sender, "agent-tool:todos-update", {
chatId: chat.id,
todos,
});
},
}; };
// Build tool set (agent tools + MCP tools) // Build tool set (agent tools + MCP tools)
......
...@@ -22,6 +22,7 @@ import { readLogsTool } from "./tools/read_logs"; ...@@ -22,6 +22,7 @@ import { readLogsTool } from "./tools/read_logs";
import { editFileTool } from "./tools/edit_file"; import { editFileTool } from "./tools/edit_file";
import { webSearchTool } from "./tools/web_search"; import { webSearchTool } from "./tools/web_search";
import { webCrawlTool } from "./tools/web_crawl"; import { webCrawlTool } from "./tools/web_crawl";
import { updateTodosTool } from "./tools/update_todos";
import type { LanguageModelV3ToolResultOutput } from "@ai-sdk/provider"; import type { LanguageModelV3ToolResultOutput } from "@ai-sdk/provider";
import { import {
escapeXmlAttr, escapeXmlAttr,
...@@ -51,6 +52,7 @@ export const TOOL_DEFINITIONS: readonly ToolDefinition[] = [ ...@@ -51,6 +52,7 @@ export const TOOL_DEFINITIONS: readonly ToolDefinition[] = [
readLogsTool, readLogsTool,
webSearchTool, webSearchTool,
webCrawlTool, webCrawlTool,
updateTodosTool,
]; ];
// ============================================================================ // ============================================================================
// Agent Tool Name Type (derived from TOOL_DEFINITIONS) // Agent Tool Name Type (derived from TOOL_DEFINITIONS)
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import { z } from "zod"; import { z } from "zod";
import { IpcMainInvokeEvent } from "electron"; import { IpcMainInvokeEvent } from "electron";
import { jsonrepair } from "jsonrepair"; import { jsonrepair } from "jsonrepair";
import { AgentToolConsent } from "@/ipc/ipc_types"; import { AgentToolConsent, AgentTodo } from "@/ipc/ipc_types";
// ============================================================================ // ============================================================================
// XML Escape Helpers // XML Escape Helpers
...@@ -23,6 +23,13 @@ export function escapeXmlContent(str: string): string { ...@@ -23,6 +23,13 @@ export function escapeXmlContent(str: string): string {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
} }
// ============================================================================
// Todo Types
// ============================================================================
// Re-export AgentTodo as Todo for backwards compatibility within this module
export type Todo = AgentTodo;
export interface AgentContext { export interface AgentContext {
event: IpcMainInvokeEvent; event: IpcMainInvokeEvent;
appPath: string; appPath: string;
...@@ -32,6 +39,8 @@ export interface AgentContext { ...@@ -32,6 +39,8 @@ export interface AgentContext {
messageId: number; messageId: number;
isSharedModulesChanged: boolean; isSharedModulesChanged: boolean;
chatSummary?: string; chatSummary?: string;
/** Turn-scoped todo list for agent task tracking */
todos: Todo[];
/** /**
* Streams accumulated XML to UI without persisting to DB (for live preview). * Streams accumulated XML to UI without persisting to DB (for live preview).
* Call this repeatedly with the full accumulated XML so far. * Call this repeatedly with the full accumulated XML so far.
...@@ -53,6 +62,11 @@ export interface AgentContext { ...@@ -53,6 +62,11 @@ export interface AgentContext {
* that models don't support in tool result messages. * that models don't support in tool result messages.
*/ */
appendUserMessage: (content: UserMessageContentPart[]) => void; appendUserMessage: (content: UserMessageContentPart[]) => void;
/**
* Sends updated todos to the renderer for UI display.
* Call this when todos are updated to show them in the chat input area.
*/
onUpdateTodos: (todos: Todo[]) => void;
} }
// ============================================================================ // ============================================================================
......
import { z } from "zod";
import { ToolDefinition, AgentContext, Todo } from "./types";
const todoSchema = z.object({
id: z.string().describe("Unique identifier for the todo item"),
content: z
.string()
.optional()
.describe("The description/content of the todo item"),
status: z
.enum(["pending", "in_progress", "completed"])
.optional()
.describe("The current status of the todo item"),
});
const updateTodosSchema = z.object({
merge: z
.boolean()
.describe(
"Whether to merge the todos with the existing todos. If true, the todos will be merged into the existing todos based on the id field. You can leave unchanged properties undefined. If false, the new todos will replace the existing todos.",
),
todos: z
.array(todoSchema)
.describe(
"Array of todo items. When merge is true, only include todos that need updates. When merge is false, this is the complete list.",
),
});
const DESCRIPTION = `
### When to Use This Tool
Use proactively for:
1. Complex multi-step tasks (3+ distinct steps)
2. Non-trivial tasks requiring careful planning
3. User explicitly requests todo list
4. User provides multiple tasks (numbered/comma-separated)
5. After completing tasks - mark complete with merge=true and add follow-ups
6. When starting new tasks - mark as in_progress (ideally only one at a time)
### When NOT to Use
Skip for:
1. Single, straightforward tasks
2. Trivial tasks with no organizational benefit
3. Tasks completable in < 3 trivial steps
4. Purely conversational/informational requests
5. Todo items should NOT include operational actions done in service of higher-level tasks.
NEVER INCLUDE THESE IN TODOS: linting; testing; searching or examining the codebase.
### Examples
<example>
User: Add dark mode toggle to settings
Assistant:
- *Creates todo list:*
1. Add state management [in_progress]
2. Implement styles
3. Create toggle component
4. Update components
- [Immediately begins working on todo 1 in the same tool call batch]
<reasoning>
Multi-step feature with dependencies.
</reasoning>
</example>
<example>
// User: Implement user registration, product catalog, shopping cart, checkout flow.
Assistant: *Creates todo list breaking down each feature into specific tasks*
<reasoning>
Multiple complex features provided as list requiring organized task management.
</reasoning>
</example>
### Task States and Management
1. **Task States:**
- pending: Not yet started
- in_progress: Currently working on
- completed: Finished successfully
2. **Task Management:**
- Update status in real-time
- Mark complete IMMEDIATELY after finishing
- Only ONE task in_progress at a time
- Complete current tasks before starting new ones
3. **Task Breakdown:**
- Create specific, actionable items
- Break complex tasks into manageable steps
- Use clear, descriptive names
4. **Parallel Todo Writes:**
- Prefer creating the first todo as in_progress
- Start working on todos by using tool calls in the same tool call batch as the todo write
- Batch todo updates with other tool calls for better latency and lower costs for the user
`;
export const updateTodosTool: ToolDefinition<
z.infer<typeof updateTodosSchema>
> = {
name: "update_todos",
description: DESCRIPTION,
inputSchema: updateTodosSchema,
defaultConsent: "always",
getConsentPreview: (args) => {
const count = args.todos.length;
const completed = args.todos.filter((t) => t.status === "completed").length;
return `${completed}/${count} todos completed`;
},
execute: async (args, ctx: AgentContext) => {
if (args.merge) {
// Merge todos based on id
const existingTodosMap = new Map(ctx.todos.map((t) => [t.id, t]));
for (const todo of args.todos) {
const existing = existingTodosMap.get(todo.id);
if (existing) {
// Merge: only update defined properties
existingTodosMap.set(todo.id, {
...existing,
...(todo.content !== undefined && { content: todo.content }),
...(todo.status !== undefined && { status: todo.status }),
});
} else {
// New todo - require all fields
if (todo.content === undefined || todo.status === undefined) {
throw new Error(
`New todo with id "${todo.id}" must have content and status defined`,
);
}
existingTodosMap.set(todo.id, todo as Todo);
}
}
ctx.todos = Array.from(existingTodosMap.values());
} else {
// Replace mode: require all fields
for (const todo of args.todos) {
if (todo.content === undefined || todo.status === undefined) {
throw new Error(
`Todo with id "${todo.id}" must have content and status defined when merge is false`,
);
}
}
ctx.todos = args.todos as Todo[];
}
// Send todos to renderer for UI display
ctx.onUpdateTodos(ctx.todos);
const completed = ctx.todos.filter((t) => t.status === "completed").length;
const inProgressTodos = ctx.todos.filter((t) => t.status === "in_progress");
const pendingTodos = ctx.todos.filter((t) => t.status === "pending");
const outstandingTodos = [...inProgressTodos, ...pendingTodos];
const outstandingList =
outstandingTodos.length > 0
? `\n\nOutstanding todos:\n${outstandingTodos.map((t) => `- [${t.status}] ${t.content}`).join("\n")}`
: "";
return `Updated todos: ${completed} completed, ${inProgressTodos.length} in progress, ${pendingTodos.length} pending${outstandingList}`;
},
};
...@@ -14,7 +14,10 @@ import { ...@@ -14,7 +14,10 @@ import {
import { showError, showMcpConsentToast } from "./lib/toast"; import { showError, showMcpConsentToast } from "./lib/toast";
import { IpcClient } from "./ipc/ipc_client"; import { IpcClient } from "./ipc/ipc_client";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { pendingAgentConsentsAtom } from "./atoms/chatAtoms"; import {
pendingAgentConsentsAtom,
agentTodosByChatIdAtom,
} from "./atoms/chatAtoms";
// @ts-ignore // @ts-ignore
console.log("Running in mode:", import.meta.env.MODE); console.log("Running in mode:", import.meta.env.MODE);
...@@ -129,6 +132,34 @@ function App() { ...@@ -129,6 +132,34 @@ function App() {
// Agent v2 tool consent requests - queue consents instead of overwriting // Agent v2 tool consent requests - queue consents instead of overwriting
const setPendingAgentConsents = useSetAtom(pendingAgentConsentsAtom); const setPendingAgentConsents = useSetAtom(pendingAgentConsentsAtom);
const setAgentTodosByChatId = useSetAtom(agentTodosByChatIdAtom);
// Agent todos updates
useEffect(() => {
const ipc = IpcClient.getInstance();
const unsubscribe = ipc.onAgentTodosUpdate((payload) => {
setAgentTodosByChatId((prev) => {
const next = new Map(prev);
next.set(payload.chatId, payload.todos);
return next;
});
});
return () => unsubscribe();
}, [setAgentTodosByChatId]);
// Clear todos when a new stream starts (so previous turn's todos don't persist)
useEffect(() => {
const ipc = IpcClient.getInstance();
const unsubscribe = ipc.onChatStreamStart((chatId) => {
setAgentTodosByChatId((prev) => {
const next = new Map(prev);
next.delete(chatId);
return next;
});
});
return () => unsubscribe();
}, [setAgentTodosByChatId]);
useEffect(() => { useEffect(() => {
const ipc = IpcClient.getInstance(); const ipc = IpcClient.getInstance();
const unsubscribe = ipc.onAgentToolConsentRequest((payload) => { const unsubscribe = ipc.onAgentToolConsentRequest((payload) => {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论