feat: forward all session updates to renderer for real-time display

- Forward full SessionNotification to renderer (not just agent_message_chunk)
- Handle agent_thought_chunk, tool_call, tool_call_update in real-time
- Display agent thoughts with purple styling in chat view
- Update AgentMessage type to include full update object
- Update useApp to accumulate all update types

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jiayuan
2026-01-14 04:04:41 +08:00
parent 07fe6b7d21
commit 2bfd9c24f4
4 changed files with 83 additions and 51 deletions

View File

@@ -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,
})
}
},
},

View File

@@ -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<string, ToolCall>()
@@ -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 && (
<div className="mb-2 rounded border-l-2 border-purple-500/50 bg-purple-500/10 py-1 pl-2 text-xs text-[var(--color-text-muted)] italic">
{message.thought}
</div>
)}
{/* Tool calls */}
{message.toolCalls.length > 0 && (
<div className="mb-2 space-y-2">

View File

@@ -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) => {

View File

@@ -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
}