diff --git a/canvas/src/components/mobile/MobileChat.tsx b/canvas/src/components/mobile/MobileChat.tsx index b5940a0ea..569f540eb 100644 --- a/canvas/src/components/mobile/MobileChat.tsx +++ b/canvas/src/components/mobile/MobileChat.tsx @@ -271,7 +271,7 @@ export function MobileChat({ const msgs = consume(agentId); for (const m of msgs) { appendMessageDeduped( - createMessage("agent", m.content, m.attachments), + createMessage(m.role ?? "agent", m.content, m.attachments), ); } }, [historyLoading, agentId, appendMessageDeduped]); diff --git a/canvas/src/components/tabs/chat/hooks/useChatSocket.ts b/canvas/src/components/tabs/chat/hooks/useChatSocket.ts index 15815e9a8..3dc532436 100644 --- a/canvas/src/components/tabs/chat/hooks/useChatSocket.ts +++ b/canvas/src/components/tabs/chat/hooks/useChatSocket.ts @@ -27,7 +27,7 @@ export function useChatSocket( const msgs = consume(workspaceId); for (const m of msgs) { callbacksRef.current.onAgentMessage?.( - createMessage("agent", m.content, m.attachments), + createMessage(m.role ?? "agent", m.content, m.attachments), ); } if (msgs.length > 0) { diff --git a/canvas/src/store/__tests__/canvas-events.test.ts b/canvas/src/store/__tests__/canvas-events.test.ts index 5e70a22a5..7a5571632 100644 --- a/canvas/src/store/__tests__/canvas-events.test.ts +++ b/canvas/src/store/__tests__/canvas-events.test.ts @@ -840,6 +840,7 @@ describe("handleCanvasEvent – USER_MESSAGE", () => { expect(agentMessages["ws-1"]).toHaveLength(1); expect(agentMessages["ws-1"][0].id).toBe("msg-abc"); expect(agentMessages["ws-1"][0].content).toBe("Hello, agent!"); + expect(agentMessages["ws-1"][0].role).toBe("user"); expect(typeof agentMessages["ws-1"][0].timestamp).toBe("string"); }); @@ -933,6 +934,7 @@ describe("handleCanvasEvent – USER_MESSAGE", () => { }; expect(agentMessages["ws-1"]).toHaveLength(1); expect(agentMessages["ws-1"][0].id).toBe("msg-with-file"); + expect(agentMessages["ws-1"][0].role).toBe("user"); expect(agentMessages["ws-1"][0].attachments).toEqual([att]); }); diff --git a/canvas/src/store/canvas-events.ts b/canvas/src/store/canvas-events.ts index f1837d068..ada09a906 100644 --- a/canvas/src/store/canvas-events.ts +++ b/canvas/src/store/canvas-events.ts @@ -71,7 +71,7 @@ export function handleCanvasEvent( nodes: Node[]; edges: Edge[]; selectedNodeId: string | null; - agentMessages: Record }>>; + agentMessages: Record }>>; }, set: (partial: Record) => void, ): void { @@ -552,9 +552,8 @@ export function handleCanvasEvent( // Render only when there's something visible. if (content || files.length > 0) { // Insert into agentMessages for rendering as a user-bubble. - // ChatTab renders role:agent bubbles from agentMessages; the - // bubble renderer uses the content/attachments fields, not the - // role field, so inserting a user message here works correctly. + // ChatTab uses msg.role === "user" for right-side alignment and + // user-toned styling, so we must set role:"user" explicitly. const { agentMessages } = get(); const existing = agentMessages[msg.workspace_id] || []; set({ @@ -565,6 +564,7 @@ export function handleCanvasEvent( { id: payload?.messageId ?? crypto.randomUUID(), content, + role: "user", timestamp: new Date().toISOString(), ...(files.length > 0 ? { attachments: files } : {}), }, diff --git a/canvas/src/store/canvas.ts b/canvas/src/store/canvas.ts index 1baa0e660..e92f2db29 100644 --- a/canvas/src/store/canvas.ts +++ b/canvas/src/store/canvas.ts @@ -224,8 +224,8 @@ interface CanvasState { batchPause: () => Promise; batchDelete: () => Promise; /** Agent-pushed messages keyed by workspace ID. ChatTab consumes and clears these. */ - agentMessages: Record }>>; - consumeAgentMessages: (workspaceId: string) => Array<{ id: string; content: string; timestamp: string; attachments?: Array<{ name: string; uri: string; mimeType?: string; size?: number }> }>; + agentMessages: Record }>>; + consumeAgentMessages: (workspaceId: string) => Array<{ id: string; content: string; timestamp: string; role?: "user" | "agent"; attachments?: Array<{ name: string; uri: string; mimeType?: string; size?: number }> }>; /** WebSocket connection status — drives the live indicator in the Toolbar. */ wsStatus: "connected" | "connecting" | "disconnected"; setWsStatus: (status: "connected" | "connecting" | "disconnected") => void;