fix(web): normalize non-sidechain text-only array user output as user message (#409)

The CLI wraps array-content user messages as agent output because
isExternalUserMessage rejects non-string content. On the web side,
detect text-only arrays in non-sidechain user output and emit them as
role:'user' so they display in the user lane.

Also handle sidechain user messages with mixed array content (e.g.
tool_result + text) by extracting text parts into a sidechain block.

Closes #407's original scope on top of the #402 base.

via [HAPI](https://hapi.run)

Co-authored-by: HAPI <noreply@hapi.run>
This commit is contained in:
Haoqing Wang
2026-04-06 20:49:11 +08:00
committed by GitHub
co-authored by HAPI
parent 73fa846df3
commit e7ba48c761
2 changed files with 74 additions and 0 deletions
+53
View File
@@ -319,4 +319,57 @@ describe('normalizeDecryptedMessage', () => {
parentUUID: null
})
})
it('normalizes non-sidechain text-only array-content user output as user message', () => {
const message = makeMessage({
role: 'agent',
content: {
type: 'output',
data: {
type: 'user',
uuid: 'u5',
isSidechain: false,
message: { content: [{ type: 'text', text: 'Regular user message' }] }
}
}
})
const normalized = normalizeDecryptedMessage(message)
expect(normalized).toMatchObject({
role: 'user',
isSidechain: false,
content: { type: 'text', text: 'Regular user message' }
})
})
it('treats sidechain user output with mixed tool_result + text array as sidechain', () => {
const message = makeMessage({
role: 'agent',
content: {
type: 'output',
data: {
type: 'user',
uuid: 'u6',
isSidechain: true,
message: { content: [
{ type: 'tool_result', tool_use_id: 'tc-1', content: 'result' },
{ type: 'text', text: 'Some subagent text' }
] }
}
}
})
const normalized = normalizeDecryptedMessage(message)
expect(normalized).toMatchObject({
role: 'agent',
isSidechain: true,
})
if (normalized?.role !== 'agent') throw new Error('Expected agent')
expect(normalized.content[0]).toMatchObject({
type: 'sidechain',
prompt: 'Some subagent text'
})
})
})
+21
View File
@@ -159,6 +159,27 @@ function normalizeUserOutput(
}
}
// Non-sidechain array content that is all text blocks — these are real
// user messages that the CLI wrapped as agent output because
// isExternalUserMessage rejects array content. Emit as role:'user' so
// they display in the user lane.
if (!isSidechain && Array.isArray(messageContent)) {
const textParts = messageContent
.filter((b: unknown) => isObject(b) && b.type === 'text' && typeof b.text === 'string')
.map((b: Record<string, unknown>) => b.text as string)
if (textParts.length > 0 && textParts.length === messageContent.length) {
return {
id: messageId,
localId,
createdAt,
role: 'user',
isSidechain: false,
content: { type: 'text', text: textParts.join('\n\n') },
meta
}
}
}
const blocks: NormalizedAgentContent[] = []
if (Array.isArray(messageContent)) {