From 725972e515b4c81f3e9f84d129aaff066db28d8d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 16:02:56 -0700 Subject: [PATCH 01/13] docs(specs): chat-debug shadcn polish design Align chat-debug chrome with the design language from palette v2 (PR #244, since deleted): persistent dark zinc-900 surface, status-pill launcher, sliding switch, custom select trigger, mount animation. Introduces --ngaf-chat-debug-* token namespace independent of chat tokens, so the devtools chrome is theme-stable regardless of host theme. Hosts can override any token. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-11-chat-debug-shadcn-polish-design.md | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-11-chat-debug-shadcn-polish-design.md diff --git a/docs/superpowers/specs/2026-05-11-chat-debug-shadcn-polish-design.md b/docs/superpowers/specs/2026-05-11-chat-debug-shadcn-polish-design.md new file mode 100644 index 000000000..de8666ebc --- /dev/null +++ b/docs/superpowers/specs/2026-05-11-chat-debug-shadcn-polish-design.md @@ -0,0 +1,297 @@ +# Chat Debug Shadcn Polish — Design + +**Date:** 2026-05-11 +**Status:** Approved — ready for implementation plan +**Surface:** `libs/chat` (`@ngaf/chat`), no consumer changes required +**Predecessor:** [2026-05-11-chat-debug-devtools-design.md](2026-05-11-chat-debug-devtools-design.md) +**Predecessor PR:** [#249](https://github.com/cacheplane/angular-agent-framework/pull/249) + +## Summary + +The merged `` works, but its visual chrome reads as "raw" — +hairline borders, light-themed surface, generic gear-icon launcher, +button-style toggles. The control palette deleted in +[PR #244](https://github.com/cacheplane/angular-agent-framework/pull/244) +established a different, more polished design language for this product's +dev tooling: persistent dark zinc-900 surface with shadcn conventions, +status-pill collapsed state, real switch component, custom select triggers, +mount animation. This work aligns chat-debug with that language. + +The slot API and public component surface are unchanged. This is purely a +visual refresh plus one primitive swap (`` → +``). + +## Goals + +- **Persistent dark devtools chrome** by default, regardless of host theme — + matches every other devtool (Chrome, React, Redux) and the deleted + palette v2. +- **Token escape hatch:** all chrome consumes `--ngaf-chat-debug-*` tokens + with shadcn-zinc defaults. Hosts that care can override. +- **Status-pill launcher** with reactive streaming indicator. +- **Visual primitives aligned with palette v2:** custom select trigger, + sliding switch, mount animation, 11px tracked uppercase section labels. +- **No new public-API concepts** beyond the toggle→switch swap. + +## Non-goals + +- No model/mode content in the launcher pill. Host puts those in the + controls slot. +- No `chat-debug-toggle` deprecation period — it has no real consumer. + Removed outright. +- No new test surface beyond updating the primitives smoke spec. +- No documentation page for the new tokens beyond an inline comment block. + +## Token namespace + +New file: `libs/chat/src/lib/compositions/chat-debug/chat-debug-tokens.ts` + +```ts +export const CHAT_DEBUG_TOKENS = ` + :host { + --ngaf-chat-debug-bg: #18181b; + --ngaf-chat-debug-bg-deep: #09090b; + --ngaf-chat-debug-surface: #1f1f23; + --ngaf-chat-debug-border: #27272a; + --ngaf-chat-debug-border-strong: #3f3f46; + --ngaf-chat-debug-text: #fafafa; + --ngaf-chat-debug-text-muted: #a1a1aa; + --ngaf-chat-debug-text-subtle: #71717a; + --ngaf-chat-debug-accent: #4f8df5; + --ngaf-chat-debug-success: #4ade80; + --ngaf-chat-debug-shadow-panel: 0 8px 32px rgba(0, 0, 0, 0.5); + --ngaf-chat-debug-shadow-pill: 0 6px 18px rgba(0, 0, 0, 0.4); + --ngaf-chat-debug-radius-panel: 12px; + --ngaf-chat-debug-radius-input: 8px; + --ngaf-chat-debug-radius-pill: 999px; + --ngaf-chat-debug-font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; + } +`; +``` + +Imported at the top of every chat-debug component / primitive's `styles` +array. Hosts override by setting any token on `chat-debug` or any +ancestor element. + +All chat-debug components stop reading `--ngaf-chat-*` chat tokens. Pure +isolation: the chat library's theme and chat-debug's chrome are +independent token surfaces from this work forward. + +## Launcher (status pill) + +When `open() === false`, render a small rounded-pill at +`position: fixed; bottom: 20px; right: 20px`: + +- ~36px tall, ~36px wide (just the dot — content can grow later). +- Background `--ngaf-chat-debug-bg`, border `--ngaf-chat-debug-border`, + shadow `--ngaf-chat-debug-shadow-pill`. +- Hover background lifts to `--ngaf-chat-debug-surface`. +- Content: an 8px `.launcher__dot` element. + - Default color `--ngaf-chat-debug-success` (green) with a faint glow. + - When `agent.status() === 'running'`, color `--ngaf-chat-debug-accent` + (blue) with stronger glow and the `chat-debug-pill-pulse` keyframe + (1.2s, scale 1 → 0.85 → 1, opacity 1 → 0.6 → 1). +- ARIA: ` + `, +}) +export class ChatDebugActionComponent { + readonly label = input.required(); + readonly clicked = output(); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-action.component.ts +git commit -m "feat(chat): re-skin chat-debug-action to debug tokens" +``` + +--- + +## Task 8: Re-skin Timeline inspector to debug tokens + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts` + +This task only changes the `styles` block and adds the tokens import; the template, class, and `stepSelection` function are unchanged. + +- [ ] **Step 1: Read the current file** + +```bash +head -1 libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts +``` + +This task assumes the file currently imports `CHAT_HOST_TOKENS` from `'../../../styles/chat-tokens'`. If it does not, look at the actual import and adjust the Edit accordingly. + +- [ ] **Step 2: Edit the import line** + +Change: +```ts +import { CHAT_HOST_TOKENS } from '../../../styles/chat-tokens'; +``` + +To: +```ts +import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; +``` + +- [ ] **Step 3: Edit the styles array — replace the `styles: [...]` block in full** + +```ts + styles: [ + CHAT_DEBUG_TOKENS, + ` + :host { display: flex; flex-direction: column; height: 100%; outline: none; } + .timeline__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px solid var(--ngaf-chat-debug-border); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ngaf-chat-debug-text-subtle); + background: var(--ngaf-chat-debug-bg); + } + .timeline__count { display: inline-flex; align-items: center; gap: 6px; } + .timeline__count-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 18px; + padding: 0 6px; + border-radius: 9px; + background: var(--ngaf-chat-debug-surface); + border: 1px solid var(--ngaf-chat-debug-border); + color: var(--ngaf-chat-debug-text); + font-size: 11px; + font-weight: 500; + letter-spacing: 0; + text-transform: none; + font-variant-numeric: tabular-nums; + } + .timeline__clear { + background: transparent; + border: 0; + cursor: pointer; + color: var(--ngaf-chat-debug-text-subtle); + font: inherit; + font-size: 11px; + letter-spacing: 0; + text-transform: none; + padding: 2px 6px; + border-radius: 6px; + transition: color 120ms ease, background 120ms ease; + } + .timeline__clear:hover:not(:disabled) { + color: var(--ngaf-chat-debug-text); + background: var(--ngaf-chat-debug-surface); + } + .timeline__clear:disabled { opacity: 0.4; cursor: default; } + .timeline__list { + flex: 1; + overflow-y: auto; + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 8px; + background: var(--ngaf-chat-debug-bg); + } + .timeline__empty { + padding: 24px 16px; + text-align: center; + color: var(--ngaf-chat-debug-text-subtle); + font-size: 13px; + } + .timeline__row { display: flex; flex-direction: column; gap: 8px; } + .timeline__row-actions { + display: none; + gap: 8px; + padding-left: 12px; + } + .timeline__row:hover .timeline__row-actions { display: flex; } + .timeline__row button.row-action { + background: var(--ngaf-chat-debug-bg-deep); + border: 1px solid var(--ngaf-chat-debug-border); + border-radius: 6px; + padding: 3px 8px; + font: inherit; + font-size: 11px; + color: var(--ngaf-chat-debug-text-muted); + cursor: pointer; + transition: color 120ms ease, border-color 120ms ease; + } + .timeline__row button.row-action:hover { + color: var(--ngaf-chat-debug-text); + border-color: var(--ngaf-chat-debug-border-strong); + } + .timeline__diff { + padding: 12px; + background: var(--ngaf-chat-debug-bg-deep); + border: 1px solid var(--ngaf-chat-debug-border); + border-radius: var(--ngaf-chat-debug-radius-input); + color: var(--ngaf-chat-debug-text); + } + `, + ], +``` + +- [ ] **Step 4: Run the timeline spec** + +```bash +cd libs/chat && npx vitest run timeline-inspector.spec +``` +Expected: PASS, 7 tests. + +- [ ] **Step 5: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts +git commit -m "feat(chat): re-skin timeline inspector to debug tokens" +``` + +--- + +## Task 9: Re-skin State inspector to debug tokens + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat-debug/inspectors/state-inspector.component.ts` + +- [ ] **Step 1: Edit the import** + +Change: +```ts +import { CHAT_HOST_TOKENS } from '../../../styles/chat-tokens'; +``` + +To: +```ts +import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; +``` + +- [ ] **Step 2: Replace the styles array** + +```ts + styles: [ + CHAT_DEBUG_TOKENS, + ` + :host { display: flex; flex-direction: column; height: 100%; background: var(--ngaf-chat-debug-bg); } + .state__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px solid var(--ngaf-chat-debug-border); + background: var(--ngaf-chat-debug-bg); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ngaf-chat-debug-text-subtle); + } + .state__copy { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--ngaf-chat-debug-bg-deep); + border: 1px solid var(--ngaf-chat-debug-border); + border-radius: 6px; + padding: 3px 8px; + font: inherit; + font-size: 11px; + letter-spacing: 0; + text-transform: none; + color: var(--ngaf-chat-debug-text-muted); + cursor: pointer; + transition: color 120ms ease, border-color 120ms ease; + } + .state__copy:hover { + color: var(--ngaf-chat-debug-text); + border-color: var(--ngaf-chat-debug-border-strong); + } + .state__copy.is-copied { + color: var(--ngaf-chat-debug-success); + border-color: var(--ngaf-chat-debug-success); + } + .state__copy svg { display: block; } + .state__body { + flex: 1; + overflow-y: auto; + padding: 12px 16px; + color: var(--ngaf-chat-debug-text); + } + `, + ], +``` + +The template (header label, Copy button with copy/check icon swap, body with the existing `` projection) and class are unchanged. + +- [ ] **Step 3: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-debug/inspectors/state-inspector.component.ts +git commit -m "feat(chat): re-skin state inspector to debug tokens" +``` + +--- + +## Task 10: Rewrite `` chrome — pill launcher + dark panel + mount animation + click-outside + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts` + +This is the largest task. It does three things at once: +1. Token swap (all `--ngaf-chat-*` → `--ngaf-chat-debug-*`). +2. Replaces the gear-icon launcher with a status pill driven by `agent.status()`. +3. Adds mount animation (`@keyframes`) and click-outside dismiss (new `HostListener`). + +The existing imports of `ICON_TOOL`, `DomSanitizer`, and `SafeHtml` become unused — remove them. + +- [ ] **Step 1: Replace the imports block at the top of the file** + +Find the existing imports (lines 1-25 or so). Replace **just the import block** with: + +```ts +// SPDX-License-Identifier: MIT +import { + Component, + ChangeDetectionStrategy, + computed, + contentChild, + contentChildren, + effect, + ElementRef, + HostListener, + inject, + input, + output, + signal, +} from '@angular/core'; +import { NgTemplateOutlet } from '@angular/common'; +import type { AgentWithHistory } from '../../agent'; +import { CHAT_DEBUG_TOKENS } from './chat-debug-tokens'; +import { ChatDebugControlsDirective } from './chat-debug-controls.directive'; +import { ChatDebugInspectorDirective } from './chat-debug-inspector.directive'; +import { TimelineInspectorComponent } from './inspectors/timeline-inspector.component'; +import { StateInspectorComponent } from './inspectors/state-inspector.component'; +import { createPersistence } from './persistence'; +``` + +(`DomSanitizer`, `SafeHtml`, `ICON_TOOL`, and `CHAT_HOST_TOKENS` are deleted.) + +- [ ] **Step 2: Replace the `styles: [...]` array in full** + +The entire `styles: [CHAT_HOST_TOKENS, \`...\`]` block goes away. Replace with: + +```ts + styles: [ + CHAT_DEBUG_TOKENS, + ` + :host { display: contents; } + + /* ── Status pill launcher ─────────────────────────────────────── */ + .launcher { + position: fixed; + bottom: 20px; + right: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: var(--ngaf-chat-debug-radius-pill); + background: var(--ngaf-chat-debug-bg); + border: 1px solid var(--ngaf-chat-debug-border); + color: var(--ngaf-chat-debug-text); + cursor: pointer; + z-index: 990; + box-shadow: var(--ngaf-chat-debug-shadow-pill); + transition: background 120ms ease, border-color 120ms ease; + padding: 0; + } + .launcher:hover { + background: var(--ngaf-chat-debug-surface); + border-color: var(--ngaf-chat-debug-border-strong); + } + .launcher__dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--ngaf-chat-debug-success); + box-shadow: 0 0 8px color-mix(in srgb, var(--ngaf-chat-debug-success) 60%, transparent); + } + .launcher__dot--streaming { + background: var(--ngaf-chat-debug-accent); + box-shadow: 0 0 8px color-mix(in srgb, var(--ngaf-chat-debug-accent) 70%, transparent); + animation: chat-debug-pill-pulse 1.2s ease-in-out infinite; + } + @keyframes chat-debug-pill-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(0.85); } + } + + /* ── Docked panel ─────────────────────────────────────────────── */ + .panel { + position: fixed; + background: var(--ngaf-chat-debug-bg); + color: var(--ngaf-chat-debug-text); + border: 1px solid var(--ngaf-chat-debug-border); + z-index: 991; + display: flex; + flex-direction: column; + box-shadow: var(--ngaf-chat-debug-shadow-panel); + animation: chat-debug-panel-enter 120ms ease; + } + .panel--right { + top: 0; right: 0; bottom: 0; + width: var(--panel-size, 420px); + border-right: 0; + border-top-left-radius: var(--ngaf-chat-debug-radius-panel); + border-bottom-left-radius: var(--ngaf-chat-debug-radius-panel); + transform-origin: bottom right; + } + .panel--left { + top: 0; left: 0; bottom: 0; + width: var(--panel-size, 420px); + border-left: 0; + border-top-right-radius: var(--ngaf-chat-debug-radius-panel); + border-bottom-right-radius: var(--ngaf-chat-debug-radius-panel); + transform-origin: bottom left; + } + .panel--bottom { + left: 0; right: 0; bottom: 0; + height: var(--panel-size, 40vh); + border-bottom: 0; + border-top-left-radius: var(--ngaf-chat-debug-radius-panel); + border-top-right-radius: var(--ngaf-chat-debug-radius-panel); + transform-origin: bottom right; + } + @keyframes chat-debug-panel-enter { + from { opacity: 0; transform: scale(0.96); } + to { opacity: 1; transform: scale(1); } + } + + .panel__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--ngaf-chat-debug-border); + min-height: 44px; + box-sizing: border-box; + } + .panel__title { + margin: 0; + font-size: 13px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--ngaf-chat-debug-text); + } + .panel__actions { display: flex; align-items: center; gap: 4px; } + + .panel__dock-group { + display: inline-flex; + gap: 0; + padding: 2px; + background: var(--ngaf-chat-debug-bg-deep); + border: 1px solid var(--ngaf-chat-debug-border); + border-radius: 6px; + } + .panel__dock-btn { + appearance: none; + background: transparent; + border: 0; + border-radius: 4px; + width: 24px; + height: 22px; + padding: 0; + color: var(--ngaf-chat-debug-text-subtle); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background 120ms ease, color 120ms ease; + } + .panel__dock-btn:hover { color: var(--ngaf-chat-debug-text); } + .panel__dock-btn.is-active { + background: var(--ngaf-chat-debug-border); + color: var(--ngaf-chat-debug-text); + } + .panel__dock-btn svg { display: block; } + + .panel__close { + appearance: none; + background: transparent; + border: 0; + border-radius: 6px; + width: 26px; + height: 26px; + margin-left: 4px; + color: var(--ngaf-chat-debug-text-subtle); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background 120ms ease, color 120ms ease; + } + .panel__close:hover { + background: var(--ngaf-chat-debug-surface); + color: var(--ngaf-chat-debug-text); + } + + .panel__controls { + border-bottom: 1px solid var(--ngaf-chat-debug-border); + overflow-y: auto; + max-height: 50%; + background: var(--ngaf-chat-debug-bg); + } + .panel__controls:empty { display: none; } + + .panel__tabs { + display: flex; + gap: 4px; + border-bottom: 1px solid var(--ngaf-chat-debug-border); + padding: 0 12px; + background: var(--ngaf-chat-debug-bg); + } + .panel__tab { + appearance: none; + background: transparent; + border: 0; + border-bottom: 2px solid transparent; + padding: 10px 8px; + font: inherit; + font-size: 13px; + font-weight: 500; + color: var(--ngaf-chat-debug-text-muted); + cursor: pointer; + transition: color 120ms ease, border-color 120ms ease; + margin-bottom: -1px; + } + .panel__tab:hover { color: var(--ngaf-chat-debug-text); } + .panel__tab.is-active { + color: var(--ngaf-chat-debug-text); + border-bottom-color: var(--ngaf-chat-debug-accent); + } + + .panel__body { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; background: var(--ngaf-chat-debug-bg); } + `, + ], +``` + +- [ ] **Step 3: Replace the `template: \`...\`` block in full** + +```ts + template: ` + @if (!open()) { + + } @else { +
+
+

Chat Debug

+
+
+ + + +
+ +
+
+ + @if (controls()) { +
+ +
+ } + + @if (tabs().length > 1) { +
+ @for (tab of tabs(); track tab.id) { + + } +
+ } + +
+ @switch (activeTab()?.kind) { + @case ('builtin-timeline') { + + } + @case ('builtin-state') { + + } + @case ('host') { + @if (activeHostInspector(); as host) { + + } + } + } +
+
+ } + `, +``` + +- [ ] **Step 4: Replace the class body** + +Find the existing `export class ChatDebugComponent { ... }` block. The fields and methods stay mostly the same; what changes is: +- Drop the `sanitizer` / `launcherIcon` fields (gear icon is gone). +- Add `isStreaming` computed. +- Replace the `@HostListener('document:keydown.escape', ...)` block with a combined handler that also catches outside clicks. +- Add `viewChild` refs for the launcher and panel elements so the click-outside check can match against them. + +Replace the entire class body (between `export class ChatDebugComponent {` and the final `}`) with: + +```ts + readonly agent = input.required(); + readonly dock = input('right'); + readonly defaultOpen = input(false); + readonly storageKey = input('chat-debug'); + + readonly replayRequested = output(); + readonly forkRequested = output(); + readonly openChange = output(); + readonly dockChange = output(); + + protected readonly controls = contentChild(ChatDebugControlsDirective); + protected readonly hostInspectors = contentChildren(ChatDebugInspectorDirective); + + protected readonly open = signal(false); + protected readonly dockState = signal('right'); + protected readonly activeTabId = signal('timeline'); + + /** Reads `agent.status()` reactively for the launcher dot. */ + protected readonly isStreaming = computed(() => { + const status = this.agent().status?.(); + return status === 'running'; + }); + + protected readonly tabs = computed((): TabEntry[] => { + const host = this.hostInspectors().map((d, i): TabEntry => ({ + id: `host-${i}`, + label: d.label(), + kind: 'host', + hostIndex: i, + })); + return [ + { id: 'timeline', label: 'Timeline', kind: 'builtin-timeline' }, + { id: 'state', label: 'State', kind: 'builtin-state' }, + ...host, + ]; + }); + + protected readonly activeTab = computed(() => + this.tabs().find((t) => t.id === this.activeTabId()), + ); + + protected readonly activeHostInspector = computed(() => { + const t = this.activeTab(); + if (!t || t.kind !== 'host' || t.hostIndex === undefined) return undefined; + return this.hostInspectors()[t.hostIndex]; + }); + + private readonly hostEl: ElementRef = inject(ElementRef); + + constructor() { + // Restore once from storage on construction; inputs seed the fallback. + // `storageKey` is read-once: rebinding it at runtime is not supported. + const restore = createPersistence(this.storageKey()); + const persistedOpen = restore.read('open'); + this.open.set(persistedOpen ?? this.defaultOpen()); + const persistedDock = restore.read('dock'); + this.dockState.set(persistedDock ?? this.dock()); + const persistedTab = restore.read('tab'); + if (persistedTab) this.activeTabId.set(persistedTab); + + // Write-through effect — reads each writable signal so subsequent + // changes trigger a fresh run that writes them all to storage. + effect(() => { + const p = createPersistence(this.storageKey()); + p.write('open', this.open()); + p.write('dock', this.dockState()); + p.write('tab', this.activeTabId()); + }); + } + + setOpen(value: boolean): void { + this.open.set(value); + this.openChange.emit(value); + } + + setDock(next: DockPosition): void { + this.dockState.set(next); + this.dockChange.emit(next); + } + + setActiveTab(id: string): void { + this.activeTabId.set(id); + } + + @HostListener('document:keydown.escape') + protected onEsc(): void { + if (this.open()) this.setOpen(false); + } + + /** + * Click-outside dismiss. When the panel is open, any click whose + * composed path doesn't include our host element closes the panel. + * Capture phase so we react before host handlers swallow the event. + */ + @HostListener('document:click', ['$event']) + protected onDocumentClick(event: MouseEvent): void { + if (!this.open()) return; + const path = event.composedPath(); + if (path.includes(this.hostEl.nativeElement)) return; + this.setOpen(false); + } +``` + +- [ ] **Step 5: Run all chat-debug tests** + +```bash +cd libs/chat && npx vitest run chat-debug +``` +Expected: PASS, 30+ tests (chat-debug.component.spec, persistence.spec, primitives.spec, timeline-inspector.spec). + +- [ ] **Step 6: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts +git commit -m "feat(chat): chat-debug shadcn chrome — pill launcher + dark panel + mount + click-outside" +``` + +--- + +## Task 11: Build verification + +**Files:** none (verification only) + +- [ ] **Step 1: Build the chat lib** + +```bash +npx nx build chat +``` +Expected: succeeds. + +- [ ] **Step 2: Build the example app** + +```bash +npx nx build examples-chat-angular +``` +Expected: succeeds. + +- [ ] **Step 3: Run lint on chat** + +```bash +npx nx lint chat --skip-nx-cache +``` +Expected: "Successfully ran target lint for project chat" (warnings are OK; no errors). + +- [ ] **Step 4: Regenerate website API docs** (the new switch primitive changes the surface) + +```bash +npm run generate-api-docs +``` + +If `apps/website/content/docs/chat/api/api-docs.json` is now different from HEAD, stage it. If unchanged, skip the commit. + +- [ ] **Step 5: Commit (only if api-docs.json changed)** + +```bash +git diff --quiet apps/website/content/docs/chat/api/api-docs.json || { + git add apps/website/content/docs/chat/api/api-docs.json + git commit -m "docs(website): regen api-docs for chat-debug switch primitive" +} +``` + +--- + +## Task 12: End-to-end browser verification + +**Files:** none (verification only) + +- [ ] **Step 1: Start the dev server on port 4201** + +```bash +npx nx serve examples-chat-angular --port=4201 +``` + +(Run in a separate terminal or background. Wait for "Application bundle generation complete".) + +- [ ] **Step 2: Open and verify in the browser** + +Navigate to `http://localhost:4201/embed`. Confirm: + +1. Bottom-right shows a small dark zinc-900 pill with a green dot — not the old gear circle. +2. Hover the pill: background lifts to a slightly lighter zinc. +3. Click the pill: dark panel appears with a brief scale+fade mount animation, docked on the right. +4. Panel background is `#18181b` (zinc-900), borders are `#27272a`, title is `Chat Debug` in white. +5. Header dock toggles render as a sunken segmented group in `#09090b`; active dock toggle is lifted to `#27272a`. +6. Controls zone has Mode segmented / Agent + Appearance selects (custom trigger, not native chevron) / New conversation action button. +7. Selects: clicking opens the native OS dropdown overlay; selecting an option updates the visible trigger label. +8. Tab strip: Timeline / State, blue (`#4f8df5`) underline on active. +9. Click anywhere outside the panel → it closes. +10. Click the pill again → it reopens, same dock + tab from persistence. +11. Esc closes the panel. +12. Send a chat message. While the agent is streaming, the pill's dot turns blue and pulses. When idle, it's back to green. +13. State tab Copy button uses the new dark chrome; the success state shows the green check + "Copied" with a green border. + +- [ ] **Step 3: Stop the dev server** (Ctrl-C the foreground process or send SIGTERM to the background one). + +- [ ] **Step 4: Done — no commit needed** + +If everything passes, the implementation is complete. + +--- + +## Out of scope (deferred) + +- Model / mode text content in the launcher pill. The pill currently shows just the dot. Adding text content requires host-supplied data; defer until the use case is concrete. +- A documented page for `--ngaf-chat-debug-*` tokens on the website. Inline comment in `chat-debug-tokens.ts` is sufficient. +- TestBed-mounted specs for the new chrome. The existing chat-debug specs are pure-function + defined-as-class — matching the lib's convention. +- Resize handles on the panel edges. Persistence wrapper supports a size key; UI not implemented. From 1656e1385ad423876ff65d3da723646141062aa3 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 16:08:47 -0700 Subject: [PATCH 03/13] feat(chat): chat-debug devtools token namespace --- .../chat-debug/chat-debug-tokens.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 libs/chat/src/lib/compositions/chat-debug/chat-debug-tokens.ts diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug-tokens.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug-tokens.ts new file mode 100644 index 000000000..49711035b --- /dev/null +++ b/libs/chat/src/lib/compositions/chat-debug/chat-debug-tokens.ts @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +/** + * Component-scoped CSS variables for the chat-debug devtools chrome. + * + * Imported into every chat-debug component / primitive's `styles` array + * so the defaults are set on each `:host` element. Hosts override by + * setting any token on `chat-debug` or any ancestor. + * + * Independent from `--ngaf-chat-*` (the chat library's theme tokens). + * Devtools chrome stays dark regardless of host theme by default — + * matches Chrome DevTools / React DevTools / Redux DevTools convention. + * + * Palette anchor: shadcn zinc-900 + accent blue. + */ +export const CHAT_DEBUG_TOKENS = ` + :host { + --ngaf-chat-debug-bg: #18181b; + --ngaf-chat-debug-bg-deep: #09090b; + --ngaf-chat-debug-surface: #1f1f23; + --ngaf-chat-debug-border: #27272a; + --ngaf-chat-debug-border-strong: #3f3f46; + --ngaf-chat-debug-text: #fafafa; + --ngaf-chat-debug-text-muted: #a1a1aa; + --ngaf-chat-debug-text-subtle: #71717a; + --ngaf-chat-debug-accent: #4f8df5; + --ngaf-chat-debug-success: #4ade80; + --ngaf-chat-debug-shadow-panel: 0 8px 32px rgba(0, 0, 0, 0.5); + --ngaf-chat-debug-shadow-pill: 0 6px 18px rgba(0, 0, 0, 0.4); + --ngaf-chat-debug-radius-panel: 12px; + --ngaf-chat-debug-radius-input: 8px; + --ngaf-chat-debug-radius-pill: 999px; + --ngaf-chat-debug-font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; + --ngaf-chat-debug-font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-family: var(--ngaf-chat-debug-font-sans); + color: var(--ngaf-chat-debug-text); + + /* + * Cascade shim: rewire the chat library's color tokens to debug + * equivalents so embedded components that consume \`--ngaf-chat-*\` + * (debug-checkpoint-card, debug-state-diff, debug-state-inspector, + * any host-projected slot content) pick up the dark devtools surface + * without each one needing its own re-skin. Geometry / font tokens + * are left alone — they're neutral. + */ + --ngaf-chat-bg: var(--ngaf-chat-debug-bg); + --ngaf-chat-text: var(--ngaf-chat-debug-text); + --ngaf-chat-text-muted: var(--ngaf-chat-debug-text-muted); + --ngaf-chat-separator: var(--ngaf-chat-debug-border); + --ngaf-chat-surface-alt: var(--ngaf-chat-debug-bg-deep); + } +`; From d6532b0e6b9736ba8a06cb973ebf707723ac16e4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 16:08:58 -0700 Subject: [PATCH 04/13] feat(chat): chat-debug-switch primitive (sliding shadcn switch) --- .../primitives/chat-debug-switch.component.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-switch.component.ts diff --git a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-switch.component.ts b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-switch.component.ts new file mode 100644 index 000000000..3a4bcd507 --- /dev/null +++ b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-switch.component.ts @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +import { Component, ChangeDetectionStrategy, input, output } from '@angular/core'; +import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; + +@Component({ + selector: 'chat-debug-switch', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + CHAT_DEBUG_TOKENS, + ` + :host { display: block; } + .row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--ngaf-chat-space-3, 12px); + font-size: 13px; + color: var(--ngaf-chat-debug-text); + } + .switch { + position: relative; + width: 36px; + height: 20px; + background: var(--ngaf-chat-debug-border); + border: 0; + border-radius: 999px; + cursor: pointer; + padding: 0; + transition: background 150ms ease; + flex-shrink: 0; + } + .switch.is-on { background: var(--ngaf-chat-debug-accent); } + .switch__thumb { + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + background: var(--ngaf-chat-debug-text); + border-radius: 50%; + transition: transform 150ms ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); + } + .switch.is-on .switch__thumb { transform: translateX(16px); } + `, + ], + template: ` +
+ {{ label() }} + +
+ `, +}) +export class ChatDebugSwitchComponent { + readonly label = input.required(); + readonly value = input.required(); + readonly valueChange = output(); +} From 6da6c0819c97482c3d1e39c6f611dad6329ca8d0 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 16:09:30 -0700 Subject: [PATCH 05/13] feat(chat): swap chat-debug-toggle for chat-debug-switch in public surface --- .../primitives/chat-debug-toggle.component.ts | 52 ------------------- .../chat-debug/primitives/primitives.spec.ts | 4 +- libs/chat/src/public-api.ts | 2 +- 3 files changed, 3 insertions(+), 55 deletions(-) delete mode 100644 libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-toggle.component.ts diff --git a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-toggle.component.ts b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-toggle.component.ts deleted file mode 100644 index 0b9227f32..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-toggle.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: MIT -import { Component, ChangeDetectionStrategy, input, output } from '@angular/core'; -import { CHAT_HOST_TOKENS } from '../../../styles/chat-tokens'; - -@Component({ - selector: 'chat-debug-toggle', - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - styles: [ - CHAT_HOST_TOKENS, - ` - :host { display: block; } - button { - display: inline-flex; - align-items: center; - gap: var(--ngaf-chat-space-2); - appearance: none; - background: transparent; - border: 1px solid var(--ngaf-chat-separator); - border-radius: var(--ngaf-chat-radius-button); - padding: 4px 10px; - font: inherit; - font-size: var(--ngaf-chat-font-size-sm); - color: var(--ngaf-chat-text); - cursor: pointer; - } - .dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--ngaf-chat-muted); - } - button.is-on .dot { background: var(--ngaf-chat-success); } - `, - ], - template: ` - - `, -}) -export class ChatDebugToggleComponent { - readonly label = input.required(); - readonly value = input.required(); - readonly valueChange = output(); -} diff --git a/libs/chat/src/lib/compositions/chat-debug/primitives/primitives.spec.ts b/libs/chat/src/lib/compositions/chat-debug/primitives/primitives.spec.ts index d440260bb..da3c12bd2 100644 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/primitives.spec.ts +++ b/libs/chat/src/lib/compositions/chat-debug/primitives/primitives.spec.ts @@ -3,13 +3,13 @@ import { describe, it, expect } from 'vitest'; import { ChatDebugSectionComponent } from './chat-debug-section.component'; import { ChatDebugSegmentedComponent } from './chat-debug-segmented.component'; import { ChatDebugSelectComponent } from './chat-debug-select.component'; -import { ChatDebugToggleComponent } from './chat-debug-toggle.component'; +import { ChatDebugSwitchComponent } from './chat-debug-switch.component'; import { ChatDebugActionComponent } from './chat-debug-action.component'; describe('chat-debug primitives are defined', () => { it('section', () => { expect(typeof ChatDebugSectionComponent).toBe('function'); }); it('segmented', () => { expect(typeof ChatDebugSegmentedComponent).toBe('function'); }); it('select', () => { expect(typeof ChatDebugSelectComponent).toBe('function'); }); - it('toggle', () => { expect(typeof ChatDebugToggleComponent).toBe('function'); }); + it('switch', () => { expect(typeof ChatDebugSwitchComponent).toBe('function'); }); it('action', () => { expect(typeof ChatDebugActionComponent).toBe('function'); }); }); diff --git a/libs/chat/src/public-api.ts b/libs/chat/src/public-api.ts index 200cd3c8c..34020a247 100644 --- a/libs/chat/src/public-api.ts +++ b/libs/chat/src/public-api.ts @@ -85,7 +85,7 @@ export { ChatDebugSegmentedComponent } from './lib/compositions/chat-debug/primi export type { SegmentedOption } from './lib/compositions/chat-debug/primitives/chat-debug-segmented.component'; export { ChatDebugSelectComponent } from './lib/compositions/chat-debug/primitives/chat-debug-select.component'; export type { SelectOption } from './lib/compositions/chat-debug/primitives/chat-debug-select.component'; -export { ChatDebugToggleComponent } from './lib/compositions/chat-debug/primitives/chat-debug-toggle.component'; +export { ChatDebugSwitchComponent } from './lib/compositions/chat-debug/primitives/chat-debug-switch.component'; export { ChatDebugActionComponent } from './lib/compositions/chat-debug/primitives/chat-debug-action.component'; export { ChatTimelineSliderComponent } from './lib/compositions/chat-timeline-slider/chat-timeline-slider.component'; export { ChatThreadDrawerComponent } from './lib/compositions/chat-thread-drawer/chat-thread-drawer.component'; From b31c2795ca424d98ea8a49cf06107d8c9cb687f5 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 16:10:42 -0700 Subject: [PATCH 06/13] feat(chat): re-skin chat-debug-section to debug tokens --- .../primitives/chat-debug-section.component.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-section.component.ts b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-section.component.ts index 4926a41d6..ba0f21e07 100644 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-section.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-section.component.ts @@ -1,29 +1,29 @@ // SPDX-License-Identifier: MIT import { Component, ChangeDetectionStrategy, input } from '@angular/core'; -import { CHAT_HOST_TOKENS } from '../../../styles/chat-tokens'; +import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; @Component({ selector: 'chat-debug-section', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, styles: [ - CHAT_HOST_TOKENS, + CHAT_DEBUG_TOKENS, ` :host { display: block; - padding: var(--ngaf-chat-space-4); - border-bottom: 1px solid color-mix(in srgb, var(--ngaf-chat-separator) 60%, transparent); + padding: 14px 16px; + border-bottom: 1px solid var(--ngaf-chat-debug-border); } :host:last-child { border-bottom: 0; } .section__label { - font-size: 10px; + font-size: 11px; font-weight: 600; + letter-spacing: 0.04em; text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--ngaf-chat-text-muted); - margin: 0 0 var(--ngaf-chat-space-3); + color: var(--ngaf-chat-debug-text-subtle); + margin: 0 0 10px; } - .section__body { display: flex; flex-direction: column; gap: var(--ngaf-chat-space-2); } + .section__body { display: flex; flex-direction: column; gap: 10px; } `, ], template: ` From 5a3c69b3f551da519a346be87c8b7c44e4f225e9 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 16:10:44 -0700 Subject: [PATCH 07/13] feat(chat): re-skin chat-debug-segmented to debug tokens --- .../chat-debug-segmented.component.ts | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-segmented.component.ts b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-segmented.component.ts index 73c89273a..e2af2ffbd 100644 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-segmented.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-segmented.component.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT import { Component, ChangeDetectionStrategy, input, output } from '@angular/core'; -import { CHAT_HOST_TOKENS } from '../../../styles/chat-tokens'; +import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; export interface SegmentedOption { readonly value: string; @@ -12,38 +12,40 @@ export interface SegmentedOption { standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, styles: [ - CHAT_HOST_TOKENS, + CHAT_DEBUG_TOKENS, ` :host { display: block; } .segmented { - display: inline-flex; - gap: 0; - padding: 3px; - background: var(--ngaf-chat-surface-alt); - border: 1px solid var(--ngaf-chat-separator); - border-radius: var(--ngaf-chat-radius-button); + display: flex; width: 100%; box-sizing: border-box; + background: var(--ngaf-chat-debug-bg-deep); + border: 1px solid var(--ngaf-chat-debug-border); + border-radius: var(--ngaf-chat-debug-radius-input); + padding: 3px; + gap: 0; } .segmented__btn { + flex: 1; appearance: none; - border: 0; background: transparent; - padding: 6px var(--ngaf-chat-space-3); + border: 0; + color: var(--ngaf-chat-debug-text-muted); + padding: 6px 8px; + border-radius: 5px; font: inherit; - font-size: var(--ngaf-chat-font-size-sm); - font-weight: 500; - color: var(--ngaf-chat-text-muted); + font-size: 12px; cursor: pointer; - border-radius: calc(var(--ngaf-chat-radius-button) - 3px); - flex: 1; - transition: color 120ms ease, background 120ms ease; + transition: background 120ms ease, color 120ms ease; + } + .segmented__btn:hover:not(.is-active) { + background: var(--ngaf-chat-debug-bg); + color: var(--ngaf-chat-debug-text); } - .segmented__btn:hover:not(.is-active) { color: var(--ngaf-chat-text); } .segmented__btn.is-active { - background: var(--ngaf-chat-bg); - color: var(--ngaf-chat-text); - box-shadow: var(--ngaf-chat-shadow-sm), 0 0 0 1px var(--ngaf-chat-separator); + background: var(--ngaf-chat-debug-border); + color: var(--ngaf-chat-debug-text); + font-weight: 500; } `, ], From fd94df4ed130d45684723bfd8ad5df6542ea6562 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 11 May 2026 16:10:46 -0700 Subject: [PATCH 08/13] feat(chat): re-skin chat-debug-select with custom trigger + overlay native select --- .../primitives/chat-debug-select.component.ts | 91 +++++++++++-------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-select.component.ts b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-select.component.ts index 9056dccf5..b4ded00bb 100644 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-select.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-select.component.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -import { Component, ChangeDetectionStrategy, input, output } from '@angular/core'; -import { CHAT_HOST_TOKENS } from '../../../styles/chat-tokens'; +import { Component, ChangeDetectionStrategy, input, output, computed } from '@angular/core'; +import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; export interface SelectOption { readonly value: string; @@ -12,53 +12,70 @@ export interface SelectOption { standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, styles: [ - CHAT_HOST_TOKENS, + CHAT_DEBUG_TOKENS, ` :host { display: block; } - label { + .row { display: flex; align-items: center; justify-content: space-between; - gap: var(--ngaf-chat-space-3); - font-size: var(--ngaf-chat-font-size-sm); - color: var(--ngaf-chat-text); + gap: 12px; + font-size: 13px; + color: var(--ngaf-chat-debug-text); } - .select-wrap { + .select { position: relative; + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 6px; + min-width: 140px; + background: var(--ngaf-chat-debug-bg-deep); + border: 1px solid var(--ngaf-chat-debug-border); + border-radius: 6px; + padding: 6px 10px; + font-size: 12px; + color: var(--ngaf-chat-debug-text); + cursor: pointer; + } + .select:hover { background: #0f0f12; } + .select:focus-within { + border-color: var(--ngaf-chat-debug-accent); + outline: 2px solid color-mix(in srgb, var(--ngaf-chat-debug-accent) 30%, transparent); + outline-offset: 1px; + } + .select__value { flex: 1; - max-width: 60%; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - select { - appearance: none; - -webkit-appearance: none; - width: 100%; - background: var(--ngaf-chat-bg); - color: var(--ngaf-chat-text); - border: 1px solid var(--ngaf-chat-separator); - border-radius: var(--ngaf-chat-radius-button); - padding: 5px 26px 5px 10px; - font: inherit; - font-size: var(--ngaf-chat-font-size-sm); - cursor: pointer; - transition: border-color 120ms ease, background 120ms ease; + .select__caret { + color: var(--ngaf-chat-debug-text-subtle); + font-size: 10px; + line-height: 1; } - select:hover { border-color: var(--ngaf-chat-text-muted); } - select:focus { outline: none; border-color: var(--ngaf-chat-primary); } - .chevron { + .select select { position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - pointer-events: none; - color: var(--ngaf-chat-text-muted); - display: flex; + inset: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; + border: 0; + background: transparent; + font: inherit; + color: inherit; } `, ], template: ` -