diff --git a/package.json b/package.json index 14b84b7..a0030e7 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@tauri-apps/plugin-store": "^2", "codemirror": "^6.0.2", "highlight.js": "^11.11.1", + "lucide-svelte": "^0.563.0", "marked": "^17.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 770b6a4..2e0e2cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: highlight.js: specifier: ^11.11.1 version: 11.11.1 + lucide-svelte: + specifier: ^0.563.0 + version: 0.563.0(svelte@5.46.3) marked: specifier: ^17.0.1 version: 17.0.1 @@ -1668,6 +1671,11 @@ packages: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} + lucide-svelte@0.563.0: + resolution: {integrity: sha512-pjZKw7TpQcamfQrx7YdbOHgmrcNeKiGGMD0tKZQaVktwSsbqw28CsKc2Q97ttwjytiCWkJyOa8ij2Q+Og0nPfQ==} + peerDependencies: + svelte: ^3 || ^4 || ^5.0.0-next.42 + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -3650,6 +3658,10 @@ snapshots: lru-cache@11.2.4: {} + lucide-svelte@0.563.0(svelte@5.46.3): + dependencies: + svelte: 5.46.3 + lz-string@1.5.0: {} magic-string@0.30.21: diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index ec4d039..19c6153 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -282,6 +282,21 @@ pub struct AgentEndEvent { pub num_turns: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TodoItem { + pub content: String, + pub status: String, // "pending", "in_progress", or "completed" + #[serde(rename = "activeForm")] + pub active_form: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TodoUpdateEvent { + pub todos: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub conversation_id: Option, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index ebac570..9294fff 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -16,8 +16,8 @@ use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats}; use crate::types::{ AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent, ConnectionStatus, ContentBlock, MessageCost, OutputEvent, PermissionPromptEvent, - PermissionPromptEventItem, QuestionOption, SessionEvent, StateChangeEvent, - UserQuestionEvent, WorkingDirectoryEvent, + PermissionPromptEventItem, QuestionOption, SessionEvent, StateChangeEvent, TodoItem, + TodoUpdateEvent, UserQuestionEvent, WorkingDirectoryEvent, }; use parking_lot::RwLock; use std::cell::RefCell; @@ -937,6 +937,34 @@ fn process_json_line( ); } + // Emit todo-update event for TodoWrite tool invocations + if name == "TodoWrite" { + if let Some(todos_value) = input.get("todos") { + if let Some(todos_array) = todos_value.as_array() { + let todos: Vec = todos_array + .iter() + .filter_map(|todo| { + serde_json::from_value(todo.clone()).ok() + }) + .collect(); + + tracing::debug!( + "Emitting todo-update: {} todos, parent={:?}", + todos.len(), + parent_tool_use_id + ); + + let _ = app.emit( + "claude:todo-update", + TodoUpdateEvent { + todos, + conversation_id: conversation_id.clone(), + }, + ); + } + } + } + let desc = format_tool_description(name, input); let _ = app.emit( "claude:output", diff --git a/src/lib/components/StatusBar.svelte b/src/lib/components/StatusBar.svelte index aef4f74..50f3366 100644 --- a/src/lib/components/StatusBar.svelte +++ b/src/lib/components/StatusBar.svelte @@ -23,6 +23,7 @@ import { achievementProgress } from "$lib/stores/achievements"; import { runningAgentCount } from "$lib/stores/agents"; import SessionHistoryPanel from "./SessionHistoryPanel.svelte"; + import TodoPanel from "./TodoPanel.svelte"; import GitPanel from "./GitPanel.svelte"; import ProfilePanel from "./ProfilePanel.svelte"; import AgentMonitorPanel from "./AgentMonitorPanel.svelte"; @@ -49,6 +50,7 @@ let showHelp = $state(false); let showKeyboardShortcuts = $state(false); let showSessionHistory = $state(false); + let showTodoPanel = $state(false); let showGitPanel = $state(false); let showProfile = $state(false); let showAgentMonitor = $state(false); @@ -438,6 +440,20 @@ /> + + + + +
+ {#if !hasTodos} +
+ + + +

No active todos

+

I'll update this when I start working on tasks!

+
+ {:else} +
+ {#each currentTodos as todo (todo.content)} +
+
+ +
+ {#if todo.status === "completed"} + + {:else if todo.status === "in_progress"} + + {:else} + + {/if} +
+ + +
+

+ {todo.status === "in_progress" ? todo.activeForm : todo.content} +

+ + +
+ {#if todo.status === "completed"} + + ✓ Completed + + {:else if todo.status === "in_progress"} + + ⚡ In Progress + + {:else} + + ○ Pending + + {/if} +
+
+
+
+ {/each} +
+ {/if} +
+ + + {#if hasTodos} +
+
+ Progress + + {Math.round((completedCount / totalCount) * 100)}% + +
+
+
+
+
+ {/if} + + + diff --git a/src/lib/stores/todos.ts b/src/lib/stores/todos.ts new file mode 100644 index 0000000..2869e67 --- /dev/null +++ b/src/lib/stores/todos.ts @@ -0,0 +1,44 @@ +import { writable } from "svelte/store"; +import { listen } from "@tauri-apps/api/event"; + +export interface TodoItem { + content: string; + status: "pending" | "in_progress" | "completed"; + activeForm: string; +} + +interface TodoUpdatePayload { + todos: TodoItem[]; + conversation_id?: string; +} + +// Create the writable store +const { subscribe, set, update } = writable([]); + +// Listen for todo updates from the backend +let unlisten: (() => void) | undefined; + +export async function initializeTodoListener(): Promise { + if (unlisten) { + return; // Already initialized + } + + unlisten = await listen("claude:todo-update", (event) => { + set(event.payload.todos); + }); +} + +export function cleanupTodoListener(): void { + if (unlisten) { + unlisten(); + unlisten = undefined; + } +} + +// Export the store +export const todos = { + subscribe, + set, + update, + clear: () => set([]), +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7cab3e8..b8a1569 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -35,6 +35,7 @@ import CloseAppConfirmModal from "$lib/components/CloseAppConfirmModal.svelte"; import MemoryBrowserPanel from "$lib/components/MemoryBrowserPanel.svelte"; import { debugConsoleStore } from "$lib/stores/debugConsole"; + import { initializeTodoListener, cleanupTodoListener } from "$lib/stores/todos"; let initialized = false; let updateNotification: UpdateNotification | undefined = $state(undefined); @@ -445,6 +446,9 @@ // Initialize Discord RPC await initializeDiscordRpc(); + // Initialize todo listener + await initializeTodoListener(); + // Listen for window close requests const unlisten = await listen("window-close-requested", () => { handleCloseRequest(); @@ -461,6 +465,7 @@ if (initialized) { cleanupTauriListeners(); cleanupNotificationSync(); + cleanupTodoListener(); stopDiscordRpc(); window.removeEventListener("keydown", handleGlobalKeydown); initialized = false;