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
- Assistant -
+
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
- 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-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 -
- - - -
+
+
A
+
+ + +
} 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); });