diff --git a/src/main/index.ts b/src/main/index.ts index 3f97b136dd..96da0f8e21 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -66,21 +66,14 @@ app.whenReady().then(async () => { conductor = new Conductor({ events: { onSessionUpdate: (params) => { - // Forward session updates to renderer + // Forward ALL session updates to renderer (not just agent_message_chunk) if (mainWindow && !mainWindow.isDestroyed()) { - const update = params.update - // Convert to AgentMessage format for renderer - if ('sessionUpdate' in update) { - const content: Array<{ type: 'text'; text: string }> = [] - if (update.sessionUpdate === 'agent_message_chunk' && update.content?.type === 'text') { - content.push({ type: 'text', text: update.content.text }) - } - mainWindow.webContents.send(IPC_CHANNELS.AGENT_MESSAGE, { - sessionId: params.sessionId, - content, - done: false, - }) - } + // Send the full SessionNotification so renderer can handle all update types + mainWindow.webContents.send(IPC_CHANNELS.AGENT_MESSAGE, { + sessionId: params.sessionId, + update: params.update, // Full update object + done: false, + }) } }, }, diff --git a/src/renderer/src/components/ChatView.tsx b/src/renderer/src/components/ChatView.tsx index 6d306767b7..bbe8a65de7 100644 --- a/src/renderer/src/components/ChatView.tsx +++ b/src/renderer/src/components/ChatView.tsx @@ -68,6 +68,7 @@ export function ChatView({ updates, isProcessing, hasSession, onNewSession }: Ch interface Message { role: 'user' | 'assistant' content: string + thought: string toolCalls: ToolCall[] } @@ -83,6 +84,7 @@ interface ToolCall { function groupUpdatesIntoMessages(updates: StoredSessionUpdate[]): Message[] { const messages: Message[] = [] let currentAssistantContent = '' + let currentThought = '' let currentToolCalls: ToolCall[] = [] const toolCallMap = new Map() @@ -97,13 +99,15 @@ function groupUpdatesIntoMessages(updates: StoredSessionUpdate[]): Message[] { switch (update.sessionUpdate) { case 'user_message' as string: // Flush any pending assistant message - if (currentAssistantContent || currentToolCalls.length > 0) { + if (currentAssistantContent || currentThought || currentToolCalls.length > 0) { messages.push({ role: 'assistant', content: currentAssistantContent, + thought: currentThought, toolCalls: currentToolCalls, }) currentAssistantContent = '' + currentThought = '' currentToolCalls = [] toolCallMap.clear() } @@ -114,6 +118,7 @@ function groupUpdatesIntoMessages(updates: StoredSessionUpdate[]): Message[] { messages.push({ role: 'user', content: userUpdate.content.text, + thought: '', toolCalls: [], }) } @@ -128,7 +133,10 @@ function groupUpdatesIntoMessages(updates: StoredSessionUpdate[]): Message[] { break case 'agent_thought_chunk': - // Skip thought chunks for now (could show them differently) + // Accumulate thought chunks + if ('content' in update && update.content?.type === 'text') { + currentThought += update.content.text + } break case 'tool_call': @@ -188,10 +196,11 @@ function groupUpdatesIntoMessages(updates: StoredSessionUpdate[]): Message[] { } // Flush any remaining assistant content - if (currentAssistantContent || currentToolCalls.length > 0) { + if (currentAssistantContent || currentThought || currentToolCalls.length > 0) { messages.push({ role: 'assistant', content: currentAssistantContent.trim(), + thought: currentThought.trim(), toolCalls: currentToolCalls, }) } @@ -215,6 +224,13 @@ function MessageBubble({ message }: MessageBubbleProps) { : 'bg-[var(--color-surface)]' }`} > + {/* Thought (agent reasoning) */} + {message.thought && ( +
+ {message.thought} +
+ )} + {/* Tool calls */} {message.toolCalls.length > 0 && (
diff --git a/src/renderer/src/hooks/useApp.ts b/src/renderer/src/hooks/useApp.ts index 21b8d1fbed..c29778b230 100644 --- a/src/renderer/src/hooks/useApp.ts +++ b/src/renderer/src/hooks/useApp.ts @@ -61,48 +61,61 @@ export function useApp(): AppState & AppActions { // Subscribe to agent events useEffect(() => { const unsubMessage = window.electronAPI.onAgentMessage((message) => { - // Accumulate streaming text - for (const content of message.content) { - if (content.type === 'text') { - streamingTextRef.current += content.text - } + const update = message.update + const updateType = update?.sessionUpdate + + // Handle streaming text accumulation for agent messages + if (updateType === 'agent_message_chunk' && update.content?.type === 'text') { + streamingTextRef.current += update.content.text } - // Create/update the streaming message in updates + // Add the update to session updates for real-time display setSessionUpdates((prev) => { const now = new Date().toISOString() - // Find or create streaming message update - const streamingUpdate: StoredSessionUpdate = { + + // For agent_message_chunk, we want to show accumulated text + if (updateType === 'agent_message_chunk') { + const streamingUpdate: StoredSessionUpdate = { + timestamp: now, + update: { + sessionId: message.sessionId, + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: streamingTextRef.current }, + }, + }, + } + + // Replace last streaming update if it was also agent_message_chunk + const lastIdx = prev.length - 1 + if (lastIdx >= 0) { + const last = prev[lastIdx] + if ( + last.update.update && + 'sessionUpdate' in last.update.update && + last.update.update.sessionUpdate === 'agent_message_chunk' + ) { + return [...prev.slice(0, lastIdx), streamingUpdate] + } + } + return [...prev, streamingUpdate] + } + + // For all other update types, add them directly + const newUpdate = { timestamp: now, update: { sessionId: message.sessionId, - update: { - sessionUpdate: 'agent_message_chunk', - content: { type: 'text', text: streamingTextRef.current }, - }, + update: update, }, - } - - // Replace last streaming update or add new one - const lastIdx = prev.length - 1 - if (lastIdx >= 0 && !message.done) { - const last = prev[lastIdx] - if ( - last.update.update && - 'sessionUpdate' in last.update.update && - last.update.update.sessionUpdate === 'agent_message_chunk' - ) { - return [...prev.slice(0, lastIdx), streamingUpdate] - } - } - - if (message.done) { - streamingTextRef.current = '' - setIsProcessing(false) - } - - return [...prev, streamingUpdate] + } as StoredSessionUpdate + return [...prev, newUpdate] }) + + if (message.done) { + streamingTextRef.current = '' + setIsProcessing(false) + } }) const unsubStatus = window.electronAPI.onAgentStatus((status) => { diff --git a/src/shared/electron-api.d.ts b/src/shared/electron-api.d.ts index 6b58ca2432..daa5a6fe5c 100644 --- a/src/shared/electron-api.d.ts +++ b/src/shared/electron-api.d.ts @@ -11,7 +11,17 @@ import type { export interface AgentMessage { sessionId: string - content: Array<{ type: 'text'; text: string } | { type: 'code'; language: string; code: string }> + update: { + sessionUpdate: string + content?: { type: string; text: string } + toolCallId?: string + title?: string + status?: string + kind?: string + rawInput?: unknown + rawOutput?: unknown + [key: string]: unknown + } done: boolean }