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..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
@@ -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: `
@@ -68,18 +66,15 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown
-
+
-
-
+
A
+
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;
diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts
index 943e5e797..b869dd0f7 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: `
@@ -109,18 +107,15 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown
-
+
-
-
+
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-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/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
-
-
-
-
-
+
}
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; }
`;
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..0620a6a69 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,29 @@ 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); });