mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 10:08:38 +02:00
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:
@@ -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,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
12
src/shared/electron-api.d.ts
vendored
12
src/shared/electron-api.d.ts
vendored
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user