fix(server): make mapBlocksToTimeline role-aware to prevent user text emitting as assistant_message

User messages with array content were routed through mapBlocksToTimeline
which hardcoded all text blocks as assistant_message. This caused user
interrupt text to appear as assistant output in paseo logs and the app UI.

Add textMessageType option so callers declare the role explicitly.
When set to "user_message", text blocks coalesce into a single item
matching extractUserMessageText semantics.
This commit is contained in:
Mohamed Boudra
2026-03-21 13:31:11 +07:00
parent 480af26b57
commit 84500cdcae
@@ -3662,7 +3662,9 @@ class ClaudeAgentSession implements AgentSession {
provider: "claude", provider: "claude",
}); });
} else if (Array.isArray(content)) { } else if (Array.isArray(content)) {
const timelineItems = this.mapBlocksToTimeline(content); const timelineItems = this.mapBlocksToTimeline(content, {
textMessageType: "user_message",
});
for (const item of timelineItems) { for (const item of timelineItems) {
if (item.type === "user_message" && messageId && !item.messageId) { if (item.type === "user_message" && messageId && !item.messageId) {
events.push({ events.push({
@@ -4181,30 +4183,40 @@ class ClaudeAgentSession implements AgentSession {
private mapBlocksToTimeline( private mapBlocksToTimeline(
content: string | ClaudeContentChunk[], content: string | ClaudeContentChunk[],
options?: { options?: {
textMessageType?: "assistant_message" | "user_message";
suppressAssistantText?: boolean; suppressAssistantText?: boolean;
suppressReasoning?: boolean; suppressReasoning?: boolean;
} }
): AgentTimelineItem[] { ): AgentTimelineItem[] {
const suppressAssistant = options?.suppressAssistantText ?? false; const textMessageType = options?.textMessageType ?? "assistant_message";
const suppressText =
textMessageType === "assistant_message" &&
(options?.suppressAssistantText ?? false);
const suppressReasoning = options?.suppressReasoning ?? false; const suppressReasoning = options?.suppressReasoning ?? false;
if (typeof content === "string") { if (typeof content === "string") {
if (!content || content === INTERRUPT_TOOL_USE_PLACEHOLDER) { if (!content || content === INTERRUPT_TOOL_USE_PLACEHOLDER) {
return []; return [];
} }
if (suppressAssistant) { if (suppressText) {
return []; return [];
} }
return [{ type: "assistant_message", text: content }]; return [{ type: textMessageType, text: content }];
} }
const items: AgentTimelineItem[] = []; const items: AgentTimelineItem[] = [];
const userTextParts: string[] = [];
for (const block of content) { for (const block of content) {
switch (block.type) { switch (block.type) {
case "text": case "text":
case "text_delta": case "text_delta":
if (block.text && block.text !== INTERRUPT_TOOL_USE_PLACEHOLDER) { if (block.text && block.text !== INTERRUPT_TOOL_USE_PLACEHOLDER) {
if (!suppressAssistant) { if (textMessageType === "user_message") {
const trimmed = block.text.trim();
if (trimmed) {
userTextParts.push(trimmed);
}
} else if (!suppressText) {
items.push({ type: "assistant_message", text: block.text }); items.push({ type: "assistant_message", text: block.text });
} }
} }
@@ -4237,6 +4249,14 @@ class ClaudeAgentSession implements AgentSession {
break; break;
} }
} }
if (textMessageType === "user_message" && userTextParts.length > 0) {
items.unshift({
type: "user_message",
text: userTextParts.join("\n\n"),
});
}
return items; return items;
} }