From ba8650cfca3855f963179b197e4b1101d57dd0f2 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Fri, 15 Aug 2025 10:25:06 +0100 Subject: [PATCH] feat: pin todo list (#464) * wip: backend todo normalisation * fe implementation * remove unused dep * cursor return ActionType::TodoManagement * use lucide icons rather than emojis in the todo list * review comments --- .gitignore | 2 +- crates/executors/src/executors/amp.rs | 56 +++++++------ crates/executors/src/executors/claude.rs | 75 +++-------------- crates/executors/src/executors/cursor.rs | 60 +++++--------- crates/executors/src/executors/opencode.rs | 42 ++++------ crates/executors/src/logs/mod.rs | 47 +++++++++-- crates/server/src/bin/generate_types.rs | 1 + frontend/package.json | 2 +- .../DisplayConversationEntry.tsx | 28 ++++--- frontend/src/components/PinnedTodoBox.tsx | 81 +++++++++++++++++++ .../components/tasks/TaskDetails/LogsTab.tsx | 34 +++++--- frontend/src/hooks/usePinnedTodos.ts | 57 +++++++++++++ pnpm-lock.yaml | 12 +-- shared/types.ts | 4 +- 14 files changed, 305 insertions(+), 196 deletions(-) create mode 100644 frontend/src/components/PinnedTodoBox.tsx create mode 100644 frontend/src/hooks/usePinnedTodos.ts diff --git a/.gitignore b/.gitignore index 271602e7..abd99ab9 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,7 @@ coverage/ .env frontend/dist -backend/bindings +crates/executors/bindings build-npm-package-codesign.sh diff --git a/crates/executors/src/executors/amp.rs b/crates/executors/src/executors/amp.rs index e86387ac..b487b105 100644 --- a/crates/executors/src/executors/amp.rs +++ b/crates/executors/src/executors/amp.rs @@ -13,7 +13,7 @@ use crate::{ command::CommandBuilder, executors::{ExecutorError, StandardCodingAgentExecutor}, logs::{ - ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, + ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem as LogsTodoItem, stderr_processor::normalize_stderr_logs, utils::{EntryIndexProvider, patch::ConversationPatch}, }, @@ -499,8 +499,21 @@ impl AmpContentItem { AmpToolData::List { .. } => ActionType::Other { description: "List directory".to_string(), }, - AmpToolData::Todo { .. } => ActionType::Other { - description: "Manage TODO list".to_string(), + AmpToolData::Todo { todos } => ActionType::TodoManagement { + todos: todos + .as_ref() + .map(|todos| { + todos + .iter() + .map(|t| LogsTodoItem { + content: t.content.clone(), + status: t.status.clone(), + priority: t.priority.clone(), + }) + .collect() + }) + .unwrap_or_default(), + operation: "write".to_string(), }, AmpToolData::Unknown { .. } => ActionType::Other { description: format!("Tool: {tool_name}"), @@ -522,32 +535,10 @@ impl AmpContentItem { ActionType::WebFetch { url } => format!("`{url}`"), ActionType::PlanPresentation { plan } => format!("Plan Presentation: `{plan}`"), ActionType::TaskCreate { description } => description.clone(), + ActionType::TodoManagement { .. } => "TODO list updated".to_string(), ActionType::Other { description: _ } => { // For other tools, try to extract key information or fall back to tool name match input { - AmpToolData::Todo { todos, .. } => { - if let Some(todos) = todos { - let mut todo_items = Vec::new(); - for todo in todos { - let emoji = match todo.status.as_str() { - "completed" => "✅", - "in_progress" | "in-progress" => "🔄", - "pending" | "todo" => "⏳", - _ => "📝", - }; - let priority = todo.priority.as_deref().unwrap_or("medium"); - todo_items - .push(format!("{} {} ({})", emoji, todo.content, priority)); - } - if !todo_items.is_empty() { - format!("TODO List:\n{}", todo_items.join("\n")) - } else { - "Managing TODO list".to_string() - } - } else { - "Managing TODO list".to_string() - } - } AmpToolData::List { path, .. } => { if let Some(path) = path { let relative_path = make_path_relative(path, worktree_path); @@ -584,6 +575,19 @@ impl AmpContentItem { } parts.join(" ") } + AmpToolData::Unknown { data } => { + // Manually check if "name" is prefixed with "todo" + // This is a hack to avoid flickering on the frontend + let name = data + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or(tool_name); + if name.starts_with("todo") { + "TODO list updated".to_string() + } else { + tool_name.to_string() + } + } _ => tool_name.to_string(), } } diff --git a/crates/executors/src/executors/claude.rs b/crates/executors/src/executors/claude.rs index 14fefae4..4ba81be9 100644 --- a/crates/executors/src/executors/claude.rs +++ b/crates/executors/src/executors/claude.rs @@ -14,7 +14,7 @@ use crate::{ command::CommandBuilder, executors::{ExecutorError, StandardCodingAgentExecutor}, logs::{ - ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, + ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem, stderr_processor::normalize_stderr_logs, utils::{EntryIndexProvider, patch::ConversationPatch}, }, @@ -505,8 +505,16 @@ impl ClaudeLogProcessor { path: make_path_relative(notebook_path, worktree_path), diffs: vec![], }, - ClaudeToolData::TodoWrite { .. } => ActionType::Other { - description: "Manage TODO list".to_string(), + ClaudeToolData::TodoWrite { todos } => ActionType::TodoManagement { + todos: todos + .iter() + .map(|t| TodoItem { + content: t.content.clone(), + status: t.status.clone(), + priority: t.priority.clone(), + }) + .collect(), + operation: "write".to_string(), }, ClaudeToolData::Glob { pattern, path: _ } => ActionType::Search { query: pattern.clone(), @@ -534,26 +542,8 @@ impl ClaudeLogProcessor { ActionType::WebFetch { url } => format!("`{url}`"), ActionType::TaskCreate { description } => description.clone(), ActionType::PlanPresentation { plan } => plan.clone(), + ActionType::TodoManagement { .. } => "TODO list updated".to_string(), ActionType::Other { description: _ } => match tool_data { - ClaudeToolData::TodoWrite { todos } => { - let mut todo_items = Vec::new(); - for todo in todos { - let status_emoji = match todo.status.as_str() { - "completed" => "✅", - "in_progress" => "🔄", - "pending" | "todo" => "⏳", - _ => "📝", - }; - let priority = todo.priority.as_deref().unwrap_or("medium"); - todo_items - .push(format!("{} {} ({})", status_emoji, todo.content, priority)); - } - if !todo_items.is_empty() { - format!("TODO List:\n{}", todo_items.join("\n")) - } else { - "Managing TODO list".to_string() - } - } ClaudeToolData::LS { path } => { let relative_path = make_path_relative(path, worktree_path); if relative_path.is_empty() { @@ -843,45 +833,6 @@ mod tests { assert_eq!(entries[0].content, "Let me think about this..."); } - #[test] - fn test_todo_tool_content_extraction() { - // Test TodoWrite with actual todo list - let todo_data = ClaudeToolData::TodoWrite { - todos: vec![ - ClaudeTodoItem { - id: Some("1".to_string()), - content: "Fix the navigation bug".to_string(), - status: "completed".to_string(), - priority: Some("high".to_string()), - }, - ClaudeTodoItem { - id: Some("2".to_string()), - content: "Add user authentication".to_string(), - status: "in_progress".to_string(), - priority: Some("medium".to_string()), - }, - ClaudeTodoItem { - id: Some("3".to_string()), - content: "Write documentation".to_string(), - status: "pending".to_string(), - priority: Some("low".to_string()), - }, - ], - }; - - let action_type = ClaudeLogProcessor::extract_action_type(&todo_data, "/tmp/test-worktree"); - let result = ClaudeLogProcessor::generate_concise_content( - &todo_data, - &action_type, - "/tmp/test-worktree", - ); - - assert!(result.contains("TODO List:")); - assert!(result.contains("✅ Fix the navigation bug (high)")); - assert!(result.contains("🔄 Add user authentication (medium)")); - assert!(result.contains("⏳ Write documentation (low)")); - } - #[test] fn test_todo_tool_empty_list() { // Test TodoWrite with empty todo list @@ -895,7 +846,7 @@ mod tests { "/tmp/test-worktree", ); - assert_eq!(result, "Managing TODO list"); + assert_eq!(result, "TODO list updated"); } #[test] diff --git a/crates/executors/src/executors/cursor.rs b/crates/executors/src/executors/cursor.rs index ef551690..19ec6fbe 100644 --- a/crates/executors/src/executors/cursor.rs +++ b/crates/executors/src/executors/cursor.rs @@ -13,7 +13,7 @@ use crate::{ command::CommandBuilder, executors::{ExecutorError, StandardCodingAgentExecutor}, logs::{ - ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, + ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem, plain_text_processor::PlainTextLogProcessor, utils::{ConversationPatch, EntryIndexProvider}, }, @@ -579,16 +579,27 @@ impl CursorToolCall { ) } CursorToolCall::Todo { args, .. } => { - let content = if let Some(todos) = args.todos.as_ref() { - format!("TODO List:\n{}", summarize_todos_typed(todos)) - } else { - "Managing TODO list".to_string() - }; + let todos = args + .todos + .as_ref() + .map(|todos| { + todos + .iter() + .map(|t| TodoItem { + content: t.content.clone(), + status: t.status.clone(), + priority: None, // CursorTodoItem doesn't have priority field + }) + .collect() + }) + .unwrap_or_default(); + ( - ActionType::Other { - description: "Manage TODO list".to_string(), + ActionType::TodoManagement { + todos, + operation: "write".to_string(), }, - content, + "TODO list updated".to_string(), ) } CursorToolCall::Unknown { .. } => ( @@ -735,37 +746,6 @@ pub struct CursorTodoItem { pub dependencies: Option>, } -/* =========================== -Helpers -=========================== */ - -fn summarize_todos_typed(items: &[CursorTodoItem]) -> String { - let mut out: Vec = Vec::new(); - for todo in items { - let content = todo.content.as_str(); - let status = todo.status.as_str(); - let emoji = if status.eq_ignore_ascii_case("completed") - || status.contains("COMPLETED") - || status.eq_ignore_ascii_case("done") - { - "✅" - } else if status.eq_ignore_ascii_case("in_progress") - || status.eq_ignore_ascii_case("in-progress") - || status.contains("IN_PROGRESS") - { - "🔄" - } else { - "⏳" - }; - out.push(format!("{emoji} {content}")); - } - if out.is_empty() { - "Managing TODO list".to_string() - } else { - out.join("\n") - } -} - /* =========================== Tests =========================== */ diff --git a/crates/executors/src/executors/opencode.rs b/crates/executors/src/executors/opencode.rs index b696c1ef..70425bc4 100644 --- a/crates/executors/src/executors/opencode.rs +++ b/crates/executors/src/executors/opencode.rs @@ -15,7 +15,7 @@ use crate::{ command::CommandBuilder, executors::{ExecutorError, StandardCodingAgentExecutor}, logs::{ - ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, + ActionType, EditDiff, NormalizedEntry, NormalizedEntryType, TodoItem, plain_text_processor::{MessageBoundary, PlainTextLogProcessor}, utils::EntryIndexProvider, }, @@ -724,8 +724,20 @@ impl ToolUtils { Tool::WebFetch { url, .. } => ActionType::Other { description: format!("Web fetch: {url}"), }, - Tool::TodoWrite { .. } | Tool::TodoRead => ActionType::Other { - description: "TODO list management".to_string(), + Tool::TodoWrite { todos } => ActionType::TodoManagement { + todos: todos + .iter() + .map(|t| TodoItem { + content: t.content.clone(), + status: t.status.clone(), + priority: t.priority.clone(), + }) + .collect(), + operation: "write".to_string(), + }, + Tool::TodoRead => ActionType::TodoManagement { + todos: vec![], + operation: "read".to_string(), }, Tool::Task { description } => ActionType::Other { description: format!("Task: {description}"), @@ -787,11 +799,11 @@ impl ToolUtils { Tool::WebFetch { url, .. } => { format!("fetch `{url}`") } - Tool::TodoWrite { todos } => Self::generate_todo_content(todos), - Tool::TodoRead => "Managing TODO list".to_string(), Tool::Task { description } => { format!("Task: `{description}`") } + Tool::TodoWrite { .. } => "TODO list updated".to_string(), + Tool::TodoRead => "TODO list read".to_string(), Tool::Other { tool_name, .. } => { // Handle MCP tools (format: client_name_tool_name) if tool_name.contains('_') { @@ -802,26 +814,6 @@ impl ToolUtils { } } } - - /// Generate formatted content for TODO tools from TodoInfo struct - fn generate_todo_content(todos: &[TodoInfo]) -> String { - if todos.is_empty() { - return "Managing TODO list".to_string(); - } - - let mut todo_items = Vec::new(); - for todo in todos { - let status_emoji = match todo.status.as_str() { - "completed" => "✅", - "in_progress" => "🔄", - "pending" | "todo" => "⏳", - _ => "📝", - }; - let priority = todo.priority.as_deref().unwrap_or("medium"); - todo_items.push(format!("{} {} ({})", status_emoji, todo.content, priority)); - } - format!("TODO List:\n{}", todo_items.join("\n")) - } } // ============================================================================= diff --git a/crates/executors/src/logs/mod.rs b/crates/executors/src/logs/mod.rs index 5223e118..cd8604f3 100644 --- a/crates/executors/src/logs/mod.rs +++ b/crates/executors/src/logs/mod.rs @@ -45,16 +45,47 @@ pub struct NormalizedEntry { pub metadata: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct TodoItem { + pub content: String, + pub status: String, + #[serde(default)] + pub priority: Option, +} + /// Types of tool actions that can be performed #[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] #[serde(tag = "action", rename_all = "snake_case")] pub enum ActionType { - FileRead { path: String }, - FileEdit { path: String, diffs: Vec }, - CommandRun { command: String }, - Search { query: String }, - WebFetch { url: String }, - TaskCreate { description: String }, - PlanPresentation { plan: String }, - Other { description: String }, + FileRead { + path: String, + }, + FileEdit { + path: String, + diffs: Vec, + }, + CommandRun { + command: String, + }, + Search { + query: String, + }, + WebFetch { + url: String, + }, + TaskCreate { + description: String, + }, + PlanPresentation { + plan: String, + }, + TodoManagement { + todos: Vec, + operation: String, + }, + Other { + description: String, + }, } diff --git a/crates/server/src/bin/generate_types.rs b/crates/server/src/bin/generate_types.rs index 38066892..d2bfdbfa 100644 --- a/crates/server/src/bin/generate_types.rs +++ b/crates/server/src/bin/generate_types.rs @@ -82,6 +82,7 @@ fn generate_types_content() -> String { executors::logs::NormalizedEntryType::decl(), executors::logs::EditDiff::decl(), executors::logs::ActionType::decl(), + executors::logs::TodoItem::decl(), executors::logs::utils::patch::PatchType::decl(), serde_json::Value::decl(), ]; diff --git a/frontend/package.json b/frontend/package.json index 9e1746d5..2b6c8257 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,7 +35,7 @@ "click-to-react-component": "^1.1.2", "clsx": "^2.0.0", "diff": "^8.0.2", - "lucide-react": "^0.303.0", + "lucide-react": "^0.539.0", "react": "^18.2.0", "react-diff-viewer-continued": "^3.4.0", "react-dom": "^18.2.0", diff --git a/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx b/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx index 5ef183f1..7923af03 100644 --- a/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx +++ b/frontend/src/components/NormalizedConversation/DisplayConversationEntry.tsx @@ -45,12 +45,13 @@ const getEntryIcon = (entryType: NormalizedEntryType) => { // Special handling for TODO tools if ( - tool_name && - (tool_name.toLowerCase() === 'todowrite' || - tool_name.toLowerCase() === 'todoread' || - tool_name.toLowerCase() === 'todo_write' || - tool_name.toLowerCase() === 'todo_read' || - tool_name.toLowerCase() === 'todo') + action_type.action === 'todo_management' || + (tool_name && + (tool_name.toLowerCase() === 'todowrite' || + tool_name.toLowerCase() === 'todoread' || + tool_name.toLowerCase() === 'todo_write' || + tool_name.toLowerCase() === 'todo_read' || + tool_name.toLowerCase() === 'todo')) ) { return ; } @@ -92,14 +93,15 @@ const getContentClassName = (entryType: NormalizedEntryType) => { // Special styling for TODO lists if ( entryType.type === 'tool_use' && - entryType.tool_name && - (entryType.tool_name.toLowerCase() === 'todowrite' || - entryType.tool_name.toLowerCase() === 'todoread' || - entryType.tool_name.toLowerCase() === 'todo_write' || - entryType.tool_name.toLowerCase() === 'todo_read' || - entryType.tool_name.toLowerCase() === 'todo') + (entryType.action_type.action === 'todo_management' || + (entryType.tool_name && + (entryType.tool_name.toLowerCase() === 'todowrite' || + entryType.tool_name.toLowerCase() === 'todoread' || + entryType.tool_name.toLowerCase() === 'todo_write' || + entryType.tool_name.toLowerCase() === 'todo_read' || + entryType.tool_name.toLowerCase() === 'todo'))) ) { - return `${baseClasses} font-mono text-purple-700 dark:text-purple-300 bg-purple-50 dark:bg-purple-950/20 px-2 py-1 rounded`; + return `${baseClasses} font-mono text-zinc-800 dark:text-zinc-200 bg-zinc-50 dark:bg-zinc-900/40 px-2 py-1 rounded`; } // Special styling for plan presentations diff --git a/frontend/src/components/PinnedTodoBox.tsx b/frontend/src/components/PinnedTodoBox.tsx new file mode 100644 index 00000000..f28dba97 --- /dev/null +++ b/frontend/src/components/PinnedTodoBox.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { + ChevronDown, + ChevronUp, + CheckSquare, + Circle, + CircleCheck, + CircleDotDashed, +} from 'lucide-react'; +import type { TodoItem } from 'shared/types'; + +interface PinnedTodoBoxProps { + todos: TodoItem[]; + lastUpdated: string | null; +} + +const getStatusIcon = (status: string): React.ReactNode => { + switch (status.toLowerCase()) { + case 'completed': + return ; + case 'in_progress': + case 'in-progress': + return ; + case 'pending': + case 'todo': + return ; + default: + return ; + } +}; + +export const PinnedTodoBox: React.FC = ({ todos }) => { + const [isCollapsed, setIsCollapsed] = useState(false); + + if (todos.length === 0) return null; + + return ( +
+
setIsCollapsed(!isCollapsed)} + > +
+ + + TODOs + +
+
+ {isCollapsed ? ( + + ) : ( + + )} +
+
+ + {!isCollapsed && ( +
+
+ {todos.map((todo, index) => ( +
+ + {getStatusIcon(todo.status)} + +
+ + {todo.content} + +
+
+ ))} +
+
+ )} +
+ ); +}; diff --git a/frontend/src/components/tasks/TaskDetails/LogsTab.tsx b/frontend/src/components/tasks/TaskDetails/LogsTab.tsx index be899360..cb2ca870 100644 --- a/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +++ b/frontend/src/components/tasks/TaskDetails/LogsTab.tsx @@ -13,7 +13,9 @@ import { TaskSelectedAttemptContext, } from '@/components/context/taskDetailsContext.ts'; import { useProcessesLogs } from '@/hooks/useProcessesLogs'; +import { usePinnedTodos } from '@/hooks/usePinnedTodos'; import LogEntryRow from '@/components/logs/LogEntryRow'; +import { PinnedTodoBox } from '@/components/PinnedTodoBox'; import { shouldShowInLogs, isAutoCollapsibleProcess, @@ -137,6 +139,9 @@ function LogsTab() { const { entries } = useProcessesLogs(filteredProcesses, true); + // Extract todos from entries using the usePinnedTodos hook + const { todos, lastUpdated } = usePinnedTodos(entries); + // Combined collapsed processes (auto + user) const allCollapsedProcesses = useMemo(() => { const combined = new Set(state.autoCollapsed); @@ -276,19 +281,22 @@ function LogsTab() { } return ( -
-
, - }} - /> +
+ +
+
, + }} + /> +
); } diff --git a/frontend/src/hooks/usePinnedTodos.ts b/frontend/src/hooks/usePinnedTodos.ts new file mode 100644 index 00000000..518a1b8f --- /dev/null +++ b/frontend/src/hooks/usePinnedTodos.ts @@ -0,0 +1,57 @@ +import { useMemo } from 'react'; +import type { TodoItem } from 'shared/types'; + +interface UsePinnedTodosResult { + todos: TodoItem[]; + lastUpdated: string | null; +} + +/** + * Hook that extracts and maintains the latest TODO state from normalized conversation entries. + * Filters for TodoManagement ActionType entries and returns the most recent todo list. + */ +export const usePinnedTodos = (entries: any[]): UsePinnedTodosResult => { + return useMemo(() => { + let latestTodos: TodoItem[] = []; + let lastUpdatedTime: string | null = null; + + for (const entry of entries) { + if (entry.channel === 'normalized' && entry.payload) { + const normalizedEntry = entry.payload as any; + + if ( + normalizedEntry.entry_type?.type === 'tool_use' && + normalizedEntry.entry_type?.action_type?.action === 'todo_management' + ) { + const actionType = normalizedEntry.entry_type.action_type; + const partialTodos = actionType.todos || []; + const currentTimestamp = + normalizedEntry.timestamp || new Date().toISOString(); + + // Only update latestTodos if we have meaningful content OR this is our first entry + const hasMeaningfulTodos = + partialTodos.length > 0 && + partialTodos.every( + (todo: TodoItem) => + todo.content && todo.content.trim().length > 0 && todo.status + ); + const isNewerThanLatest = + !lastUpdatedTime || currentTimestamp >= lastUpdatedTime; + + if ( + hasMeaningfulTodos || + (isNewerThanLatest && latestTodos.length === 0) + ) { + latestTodos = partialTodos; + lastUpdatedTime = currentTimestamp; + } + } + } + } + + return { + todos: latestTodos, + lastUpdated: lastUpdatedTime, + }; + }, [entries]); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d948cf9..90ec0dc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,8 +81,8 @@ importers: specifier: ^8.0.2 version: 8.0.2 lucide-react: - specifier: ^0.303.0 - version: 0.303.0(react@18.3.1) + specifier: ^0.539.0 + version: 0.539.0(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 @@ -2070,10 +2070,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.303.0: - resolution: {integrity: sha512-B0B9T3dLEFBYPCUlnUS1mvAhW1craSbF9HO+JfBjAtpFUJ7gMIqmEwNSclikY3RiN2OnCkj/V1ReAQpaHae8Bg==} + lucide-react@0.539.0: + resolution: {integrity: sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==} peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 magic-string@0.30.8: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} @@ -4797,7 +4797,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.303.0(react@18.3.1): + lucide-react@0.539.0(react@18.3.1): dependencies: react: 18.3.1 diff --git a/shared/types.ts b/shared/types.ts index 508d27cd..fd6d1945 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -176,7 +176,9 @@ export type NormalizedEntryType = { "type": "user_message" } | { "type": "assist export type EditDiff = { "format": "unified", unified_diff: string, } | { "format": "replace", old: string, new: string, }; -export type ActionType = { "action": "file_read", path: string, } | { "action": "file_edit", path: string, diffs: Array, } | { "action": "command_run", command: string, } | { "action": "search", query: string, } | { "action": "web_fetch", url: string, } | { "action": "task_create", description: string, } | { "action": "plan_presentation", plan: string, } | { "action": "other", description: string, }; +export type ActionType = { "action": "file_read", path: string, } | { "action": "file_edit", path: string, diffs: Array, } | { "action": "command_run", command: string, } | { "action": "search", query: string, } | { "action": "web_fetch", url: string, } | { "action": "task_create", description: string, } | { "action": "plan_presentation", plan: string, } | { "action": "todo_management", todos: Array, operation: string, } | { "action": "other", description: string, }; + +export type TodoItem = { content: string, status: string, priority: string | null, }; export type PatchType = { "type": "NORMALIZED_ENTRY", "content": NormalizedEntry } | { "type": "STDOUT", "content": string } | { "type": "STDERR", "content": string } | { "type": "DIFF", "content": Diff };