From af1625a1327182b3227f2f1a751c50d04814dd50 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Feb 2026 18:38:15 +0000 Subject: [PATCH] story-kit: merge 86_story_show_live_activity_status_instead_of_static_thinking_indicator_in_chat --- frontend/src/api/client.ts | 7 ++++++- frontend/src/components/Chat.tsx | 30 ++++++++++++++++++++++++++- server/src/http/ws.rs | 10 +++++++++ server/src/llm/chat.rs | 6 +++++- server/src/llm/providers/anthropic.rs | 5 ++++- 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index c951caa..89fa946 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -50,7 +50,8 @@ export type WsResponse = request_id: string; tool_name: string; tool_input: Record; - }; + } + | { type: "tool_activity"; tool_name: string }; export interface ProviderConfig { provider: string; @@ -260,6 +261,7 @@ export class ChatWebSocket { toolName: string, toolInput: Record, ) => void; + private onActivity?: (toolName: string) => void; private connected = false; private closeTimer?: number; private wsPath = DEFAULT_WS_PATH; @@ -302,6 +304,7 @@ export class ChatWebSocket { data.tool_name, data.tool_input, ); + if (data.type === "tool_activity") this.onActivity?.(data.tool_name); } catch (err) { this.onError?.(String(err)); } @@ -341,6 +344,7 @@ export class ChatWebSocket { toolName: string, toolInput: Record, ) => void; + onActivity?: (toolName: string) => void; }, wsPath = DEFAULT_WS_PATH, ) { @@ -350,6 +354,7 @@ export class ChatWebSocket { this.onError = handlers.onError; this.onPipelineState = handlers.onPipelineState; this.onPermissionRequest = handlers.onPermissionRequest; + this.onActivity = handlers.onActivity; this.wsPath = wsPath; this.shouldReconnect = true; diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx index 12d0b9e..b51105d 100644 --- a/frontend/src/components/Chat.tsx +++ b/frontend/src/components/Chat.tsx @@ -14,6 +14,23 @@ const { useCallback, useEffect, useRef, useState } = React; const NARROW_BREAKPOINT = 900; +function formatToolActivity(toolName: string): string { + switch (toolName) { + case "read_file": + return "Reading file..."; + case "write_file": + return "Writing file..."; + case "list_directory": + return "Listing directory..."; + case "search_files": + return "Searching files..."; + case "exec_shell": + return "Executing command..."; + default: + return `Using ${toolName}...`; + } +} + interface ChatProps { projectPath: string; onCloseProject: () => void; @@ -38,6 +55,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { merge: [], }); const [claudeSessionId, setClaudeSessionId] = useState(null); + const [activityStatus, setActivityStatus] = useState(null); const [permissionRequest, setPermissionRequest] = useState<{ requestId: string; toolName: string; @@ -159,6 +177,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { const last = history[history.length - 1]; if (last?.role === "assistant" && !last.tool_calls) { setLoading(false); + setActivityStatus(null); } }, onSessionId: (sessionId) => { @@ -167,6 +186,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { onError: (message) => { console.error("WebSocket error:", message); setLoading(false); + setActivityStatus(null); }, onPipelineState: (state) => { setPipeline(state); @@ -174,6 +194,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { onPermissionRequest: (requestId, toolName, toolInput) => { setPermissionRequest({ requestId, toolName, toolInput }); }, + onActivity: (toolName) => { + setActivityStatus(formatToolActivity(toolName)); + }, }); return () => { @@ -248,6 +271,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { } setLoading(false); + setActivityStatus(null); } catch (e) { console.error("Failed to cancel chat:", e); } @@ -276,6 +300,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { } setLoading(true); setStreamingContent(""); + setActivityStatus(null); try { const provider = isClaudeCode @@ -352,6 +377,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { setMessages([]); setStreamingContent(""); setLoading(false); + setActivityStatus(null); setClaudeSessionId(null); } }; @@ -644,7 +670,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { marginTop: "10px", }} > - Thinking... + + {activityStatus ?? "Thinking..."} + )}
diff --git a/server/src/http/ws.rs b/server/src/http/ws.rs index a0f6004..72fca4b 100644 --- a/server/src/http/ws.rs +++ b/server/src/http/ws.rs @@ -75,6 +75,10 @@ enum WsResponse { tool_name: String, tool_input: serde_json::Value, }, + /// The agent started assembling a tool call; shows live status in the UI. + ToolActivity { + tool_name: String, + }, } impl From for WsResponse { @@ -168,6 +172,7 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc>) -> impl poem Ok(WsRequest::Chat { messages, config }) => { let tx_updates = tx.clone(); let tx_tokens = tx.clone(); + let tx_activity = tx.clone(); let ctx_clone = ctx.clone(); let perm_tx = perm_req_tx.clone(); @@ -188,6 +193,11 @@ pub async fn ws_handler(ws: WebSocket, ctx: Data<&Arc>) -> impl poem content: token.to_string(), }); }, + move |tool_name: &str| { + let _ = tx_activity.send(WsResponse::ToolActivity { + tool_name: tool_name.to_string(), + }); + }, Some(perm_tx), ); tokio::pin!(chat_fut); diff --git a/server/src/llm/chat.rs b/server/src/llm/chat.rs index a32f0bd..017bdcb 100644 --- a/server/src/llm/chat.rs +++ b/server/src/llm/chat.rs @@ -176,13 +176,15 @@ pub fn set_anthropic_api_key(store: &dyn StoreOps, api_key: String) -> Result<() set_anthropic_api_key_impl(store, &api_key) } -pub async fn chat( +#[allow(clippy::too_many_arguments)] +pub async fn chat( messages: Vec, config: ProviderConfig, state: &SessionState, store: &dyn StoreOps, mut on_update: F, mut on_token: U, + mut on_activity: A, permission_tx: Option< tokio::sync::mpsc::UnboundedSender< crate::llm::providers::claude_code::PermissionReqMsg, @@ -192,6 +194,7 @@ pub async fn chat( where F: FnMut(&[Message]) + Send, U: FnMut(&str) + Send, + A: FnMut(&str) + Send, { use crate::llm::providers::anthropic::AnthropicProvider; use crate::llm::providers::ollama::OllamaProvider; @@ -322,6 +325,7 @@ where tools, &mut cancel_rx, |token| on_token(token), + |tool_name| on_activity(tool_name), ) .await .map_err(|e| format!("Anthropic Error: {e}"))? diff --git a/server/src/llm/providers/anthropic.rs b/server/src/llm/providers/anthropic.rs index 64a6bab..abd9ee1 100644 --- a/server/src/llm/providers/anthropic.rs +++ b/server/src/llm/providers/anthropic.rs @@ -156,16 +156,18 @@ impl AnthropicProvider { .join("\n\n") } - pub async fn chat_stream( + pub async fn chat_stream( &self, model: &str, messages: &[Message], tools: &[ToolDefinition], cancel_rx: &mut Receiver, mut on_token: F, + mut on_activity: A, ) -> Result where F: FnMut(&str), + A: FnMut(&str), { let anthropic_messages = Self::convert_messages(messages); let anthropic_tools = Self::convert_tools(tools); @@ -257,6 +259,7 @@ impl AnthropicProvider { { let id = content_block["id"].as_str().unwrap_or("").to_string(); let name = content_block["name"].as_str().unwrap_or("").to_string(); + on_activity(&name); current_tool_use = Some((id, name, String::new())); } }