fix(canvas): set role field on USER_MESSAGE entries so bubbles render correctly #1517

Merged
agent-dev-b merged 1 commits from fix/user-message-role-1514 into fix/user-message-fanout-1440 2026-05-24 13:56:47 +00:00
5 changed files with 10 additions and 8 deletions
+1 -1
View File
@@ -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]);
@@ -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) {
@@ -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]);
});
+4 -4
View File
@@ -71,7 +71,7 @@ export function handleCanvasEvent(
nodes: Node<WorkspaceNodeData>[];
edges: Edge[];
selectedNodeId: string | null;
agentMessages: Record<string, Array<{ id: string; content: string; timestamp: string; attachments?: Array<{ name: string; uri: string; mimeType?: string; size?: number }> }>>;
agentMessages: Record<string, Array<{ id: string; content: string; timestamp: string; role?: "user" | "agent"; attachments?: Array<{ name: string; uri: string; mimeType?: string; size?: number }> }>>;
},
set: (partial: Record<string, unknown>) => 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 } : {}),
},
+2 -2
View File
@@ -224,8 +224,8 @@ interface CanvasState {
batchPause: () => Promise<void>;
batchDelete: () => Promise<void>;
/** Agent-pushed messages keyed by workspace ID. ChatTab consumes and clears these. */
agentMessages: Record<string, Array<{ id: string; content: string; timestamp: string; attachments?: Array<{ name: string; uri: string; mimeType?: string; size?: number }> }>>;
consumeAgentMessages: (workspaceId: string) => Array<{ id: string; content: string; timestamp: string; attachments?: Array<{ name: string; uri: string; mimeType?: string; size?: number }> }>;
agentMessages: Record<string, Array<{ id: string; content: string; timestamp: string; role?: "user" | "agent"; attachments?: Array<{ name: string; uri: string; mimeType?: string; size?: number }> }>>;
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;