From 39d167210ae7725223966e35a5ccaa7f2bdb5b9a Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 10:06:35 -0700 Subject: [PATCH 1/7] feat(chat): mount chat-timeline-slider inside chat-debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demotes the slider from a primary nav surface to an advanced affordance inside the Debug overlay. The two UX patterns — inline gutter markers (new) + panel slider (legacy) — now ship side-by-side as a library-consumer reference. --- .../chat-debug/chat-debug.component.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) 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 01ec1308e..ee8771e89 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 @@ -5,6 +5,7 @@ import { effect, input, inject, + output, signal, viewChild, ElementRef, @@ -26,6 +27,7 @@ import { DebugControlsComponent } from './debug-controls.component'; import { DebugSummaryComponent } from './debug-summary.component'; import type { DebugCheckpoint } from './debug-checkpoint-card.component'; import { toDebugCheckpoint, extractStateValues } from './debug-utils'; +import { ChatTimelineSliderComponent } from '../chat-timeline-slider/chat-timeline-slider.component'; @Component({ selector: 'chat-debug', @@ -40,6 +42,7 @@ import { toDebugCheckpoint, extractStateValues } from './debug-utils'; DebugDetailComponent, DebugControlsComponent, DebugSummaryComponent, + ChatTimelineSliderComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, styles: [ @@ -206,6 +209,15 @@ import { toDebugCheckpoint, extractStateValues } from './debug-utils'; overflow-y: auto; } + .chat-debug__section { padding: 8px 12px; border-top: 1px solid var(--ngaf-chat-separator); } + .chat-debug__section-title { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ngaf-chat-text-muted); + margin: 0 0 6px; + } + /* Markdown rendering */ :host ::ng-deep .chat-md p { margin: 0 0 0.75em; } :host ::ng-deep .chat-md p:last-child { margin-bottom: 0; } @@ -363,6 +375,16 @@ import { toDebugCheckpoint, extractStateValues } from './debug-utils'; /> } + + +
+

Legacy timeline slider

+ +
} @@ -373,6 +395,9 @@ export class ChatDebugComponent { readonly agent = input.required(); + readonly replayRequested = output(); + readonly forkRequested = output(); + readonly debugOpen = signal(true); readonly selectedCheckpointIndex = signal(-1); From bc1e23a09a1fa5b486215018f6cfb7a826d8a68c Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 10:08:33 -0700 Subject: [PATCH 2/7] refactor(examples-chat): remove Phase 6/7 fixed side panels Drops the timeline-panel and threads-panel side panels along with their palette toggles, persistence keys, and responsive overrides from PR #240. The replacement threads-drawer + inline checkpoint markers land in the next commit. The legacy timeline slider is still reachable via the Debug overlay. --- .../app/shell/control-palette.component.html | 22 -------- .../app/shell/control-palette.component.ts | 12 ----- .../src/app/shell/demo-shell.component.css | 54 ------------------- .../src/app/shell/demo-shell.component.html | 25 --------- .../src/app/shell/demo-shell.component.ts | 19 ------- .../app/shell/palette-persistence.service.ts | 2 - 6 files changed, 134 deletions(-) diff --git a/examples/chat/angular/src/app/shell/control-palette.component.html b/examples/chat/angular/src/app/shell/control-palette.component.html index db66e9f1d..c8dbd0ef3 100644 --- a/examples/chat/angular/src/app/shell/control-palette.component.html +++ b/examples/chat/angular/src/app/shell/control-palette.component.html @@ -80,28 +80,6 @@ Debug {{ debugOpen() ? 'on' : 'off' }} - - - - + + - - - @if (agent.interrupt && agent.interrupt()) { -
- -
- } - - @if (agent.subagents && agent.subagents().size > 0) { -
- -
- } - - @if (debugOpen()) { -
- -
- } + + + +
+ + @if (agent.interrupt && agent.interrupt()) { +
+ +
+ } + @if (agent.subagents && agent.subagents().size > 0) { +
+ +
+ } + @if (debugOpen()) { +
+ +
+ } +
diff --git a/examples/chat/angular/src/app/shell/demo-shell.component.ts b/examples/chat/angular/src/app/shell/demo-shell.component.ts index 60adb4f68..e1f468184 100644 --- a/examples/chat/angular/src/app/shell/demo-shell.component.ts +++ b/examples/chat/angular/src/app/shell/demo-shell.component.ts @@ -3,6 +3,8 @@ import { Component, ChangeDetectionStrategy, DOCUMENT, + DestroyRef, + computed, effect, signal, inject, @@ -15,6 +17,8 @@ import { ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, + ChatThreadDrawerComponent, + ChatThreadListComponent, type InterruptAction, } from '@ngaf/chat'; import { ControlPalette } from './control-palette.component'; @@ -40,6 +44,8 @@ function modeFromUrl(url: string): DemoMode { ChatDebugComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, + ChatThreadDrawerComponent, + ChatThreadListComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './demo-shell.component.html', @@ -69,6 +75,12 @@ export class DemoShell { void this.threadIdSignal(); void this.threadsSvc.refresh(); }); + + if (typeof window !== 'undefined') { + const onResize = () => this.viewportWidth.set(window.innerWidth); + window.addEventListener('resize', onResize); + inject(DestroyRef).onDestroy(() => window.removeEventListener('resize', onResize)); + } } protected readonly mode = toSignal( @@ -106,6 +118,19 @@ export class DemoShell { protected readonly debugOpen = signal(this.persistence.read('debug') ?? false); + /** Whether the threads drawer is open. Persisted across reloads. */ + protected readonly drawerOpen = signal(this.persistence.read('drawerOpen') ?? false); + + /** Viewport width, refreshed on window resize. Drives drawer push/overlay decision. */ + private readonly viewportWidth = signal( + typeof window !== 'undefined' ? window.innerWidth : 1440, + ); + + /** Computed drawer mode based on viewport width. */ + protected readonly drawerMode = computed<'push' | 'overlay'>(() => + this.viewportWidth() >= 1024 ? 'push' : 'overlay', + ); + protected readonly modelOptions = signal([ { value: 'gpt-5', label: 'gpt-5' }, { value: 'gpt-5-mini', label: 'gpt-5-mini' }, @@ -202,11 +227,20 @@ export class DemoShell { this.persistence.write('debug', next); } - protected onTimelineReplay(checkpointId: string): void { + protected onDrawerOpenChange(next: boolean): void { + this.drawerOpen.set(next); + this.persistence.write('drawerOpen', next); + } + + protected toggleDrawer(): void { + this.onDrawerOpenChange(!this.drawerOpen()); + } + + onTimelineReplay(checkpointId: string): void { void this.agent.submit(null as never, { checkpointId } as never); } - protected async onTimelineFork(checkpointId: string): Promise { + async onTimelineFork(checkpointId: string): Promise { await fetch('http://localhost:2024/threads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/examples/chat/angular/src/app/shell/palette-persistence.service.ts b/examples/chat/angular/src/app/shell/palette-persistence.service.ts index 2742feb40..0a356df1d 100644 --- a/examples/chat/angular/src/app/shell/palette-persistence.service.ts +++ b/examples/chat/angular/src/app/shell/palette-persistence.service.ts @@ -11,6 +11,7 @@ interface PaletteState { debug?: boolean | null; threadId?: string | null; collapsed?: boolean | null; + drawerOpen?: boolean | null; } type PaletteKey = keyof PaletteState; diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index 1fc3a25a9..93c1b58d4 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -143,6 +143,9 @@ import type { ChatRenderEvent } from './chat-render-event'; [prevRole]="prevRole(i)" [streaming]="agent().isLoading() && i === agent().messages().length - 1" [current]="i === agent().messages().length - 1" + [checkpointId]="checkpointFor(message)" + (replayRequested)="replayRequested.emit($event)" + (forkRequested)="forkRequested.emit($event)" > @if (message.reasoning) { (); /** Emitted when the user copies an assistant message. */ readonly messageCopy = output<{ messageIndex: number; content: string }>(); + /** Bubbled from chat-message gutter markers when the user requests a checkpoint replay. */ + readonly replayRequested = output(); + /** Bubbled from chat-message gutter markers when the user requests a checkpoint fork. */ + readonly forkRequested = output(); private readonly _internalStore = signalStateStore({}); readonly resolvedStore = computed(() => { @@ -415,4 +422,14 @@ export class ChatComponent { const idx = this.agent().messages().indexOf(message as never); this.messageCopy.emit({ messageIndex: idx, content }); } + + /** Returns the checkpoint id associated with an AI message, if the + * underlying agent exposes messageCheckpoints(). */ + protected checkpointFor(msg: Message): string | undefined { + const id = (msg as unknown as { id?: string }).id; + if (typeof id !== 'string') return undefined; + const map = (this.agent() as unknown as { messageCheckpoints?: () => ReadonlyMap }) + .messageCheckpoints?.(); + return map?.get(id); + } } From 48bbab57dd6f31a194b6daf3fc7beb4497e96a6b Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 10:14:24 -0700 Subject: [PATCH 4/7] feat(chat,examples-chat): forward replay/fork outputs through chat-popup + chat-sidebar Completes cross-mode wiring for the nav v2 redesign. Both wrapper compositions (chat-popup, chat-sidebar) now expose replayRequested + forkRequested outputs that forward from their internal instance, and the demo-shell's popup and sidebar mode routes bind those outputs to the same onTimelineReplay / onTimelineFork handlers /embed uses. Co-Authored-By: Claude Haiku 4.5 --- .../angular/src/app/modes/popup-mode.component.ts | 9 ++++++++- .../angular/src/app/modes/sidebar-mode.component.ts | 9 ++++++++- .../compositions/chat-popup/chat-popup.component.ts | 11 +++++++++-- .../chat-sidebar/chat-sidebar.component.ts | 11 +++++++++-- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/examples/chat/angular/src/app/modes/popup-mode.component.ts b/examples/chat/angular/src/app/modes/popup-mode.component.ts index cc5343c79..902336f44 100644 --- a/examples/chat/angular/src/app/modes/popup-mode.component.ts +++ b/examples/chat/angular/src/app/modes/popup-mode.component.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; import { ChatPopupComponent, ChatWelcomeSuggestionComponent, a2uiBasicCatalog } from '@ngaf/chat'; +import { DemoShell } from '../shell/demo-shell.component'; import { DEMO_AGENT } from '../shell/shell-tokens'; import { WELCOME_SUGGESTIONS } from './welcome-suggestions'; @@ -15,7 +16,12 @@ import { WELCOME_SUGGESTIONS } from './welcome-suggestions'; Click the launcher button (bottom-right) to open the chat.

- +
@for (s of suggestions; track s.value) { . protected readonly catalog = a2uiBasicCatalog(); diff --git a/examples/chat/angular/src/app/modes/sidebar-mode.component.ts b/examples/chat/angular/src/app/modes/sidebar-mode.component.ts index 4ff81d99d..8dedd5e70 100644 --- a/examples/chat/angular/src/app/modes/sidebar-mode.component.ts +++ b/examples/chat/angular/src/app/modes/sidebar-mode.component.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; import { ChatSidebarComponent, ChatWelcomeSuggestionComponent, a2uiBasicCatalog } from '@ngaf/chat'; +import { DemoShell } from '../shell/demo-shell.component'; import { DEMO_AGENT } from '../shell/shell-tokens'; import { WELCOME_SUGGESTIONS } from './welcome-suggestions'; @@ -15,7 +16,12 @@ import { WELCOME_SUGGESTIONS } from './welcome-suggestions'; Click the launcher button (right edge) to slide in the chat panel.

- +
@for (s of suggestions; track s.value) { . protected readonly catalog = a2uiBasicCatalog(); diff --git a/libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts b/libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts index 83d0102f1..53bd5378f 100644 --- a/libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts +++ b/libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts @@ -1,6 +1,6 @@ // libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts // SPDX-License-Identifier: MIT -import { Component, ChangeDetectionStrategy, input, model, DestroyRef, inject, DOCUMENT, effect } from '@angular/core'; +import { Component, ChangeDetectionStrategy, input, model, output, DestroyRef, inject, DOCUMENT, effect } from '@angular/core'; import type { Agent } from '../../agent'; import type { ViewRegistry } from '@ngaf/render'; import { ChatComponent } from '../chat/chat.component'; @@ -64,7 +64,12 @@ import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens'; - +
@@ -77,6 +82,8 @@ export class ChatPopupComponent { * surface. Pass `a2uiBasicCatalog()` from `@ngaf/chat`. */ readonly views = input(undefined); readonly open = model(false); + readonly replayRequested = output(); + readonly forkRequested = output(); /** * Keyboard shortcut (single key) that toggles the popup with cmd (mac) * or ctrl (other). Set to `null` to disable. Default: 'k' — matches the diff --git a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts index 24f7eff87..cd27a4d3e 100644 --- a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts +++ b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts @@ -1,6 +1,6 @@ // libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts // SPDX-License-Identifier: MIT -import { Component, ChangeDetectionStrategy, input, model } from '@angular/core'; +import { Component, ChangeDetectionStrategy, input, model, output } from '@angular/core'; import type { Agent } from '../../agent'; import type { ViewRegistry } from '@ngaf/render'; import { ChatComponent } from '../chat/chat.component'; @@ -57,7 +57,12 @@ import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens'; - + @@ -71,6 +76,8 @@ export class ChatSidebarComponent { readonly views = input(undefined); readonly open = model(false); readonly pushContent = input(false); + readonly replayRequested = output(); + readonly forkRequested = output(); toggle(): void { this.open.update((v) => !v); } openWindow(): void { this.open.set(true); } From 543a053936e1768ab74e61c7d5b38b491c64d46d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 10:18:37 -0700 Subject: [PATCH 5/7] fix(examples-chat): refresh threads list after run completes The backend writes metadata.title on the first user message via _maybe_write_thread_title (PR #242). Without this refresh, the drawer keeps showing 'Thread ' for the active thread until the user manually switches threads. New effect listens for status transitioning out of 'running' and re-fetches the threads list so the title lands in the drawer as soon as the run ends. --- .../angular/src/app/shell/demo-shell.component.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/examples/chat/angular/src/app/shell/demo-shell.component.ts b/examples/chat/angular/src/app/shell/demo-shell.component.ts index e1f468184..b2ec5bafe 100644 --- a/examples/chat/angular/src/app/shell/demo-shell.component.ts +++ b/examples/chat/angular/src/app/shell/demo-shell.component.ts @@ -76,6 +76,19 @@ export class DemoShell { void this.threadsSvc.refresh(); }); + // Refresh threads list when an agent run completes. The backend writes + // metadata.title on the first user message via _maybe_write_thread_title; + // a refresh after run-end picks up the new title in the drawer without + // needing a manual thread switch or reload. + let lastStatus = this.agent.status(); + effect(() => { + const status = this.agent.status(); + if (lastStatus === 'running' && status !== 'running') { + void this.threadsSvc.refresh(); + } + lastStatus = status; + }); + if (typeof window !== 'undefined') { const onResize = () => this.viewportWidth.set(window.innerWidth); window.addEventListener('resize', onResize); From ceda2539528ca5ccb01f89594dfb4a4ae828796d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 10:20:27 -0700 Subject: [PATCH 6/7] chore: regenerate api-docs for nav v2 demo wiring --- .../content/docs/chat/api/api-docs.json | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/apps/website/content/docs/chat/api/api-docs.json b/apps/website/content/docs/chat/api/api-docs.json index 75b3f4c8e..19ed84ff3 100644 --- a/apps/website/content/docs/chat/api/api-docs.json +++ b/apps/website/content/docs/chat/api/api-docs.json @@ -1557,6 +1557,12 @@ "description": "", "optional": false }, + { + "name": "forkRequested", + "type": "OutputEmitterRef", + "description": "Bubbled from chat-message gutter markers when the user requests a checkpoint fork.", + "optional": false + }, { "name": "handlers", "type": "InputSignal>", @@ -1611,6 +1617,12 @@ "description": "", "optional": false }, + { + "name": "replayRequested", + "type": "OutputEmitterRef", + "description": "Bubbled from chat-message gutter markers when the user requests a checkpoint replay.", + "optional": false + }, { "name": "resolvedStore", "type": "Signal", @@ -1661,6 +1673,19 @@ } ], "methods": [ + { + "name": "checkpointFor", + "signature": "checkpointFor(msg: Message)", + "description": "Returns the checkpoint id associated with an AI message, if the\n underlying agent exposes messageCheckpoints().", + "params": [ + { + "name": "msg", + "type": "Message", + "description": "", + "optional": false + } + ] + }, { "name": "classifyMessage", "signature": "classifyMessage(content: string, message: object)", @@ -1853,6 +1878,12 @@ "description": "", "optional": false }, + { + "name": "forkRequested", + "type": "OutputEmitterRef", + "description": "", + "optional": false + }, { "name": "messageContent", "type": "object", @@ -1865,6 +1896,12 @@ "description": "", "optional": false }, + { + "name": "replayRequested", + "type": "OutputEmitterRef", + "description": "", + "optional": false + }, { "name": "selectedCheckpointIndex", "type": "WritableSignal", @@ -2406,12 +2443,24 @@ "description": "Close the popup on Escape (default true).", "optional": false }, + { + "name": "forkRequested", + "type": "OutputEmitterRef", + "description": "", + "optional": false + }, { "name": "open", "type": "ModelSignal", "description": "", "optional": false }, + { + "name": "replayRequested", + "type": "OutputEmitterRef", + "description": "", + "optional": false + }, { "name": "shortcut", "type": "InputSignal", @@ -2628,6 +2677,12 @@ "description": "", "optional": false }, + { + "name": "forkRequested", + "type": "OutputEmitterRef", + "description": "", + "optional": false + }, { "name": "open", "type": "ModelSignal", @@ -2640,6 +2695,12 @@ "description": "", "optional": false }, + { + "name": "replayRequested", + "type": "OutputEmitterRef", + "description": "", + "optional": false + }, { "name": "views", "type": "InputSignal>> | undefined>", From 35cf624ac0362ceb9af55ea83a7fd8ea93724372 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 10:32:47 -0700 Subject: [PATCH 7/7] fix(examples-chat,chat): drawer + layout fixes from live smoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues uncovered by live smoke against the dev server: 1. Drawer animation: the `[data-open] transform: translateX(0)` CSS rule unreliable across HMR + change detection — switch the chat-thread-drawer composition to a direct `[style.left.px]` binding (0 when open, -320 when closed) with `transition: left 200ms ease`. Drops the CSS attribute selector approach. 2. Redundant header: the demo-shell wrapped both the hamburger and the existing floating-fixed `` in a new top header strip, which double-anchored the palette and visually crowded the top. Drop the header; pin the hamburger as its own fixed top-left floating button so the palette keeps its original top-right floating-corner placement. 3. Hamburger sizing: scaled up the default size (32 → 36) to match the palette's visual weight and added a subtle drop shadow so it reads as a floating control like the palette. --- .../src/app/shell/demo-shell.component.css | 30 +++------- .../src/app/shell/demo-shell.component.html | 55 +++++++++---------- .../chat-thread-drawer.component.ts | 6 +- 3 files changed, 36 insertions(+), 55 deletions(-) diff --git a/examples/chat/angular/src/app/shell/demo-shell.component.css b/examples/chat/angular/src/app/shell/demo-shell.component.css index 693618932..54181c06e 100644 --- a/examples/chat/angular/src/app/shell/demo-shell.component.css +++ b/examples/chat/angular/src/app/shell/demo-shell.component.css @@ -9,27 +9,13 @@ height: 100%; } -.demo-shell__header { - position: fixed; - top: 0; - left: 0; - right: 0; - height: 56px; - z-index: 100; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 12px; - pointer-events: none; -} - -.demo-shell__header > * { - pointer-events: auto; -} - .demo-shell__hamburger { - width: 32px; - height: 32px; + position: fixed; + top: 12px; + left: 12px; + z-index: 1100; + width: 36px; + height: 36px; border: 1px solid #303540; background: #1a1d23; color: #e6e9ef; @@ -39,12 +25,10 @@ display: inline-flex; align-items: center; justify-content: center; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.3); } .demo-shell__hamburger:hover { background: #232730; } -@media (max-width: 1023px) { - .demo-shell__hamburger { width: 36px; height: 36px; } -} @media (max-width: 767px) { .demo-shell__hamburger { width: 44px; height: 44px; } } diff --git a/examples/chat/angular/src/app/shell/demo-shell.component.html b/examples/chat/angular/src/app/shell/demo-shell.component.html index 34672feac..0cb6be6e5 100644 --- a/examples/chat/angular/src/app/shell/demo-shell.component.html +++ b/examples/chat/angular/src/app/shell/demo-shell.component.html @@ -1,32 +1,31 @@
-
- - -
+ + +