diff --git a/web/src/chat/normalize.test.ts b/web/src/chat/normalize.test.ts index 9a56c102..3fc94709 100644 --- a/web/src/chat/normalize.test.ts +++ b/web/src/chat/normalize.test.ts @@ -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' + }) + }) }) diff --git a/web/src/chat/normalizeAgent.ts b/web/src/chat/normalizeAgent.ts index b0084b6c..cd9bcdb9 100644 --- a/web/src/chat/normalizeAgent.ts +++ b/web/src/chat/normalizeAgent.ts @@ -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) => 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)) {