From aca5b1d339e3bdf0b99043e6d00ff591fa838527 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 11:55:12 -0700 Subject: [PATCH 1/5] fix(stream-resource): fix 3 runtime errors in streaming chat flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Guard Object.keys(v) against null/undefined in values$ subscriber (stream-resource.fn.ts:94) — crashed when values event had no data 2. Handle plain JSON messages from SSE (not hydrated BaseMessage instances) in getMessageType() — _getType() is a class method not available on plain objects; fall back to reading the `type` property 3. Fix event data extraction in processEvent — normalizeSdkEvent spreads data into the event object, so event['values'] was always undefined; use extractEventData() to read from event['data'] instead. Also sync messages$ from values events and merge messages/partial updates by id to preserve the full message history including human messages. --- .../chat-messages/chat-messages.component.ts | 9 ++- .../lib/internals/stream-manager.bridge.ts | 76 ++++++++++++++++--- .../src/lib/stream-resource.fn.ts | 2 +- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/libs/chat/src/lib/primitives/chat-messages/chat-messages.component.ts b/libs/chat/src/lib/primitives/chat-messages/chat-messages.component.ts index d0adc213e..6f9c5bb34 100644 --- a/libs/chat/src/lib/primitives/chat-messages/chat-messages.component.ts +++ b/libs/chat/src/lib/primitives/chat-messages/chat-messages.component.ts @@ -13,11 +13,16 @@ import { MessageTemplateDirective } from './message-template.directive'; import type { MessageTemplateType } from '../../chat.types'; /** - * Maps a LangChain message `_getType()` string to a {@link MessageTemplateType}. + * Maps a LangChain message to a {@link MessageTemplateType}. + * Handles both class instances (with `_getType()`) and plain objects (with `type` property) + * since SSE stream events deliver plain JSON, not hydrated BaseMessage instances. * Exported as a standalone function so it can be unit-tested without DOM rendering. */ export function getMessageType(message: BaseMessage): MessageTemplateType { - const type = message._getType(); + // Try class method first, fall back to plain object property + const type = typeof message._getType === 'function' + ? message._getType() + : (message as unknown as Record)['type'] as string ?? 'ai'; switch (type) { case 'human': return 'human'; diff --git a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts index f48d1ca3f..9d53086a9 100644 --- a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts +++ b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts @@ -102,24 +102,64 @@ export function createStreamManagerBridge)['id']; + const idx = id ? merged.findIndex(m => (m as unknown as Record)['id'] === id) : -1; + if (idx >= 0) { + merged[idx] = msg; + } else { + merged.push(msg); + } + } + subjects.messages$.next(merged); } else { - subjects.messages$.next(msgs as BaseMessage[]); + subjects.messages$.next(normalized); } return; } + // normalizeSdkEvent spreads event data directly into the event object, + // so the values/updates payload is at event['data'] (the original data object), + // NOT at event['values'] or event['updates']. switch (event.type) { - case 'values': - subjects.values$.next(event['values'] as T); + case 'values': { + const vals = extractEventData(event); + if (vals != null) { + subjects.values$.next(vals as T); + // Also sync messages$ from the values state so the full message + // history (including human messages) is available to consumers. + const stateMessages = (vals as Record)['messages']; + if (Array.isArray(stateMessages)) { + if (options.toMessage) { + subjects.messages$.next(stateMessages.map(options.toMessage)); + } else { + subjects.messages$.next(stateMessages as BaseMessage[]); + } + } + } break; - case 'updates': - subjects.values$.next({ - ...subjects.values$.value, - ...(event['updates'] as object), - } as T); + } + case 'updates': { + const upd = extractEventData(event); + if (upd != null) { + subjects.values$.next({ + ...subjects.values$.value, + ...(upd as object), + } as T); + } break; + } case 'error': subjects.error$.next(event['error']); subjects.status$.next(ResourceStatus.Error); @@ -177,6 +217,22 @@ export function createStreamManagerBridge 0 ? rest : d; +} + function isMessagesEvent(type: StreamEvent['type']): boolean { return type === 'messages' || type.startsWith('messages/'); } diff --git a/libs/stream-resource/src/lib/stream-resource.fn.ts b/libs/stream-resource/src/lib/stream-resource.fn.ts index 517459317..faa25aeb1 100644 --- a/libs/stream-resource/src/lib/stream-resource.fn.ts +++ b/libs/stream-resource/src/lib/stream-resource.fn.ts @@ -91,7 +91,7 @@ export function streamResource< // Track hasValue — becomes true once values or messages arrive values$.pipe(takeUntil(destroy$)).subscribe(v => { - if (Object.keys(v as object).length > 0) hasValue$.next(true); + if (v != null && Object.keys(v as object).length > 0) hasValue$.next(true); }); messages$.pipe(takeUntil(destroy$)).subscribe(m => { if (m.length > 0) hasValue$.next(true); }); From 7214a96e9197c8d1ca9f07441071796c040639a4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 12:49:53 -0700 Subject: [PATCH 2/5] fix(chat): fix ViewEncapsulation breaking all CSS theme variables ViewEncapsulation.None caused :host selectors in CHAT_THEME_STYLES to not match anything (no shadow DOM = :host doesn't apply). All 40+ CSS custom properties were empty, breaking the entire design. Fix: remove ViewEncapsulation.None from ChatComponent and ChatDebugComponent (default Emulated encapsulation processes :host correctly). Prefix markdown styles with ::ng-deep for innerHTML content penetration. --- .../chat-debug/chat-debug.component.ts | 2 -- .../lib/compositions/chat/chat.component.ts | 2 -- libs/chat/src/lib/styles/chat-markdown.ts | 34 +++++++++---------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts index 11a126a27..90c4a9e8f 100644 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts @@ -9,7 +9,6 @@ import { viewChild, ElementRef, ChangeDetectionStrategy, - ViewEncapsulation, } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import type { StreamResourceRef } from '@cacheplane/stream-resource'; @@ -43,7 +42,6 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown DebugSummaryComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, styles: [CHAT_THEME_STYLES, CHAT_MARKDOWN_STYLES], template: `
diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index 943e5e797..47ab78335 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -11,7 +11,6 @@ import { ElementRef, ChangeDetectionStrategy, inject, - ViewEncapsulation, } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import type { StreamResourceRef } from '@cacheplane/stream-resource'; @@ -39,7 +38,6 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown ChatThreadListComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, styles: [CHAT_THEME_STYLES, CHAT_MARKDOWN_STYLES], template: `
diff --git a/libs/chat/src/lib/styles/chat-markdown.ts b/libs/chat/src/lib/styles/chat-markdown.ts index ca64e2772..ec85e32a4 100644 --- a/libs/chat/src/lib/styles/chat-markdown.ts +++ b/libs/chat/src/lib/styles/chat-markdown.ts @@ -52,38 +52,38 @@ export function renderMarkdown(content: string, sanitizer: DomSanitizer): SafeHt * Must be included in a component with ViewEncapsulation.None or via ::ng-deep. */ export const CHAT_MARKDOWN_STYLES = ` - .chat-md p { margin: 0 0 0.75em; } - .chat-md p:last-child { margin-bottom: 0; } - .chat-md code { + :host ::ng-deep .chat-md p { margin: 0 0 0.75em; } + :host ::ng-deep .chat-md p:last-child { margin-bottom: 0; } + :host ::ng-deep .chat-md code { background: var(--chat-bg-alt); padding: 2px 6px; border-radius: 4px; font-size: 0.875em; font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; } - .chat-md pre { + :host ::ng-deep .chat-md pre { background: var(--chat-bg-alt); padding: 12px 16px; border-radius: var(--chat-radius-card); overflow-x: auto; margin: 0.75em 0; } - .chat-md pre code { background: none; padding: 0; } - .chat-md ul, .chat-md ol { margin: 0.5em 0; padding-left: 1.5em; } - .chat-md li { margin: 0.25em 0; } - .chat-md a { color: var(--chat-text); text-decoration: underline; } - .chat-md strong { font-weight: 600; } - .chat-md blockquote { + :host ::ng-deep .chat-md pre code { background: none; padding: 0; } + :host ::ng-deep .chat-md ul, :host ::ng-deep .chat-md ol { margin: 0.5em 0; padding-left: 1.5em; } + :host ::ng-deep .chat-md li { margin: 0.25em 0; } + :host ::ng-deep .chat-md a { color: var(--chat-text); text-decoration: underline; } + :host ::ng-deep .chat-md strong { font-weight: 600; } + :host ::ng-deep .chat-md blockquote { border-left: 3px solid var(--chat-border); padding-left: 12px; margin: 0.75em 0; color: var(--chat-text-muted); } - .chat-md h1, .chat-md h2, .chat-md h3, .chat-md h4 { margin: 1em 0 0.5em; font-weight: 600; } - .chat-md h1 { font-size: 1.25em; } - .chat-md h2 { font-size: 1.125em; } - .chat-md h3 { font-size: 1em; } - .chat-md table { border-collapse: collapse; width: 100%; margin: 0.75em 0; } - .chat-md th, .chat-md td { border: 1px solid var(--chat-border); padding: 6px 12px; text-align: left; } - .chat-md th { background: var(--chat-bg-alt); font-weight: 600; font-size: 0.875em; } + :host ::ng-deep .chat-md h1, :host ::ng-deep .chat-md h2, :host ::ng-deep .chat-md h3, :host ::ng-deep .chat-md h4 { margin: 1em 0 0.5em; font-weight: 600; } + :host ::ng-deep .chat-md h1 { font-size: 1.25em; } + :host ::ng-deep .chat-md h2 { font-size: 1.125em; } + :host ::ng-deep .chat-md h3 { font-size: 1em; } + :host ::ng-deep .chat-md table { border-collapse: collapse; width: 100%; margin: 0.75em 0; } + :host ::ng-deep .chat-md th, :host ::ng-deep .chat-md td { border: 1px solid var(--chat-border); padding: 6px 12px; text-align: left; } + :host ::ng-deep .chat-md th { background: var(--chat-bg-alt); font-weight: 600; font-size: 0.875em; } `; From 1e201cceb00d96e2539a7b6df9bc40c11c585fcd Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 12:56:39 -0700 Subject: [PATCH 3/5] fix(chat): replace [innerHTML] icon bindings with inline SVG (sanitizer fix) --- .../chat-interrupt-panel.component.ts | 4 +--- .../chat-subagent-card.component.ts | 8 ++------ .../chat-tool-call-card.component.ts | 11 +++-------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat-interrupt-panel/chat-interrupt-panel.component.ts b/libs/chat/src/lib/compositions/chat-interrupt-panel/chat-interrupt-panel.component.ts index a73dddd1b..5df1a40ef 100644 --- a/libs/chat/src/lib/compositions/chat-interrupt-panel/chat-interrupt-panel.component.ts +++ b/libs/chat/src/lib/compositions/chat-interrupt-panel/chat-interrupt-panel.component.ts @@ -7,7 +7,6 @@ import { ChangeDetectionStrategy, } from '@angular/core'; import type { StreamResourceRef } from '@cacheplane/stream-resource'; -import { ICON_WARNING } from '../../styles/chat-icons'; export type InterruptAction = 'accept' | 'edit' | 'respond' | 'ignore'; @@ -24,7 +23,7 @@ export type InterruptAction = 'accept' | 'edit' | 'respond' | 'ignore'; >
- +

Agent Interrupt

{{ interruptPayload() }}

@@ -80,7 +79,6 @@ export class ChatInterruptPanelComponent { readonly action = output(); - readonly warningIcon = ICON_WARNING; readonly interrupt = computed(() => this.ref().interrupt()); diff --git a/libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts b/libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts index 10e70833a..bebc0dab0 100644 --- a/libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts +++ b/libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts @@ -7,7 +7,6 @@ import { ChangeDetectionStrategy, } from '@angular/core'; import type { SubagentStreamRef } from '@cacheplane/stream-resource'; -import { ICON_AGENT, ICON_CHEVRON_UP, ICON_CHEVRON_DOWN } from '../../styles/chat-icons'; type SubagentStatus = 'pending' | 'running' | 'complete' | 'error'; @@ -36,7 +35,7 @@ export { statusColor }; aria-label="Toggle subagent details" >
- + Subagent {{ subagent().toolCallId }} @@ -46,7 +45,7 @@ export { statusColor }; {{ subagent().status() }}
- + @if (expanded()) {} @else {} @@ -76,9 +75,6 @@ export class ChatSubagentCardComponent { readonly expanded = signal(false); - readonly agentIcon = ICON_AGENT; - readonly chevronUp = ICON_CHEVRON_UP; - readonly chevronDown = ICON_CHEVRON_DOWN; readonly statusColor = computed(() => statusColor(this.subagent().status())); diff --git a/libs/chat/src/lib/compositions/chat-tool-call-card/chat-tool-call-card.component.ts b/libs/chat/src/lib/compositions/chat-tool-call-card/chat-tool-call-card.component.ts index 356b0b695..65ad1e008 100644 --- a/libs/chat/src/lib/compositions/chat-tool-call-card/chat-tool-call-card.component.ts +++ b/libs/chat/src/lib/compositions/chat-tool-call-card/chat-tool-call-card.component.ts @@ -5,7 +5,6 @@ import { signal, ChangeDetectionStrategy, } from '@angular/core'; -import { ICON_TOOL, ICON_CHECK, ICON_CHEVRON_UP, ICON_CHEVRON_DOWN } from '../../styles/chat-icons'; export interface ToolCallInfo { id: string; @@ -29,13 +28,13 @@ export interface ToolCallInfo { aria-label="Toggle tool call details" >
- + {{ toolCall().name }} @if (toolCall().result !== undefined) { - done + done }
- + @if (expanded()) {} @else {} @@ -61,10 +60,6 @@ export class ChatToolCallCardComponent { readonly expanded = signal(false); - readonly toolIcon = ICON_TOOL; - readonly checkIcon = ICON_CHECK; - readonly chevronUp = ICON_CHEVRON_UP; - readonly chevronDown = ICON_CHEVRON_DOWN; formatJson(value: unknown): string { if (typeof value === 'string') return value; From 287b69b5a8dd9b4fcfca21f15e886df1676b097a Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 12:57:02 -0700 Subject: [PATCH 4/5] fix(chat): center input text, remove Assistant label, ChatGPT pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Input: items-end → items-center for vertical centering of single-line text - Input: inline SVG for send button (replaces [innerHTML] which Angular sanitizes) - AI messages: remove "Assistant" label, use avatar inline with content (ChatGPT pattern) - Typing indicator: match new AI message layout (avatar + dots, no label) --- .../chat-debug/chat-debug.component.ts | 17 +++++++-------- .../lib/compositions/chat/chat.component.ts | 17 +++++++-------- .../chat-input/chat-input.component.ts | 8 +++---- .../chat-typing-indicator.component.ts | 21 ++++++++----------- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts index 90c4a9e8f..bef110142 100644 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts @@ -66,18 +66,15 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown
- + -
-
-
A
- Assistant -
+
A
+
diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index 47ab78335..b869dd0f7 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -107,18 +107,15 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown
- + -
-
-
A
- Assistant -
+
A
+
diff --git a/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts b/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts index 20b898a24..a5b453496 100644 --- a/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts +++ b/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts @@ -9,7 +9,6 @@ import { } from '@angular/core'; import { FormsModule } from '@angular/forms'; import type { StreamResourceRef } from '@cacheplane/stream-resource'; -import { ICON_SEND } from '../../styles/chat-icons'; export function submitMessage( ref: StreamResourceRef, @@ -34,7 +33,7 @@ export function submitMessage( template: `
+ + +
`, @@ -77,7 +78,6 @@ export class ChatInputComponent { readonly messageText = signal(''); readonly isDisabled = computed(() => this.ref().isLoading()); readonly focused = signal(false); - readonly sendIcon = ICON_SEND; onSubmit(): void { const submitted = submitMessage(this.ref(), this.messageText()); diff --git a/libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts b/libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts index bde3a4708..3859a3e61 100644 --- a/libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts +++ b/libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts @@ -33,18 +33,15 @@ export function isTyping(ref: StreamResourceRef): boolean { `], template: ` @if (visible()) { -
-
-
A
- Assistant -
- - - -
+
+
A
+
+ + +
} From 6c0a284e9d3abe582ade18ccaad138ba36485fa2 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:07:05 -0700 Subject: [PATCH 5/5] fix(stream-resource): handle both SDK and mock event formats in extractEventData --- .../src/lib/internals/stream-manager.bridge.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts index 9d53086a9..0620a6a69 100644 --- a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts +++ b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts @@ -219,16 +219,23 @@ export function createStreamManagerBridge 0 ? rest : d; }