From 2299fcc9cdf796a9916b16dd2c5bd9bf852255db Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 18 May 2026 11:41:51 -0700 Subject: [PATCH] Integrate chat debug into sidenav footer --- .../ag-ui/getting-started/introduction.mdx | 3 +- .../content/docs/chat/api/api-docs.json | 542 +++++------------- .../docs/chat/components/chat-debug.mdx | 100 +--- .../concepts/primitives-vs-compositions.mdx | 6 +- .../chat/getting-started/introduction.mdx | 2 +- apps/website/public/AGENTS.md | 4 +- apps/website/public/CLAUDE.md | 4 +- apps/website/scripts/generate-whitepaper.ts | 4 +- .../src/components/blog/AuthorByline.tsx | 1 - cockpit/ag-ui/streaming/angular/project.json | 4 +- cockpit/chat/a2ui/angular/project.json | 4 +- cockpit/chat/debug/angular/project.json | 4 +- .../debug/angular/src/app/debug.component.ts | 2 +- cockpit/chat/debug/python/docs/guide.md | 4 +- .../chat/generative-ui/angular/project.json | 4 +- cockpit/chat/input/angular/project.json | 4 +- cockpit/chat/interrupts/angular/project.json | 4 +- cockpit/chat/messages/angular/project.json | 4 +- cockpit/chat/subagents/angular/project.json | 4 +- cockpit/chat/theming/angular/project.json | 4 +- cockpit/chat/threads/angular/project.json | 4 +- cockpit/chat/timeline/angular/project.json | 4 +- cockpit/chat/tool-calls/angular/project.json | 4 +- .../filesystem/angular/project.json | 4 +- .../deep-agents/memory/angular/project.json | 4 +- .../deep-agents/planning/angular/project.json | 4 +- .../sandboxes/angular/project.json | 4 +- .../deep-agents/skills/angular/project.json | 4 +- .../subagents/angular/project.json | 4 +- .../deployment-runtime/angular/project.json | 4 +- .../durable-execution/angular/project.json | 4 +- .../langgraph/interrupts/angular/project.json | 4 +- cockpit/langgraph/memory/angular/project.json | 4 +- .../persistence/angular/project.json | 4 +- .../langgraph/streaming/angular/project.json | 4 +- .../langgraph/subgraphs/angular/project.json | 4 +- .../time-travel/angular/project.json | 4 +- .../computed-functions/angular/project.json | 4 +- .../element-rendering/angular/project.json | 4 +- cockpit/render/registry/angular/project.json | 4 +- .../render/repeat-loops/angular/project.json | 4 +- .../spec-rendering/angular/project.json | 4 +- .../state-management/angular/project.json | 4 +- .../e2e/sidebar-mode-coexistence.spec.ts | 12 +- examples/chat/angular/project.json | 13 +- .../src/app/shell/demo-shell.component.css | 88 +++ .../src/app/shell/demo-shell.component.html | 111 ++-- .../src/app/shell/demo-shell.component.ts | 17 - examples/chat/smoke/CHECKLIST.md | 25 +- examples/chat/smoke/template/angular.json | 2 +- libs/chat/debug/ng-package.json | 6 + libs/chat/debug/public-api.ts | 3 + .../chat-debug/chat-debug-root-styles.ts | 33 ++ .../chat-debug/chat-debug-tokens.ts | 8 + .../chat-debug/chat-debug.component.ts | 117 ++-- .../compositions/chat-debug/debug-agent.ts | 21 + .../debug-checkpoint-card.component.ts | 4 +- .../chat-debug/debug-state-diff.component.ts | 4 +- .../debug-state-inspector.component.ts | 4 +- .../compositions/chat-debug/debug-utils.ts | 14 + .../inspectors/state-inspector.component.ts | 9 +- .../timeline-inspector.component.ts | 4 +- .../compositions/chat-debug/persistence.ts | 0 .../lib/compositions/chat-debug/state-diff.ts | 0 libs/chat/package.json | 8 - libs/chat/src/lib/a2ui/surface.component.ts | 2 - .../compositions/chat-debug-secondary.spec.ts | 35 ++ .../chat-debug-controls.directive.ts | 15 - .../chat-debug-inspector.directive.ts | 16 - .../chat-debug/chat-debug.component.spec.ts | 179 ------ .../compositions/chat-debug/debug-utils.ts | 14 - .../inspectors/timeline-inspector.spec.ts | 34 -- .../chat-debug/persistence.spec.ts | 46 -- .../primitives/chat-debug-action.component.ts | 40 -- .../chat-debug-section.component.ts | 38 -- .../chat-debug-segmented.component.ts | 71 --- .../primitives/chat-debug-select.component.ts | 106 ---- .../primitives/chat-debug-switch.component.ts | 69 --- .../chat-debug/primitives/primitives.spec.ts | 15 - .../chat-sidenav/chat-debug-gate.ts | 7 + .../chat-sidenav.component.spec.ts | 46 +- .../chat-sidenav/chat-sidenav.component.ts | 149 +++++ .../chat-message-list.component.ts | 2 +- .../src/lib/styles/chat-sidenav.styles.ts | 53 +- libs/chat/src/public-api.ts | 12 - libs/chat/tsconfig.lib.json | 2 +- tools/verify-chat-debug-bundle.mjs | 46 ++ tsconfig.base.json | 1 + 88 files changed, 895 insertions(+), 1406 deletions(-) create mode 100644 libs/chat/debug/ng-package.json create mode 100644 libs/chat/debug/public-api.ts create mode 100644 libs/chat/debug/src/lib/compositions/chat-debug/chat-debug-root-styles.ts rename libs/chat/{ => debug}/src/lib/compositions/chat-debug/chat-debug-tokens.ts (84%) rename libs/chat/{ => debug}/src/lib/compositions/chat-debug/chat-debug.component.ts (86%) create mode 100644 libs/chat/debug/src/lib/compositions/chat-debug/debug-agent.ts rename libs/chat/{ => debug}/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts (96%) rename libs/chat/{ => debug}/src/lib/compositions/chat-debug/debug-state-diff.component.ts (97%) rename libs/chat/{ => debug}/src/lib/compositions/chat-debug/debug-state-inspector.component.ts (91%) create mode 100644 libs/chat/debug/src/lib/compositions/chat-debug/debug-utils.ts rename libs/chat/{ => debug}/src/lib/compositions/chat-debug/inspectors/state-inspector.component.ts (91%) rename libs/chat/{ => debug}/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts (98%) rename libs/chat/{ => debug}/src/lib/compositions/chat-debug/persistence.ts (100%) rename libs/chat/{ => debug}/src/lib/compositions/chat-debug/state-diff.ts (100%) create mode 100644 libs/chat/src/lib/compositions/chat-debug-secondary.spec.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/chat-debug-controls.directive.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/chat-debug-inspector.directive.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/debug-utils.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.spec.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/persistence.spec.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-action.component.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-section.component.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-segmented.component.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-select.component.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-switch.component.ts delete mode 100644 libs/chat/src/lib/compositions/chat-debug/primitives/primitives.spec.ts create mode 100644 libs/chat/src/lib/compositions/chat-sidenav/chat-debug-gate.ts create mode 100644 tools/verify-chat-debug-bundle.mjs diff --git a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx index 42b5b1024..2ce83b8b1 100644 --- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx @@ -10,7 +10,8 @@ AG-UI is the open agent-to-UI protocol from the CopilotKit ecosystem. It standar ```text @ngaf/chat - , , , + , , + @ngaf/chat/debug -> | | Agent contract (signals + events$) | diff --git a/apps/website/content/docs/chat/api/api-docs.json b/apps/website/content/docs/chat/api/api-docs.json index 0d615d726..1ea5dfd14 100644 --- a/apps/website/content/docs/chat/api/api-docs.json +++ b/apps/website/content/docs/chat/api/api-docs.json @@ -2048,363 +2048,6 @@ } ] }, - { - "name": "ChatDebugActionComponent", - "kind": "class", - "description": "", - "params": [], - "examples": [], - "properties": [ - { - "name": "clicked", - "type": "OutputEmitterRef", - "description": "", - "optional": false - }, - { - "name": "label", - "type": "InputSignal", - "description": "", - "optional": false - } - ], - "methods": [] - }, - { - "name": "ChatDebugComponent", - "kind": "class", - "description": "", - "params": [], - "examples": [], - "properties": [ - { - "name": "activeHostInspector", - "type": "Signal | undefined>", - "description": "", - "optional": false - }, - { - "name": "activeTab", - "type": "Signal", - "description": "", - "optional": false - }, - { - "name": "activeTabId", - "type": "WritableSignal", - "description": "", - "optional": false - }, - { - "name": "agent", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "controls", - "type": "Signal | undefined>", - "description": "", - "optional": false - }, - { - "name": "defaultOpen", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "dock", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "dockChange", - "type": "OutputEmitterRef", - "description": "", - "optional": false - }, - { - "name": "dockState", - "type": "WritableSignal", - "description": "", - "optional": false - }, - { - "name": "forkRequested", - "type": "OutputEmitterRef", - "description": "", - "optional": false - }, - { - "name": "hostInspectors", - "type": "Signal", - "description": "", - "optional": false - }, - { - "name": "isStreaming", - "type": "Signal", - "description": "Reads `agent.status()` reactively for the launcher dot.", - "optional": false - }, - { - "name": "open", - "type": "WritableSignal", - "description": "", - "optional": false - }, - { - "name": "openChange", - "type": "OutputEmitterRef", - "description": "", - "optional": false - }, - { - "name": "replayRequested", - "type": "OutputEmitterRef", - "description": "", - "optional": false - }, - { - "name": "storageKey", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "tabs", - "type": "Signal", - "description": "", - "optional": false - } - ], - "methods": [ - { - "name": "onDocumentClick", - "signature": "onDocumentClick(event: MouseEvent)", - "description": "Click-outside dismiss. When the panel is open, any click whose\ncomposed path doesn't include our host element closes the panel.", - "params": [ - { - "name": "event", - "type": "MouseEvent", - "description": "", - "optional": false - } - ] - }, - { - "name": "onEsc", - "signature": "onEsc()", - "description": "", - "params": [] - }, - { - "name": "setActiveTab", - "signature": "setActiveTab(id: string)", - "description": "", - "params": [ - { - "name": "id", - "type": "string", - "description": "", - "optional": false - } - ] - }, - { - "name": "setDock", - "signature": "setDock(next: DockPosition)", - "description": "", - "params": [ - { - "name": "next", - "type": "DockPosition", - "description": "", - "optional": false - } - ] - }, - { - "name": "setOpen", - "signature": "setOpen(value: boolean)", - "description": "", - "params": [ - { - "name": "value", - "type": "boolean", - "description": "", - "optional": false - } - ] - } - ] - }, - { - "name": "ChatDebugControlsDirective", - "kind": "class", - "description": "Marks an `` as the controls slot of ``. Rendered\npinned at the top of the docked panel. Host apps put their app-specific\ncontrols (mode picker, model select, etc.) inside this template.", - "params": [], - "examples": [], - "properties": [ - { - "name": "templateRef", - "type": "TemplateRef", - "description": "", - "optional": false - } - ], - "methods": [] - }, - { - "name": "ChatDebugInspectorDirective", - "kind": "class", - "description": "Marks an `` as a host-registered inspector tab. Each instance\nadds a tab in the docked panel's tab strip, appended after the built-in\nTimeline and State tabs.", - "params": [], - "examples": [], - "properties": [ - { - "name": "label", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "templateRef", - "type": "TemplateRef", - "description": "", - "optional": false - } - ], - "methods": [] - }, - { - "name": "ChatDebugSectionComponent", - "kind": "class", - "description": "", - "params": [], - "examples": [], - "properties": [ - { - "name": "label", - "type": "InputSignal", - "description": "", - "optional": false - } - ], - "methods": [] - }, - { - "name": "ChatDebugSegmentedComponent", - "kind": "class", - "description": "", - "params": [], - "examples": [], - "properties": [ - { - "name": "options", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "value", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "valueChange", - "type": "OutputEmitterRef", - "description": "", - "optional": false - } - ], - "methods": [] - }, - { - "name": "ChatDebugSelectComponent", - "kind": "class", - "description": "", - "params": [], - "examples": [], - "properties": [ - { - "name": "currentLabel", - "type": "Signal", - "description": "", - "optional": false - }, - { - "name": "label", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "options", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "value", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "valueChange", - "type": "OutputEmitterRef", - "description": "", - "optional": false - } - ], - "methods": [ - { - "name": "onChange", - "signature": "onChange(event: Event)", - "description": "", - "params": [ - { - "name": "event", - "type": "Event", - "description": "", - "optional": false - } - ] - } - ] - }, - { - "name": "ChatDebugSwitchComponent", - "kind": "class", - "description": "", - "params": [], - "examples": [], - "properties": [ - { - "name": "label", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "value", - "type": "InputSignal", - "description": "", - "optional": false - }, - { - "name": "valueChange", - "type": "OutputEmitterRef", - "description": "", - "optional": false - } - ], - "methods": [] - }, { "name": "ChatErrorComponent", "kind": "class", @@ -3689,6 +3332,12 @@ "description": "", "optional": false }, + { + "name": "agent", + "type": "InputSignal", + "description": "", + "optional": false + }, { "name": "archivedOpen", "type": "WritableSignal", @@ -3701,6 +3350,24 @@ "description": "", "optional": false }, + { + "name": "debug", + "type": "InputSignal", + "description": "", + "optional": false + }, + { + "name": "forkRequested", + "type": "OutputEmitterRef", + "description": "", + "optional": false + }, + { + "name": "isDebugStreaming", + "type": "Signal", + "description": "", + "optional": false + }, { "name": "mode", "type": "InputSignal", @@ -3755,6 +3422,12 @@ "description": "", "optional": false }, + { + "name": "replayRequested", + "type": "OutputEmitterRef", + "description": "", + "optional": false + }, { "name": "searchOpened", "type": "OutputEmitterRef", @@ -3767,6 +3440,12 @@ "description": "", "optional": false }, + { + "name": "showDebugButton", + "type": "Signal", + "description": "", + "optional": false + }, { "name": "threads", "type": "InputSignal", @@ -3792,6 +3471,19 @@ "signature": "onEscape()", "description": "", "params": [] + }, + { + "name": "openDebug", + "signature": "openDebug(event: MouseEvent)", + "description": "", + "params": [ + { + "name": "event", + "type": "MouseEvent", + "description": "", + "optional": false + } + ] } ] }, @@ -6481,46 +6173,6 @@ ], "examples": [] }, - { - "name": "SegmentedOption", - "kind": "interface", - "description": "", - "properties": [ - { - "name": "label", - "type": "string", - "description": "", - "optional": false - }, - { - "name": "value", - "type": "string", - "description": "", - "optional": false - } - ], - "examples": [] - }, - { - "name": "SelectOption", - "kind": "interface", - "description": "", - "properties": [ - { - "name": "label", - "type": "string", - "description": "", - "optional": false - }, - { - "name": "value", - "type": "string", - "description": "", - "optional": false - } - ], - "examples": [] - }, { "name": "Subagent", "kind": "interface", @@ -6807,13 +6459,6 @@ "signature": "\"pending\" | \"markdown\" | \"json-render\" | \"a2ui\" | \"mixed\"", "examples": [] }, - { - "name": "DockPosition", - "kind": "type", - "description": "", - "signature": "\"right\" | \"bottom\" | \"left\"", - "examples": [] - }, { "name": "InterruptAction", "kind": "type", @@ -7473,5 +7118,94 @@ "description": "" }, "examples": [] + }, + { + "name": "AbstractEvent", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "delta", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "kind", + "type": "\"reasoning-start\" | \"reasoning-chunk\" | \"reasoning-end\" | \"text-start\" | \"text-chunk\" | \"text-end\"", + "description": "", + "optional": false + } + ], + "examples": [] + }, + { + "name": "assertReasoningFixtureMessages", + "kind": "function", + "description": "Assertion — common to both adapters. Throws if the produced messages\ndon't match the shared expectation.", + "signature": "assertReasoningFixtureMessages(messages: readonly Message[]): void", + "params": [ + { + "name": "messages", + "type": "readonly Message[]", + "description": "", + "optional": false + } + ], + "returns": { + "type": "void", + "description": "" + }, + "examples": [] + }, + { + "name": "runAgentConformance", + "kind": "function", + "description": "Runs a suite of contract conformance assertions against a factory that\nproduces a fresh Agent. Adapter packages should call this in their\nown test suites to verify the contract is satisfied.", + "signature": "runAgentConformance(label: string, factory: object): void", + "params": [ + { + "name": "label", + "type": "string", + "description": "", + "optional": false + }, + { + "name": "factory", + "type": "object", + "description": "", + "optional": false + } + ], + "returns": { + "type": "void", + "description": "" + }, + "examples": [] + }, + { + "name": "runAgentWithHistoryConformance", + "kind": "function", + "description": "Conformance suite for AgentWithHistory implementations.\n\nRuns the base Agent conformance suite, then verifies the history\nsignal is present and returns an array of AgentCheckpoint-shaped entries.", + "signature": "runAgentWithHistoryConformance(label: string, factory: object): void", + "params": [ + { + "name": "label", + "type": "string", + "description": "", + "optional": false + }, + { + "name": "factory", + "type": "object", + "description": "", + "optional": false + } + ], + "returns": { + "type": "void", + "description": "" + }, + "examples": [] } ] \ No newline at end of file diff --git a/apps/website/content/docs/chat/components/chat-debug.mdx b/apps/website/content/docs/chat/components/chat-debug.mdx index 808d0bfa7..08ead9444 100644 --- a/apps/website/content/docs/chat/components/chat-debug.mdx +++ b/apps/website/content/docs/chat/components/chat-debug.mdx @@ -1,13 +1,13 @@ # ChatDebugComponent -`ChatDebugComponent` provides a docked development panel for inspecting an `AgentWithHistory`. It includes built-in Timeline and State tabs, emits replay/fork events from timeline checkpoints, and supports host-provided controls and inspector tabs. +`ChatDebugComponent` provides a docked development panel for inspecting an `Agent` or `AgentWithHistory`. It ships from the debug-only secondary entry point so applications can keep debug implementation code out of production bundles unless they explicitly opt in. **Selector:** `chat-debug` **Import:** ```typescript -import { ChatDebugComponent } from '@ngaf/chat'; +import { ChatDebugComponent } from '@ngaf/chat/debug'; ``` ## Basic Usage @@ -15,7 +15,7 @@ import { ChatDebugComponent } from '@ngaf/chat'; ```typescript import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import { ChatDebugComponent } from '@ngaf/chat'; +import { ChatDebugComponent } from '@ngaf/chat/debug'; @Component({ selector: 'app-debug-page', @@ -59,9 +59,10 @@ export class DebugPageComponent { | Input | Type | Default | Description | | --- | --- | --- | --- | -| `agent` | `AgentWithHistory` | **Required** | Agent state and checkpoint history. | +| `agent` | `Agent \| AgentWithHistory` | **Required** | Agent state. Agents with `history()` also enable the Timeline tab. | | `dock` | `'right' \| 'bottom' \| 'left'` | `'right'` | Initial dock position. | | `defaultOpen` | `boolean` | `false` | Initial open state when no persisted state exists. | +| `launcher` | `'floating' \| 'none'` | `'floating'` | Shows the built-in floating launcher, or hides it when another surface opens the panel. | | `storageKey` | `string` | `'chat-debug'` | Local storage key prefix for persisted open/dock/tab state. | ## Outputs @@ -75,88 +76,23 @@ export class DebugPageComponent { `replayRequested` and `forkRequested` are integration hooks. The debug panel does not mutate the agent by itself; the host app decides whether a checkpoint opens a replay view, starts a forked thread, or maps to a backend-specific time travel operation. -## Host Controls +## Sidenav Integration -Use `ChatDebugControlsDirective` to pin app-specific controls at the top of the debug panel. +`ChatSidenavComponent` can own the launcher for apps that already use the sidenav footer. Pass the same agent and keep `[debug]` enabled: -```typescript -import { - ChatDebugComponent, - ChatDebugControlsDirective, - ChatDebugActionComponent, -} from '@ngaf/chat'; - -@Component({ - standalone: true, - imports: [ - ChatDebugComponent, - ChatDebugControlsDirective, - ChatDebugActionComponent, - ], - template: ` - - - - - - `, -}) -export class DebugControlsExample { - // chat = agent(...) - - resetThread() { - // App-specific reset behavior. - } -} +```html + ``` -## Custom Inspector Tabs - -Use `ChatDebugInspectorDirective` to add tabs after the built-in Timeline and State tabs. +The footer button is labelled in expanded and drawer modes. In collapsed mode it uses the status dot only. The dot pulses while `agent.status()` is `running`. -```typescript -import { - ChatDebugComponent, - ChatDebugInspectorDirective, - ChatDebugSectionComponent, -} from '@ngaf/chat'; +## Production Bundles -@Component({ - standalone: true, - imports: [ - ChatDebugComponent, - ChatDebugInspectorDirective, - ChatDebugSectionComponent, - ], - template: ` - - - -
Environment: {{ environment }}
-
-
-
- `, -}) -export class RuntimeInspectorExample { - // chat = agent(...) - protected readonly environment = 'local'; -} -``` - -## Public Debug Primitives +The debug implementation lives under `@ngaf/chat/debug`; the main `@ngaf/chat` entry point no longer exports it. In the canonical Angular demo, normal production builds set `NGAF_CHAT_DEBUG=false` and externalize `@ngaf/chat/debug`, so the debug implementation is absent from the emitted bundle. The `production-debug` build opts back in with `NGAF_CHAT_DEBUG=true`. -These debug primitives are exported for host-provided controls and inspectors: - -| Export | Selector | Purpose | -| --- | --- | --- | -| `ChatDebugControlsDirective` | `ng-template[chatDebugControls]` | Registers the pinned controls slot. | -| `ChatDebugInspectorDirective` | `ng-template[chatDebugInspector]` | Registers a host inspector tab with a label. | -| `ChatDebugSectionComponent` | `chat-debug-section` | Section container for custom inspector content. | -| `ChatDebugSegmentedComponent` | `chat-debug-segmented` | Segmented control primitive. | -| `ChatDebugSelectComponent` | `chat-debug-select` | Select primitive. | -| `ChatDebugSwitchComponent` | `chat-debug-switch` | Switch primitive. | -| `ChatDebugActionComponent` | `chat-debug-action` | Full-width action button primitive. | +Keep debug controls for your app outside the debug panel. The debug panel intentionally exposes a small fixed surface so consumers do not expect demo-specific controls to appear in their own applications. diff --git a/apps/website/content/docs/chat/concepts/primitives-vs-compositions.mdx b/apps/website/content/docs/chat/concepts/primitives-vs-compositions.mdx index c16866ad2..7c81cf7b8 100644 --- a/apps/website/content/docs/chat/concepts/primitives-vs-compositions.mdx +++ b/apps/website/content/docs/chat/concepts/primitives-vs-compositions.mdx @@ -43,10 +43,10 @@ Use `` when you want the library to own the chat shell. /> ``` -`` is also a composition. It is built for development and inspection, not end-user chat layout. +`` is also a composition. It is built for development and inspection, not end-user chat layout, and ships from the debug-only secondary entry point. ```ts -import { ChatDebugComponent } from '@ngaf/chat'; +import { ChatDebugComponent } from '@ngaf/chat/debug'; ``` ```html @@ -56,6 +56,8 @@ import { ChatDebugComponent } from '@ngaf/chat'; Use `chat-debug` when you need timeline and state inspection while building. Do not treat it as a customer-facing control surface unless you intentionally design around that. +If you use ``, pass `[agent]="agent"` and `[debug]="true"` to get the built-in footer launcher instead of adding a separate floating debug button. + ## Primitives Primitives are standalone Angular components and directives. They let you assemble your own shell while keeping the tested chat pieces. diff --git a/apps/website/content/docs/chat/getting-started/introduction.mdx b/apps/website/content/docs/chat/getting-started/introduction.mdx index 8806bf8d0..6d160844c 100644 --- a/apps/website/content/docs/chat/getting-started/introduction.mdx +++ b/apps/website/content/docs/chat/getting-started/introduction.mdx @@ -37,7 +37,7 @@ Compositions are opinionated, styled components that combine primitives into rea | `ChatInterruptPanelComponent` | `chat-interrupt-panel` | Styled interrupt card with accept/edit/respond/ignore actions | | `ChatToolCallCardComponent` | `chat-tool-call-card` | Expandable card showing tool name, inputs, and output | | `ChatSubagentCardComponent` | `chat-subagent-card` | Expandable card showing subagent status and messages | -| `ChatDebugComponent` | `chat-debug` | Full debug cockpit with timeline, state inspector, and diff viewer | +| `ChatDebugComponent` | `chat-debug` | Debug cockpit from `@ngaf/chat/debug` with timeline, state inspector, and diff viewer | | `ChatTimelineSliderComponent` | `chat-timeline-slider` | Slider-based timeline navigation | ## How the Stack Fits Together diff --git a/apps/website/public/AGENTS.md b/apps/website/public/AGENTS.md index b42a35792..904575732 100644 --- a/apps/website/public/AGENTS.md +++ b/apps/website/public/AGENTS.md @@ -1,4 +1,4 @@ -# Agent UI for Angular v0.0.32 +# Agent UI for Angular v0.0.37 Agent UI primitives and LangGraph streaming adapters for Angular. @@ -37,7 +37,7 @@ export class ChatComponent { - Thread persistence: `threadId: signal(localStorage.getItem('t'))` + `onThreadId: (id) => localStorage.setItem('t', id)` - Global config: `provideAgent({ apiUrl })` in app.config.ts - Per-call override: pass `apiUrl` directly to `agent()` -- Testing: use `MockAgentTransport` - never mock `agent()` itself +- Testing: use `MockAgentTransport` — never mock `agent()` itself ## Version check If this file is stale, fetch the latest: https://cacheplane.ai/llms-full.txt diff --git a/apps/website/public/CLAUDE.md b/apps/website/public/CLAUDE.md index b42a35792..904575732 100644 --- a/apps/website/public/CLAUDE.md +++ b/apps/website/public/CLAUDE.md @@ -1,4 +1,4 @@ -# Agent UI for Angular v0.0.32 +# Agent UI for Angular v0.0.37 Agent UI primitives and LangGraph streaming adapters for Angular. @@ -37,7 +37,7 @@ export class ChatComponent { - Thread persistence: `threadId: signal(localStorage.getItem('t'))` + `onThreadId: (id) => localStorage.setItem('t', id)` - Global config: `provideAgent({ apiUrl })` in app.config.ts - Per-call override: pass `apiUrl` directly to `agent()` -- Testing: use `MockAgentTransport` - never mock `agent()` itself +- Testing: use `MockAgentTransport` — never mock `agent()` itself ## Version check If this file is stale, fetch the latest: https://cacheplane.ai/llms-full.txt diff --git a/apps/website/scripts/generate-whitepaper.ts b/apps/website/scripts/generate-whitepaper.ts index 10876c695..8d1ad7a07 100644 --- a/apps/website/scripts/generate-whitepaper.ts +++ b/apps/website/scripts/generate-whitepaper.ts @@ -18,7 +18,7 @@ Use only the current API surface: - @ngaf/langgraph exposes agent(), provideAgent(), LangGraphAgent, MockAgentTransport, FetchStreamTransport, and mockLangGraphAgent(). - agent() returns a runtime-neutral chat surface with messages(), status(), isLoading(), error(), toolCalls(), state(), submit(), stop(), regenerate(), interrupt(), subagents(), and LangGraph-specific langGraph* signals. status() returns only 'idle' | 'running' | 'error'. Use isLoading() for loading UI. interrupt() is AgentInterrupt | undefined on the runtime-neutral surface. - Configure LangGraph with assistantId and apiUrl. Do not use graphId or url as @ngaf/langgraph option names. -- @ngaf/chat consumes the runtime-neutral Agent contract and exports ChatComponent, ChatMessageListComponent, ChatInputComponent, ChatToolCallsComponent, ChatToolCallCardComponent, ChatInterruptPanelComponent, and ChatDebugComponent. Selectors are , , , , , , and . +- @ngaf/chat consumes the runtime-neutral Agent contract and exports ChatComponent, ChatMessageListComponent, ChatInputComponent, ChatToolCallsComponent, ChatToolCallCardComponent, and ChatInterruptPanelComponent. The debug-only secondary entry point @ngaf/chat/debug exports ChatDebugComponent. Selectors are , , , , , , and . - Chat messages use Message[] from @ngaf/chat for the runtime-neutral surface. Raw LangGraph messages, when needed, are exposed through langGraphMessages(). - Angular examples should call agent() directly in a component field initializer, for example: readonly chat = agent({ assistantId: 'chat', threadId: this.threadId, onThreadId: id => this.threadId.set(id) }). Do not inject LangGraphAgent as a service. Do not invent a wrapper service around LangGraphAgent. - @ngaf/render exposes render-spec, defineAngularRegistry(), provideRender(), signalStateStore(), JSON Patch streaming, and A2UI support. Registry examples may pass [registry] directly to or configure provideRender({ registry }). @@ -523,7 +523,7 @@ Tone: Direct, technical, peer-to-peer. No fluff. Audience is senior Angular engi Chapter topic: Debug Tooling -Context: Debugging agent chat is hard. The message stream is opaque, tool call state transitions are fast, and interrupt flows have timing edge cases. chat-debug is @ngaf/chat's built-in debug panel — a developer overlay that surfaces agent state, raw message events, tool call history, and interrupt state in real time. +Context: Debugging agent chat is hard. The message stream is opaque, tool call state transitions are fast, and interrupt flows have timing edge cases. chat-debug is @ngaf/chat/debug's built-in debug panel — a developer overlay that surfaces agent state, raw message events, tool call history, and interrupt state in real time. Cover: - What chat-debug shows: Message[] state, streaming event log, tool call state machine, interrupt payload diff --git a/apps/website/src/components/blog/AuthorByline.tsx b/apps/website/src/components/blog/AuthorByline.tsx index c33403bbd..6be0acd52 100644 --- a/apps/website/src/components/blog/AuthorByline.tsx +++ b/apps/website/src/components/blog/AuthorByline.tsx @@ -12,7 +12,6 @@ export function AuthorByline({ author }: { author: Author }) { }} > {author.avatar ? ( - // eslint-disable-next-line @next/next/no-img-element {`${author.name} Add a debug panel to your chat interface using `ChatDebugComponent` -from `@ngaf/chat`. This replaces `ChatComponent` and provides +from `@ngaf/chat/debug`. This replaces `ChatComponent` and provides full development inspection capabilities. @@ -19,7 +19,7 @@ Use `ChatDebugComponent` instead of `ChatComponent` for the full debug experience: ```typescript -import { ChatDebugComponent } from '@ngaf/chat'; +import { ChatDebugComponent } from '@ngaf/chat/debug'; ``` diff --git a/cockpit/chat/generative-ui/angular/project.json b/cockpit/chat/generative-ui/angular/project.json index df0489f91..fcb2f1f31 100644 --- a/cockpit/chat/generative-ui/angular/project.json +++ b/cockpit/chat/generative-ui/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/chat/input/angular/project.json b/cockpit/chat/input/angular/project.json index 1ac396d5d..39d448429 100644 --- a/cockpit/chat/input/angular/project.json +++ b/cockpit/chat/input/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/chat/interrupts/angular/project.json b/cockpit/chat/interrupts/angular/project.json index a59abba46..dfe98eef7 100644 --- a/cockpit/chat/interrupts/angular/project.json +++ b/cockpit/chat/interrupts/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/chat/messages/angular/project.json b/cockpit/chat/messages/angular/project.json index a48080255..7666bfb97 100644 --- a/cockpit/chat/messages/angular/project.json +++ b/cockpit/chat/messages/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/chat/subagents/angular/project.json b/cockpit/chat/subagents/angular/project.json index 41b40529d..8747981c3 100644 --- a/cockpit/chat/subagents/angular/project.json +++ b/cockpit/chat/subagents/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/chat/theming/angular/project.json b/cockpit/chat/theming/angular/project.json index 0ff5c5c17..a9779cb8b 100644 --- a/cockpit/chat/theming/angular/project.json +++ b/cockpit/chat/theming/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/chat/threads/angular/project.json b/cockpit/chat/threads/angular/project.json index f93127671..f4ebf004f 100644 --- a/cockpit/chat/threads/angular/project.json +++ b/cockpit/chat/threads/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/chat/timeline/angular/project.json b/cockpit/chat/timeline/angular/project.json index d7b50c9c7..b3e9c34e1 100644 --- a/cockpit/chat/timeline/angular/project.json +++ b/cockpit/chat/timeline/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/chat/tool-calls/angular/project.json b/cockpit/chat/tool-calls/angular/project.json index 0ae421187..748dae6fe 100644 --- a/cockpit/chat/tool-calls/angular/project.json +++ b/cockpit/chat/tool-calls/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/deep-agents/filesystem/angular/project.json b/cockpit/deep-agents/filesystem/angular/project.json index 8ea454c56..649cf78ea 100644 --- a/cockpit/deep-agents/filesystem/angular/project.json +++ b/cockpit/deep-agents/filesystem/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/deep-agents/memory/angular/project.json b/cockpit/deep-agents/memory/angular/project.json index 8de461993..3a7df8372 100644 --- a/cockpit/deep-agents/memory/angular/project.json +++ b/cockpit/deep-agents/memory/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/deep-agents/planning/angular/project.json b/cockpit/deep-agents/planning/angular/project.json index a66fdf3bf..42f115982 100644 --- a/cockpit/deep-agents/planning/angular/project.json +++ b/cockpit/deep-agents/planning/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/deep-agents/sandboxes/angular/project.json b/cockpit/deep-agents/sandboxes/angular/project.json index fc812171e..9b9f1ab66 100644 --- a/cockpit/deep-agents/sandboxes/angular/project.json +++ b/cockpit/deep-agents/sandboxes/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/deep-agents/skills/angular/project.json b/cockpit/deep-agents/skills/angular/project.json index 837c6d41d..18cb12bdf 100644 --- a/cockpit/deep-agents/skills/angular/project.json +++ b/cockpit/deep-agents/skills/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/deep-agents/subagents/angular/project.json b/cockpit/deep-agents/subagents/angular/project.json index 2bb825312..644ccc00f 100644 --- a/cockpit/deep-agents/subagents/angular/project.json +++ b/cockpit/deep-agents/subagents/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/langgraph/deployment-runtime/angular/project.json b/cockpit/langgraph/deployment-runtime/angular/project.json index 2843a0ff7..bd9809d87 100644 --- a/cockpit/langgraph/deployment-runtime/angular/project.json +++ b/cockpit/langgraph/deployment-runtime/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/langgraph/durable-execution/angular/project.json b/cockpit/langgraph/durable-execution/angular/project.json index 320e36cba..84185da15 100644 --- a/cockpit/langgraph/durable-execution/angular/project.json +++ b/cockpit/langgraph/durable-execution/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/langgraph/interrupts/angular/project.json b/cockpit/langgraph/interrupts/angular/project.json index acb85fd8d..20e4a9c1c 100644 --- a/cockpit/langgraph/interrupts/angular/project.json +++ b/cockpit/langgraph/interrupts/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/langgraph/memory/angular/project.json b/cockpit/langgraph/memory/angular/project.json index 49f84126e..a097b9915 100644 --- a/cockpit/langgraph/memory/angular/project.json +++ b/cockpit/langgraph/memory/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/langgraph/persistence/angular/project.json b/cockpit/langgraph/persistence/angular/project.json index ebfecf197..32cee1844 100644 --- a/cockpit/langgraph/persistence/angular/project.json +++ b/cockpit/langgraph/persistence/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/langgraph/streaming/angular/project.json b/cockpit/langgraph/streaming/angular/project.json index fcd2584fe..d4dc9f373 100644 --- a/cockpit/langgraph/streaming/angular/project.json +++ b/cockpit/langgraph/streaming/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/langgraph/subgraphs/angular/project.json b/cockpit/langgraph/subgraphs/angular/project.json index 41b601ace..4171db44e 100644 --- a/cockpit/langgraph/subgraphs/angular/project.json +++ b/cockpit/langgraph/subgraphs/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/langgraph/time-travel/angular/project.json b/cockpit/langgraph/time-travel/angular/project.json index 19a4f9de6..c4a763db3 100644 --- a/cockpit/langgraph/time-travel/angular/project.json +++ b/cockpit/langgraph/time-travel/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/render/computed-functions/angular/project.json b/cockpit/render/computed-functions/angular/project.json index 2d4d095e2..cf2cc8080 100644 --- a/cockpit/render/computed-functions/angular/project.json +++ b/cockpit/render/computed-functions/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/render/element-rendering/angular/project.json b/cockpit/render/element-rendering/angular/project.json index acfd2caca..92f3423dd 100644 --- a/cockpit/render/element-rendering/angular/project.json +++ b/cockpit/render/element-rendering/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/render/registry/angular/project.json b/cockpit/render/registry/angular/project.json index 6168371c5..ac3b01917 100644 --- a/cockpit/render/registry/angular/project.json +++ b/cockpit/render/registry/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/render/repeat-loops/angular/project.json b/cockpit/render/repeat-loops/angular/project.json index 4b45d2e82..d20e63f2e 100644 --- a/cockpit/render/repeat-loops/angular/project.json +++ b/cockpit/render/repeat-loops/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/render/spec-rendering/angular/project.json b/cockpit/render/spec-rendering/angular/project.json index b43976268..091b4e623 100644 --- a/cockpit/render/spec-rendering/angular/project.json +++ b/cockpit/render/spec-rendering/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/cockpit/render/state-management/angular/project.json b/cockpit/render/state-management/angular/project.json index 252b14998..94eee30f7 100644 --- a/cockpit/render/state-management/angular/project.json +++ b/cockpit/render/state-management/angular/project.json @@ -30,8 +30,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "10kb", + "maximumError": "16kb" } ], "outputHashing": "none" diff --git a/examples/chat/angular/e2e/sidebar-mode-coexistence.spec.ts b/examples/chat/angular/e2e/sidebar-mode-coexistence.spec.ts index 34c6db052..239e0c28b 100644 --- a/examples/chat/angular/e2e/sidebar-mode-coexistence.spec.ts +++ b/examples/chat/angular/e2e/sidebar-mode-coexistence.spec.ts @@ -4,10 +4,10 @@ import { test, expect } from '@playwright/test'; test.describe('chat-debug × chat-sidebar coexistence', () => { test('sidebar launcher remains reachable while chat-debug is open', async ({ page }) => { await page.goto('/sidebar'); + await expect(page.locator('chat-sidebar')).toBeAttached(); - // Open chat-debug via its floating top-right launcher (class `.launcher` - // on the chat-debug host — sidebar's launcher uses a different class). - await page.locator('.launcher').click(); + // Open chat-debug from the sidenav footer. + await page.locator('.chat-sidenav__debug').click(); // Debug auto-picks bottom dock because is present. const debugPanel = page.locator('.panel.panel--bottom'); @@ -35,11 +35,11 @@ test.describe('chat-debug × chat-sidebar coexistence', () => { test('user override survives mode switch: explicit right-dock stays right', async ({ page }) => { await page.goto('/embed'); - await page.locator('.launcher').click(); + await page.locator('.chat-sidenav__debug').click(); // Click right-dock explicitly — sets userDockOverride. await page.locator('.panel__dock-btn').nth(2).click(); // 0=left, 1=bottom, 2=right - // Switch to sidebar mode via the debug palette's Mode segmented control. - await page.locator('.segmented__btn', { hasText: 'Sidebar' }).click(); + // Switch to sidebar mode via the demo-owned top toolbar. + await page.locator('.demo-shell__segmented-button', { hasText: 'Sidebar' }).click(); // Debug stays right-docked despite chat-sidebar now being on the page. await expect(page.locator('.panel.panel--right')).toBeVisible(); await expect(page.locator('.panel.panel--bottom')).not.toBeVisible(); diff --git a/examples/chat/angular/project.json b/examples/chat/angular/project.json index 06814c630..8505eb0c1 100644 --- a/examples/chat/angular/project.json +++ b/examples/chat/angular/project.json @@ -23,13 +23,24 @@ }, "configurations": { "production": { + "define": { "NGAF_CHAT_DEBUG": "false" }, + "externalDependencies": ["@ngaf/chat/debug"], "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1.5mb" }, - { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "16kb" } + { "type": "anyComponentStyle", "maximumWarning": "10kb", "maximumError": "16kb" } + ], + "outputHashing": "all" + }, + "production-debug": { + "define": { "NGAF_CHAT_DEBUG": "true" }, + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1.5mb" }, + { "type": "anyComponentStyle", "maximumWarning": "10kb", "maximumError": "16kb" } ], "outputHashing": "all" }, "development": { + "define": { "NGAF_CHAT_DEBUG": "false" }, "optimization": false, "extractLicenses": false, "sourceMap": true, 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 8379f9468..a3a48eced 100644 --- a/examples/chat/angular/src/app/shell/demo-shell.component.css +++ b/examples/chat/angular/src/app/shell/demo-shell.component.css @@ -37,6 +37,9 @@ height: 100%; transition: padding-left 200ms ease; padding-left: 0; + display: flex; + flex-direction: column; + min-width: 0; } .demo-shell__main[data-sidenav-mode="expanded"] { padding-left: var(--ngaf-chat-sidenav-width-expanded, 280px); @@ -48,6 +51,91 @@ .demo-shell__main[data-sidenav-mode] { padding-left: 0; } } +.demo-shell__toolbar { + flex: 0 0 auto; + min-height: 48px; + display: flex; + align-items: center; + gap: 10px; + padding: 8px 14px; + border-bottom: 1px solid color-mix(in srgb, var(--ngaf-chat-text, #e6e9ef) 12%, transparent); + background: color-mix(in srgb, var(--ngaf-chat-bg, #0f1115) 94%, transparent); + color: var(--ngaf-chat-text, #e6e9ef); + box-sizing: border-box; + overflow-x: auto; +} + +.demo-shell__segmented { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 2px; + border-radius: 8px; + border: 1px solid color-mix(in srgb, var(--ngaf-chat-text, #e6e9ef) 14%, transparent); + background: color-mix(in srgb, var(--ngaf-chat-text, #e6e9ef) 4%, transparent); + flex: 0 0 auto; +} + +.demo-shell__segmented-button, +.demo-shell__toolbar-action, +.demo-shell__field select { + font: inherit; + font-size: 12px; + color: var(--ngaf-chat-text, #e6e9ef); +} + +.demo-shell__segmented-button { + border: 0; + background: transparent; + border-radius: 6px; + min-height: 28px; + padding: 0 10px; + cursor: pointer; +} + +.demo-shell__segmented-button.is-active { + background: var(--ngaf-chat-text, #e6e9ef); + color: var(--ngaf-chat-bg, #0f1115); +} + +.demo-shell__field { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--ngaf-chat-text-muted, #9aa3b2); + font-size: 12px; + flex: 0 0 auto; +} + +.demo-shell__field select { + height: 30px; + border-radius: 7px; + border: 1px solid color-mix(in srgb, var(--ngaf-chat-text, #e6e9ef) 14%, transparent); + background: var(--ngaf-chat-bg, #0f1115); + padding: 0 26px 0 8px; +} + +.demo-shell__toolbar-action { + height: 30px; + border-radius: 7px; + border: 1px solid color-mix(in srgb, var(--ngaf-chat-text, #e6e9ef) 18%, transparent); + background: transparent; + padding: 0 10px; + cursor: pointer; + flex: 0 0 auto; +} + +.demo-shell__toolbar-action:hover, +.demo-shell__segmented-button:hover:not(.is-active) { + background: color-mix(in srgb, var(--ngaf-chat-text, #e6e9ef) 8%, transparent); +} + +@media (max-width: 767px) { + .demo-shell__toolbar { + padding-left: 60px; + } +} + .demo-shell__interrupt-panel { position: fixed; left: 50%; 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 dda897b91..8ff2c1d8b 100644 --- a/examples/chat/angular/src/app/shell/demo-shell.component.html +++ b/examples/chat/angular/src/app/shell/demo-shell.component.html @@ -17,6 +17,8 @@ [projectActions]="projectActions" [activeThreadId]="threadIdSignal() ?? ''" [mode]="sidenavMode()" + [agent]="agent" + [debug]="true" [(open)]="drawerOpen" [actions]="threadActions" (newChat)="onNewThread()" @@ -26,6 +28,8 @@ (searchOpened)="paletteOpen.set(true)" (openChange)="onSidenavOpenChange($event)" (modeChange)="onSidenavModeChange($event)" + (replayRequested)="onTimelineReplay($event)" + (forkRequested)="onTimelineFork($event)" > + } + + + + + + + + + + + + + @if (agent.interrupt && agent.interrupt()) {
@@ -78,57 +136,4 @@ (threadSelected)="onSearchSelect($event)" (closed)="paletteOpen.set(false)" /> - - - - - - - - - - - - - - - - - - - -
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 ab275e21e..e6243fcea 100644 --- a/examples/chat/angular/src/app/shell/demo-shell.component.ts +++ b/examples/chat/angular/src/app/shell/demo-shell.component.ts @@ -15,12 +15,6 @@ import { filter, map, startWith } from 'rxjs/operators'; import { agent } from '@ngaf/langgraph'; import { NgafTelemetryService } from '@ngaf/telemetry/browser'; import { - ChatDebugComponent, - ChatDebugControlsDirective, - ChatDebugSectionComponent, - ChatDebugSegmentedComponent, - ChatDebugSelectComponent, - ChatDebugActionComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, ChatSidenavComponent, @@ -54,12 +48,6 @@ function modeFromUrl(url: string): DemoMode { standalone: true, imports: [ RouterOutlet, - ChatDebugComponent, - ChatDebugControlsDirective, - ChatDebugSectionComponent, - ChatDebugSegmentedComponent, - ChatDebugSelectComponent, - ChatDebugActionComponent, ChatInterruptPanelComponent, ChatSubagentsComponent, ChatSidenavComponent, @@ -264,11 +252,6 @@ export class DemoShell { { value: 'json-render', label: 'json-render' }, ]); - protected readonly colorSchemeOptions = [ - { value: 'light', label: 'Light' }, - { value: 'dark', label: 'Dark' }, - ] as const; - protected readonly themeOptions = signal([ { value: 'default-dark', label: 'Default dark' }, { value: 'default-light', label: 'Default light' }, diff --git a/examples/chat/smoke/CHECKLIST.md b/examples/chat/smoke/CHECKLIST.md index 41b5ef09a..e678726a1 100644 --- a/examples/chat/smoke/CHECKLIST.md +++ b/examples/chat/smoke/CHECKLIST.md @@ -114,18 +114,17 @@ renders correctly both during streaming and after completion. ## chat-debug devtools -- [ ] Floating debug launcher button visible in the top-right corner -- [ ] Launcher has `role="button"`, accessible name "Open chat devtools" -- [ ] Click launcher — debug panel opens (zinc-dark chrome, shadcn-styled) -- [ ] Panel header shows a status pill (idle / loading / streaming) that updates live -- [ ] Panel has a switch toggle for verbose/quiet mode -- [ ] Panel shows current agent signals — status, message count, thread id, model +- [ ] Sidenav footer shows a labelled `Debug` button in expanded/drawer mode +- [ ] Collapsed sidenav shows the debug status dot without the text label +- [ ] Debug dot pulses while the agent is streaming +- [ ] Click the footer debug button — debug panel opens +- [ ] Panel shows current agent state and timeline when checkpoint history is available - [ ] Panel updates live as messages stream - [ ] Click the close affordance — panel unmounts; launcher remains ### Coexistence with chat-sidebar -- [ ] Switch to Sidebar mode via the palette — debug panel auto-redocks to the bottom (was: right) +- [ ] Switch to Sidebar mode via the demo toolbar — debug panel auto-redocks to the bottom (was: right) - [ ] Open the sidebar launcher (bottom-right) — slides in over the demo bg; debug bottom panel stays visible at the LEFT of the sidebar - [ ] Manually click the right-dock icon — debug moves to the right edge of the demo bg (NOT under the sidebar); user override sticks for the rest of the session @@ -133,15 +132,11 @@ renders correctly both during streaming and after completion. - [ ] No `console.error` while toggling - [ ] DOM has no `` element when closed -## Control palette +## Demo toolbar -- [ ] Palette renders top-right with the v2 shadcn-styled panel (rounded, zinc surface) -- [ ] Status pill near the palette top reflects agent state (idle / loading / streaming) -- [ ] Click collapse handle — palette shrinks to a single icon -- [ ] Click icon — palette re-expands -- [ ] Collapsed/expanded state persists across reload -- [ ] Palette never overlaps the chat input -- [ ] Palette stays above any popup/sidebar in z-order +- [ ] Top toolbar controls mode, model, effort, Gen UI mode, theme, and new conversation +- [ ] Toolbar remains demo-owned and does not appear inside the debug panel +- [ ] Toolbar stays reachable in embed, popup, and sidebar modes ## Color scheme (Light / Dark) diff --git a/examples/chat/smoke/template/angular.json b/examples/chat/smoke/template/angular.json index 1e228d2e4..ec62e47bd 100644 --- a/examples/chat/smoke/template/angular.json +++ b/examples/chat/smoke/template/angular.json @@ -23,7 +23,7 @@ "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kB", "maximumError": "2MB" }, - { "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "16kB" } + { "type": "anyComponentStyle", "maximumWarning": "10kB", "maximumError": "16kB" } ], "outputHashing": "all" }, diff --git a/libs/chat/debug/ng-package.json b/libs/chat/debug/ng-package.json new file mode 100644 index 000000000..ed278942e --- /dev/null +++ b/libs/chat/debug/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/libs/chat/debug/public-api.ts b/libs/chat/debug/public-api.ts new file mode 100644 index 000000000..688b0f15a --- /dev/null +++ b/libs/chat/debug/public-api.ts @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: MIT +export { ChatDebugComponent } from './src/lib/compositions/chat-debug/chat-debug.component'; +export type { DockPosition } from './src/lib/compositions/chat-debug/chat-debug.component'; diff --git a/libs/chat/debug/src/lib/compositions/chat-debug/chat-debug-root-styles.ts b/libs/chat/debug/src/lib/compositions/chat-debug/chat-debug-root-styles.ts new file mode 100644 index 000000000..cf6fe5032 --- /dev/null +++ b/libs/chat/debug/src/lib/compositions/chat-debug/chat-debug-root-styles.ts @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +const CHAT_DEBUG_ROOT_STYLES = ` +@layer ngaf-chat-debug { + :root { + --ngaf-chat-sidebar-claim-right: 0px; + --ngaf-chat-debug-panel-size-h: 40vh; + --ngaf-chat-debug-panel-size-w: 420px; + } + :root[data-ngaf-chat-debug="bottom"] { + --ngaf-chat-debug-claim-bottom: var(--ngaf-chat-debug-panel-size-h, 40vh); + --ngaf-chat-occupy-bottom: var(--ngaf-chat-debug-panel-size-h, 40vh); + } + :root[data-ngaf-chat-debug="right"] { + --ngaf-chat-debug-claim-right: var(--ngaf-chat-debug-panel-size-w, 420px); + --ngaf-chat-occupy-right: var(--ngaf-chat-debug-panel-size-w, 420px); + } + :root[data-ngaf-chat-debug="left"] { + --ngaf-chat-debug-claim-left: var(--ngaf-chat-debug-panel-size-w, 420px); + --ngaf-chat-occupy-left: var(--ngaf-chat-debug-panel-size-w, 420px); + } +} +`; + +const STYLE_ELEMENT_ID = 'ngaf-chat-debug-root-styles'; + +export function ensureChatDebugRootStyles(): void { + if (typeof document === 'undefined') return; + if (document.getElementById(STYLE_ELEMENT_ID)) return; + const style = document.createElement('style'); + style.id = STYLE_ELEMENT_ID; + style.textContent = CHAT_DEBUG_ROOT_STYLES; + document.head.appendChild(style); +} diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug-tokens.ts b/libs/chat/debug/src/lib/compositions/chat-debug/chat-debug-tokens.ts similarity index 84% rename from libs/chat/src/lib/compositions/chat-debug/chat-debug-tokens.ts rename to libs/chat/debug/src/lib/compositions/chat-debug/chat-debug-tokens.ts index 49711035b..45c4411c0 100644 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug-tokens.ts +++ b/libs/chat/debug/src/lib/compositions/chat-debug/chat-debug-tokens.ts @@ -48,5 +48,13 @@ export const CHAT_DEBUG_TOKENS = ` --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); + --ngaf-chat-font-size-xs: 12px; + --ngaf-chat-font-mono: var(--ngaf-chat-debug-font-mono); + --ngaf-chat-radius-card: 8px; + --ngaf-chat-success: var(--ngaf-chat-debug-success); + --ngaf-chat-error-bg: color-mix(in srgb, #ef4444 18%, transparent); + --ngaf-chat-error-text: #fca5a5; + --ngaf-chat-warning-bg: color-mix(in srgb, #f59e0b 18%, transparent); + --ngaf-chat-warning-text: #fcd34d; } `; diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts b/libs/chat/debug/src/lib/compositions/chat-debug/chat-debug.component.ts similarity index 86% rename from libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts rename to libs/chat/debug/src/lib/compositions/chat-debug/chat-debug.component.ts index 7b30c3acc..343d96857 100644 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts +++ b/libs/chat/debug/src/lib/compositions/chat-debug/chat-debug.component.ts @@ -3,22 +3,18 @@ import { Component, ChangeDetectionStrategy, computed, - contentChild, - contentChildren, effect, ElementRef, HostListener, inject, input, + OnInit, output, signal, } from '@angular/core'; -import { NgTemplateOutlet } from '@angular/common'; -import type { AgentWithHistory } from '../../agent'; import { CHAT_DEBUG_TOKENS } from './chat-debug-tokens'; -import { ensureChatRootStyles } from '../../styles/chat-tokens'; -import { ChatDebugControlsDirective } from './chat-debug-controls.directive'; -import { ChatDebugInspectorDirective } from './chat-debug-inspector.directive'; +import { ensureChatDebugRootStyles } from './chat-debug-root-styles'; +import type { DebugAgent, DebugAgentWithHistory } from './debug-agent'; import { TimelineInspectorComponent } from './inspectors/timeline-inspector.component'; import { StateInspectorComponent } from './inspectors/state-inspector.component'; import { createPersistence } from './persistence'; @@ -28,18 +24,17 @@ export type DockPosition = 'right' | 'bottom' | 'left'; interface TabEntry { readonly id: string; readonly label: string; - readonly kind: 'builtin-timeline' | 'builtin-state' | 'host'; - readonly hostIndex?: number; + readonly kind: 'builtin-timeline' | 'builtin-state'; +} + +function hasHistory(agent: DebugAgent | DebugAgentWithHistory): agent is DebugAgentWithHistory { + return typeof (agent as DebugAgentWithHistory).history === 'function'; } @Component({ selector: 'chat-debug', standalone: true, - imports: [ - NgTemplateOutlet, - TimelineInspectorComponent, - StateInspectorComponent, - ], + imports: [TimelineInspectorComponent, StateInspectorComponent], changeDetection: ChangeDetectionStrategy.OnPush, styles: [ CHAT_DEBUG_TOKENS, @@ -248,7 +243,7 @@ interface TabEntry { `, ], template: ` - @if (!open()) { + @if (!open() && launcher() === 'floating') { - } @else { + } @else if (agent(); as currentAgent) {
- @if (controls()) { -
- -
- } - @if (tabs().length > 1) {
@for (tab of tabs(); track tab.id) { @@ -325,19 +314,16 @@ interface TabEntry {
@switch (activeTab()?.kind) { @case ('builtin-timeline') { - + @if (historyAgent(); as history) { + + } } @case ('builtin-state') { - - } - @case ('host') { - @if (activeHostInspector(); as host) { - - } + } }
@@ -345,10 +331,11 @@ interface TabEntry { } `, }) -export class ChatDebugComponent { - readonly agent = input.required(); +export class ChatDebugComponent implements OnInit { + readonly agent = input(null); readonly dock = input('right'); readonly defaultOpen = input(false); + readonly launcher = input<'floating' | 'none'>('floating'); readonly storageKey = input('chat-debug'); readonly replayRequested = output(); @@ -356,9 +343,6 @@ export class ChatDebugComponent { 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'); /** Set to `true` the first time the user explicitly clicks a dock button. @@ -366,24 +350,24 @@ export class ChatDebugComponent { * fresh session = fresh chance for the smart default. */ private readonly userDockOverride = signal(false); protected readonly activeTabId = signal('timeline'); + protected readonly historyAgent = computed(() => { + const agent = this.agent(); + return agent && hasHistory(agent) ? agent : null; + }); /** Reads `agent.status()` reactively for the launcher dot. */ protected readonly isStreaming = computed(() => { - const status = this.agent().status?.(); + 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, - })); + if (!this.agent()) return []; return [ - { id: 'timeline', label: 'Timeline', kind: 'builtin-timeline' }, + ...(this.historyAgent() + ? [{ id: 'timeline', label: 'Timeline', kind: 'builtin-timeline' } satisfies TabEntry] + : []), { id: 'state', label: 'State', kind: 'builtin-state' }, - ...host, ]; }); @@ -391,28 +375,20 @@ export class ChatDebugComponent { 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() { // Inject chat lib root CSS custom properties so the theme-attribute // mappings + edge-claim primitive are in the document, even when // chat-debug is mounted without a sibling chat composition. - ensureChatRootStyles(); - // 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); + ensureChatDebugRootStyles(); + + effect(() => { + const tabs = this.tabs(); + if (tabs.length === 0) return; + if (tabs.some((tab) => tab.id === this.activeTabId())) return; + this.activeTabId.set(tabs[0].id); + }); // Write-through effect — reads each writable signal so subsequent // changes trigger a fresh run that writes them all to storage. @@ -446,11 +422,22 @@ export class ChatDebugComponent { if (this.userDockOverride()) return; if (typeof document === 'undefined') return; if (!document.querySelector('chat-sidebar')) return; - // Untracked write so we don't re-trigger this effect via dockState. - queueMicrotask(() => this.dockState.set('bottom')); + this.dockState.set('bottom'); }); } + ngOnInit(): void { + // Restore once after inputs are initialized; rebinding storageKey/defaults + // later is intentionally 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); + } + setOpen(value: boolean): void { this.open.set(value); this.openChange.emit(value); diff --git a/libs/chat/debug/src/lib/compositions/chat-debug/debug-agent.ts b/libs/chat/debug/src/lib/compositions/chat-debug/debug-agent.ts new file mode 100644 index 000000000..891aa874a --- /dev/null +++ b/libs/chat/debug/src/lib/compositions/chat-debug/debug-agent.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +import type { Signal } from '@angular/core'; + +export interface DebugAgentCheckpoint { + id?: string; + label?: string; + values?: Record; +} + +export interface DebugAgent { + messages: Signal; + status: Signal; + isLoading: Signal; + error: Signal; + toolCalls: Signal; + state: Signal>; +} + +export interface DebugAgentWithHistory extends DebugAgent { + history: Signal; +} diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts b/libs/chat/debug/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts similarity index 96% rename from libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts rename to libs/chat/debug/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts index 6545639d5..accef9cf6 100644 --- a/libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts +++ b/libs/chat/debug/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts @@ -5,7 +5,7 @@ import { output, ChangeDetectionStrategy, } from '@angular/core'; -import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens'; +import { CHAT_DEBUG_TOKENS } from './chat-debug-tokens'; export interface DebugCheckpoint { node?: string; @@ -19,7 +19,7 @@ export interface DebugCheckpoint { standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, styles: [ - CHAT_HOST_TOKENS, + CHAT_DEBUG_TOKENS, ` .debug-checkpoint-card { width: 100%; diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts b/libs/chat/debug/src/lib/compositions/chat-debug/debug-state-diff.component.ts similarity index 97% rename from libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts rename to libs/chat/debug/src/lib/compositions/chat-debug/debug-state-diff.component.ts index 232b3c375..cafd7b601 100644 --- a/libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts +++ b/libs/chat/debug/src/lib/compositions/chat-debug/debug-state-diff.component.ts @@ -8,7 +8,7 @@ import { import { JsonPipe } from '@angular/common'; import { computeStateDiff } from './state-diff'; import type { DiffEntry } from './state-diff'; -import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens'; +import { CHAT_DEBUG_TOKENS } from './chat-debug-tokens'; @Component({ selector: 'chat-debug-state-diff', @@ -16,7 +16,7 @@ import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens'; imports: [JsonPipe], changeDetection: ChangeDetectionStrategy.OnPush, styles: [ - CHAT_HOST_TOKENS, + CHAT_DEBUG_TOKENS, ` .debug-state-diff__empty { font-size: var(--ngaf-chat-font-size-xs); diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts b/libs/chat/debug/src/lib/compositions/chat-debug/debug-state-inspector.component.ts similarity index 91% rename from libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts rename to libs/chat/debug/src/lib/compositions/chat-debug/debug-state-inspector.component.ts index 4b4f569ef..43b7e8df0 100644 --- a/libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts +++ b/libs/chat/debug/src/lib/compositions/chat-debug/debug-state-inspector.component.ts @@ -5,7 +5,7 @@ import { ChangeDetectionStrategy, } from '@angular/core'; import { JsonPipe } from '@angular/common'; -import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens'; +import { CHAT_DEBUG_TOKENS } from './chat-debug-tokens'; @Component({ selector: 'chat-debug-state-inspector', @@ -13,7 +13,7 @@ import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens'; imports: [JsonPipe], changeDetection: ChangeDetectionStrategy.OnPush, styles: [ - CHAT_HOST_TOKENS, + CHAT_DEBUG_TOKENS, ` .debug-state-inspector { overflow: auto; diff --git a/libs/chat/debug/src/lib/compositions/chat-debug/debug-utils.ts b/libs/chat/debug/src/lib/compositions/chat-debug/debug-utils.ts new file mode 100644 index 000000000..d089396ea --- /dev/null +++ b/libs/chat/debug/src/lib/compositions/chat-debug/debug-utils.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +import type { DebugAgentCheckpoint } from './debug-agent'; +import type { DebugCheckpoint } from './debug-checkpoint-card.component'; + +export function toDebugCheckpoint(cp: DebugAgentCheckpoint, index: number): DebugCheckpoint { + return { + node: cp.label ?? `Step ${index + 1}`, + checkpointId: cp.id, + }; +} + +export function extractStateValues(cp: DebugAgentCheckpoint | undefined): Record { + return cp?.values ?? {}; +} diff --git a/libs/chat/src/lib/compositions/chat-debug/inspectors/state-inspector.component.ts b/libs/chat/debug/src/lib/compositions/chat-debug/inspectors/state-inspector.component.ts similarity index 91% rename from libs/chat/src/lib/compositions/chat-debug/inspectors/state-inspector.component.ts rename to libs/chat/debug/src/lib/compositions/chat-debug/inspectors/state-inspector.component.ts index 923b6bceb..70b23c4e4 100644 --- a/libs/chat/src/lib/compositions/chat-debug/inspectors/state-inspector.component.ts +++ b/libs/chat/debug/src/lib/compositions/chat-debug/inspectors/state-inspector.component.ts @@ -1,9 +1,8 @@ // SPDX-License-Identifier: MIT import { Component, ChangeDetectionStrategy, input, computed, signal } from '@angular/core'; -import type { AgentWithHistory } from '../../../agent'; +import type { DebugAgent, DebugAgentWithHistory } from '../debug-agent'; import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; import { DebugStateInspectorComponent } from '../debug-state-inspector.component'; -import { extractStateValues } from '../debug-utils'; @Component({ selector: 'chat-debug-state-tab', @@ -79,12 +78,10 @@ import { extractStateValues } from '../debug-utils'; `, }) export class StateInspectorComponent { - readonly agent = input.required(); + readonly agent = input.required(); readonly state = computed((): Record => { - const history = this.agent().history(); - const last = history[history.length - 1]; - return extractStateValues(last); + return this.agent().state(); }); protected readonly justCopied = signal(false); diff --git a/libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts b/libs/chat/debug/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts similarity index 98% rename from libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts rename to libs/chat/debug/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts index f3c7de657..830d8f6a1 100644 --- a/libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts +++ b/libs/chat/debug/src/lib/compositions/chat-debug/inspectors/timeline-inspector.component.ts @@ -8,7 +8,7 @@ import { signal, HostListener, } from '@angular/core'; -import type { AgentWithHistory } from '../../../agent'; +import type { DebugAgentWithHistory } from '../debug-agent'; import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; import { DebugCheckpointCardComponent, type DebugCheckpoint } from '../debug-checkpoint-card.component'; import { DebugStateDiffComponent } from '../debug-state-diff.component'; @@ -183,7 +183,7 @@ export function stepSelection(dir: Direction, current: number, count: number): n `, }) export class TimelineInspectorComponent { - readonly agent = input.required(); + readonly agent = input.required(); readonly replayRequested = output(); readonly forkRequested = output(); diff --git a/libs/chat/src/lib/compositions/chat-debug/persistence.ts b/libs/chat/debug/src/lib/compositions/chat-debug/persistence.ts similarity index 100% rename from libs/chat/src/lib/compositions/chat-debug/persistence.ts rename to libs/chat/debug/src/lib/compositions/chat-debug/persistence.ts diff --git a/libs/chat/src/lib/compositions/chat-debug/state-diff.ts b/libs/chat/debug/src/lib/compositions/chat-debug/state-diff.ts similarity index 100% rename from libs/chat/src/lib/compositions/chat-debug/state-diff.ts rename to libs/chat/debug/src/lib/compositions/chat-debug/state-diff.ts diff --git a/libs/chat/package.json b/libs/chat/package.json index 6f5eaa881..aecce5375 100644 --- a/libs/chat/package.json +++ b/libs/chat/package.json @@ -2,14 +2,6 @@ "name": "@ngaf/chat", "version": "0.0.37", "exports": { - ".": { - "types": "./index.d.ts", - "default": "./fesm2022/ngaf-chat.mjs" - }, - "./testing": { - "types": "./testing.d.ts", - "default": "./fesm2022/ngaf-chat-testing.mjs" - }, "./chat.css": "./chat.css", "./themes/default-dark.css": "./themes/default-dark.css", "./themes/default-light.css": "./themes/default-light.css", diff --git a/libs/chat/src/lib/a2ui/surface.component.ts b/libs/chat/src/lib/a2ui/surface.component.ts index 067f876aa..a2caeda26 100644 --- a/libs/chat/src/lib/a2ui/surface.component.ts +++ b/libs/chat/src/lib/a2ui/surface.component.ts @@ -8,7 +8,6 @@ import { RenderSpecComponent, toRenderRegistry } from '@ngaf/render'; import type { ViewRegistry, RenderEvent } from '@ngaf/render'; import { surfaceToSpec } from './surface-to-spec'; import { buildA2uiActionMessage } from './build-action-message'; -import { A2uiSlotDirective } from './a2ui-slot.directive'; import { A2uiDefaultFallbackComponent } from './a2ui-default-fallback.component'; import type { A2uiSurfaceState } from './surface-store'; import type { A2uiViews } from './views'; @@ -18,7 +17,6 @@ import type { A2uiViews } from './views'; standalone: true, imports: [ RenderSpecComponent, - A2uiSlotDirective, A2uiDefaultFallbackComponent, NgComponentOutlet, ], diff --git a/libs/chat/src/lib/compositions/chat-debug-secondary.spec.ts b/libs/chat/src/lib/compositions/chat-debug-secondary.spec.ts new file mode 100644 index 000000000..588823e0c --- /dev/null +++ b/libs/chat/src/lib/compositions/chat-debug-secondary.spec.ts @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { describe, expect, it } from 'vitest'; +import { mockAgent } from '../testing/mock-agent'; +import { ChatDebugComponent } from '../../../debug/public-api'; +import { StateInspectorComponent } from '../../../debug/src/lib/compositions/chat-debug/inspectors/state-inspector.component'; + +describe('chat debug secondary entrypoint', () => { + it('exports ChatDebugComponent from @ngaf/chat/debug', () => { + expect(typeof ChatDebugComponent).toBe('function'); + }); + + it('accepts a base Agent without checkpoint history for state inspection', () => { + const fixture = TestBed.createComponent(StateInspectorComponent); + fixture.componentRef.setInput('agent', mockAgent({ state: { count: 2 } })); + fixture.detectChanges(); + expect((fixture.componentInstance as unknown as { state: () => Record }).state()).toEqual({ count: 2 }); + }); + + it('renders through the secondary component with a base Agent', () => { + @Component({ + standalone: true, + imports: [ChatDebugComponent], + template: ``, + }) + class Host { + agent = mockAgent({ state: { ready: true } }); + } + + const fixture = TestBed.createComponent(Host); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('[aria-label="Chat debug"]')).not.toBeNull(); + }); +}); diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug-controls.directive.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug-controls.directive.ts deleted file mode 100644 index a652c7977..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug-controls.directive.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -import { Directive, TemplateRef, inject } from '@angular/core'; - -/** - * Marks an `` as the controls slot of ``. Rendered - * pinned at the top of the docked panel. Host apps put their app-specific - * controls (mode picker, model select, etc.) inside this template. - */ -@Directive({ - selector: 'ng-template[chatDebugControls]', - standalone: true, -}) -export class ChatDebugControlsDirective { - readonly templateRef = inject(TemplateRef); -} diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug-inspector.directive.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug-inspector.directive.ts deleted file mode 100644 index 319fe5290..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug-inspector.directive.ts +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -import { Directive, TemplateRef, inject, input } from '@angular/core'; - -/** - * Marks an `` as a host-registered inspector tab. Each instance - * adds a tab in the docked panel's tab strip, appended after the built-in - * Timeline and State tabs. - */ -@Directive({ - selector: 'ng-template[chatDebugInspector]', - standalone: true, -}) -export class ChatDebugInspectorDirective { - readonly label = input.required(); - readonly templateRef = inject(TemplateRef); -} diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts deleted file mode 100644 index d3061f7a0..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: MIT -import { describe, it, expect, afterEach } from 'vitest'; -import { TestBed } from '@angular/core/testing'; -import { ElementRef } from '@angular/core'; -import { computeStateDiff } from './state-diff'; -import type { DiffEntry } from './state-diff'; -import { toDebugCheckpoint, extractStateValues } from './debug-utils'; -import type { AgentCheckpoint } from '../../agent'; -import { DebugCheckpointCardComponent } from './debug-checkpoint-card.component'; -import { ChatDebugComponent } from './chat-debug.component'; - -// ── computeStateDiff (unchanged from previous spec) ──────────────────────── - -describe('computeStateDiff', () => { - it('detects added keys', () => { - const result = computeStateDiff({}, { name: 'Alice' }); - expect(result).toEqual([ - { path: 'name', type: 'added', after: 'Alice' }, - ]); - }); - it('detects removed keys', () => { - const result = computeStateDiff({ name: 'Alice' }, {}); - expect(result).toEqual([ - { path: 'name', type: 'removed', before: 'Alice' }, - ]); - }); - it('detects changed keys', () => { - const result = computeStateDiff({ count: 1 }, { count: 2 }); - expect(result).toEqual([ - { path: 'count', type: 'changed', before: 1, after: 2 }, - ]); - }); - it('returns empty array when states are identical', () => { - expect(computeStateDiff({ a: 1 }, { a: 1 })).toEqual([]); - }); - it('recurses into nested objects', () => { - const result = computeStateDiff( - { config: { theme: 'light' } }, - { config: { theme: 'dark' } }, - ); - expect(result).toEqual([ - { path: 'config.theme', type: 'changed', before: 'light', after: 'dark' }, - ]); - }); - it('treats array changes as a single changed entry', () => { - const result = computeStateDiff({ items: [1] }, { items: [1, 2] }); - expect(result).toEqual([ - { path: 'items', type: 'changed', before: [1], after: [1, 2] }, - ]); - }); -}); - -// ── toDebugCheckpoint ────────────────────────────────────────────────────── - -describe('toDebugCheckpoint', () => { - it('uses label as node name when available', () => { - const cp: AgentCheckpoint = { id: 'cp1', label: 'agent', values: {} }; - const result = toDebugCheckpoint(cp, 0); - expect(result.node).toBe('agent'); - expect(result.checkpointId).toBe('cp1'); - }); - it('falls back to Step N when label is absent', () => { - const cp: AgentCheckpoint = { values: {} }; - expect(toDebugCheckpoint(cp, 2).node).toBe('Step 3'); - }); -}); - -// ── extractStateValues ───────────────────────────────────────────────────── - -describe('extractStateValues', () => { - it('returns empty object for undefined checkpoint', () => { - expect(extractStateValues(undefined)).toEqual({}); - }); - it('extracts values from a AgentCheckpoint', () => { - const cp: AgentCheckpoint = { values: { messages: [], count: 5 } }; - expect(extractStateValues(cp)).toEqual({ messages: [], count: 5 }); - }); -}); - -// ── Defined-as-class smoke tests ────────────────────────────────────────── - -describe('ChatDebugComponent', () => { - it('is defined as a class', () => { - expect(typeof ChatDebugComponent).toBe('function'); - }); -}); - -describe('DebugCheckpointCardComponent', () => { - it('is defined as a class', () => { - expect(typeof DebugCheckpointCardComponent).toBe('function'); - }); -}); - -describe('ChatDebugComponent — edge-claim attribute', () => { - afterEach(() => { - document.documentElement.removeAttribute('data-ngaf-chat-debug'); - }); - - it('reads PEER --ngaf-chat-sidebar-claim-right (not aggregate occupy-right)', () => { - // Reading the aggregate occupy-right causes self-feedback: when - // chat-debug docks right, it WRITES occupy-right; if it also READS - // occupy-right, the panel offsets itself by its own width. Read the - // peer-specific sidebar-claim-right instead. - const styles = (ChatDebugComponent as unknown as { ɵcmp: { styles: string[] } }).ɵcmp.styles.join('\n'); - expect(styles).toMatch(/\.panel--bottom[^{]*\{[^}]*right:\s*var\(--ngaf-chat-sidebar-claim-right/); - expect(styles).toMatch(/\.panel--right[^{]*\{[^}]*right:\s*var\(--ngaf-chat-sidebar-claim-right/); - }); -}); - -describe('ChatDebugComponent — mobile coexistence', () => { - it('contains a mobile-breakpoint rule guarding the bottom panel', () => { - const styles = (ChatDebugComponent as unknown as { ɵcmp: { styles: string[] } }).ɵcmp.styles.join('\n'); - expect(styles).toMatch(/@media[^{]*max-width:\s*767px[^{]*\{[^}]*\.panel--bottom[^}]*display:\s*none/); - }); -}); - -describe('ChatDebugComponent — auto-dock', () => { - afterEach(() => { - document.documentElement.removeAttribute('data-ngaf-chat-debug'); - document.querySelectorAll('chat-sidebar').forEach((n) => n.remove()); - if (typeof localStorage !== 'undefined') localStorage.clear(); - }); - - it('auto-switches to bottom dock when a sibling chat-sidebar exists', async () => { - // Stage a chat-sidebar element on the page so the detector finds it. - const sidebarEl = document.createElement('chat-sidebar'); - document.body.appendChild(sidebarEl); - - TestBed.configureTestingModule({ - providers: [ - { provide: ElementRef, useValue: new ElementRef(document.createElement('div')) }, - ], - }); - const debug = TestBed.runInInjectionContext(() => { - const d = new ChatDebugComponent(); - d.setOpen(true); - TestBed.flushEffects(); - return d; - }); - // Drain the microtask queued by the auto-dock effect. - await Promise.resolve(); - TestBed.flushEffects(); - // dockState was 'right' default, sidebar detection flips to 'bottom'. - expect((debug as unknown as { dockState: () => string }).dockState()).toBe('bottom'); - }); - - it('does NOT auto-switch when no chat-sidebar is present', () => { - TestBed.configureTestingModule({ - providers: [ - { provide: ElementRef, useValue: new ElementRef(document.createElement('div')) }, - ], - }); - TestBed.runInInjectionContext(() => { - const debug = new ChatDebugComponent(); - debug.setOpen(true); - TestBed.flushEffects(); - expect((debug as unknown as { dockState: () => string }).dockState()).toBe('right'); - }); - }); - - it('user clicking a dock button prevents subsequent auto-switching', () => { - TestBed.configureTestingModule({ - providers: [ - { provide: ElementRef, useValue: new ElementRef(document.createElement('div')) }, - ], - }); - TestBed.runInInjectionContext(() => { - const debug = new ChatDebugComponent(); - // User explicitly picks right - debug.setDock('right'); - // Now stage a sidebar — should NOT override the user's choice - const sidebarEl = document.createElement('chat-sidebar'); - document.body.appendChild(sidebarEl); - debug.setOpen(true); - TestBed.flushEffects(); - expect((debug as unknown as { dockState: () => string }).dockState()).toBe('right'); - }); - }); -}); diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-utils.ts b/libs/chat/src/lib/compositions/chat-debug/debug-utils.ts deleted file mode 100644 index 66ca5a0ec..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/debug-utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -import type { AgentCheckpoint } from '../../agent'; -import type { DebugCheckpoint } from './debug-checkpoint-card.component'; - -export function toDebugCheckpoint(cp: AgentCheckpoint, index: number): DebugCheckpoint { - return { - node: cp.label ?? `Step ${index + 1}`, - checkpointId: cp.id, - }; -} - -export function extractStateValues(cp: AgentCheckpoint | undefined): Record { - return cp?.values ?? {}; -} diff --git a/libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.spec.ts b/libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.spec.ts deleted file mode 100644 index e3c2480c3..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/inspectors/timeline-inspector.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -import { describe, it, expect } from 'vitest'; -import { stepSelection, type Direction } from './timeline-inspector.component'; - -describe('stepSelection', () => { - it('moves down when not at end', () => { - expect(stepSelection('down', 0, 3)).toBe(1); - }); - - it('does not move past last index', () => { - expect(stepSelection('down', 2, 3)).toBe(2); - }); - - it('moves up when not at start', () => { - expect(stepSelection('up', 2, 3)).toBe(1); - }); - - it('does not move below 0', () => { - expect(stepSelection('up', 0, 3)).toBe(0); - }); - - it('jumps to start', () => { - expect(stepSelection('home', 5, 10)).toBe(0); - }); - - it('jumps to end', () => { - expect(stepSelection('end', 0, 4)).toBe(3); - }); - - it('returns -1 when count is 0', () => { - expect(stepSelection('down', -1, 0)).toBe(-1); - expect(stepSelection('end', -1, 0)).toBe(-1); - }); -}); diff --git a/libs/chat/src/lib/compositions/chat-debug/persistence.spec.ts b/libs/chat/src/lib/compositions/chat-debug/persistence.spec.ts deleted file mode 100644 index 16e75a2bc..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/persistence.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT -import { describe, it, expect, beforeEach } from 'vitest'; -import { createPersistence } from './persistence'; - -describe('createPersistence', () => { - beforeEach(() => { - localStorage.clear(); - }); - - it('reads undefined when no key set', () => { - const p = createPersistence('test'); - expect(p.read('dock')).toBeUndefined(); - }); - - it('round-trips a string value under the namespaced key', () => { - const p = createPersistence('test'); - p.write('dock', 'bottom'); - expect(p.read('dock')).toBe('bottom'); - expect(localStorage.getItem('test:dock')).toBe('"bottom"'); - }); - - it('round-trips a number value', () => { - const p = createPersistence('test'); - p.write('size', 480); - expect(p.read('size')).toBe(480); - }); - - it('round-trips a boolean value', () => { - const p = createPersistence('test'); - p.write('open', true); - expect(p.read('open')).toBe(true); - }); - - it('returns undefined when stored JSON is malformed', () => { - localStorage.setItem('test:dock', '{not-json'); - const p = createPersistence('test'); - expect(p.read('dock')).toBeUndefined(); - }); - - it('isolates by prefix', () => { - const a = createPersistence('a'); - const b = createPersistence('b'); - a.write('open', true); - expect(b.read('open')).toBeUndefined(); - }); -}); diff --git a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-action.component.ts b/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-action.component.ts deleted file mode 100644 index 80337a356..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-action.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -import { Component, ChangeDetectionStrategy, input, output } from '@angular/core'; -import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; - -@Component({ - selector: 'chat-debug-action', - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - styles: [ - CHAT_DEBUG_TOKENS, - ` - :host { display: block; } - button { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - background: var(--ngaf-chat-debug-bg); - color: var(--ngaf-chat-debug-text); - border: 1px solid var(--ngaf-chat-debug-border-strong); - border-radius: var(--ngaf-chat-debug-radius-input); - padding: 8px; - font: inherit; - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: background 120ms ease, transform 80ms ease; - } - button:hover { background: var(--ngaf-chat-debug-surface); } - button:active { transform: translateY(1px); } - `, - ], - template: ` - - `, -}) -export class ChatDebugActionComponent { - readonly label = input.required(); - readonly clicked = output(); -} 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 deleted file mode 100644 index ba0f21e07..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-section.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT -import { Component, ChangeDetectionStrategy, input } from '@angular/core'; -import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; - -@Component({ - selector: 'chat-debug-section', - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - styles: [ - CHAT_DEBUG_TOKENS, - ` - :host { - display: block; - padding: 14px 16px; - border-bottom: 1px solid var(--ngaf-chat-debug-border); - } - :host:last-child { border-bottom: 0; } - .section__label { - font-size: 11px; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--ngaf-chat-debug-text-subtle); - margin: 0 0 10px; - } - .section__body { display: flex; flex-direction: column; gap: 10px; } - `, - ], - template: ` - @if (label()) { - - } -
- `, -}) -export class ChatDebugSectionComponent { - readonly label = input(''); -} 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 deleted file mode 100644 index e2af2ffbd..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-segmented.component.ts +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: MIT -import { Component, ChangeDetectionStrategy, input, output } from '@angular/core'; -import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; - -export interface SegmentedOption { - readonly value: string; - readonly label: string; -} - -@Component({ - selector: 'chat-debug-segmented', - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - styles: [ - CHAT_DEBUG_TOKENS, - ` - :host { display: block; } - .segmented { - 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; - background: transparent; - border: 0; - color: var(--ngaf-chat-debug-text-muted); - padding: 6px 8px; - border-radius: 5px; - font: inherit; - font-size: 12px; - cursor: pointer; - 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.is-active { - background: var(--ngaf-chat-debug-border); - color: var(--ngaf-chat-debug-text); - font-weight: 500; - } - `, - ], - template: ` -
- @for (opt of options(); track opt.value) { - - } -
- `, -}) -export class ChatDebugSegmentedComponent { - readonly options = input.required(); - readonly value = input.required(); - readonly valueChange = output(); -} 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 deleted file mode 100644 index b4ded00bb..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-select.component.ts +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: MIT -import { Component, ChangeDetectionStrategy, input, output, computed } from '@angular/core'; -import { CHAT_DEBUG_TOKENS } from '../chat-debug-tokens'; - -export interface SelectOption { - readonly value: string; - readonly label: string; -} - -@Component({ - selector: 'chat-debug-select', - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - styles: [ - CHAT_DEBUG_TOKENS, - ` - :host { display: block; } - .row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - font-size: 13px; - color: var(--ngaf-chat-debug-text); - } - .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; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .select__caret { - color: var(--ngaf-chat-debug-text-subtle); - font-size: 10px; - line-height: 1; - } - .select select { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - opacity: 0; - cursor: pointer; - border: 0; - background: transparent; - font: inherit; - color: inherit; - } - `, - ], - template: ` - - `, -}) -export class ChatDebugSelectComponent { - readonly label = input.required(); - readonly options = input.required(); - readonly value = input.required(); - readonly valueChange = output(); - - protected readonly currentLabel = computed((): string => { - const v = this.value(); - return this.options().find((o) => o.value === v)?.label ?? v; - }); - - protected onChange(event: Event): void { - this.valueChange.emit((event.target as HTMLSelectElement).value); - } -} 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 deleted file mode 100644 index 3a4bcd507..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/chat-debug-switch.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -// 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(); -} 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 deleted file mode 100644 index da3c12bd2..000000000 --- a/libs/chat/src/lib/compositions/chat-debug/primitives/primitives.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -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 { 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('switch', () => { expect(typeof ChatDebugSwitchComponent).toBe('function'); }); - it('action', () => { expect(typeof ChatDebugActionComponent).toBe('function'); }); -}); diff --git a/libs/chat/src/lib/compositions/chat-sidenav/chat-debug-gate.ts b/libs/chat/src/lib/compositions/chat-sidenav/chat-debug-gate.ts new file mode 100644 index 000000000..4b21cc8af --- /dev/null +++ b/libs/chat/src/lib/compositions/chat-sidenav/chat-debug-gate.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +declare const NGAF_CHAT_DEBUG: boolean; +declare const ngDevMode: boolean; + +export const CHAT_DEBUG_INCLUDED = + ngDevMode || + (typeof NGAF_CHAT_DEBUG !== 'undefined' && NGAF_CHAT_DEBUG === true); diff --git a/libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.spec.ts b/libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.spec.ts index ae722d7e1..c5014d21e 100644 --- a/libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.spec.ts +++ b/libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.spec.ts @@ -4,12 +4,21 @@ import { Component } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { describe, expect, it } from 'vitest'; import { ChatSidenavComponent } from './chat-sidenav.component'; - -function render(opts: { mode?: 'expanded' | 'collapsed' | 'drawer'; open?: boolean; threads?: unknown[] | null } = {}) { +import { mockAgent } from '../../testing/mock-agent'; + +function render(opts: { + mode?: 'expanded' | 'collapsed' | 'drawer'; + open?: boolean; + threads?: unknown[] | null; + agent?: ReturnType | null; + debug?: boolean; +} = {}) { const fixture = TestBed.createComponent(ChatSidenavComponent); if (opts.mode) fixture.componentRef.setInput('mode', opts.mode); if (opts.open !== undefined) fixture.componentRef.setInput('open', opts.open); if (opts.threads !== undefined) fixture.componentRef.setInput('threads', opts.threads); + if (opts.agent !== undefined) fixture.componentRef.setInput('agent', opts.agent); + if (opts.debug !== undefined) fixture.componentRef.setInput('debug', opts.debug); fixture.detectChanges(); return fixture; } @@ -328,6 +337,39 @@ describe('ChatSidenavComponent — footer slots', () => { }); }); +describe('ChatSidenavComponent — debug footer affordance', () => { + it('omits the debug footer button when no agent is provided', () => { + const fixture = render(); + expect(fixture.nativeElement.querySelector('.chat-sidenav__debug')).toBeNull(); + }); + + it('omits the debug footer button when debug is disabled', () => { + const fixture = render({ agent: mockAgent(), debug: false }); + expect(fixture.nativeElement.querySelector('.chat-sidenav__debug')).toBeNull(); + }); + + it('renders a labeled debug button in expanded mode when an agent is provided', () => { + const fixture = render({ mode: 'expanded', agent: mockAgent() }); + const button = fixture.nativeElement.querySelector('.chat-sidenav__debug') as HTMLButtonElement | null; + expect(button).not.toBeNull(); + expect(button?.getAttribute('aria-label')).toBe('Open chat debug'); + expect(button?.textContent?.trim()).toBe('Debug'); + }); + + it('renders the debug button without visible label in collapsed mode', () => { + const fixture = render({ mode: 'collapsed', agent: mockAgent() }); + const button = fixture.nativeElement.querySelector('.chat-sidenav__debug') as HTMLButtonElement | null; + expect(button).not.toBeNull(); + expect(button?.textContent?.trim()).toBe(''); + }); + + it('marks the debug status dot as streaming while the agent is running', () => { + const agent = mockAgent({ status: 'running' }); + const fixture = render({ agent }); + expect(fixture.nativeElement.querySelector('.chat-sidenav__debug-dot--streaming')).not.toBeNull(); + }); +}); + describe('ChatSidenavComponent — New chat primary CTA', () => { it('renders the new-chat button with a monochrome text-color CTA token', () => { // Styles array is the second member of @Component decorator metadata. diff --git a/libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.ts b/libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.ts index c93e1743f..752456167 100644 --- a/libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.ts +++ b/libs/chat/src/lib/compositions/chat-sidenav/chat-sidenav.component.ts @@ -3,11 +3,18 @@ import { Component, ChangeDetectionStrategy, + ComponentRef, DestroyRef, + effect, + Injector, + computed, inject, input, output, signal, + viewChild, + ViewContainerRef, + type OutputRefSubscription, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { fromEvent } from 'rxjs'; @@ -23,8 +30,20 @@ import { type Project, type ProjectActionAdapter, } from '../../primitives/chat-project-list/chat-project-list.component'; +import type { Agent, AgentWithHistory } from '../../agent'; +import { CHAT_DEBUG_INCLUDED } from './chat-debug-gate'; export type ChatSidenavMode = 'expanded' | 'collapsed' | 'drawer'; +type ChatDebugDock = 'right' | 'bottom' | 'left'; + +interface ChatDebugInstance { + setOpen(value: boolean): void; + setDock?(dock: ChatDebugDock): void; + replayRequested?: { subscribe(callback: (checkpointId: string) => void): OutputRefSubscription }; + forkRequested?: { subscribe(callback: (checkpointId: string) => void): OutputRefSubscription }; + openChange?: { subscribe(callback: (open: boolean) => void): OutputRefSubscription }; + dockChange?: { subscribe(callback: (dock: ChatDebugDock) => void): OutputRefSubscription }; +} @Component({ selector: 'chat-sidenav', @@ -175,6 +194,24 @@ export type ChatSidenavMode = 'expanded' | 'collapsed' | 'drawer'; + `, }) export class ChatSidenavComponent { @@ -217,6 +255,8 @@ export class ChatSidenavComponent { readonly projects = input(null); readonly selectedProjectId = input(null); readonly projectActions = input(null); + readonly agent = input(null); + readonly debug = input(true); readonly newChat = output(); readonly threadSelected = output(); @@ -225,12 +265,37 @@ export class ChatSidenavComponent { readonly modeChange = output(); readonly projectSelected = output(); readonly newProjectRequested = output(); + readonly replayRequested = output(); + readonly forkRequested = output(); protected readonly archivedOpen = signal(false); + protected readonly showDebugButton = computed(() => + CHAT_DEBUG_INCLUDED && this.debug() && this.agent() !== null, + ); + protected readonly isDebugStreaming = computed(() => + this.agent()?.status?.() === 'running', + ); private readonly destroyRef = inject(DestroyRef); + private readonly injector = inject(Injector); + private readonly debugHost = viewChild('debugHost', { read: ViewContainerRef }); + private debugRef: ComponentRef | null = null; + private debugOutputSubscriptions: OutputRefSubscription[] = []; + private currentDebugDock: ChatDebugDock = 'right'; constructor() { + this.destroyRef.onDestroy(() => this.destroyDebug()); + + effect(() => { + const showDebug = this.showDebugButton(); + const agent = this.agent(); + if (!showDebug || !agent) { + this.destroyDebug(); + return; + } + this.debugRef?.setInput('agent', agent); + }); + fromEvent(window, 'keydown') .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((e) => { @@ -254,6 +319,11 @@ export class ChatSidenavComponent { }); } + protected openDebug(event: MouseEvent): void { + event.stopPropagation(); + void this.ensureDebugPanel(); + } + protected onEscape(): void { if (this.mode() === 'drawer' && this.open()) { this.openChange.emit(false); @@ -265,4 +335,83 @@ export class ChatSidenavComponent { if (m === 'drawer') return; this.modeChange.emit(m === 'collapsed' ? 'expanded' : 'collapsed'); } + + private async ensureDebugPanel(): Promise { + if (!CHAT_DEBUG_INCLUDED) { + return; + } + if (!this.showDebugButton()) { + this.destroyDebug(); + return; + } + const host = this.debugHost(); + const agent = this.agent(); + if (!host || !agent) return; + + if (!this.debugRef) { + const { ChatDebugComponent } = await import('@ngaf/chat/debug'); + if (!this.showDebugButton()) return; + this.debugRef = host.createComponent(ChatDebugComponent, { + injector: this.injector, + }); + this.debugRef.setInput('launcher', 'none'); + this.debugRef.setInput('storageKey', 'chat-sidenav-debug'); + const initialDock = this.defaultDebugDock(); + this.debugRef.setInput('dock', initialDock); + const replaySub = this.debugRef.instance.replayRequested?.subscribe((checkpointId) => { + this.replayRequested.emit(checkpointId); + }); + const forkSub = this.debugRef.instance.forkRequested?.subscribe((checkpointId) => { + this.forkRequested.emit(checkpointId); + }); + const openSub = this.debugRef.instance.openChange?.subscribe((open) => { + if (open) { + this.setDebugEdgeClaim(this.currentDebugDock); + } else { + this.clearDebugEdgeClaim(); + } + }); + const dockSub = this.debugRef.instance.dockChange?.subscribe((dock) => { + this.currentDebugDock = dock; + this.setDebugEdgeClaim(dock); + }); + this.debugOutputSubscriptions = [replaySub, forkSub, openSub, dockSub].filter((sub): sub is OutputRefSubscription => !!sub); + this.currentDebugDock = initialDock; + this.setDebugEdgeClaim(initialDock); + } + + this.debugRef.setInput('agent', agent); + this.debugRef.instance.setOpen(true); + this.debugRef.changeDetectorRef.detectChanges(); + if (this.currentDebugDock === 'bottom') { + this.debugRef.instance.setDock?.('bottom'); + this.debugRef.changeDetectorRef.detectChanges(); + } + this.setDebugEdgeClaim(this.currentDebugDock); + } + + private destroyDebug(): void { + for (const subscription of this.debugOutputSubscriptions) { + subscription.unsubscribe(); + } + this.debugOutputSubscriptions = []; + this.debugRef?.destroy(); + this.debugRef = null; + this.clearDebugEdgeClaim(); + } + + private defaultDebugDock(): ChatDebugDock { + if (typeof document === 'undefined') return 'right'; + return document.querySelector('chat-sidebar') ? 'bottom' : 'right'; + } + + private setDebugEdgeClaim(dock: ChatDebugDock): void { + if (typeof document === 'undefined') return; + document.documentElement.dataset['ngafChatDebug'] = dock; + } + + private clearDebugEdgeClaim(): void { + if (typeof document === 'undefined') return; + delete document.documentElement.dataset['ngafChatDebug']; + } } diff --git a/libs/chat/src/lib/primitives/chat-message-list/chat-message-list.component.ts b/libs/chat/src/lib/primitives/chat-message-list/chat-message-list.component.ts index 1b26cbfba..1253f51a8 100644 --- a/libs/chat/src/lib/primitives/chat-message-list/chat-message-list.component.ts +++ b/libs/chat/src/lib/primitives/chat-message-list/chat-message-list.component.ts @@ -35,7 +35,7 @@ export function getMessageType(message: Message): MessageTemplateType { @Component({ selector: 'chat-message-list', standalone: true, - imports: [NgTemplateOutlet, MessageTemplateDirective], + imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, styles: [CHAT_HOST_TOKENS, CHAT_MESSAGE_LIST_STYLES], template: ` diff --git a/libs/chat/src/lib/styles/chat-sidenav.styles.ts b/libs/chat/src/lib/styles/chat-sidenav.styles.ts index 28a4bd154..f7df93618 100644 --- a/libs/chat/src/lib/styles/chat-sidenav.styles.ts +++ b/libs/chat/src/lib/styles/chat-sidenav.styles.ts @@ -223,6 +223,46 @@ export const CHAT_SIDENAV_STYLES = ` gap: 4px; min-height: 28px; } + .chat-sidenav__debug { + height: 28px; + min-width: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 0 9px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--ngaf-chat-text-muted); + cursor: pointer; + font: inherit; + font-size: 12px; + font-weight: 500; + } + .chat-sidenav__debug:hover { + background: var(--ngaf-chat-surface-alt); + color: var(--ngaf-chat-text); + } + .chat-sidenav__debug:focus-visible { + outline: 2px solid var(--ngaf-chat-primary); + outline-offset: 2px; + } + .chat-sidenav__debug-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #22c55e; + box-shadow: 0 0 8px color-mix(in srgb, #22c55e 60%, transparent); + flex: 0 0 auto; + } + .chat-sidenav__debug-dot--streaming { + animation: chat-sidenav-debug-pulse 1.2s ease-in-out infinite; + } + @keyframes chat-sidenav-debug-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(0.85); } + } .chat-sidenav__footer-right { display: flex; align-items: center; @@ -246,13 +286,24 @@ export const CHAT_SIDENAV_STYLES = ` background: var(--ngaf-chat-surface-alt); color: var(--ngaf-chat-text); } - /* Collapsed mode: footer becomes a vertical stack; left slot hides. */ + /* Collapsed mode: footer becomes a vertical stack; debug stays visible. */ :host([data-mode="collapsed"]) .chat-sidenav__footer { flex-direction: column; align-items: center; padding: 10px 4px; } :host([data-mode="collapsed"]) .chat-sidenav__footer-left { + display: flex; + flex-direction: column; + align-items: center; + min-height: 28px; + } + :host([data-mode="collapsed"]) .chat-sidenav__debug { + width: 28px; + padding: 0; + } + :host([data-mode="collapsed"]) .chat-sidenav__debug-label, + :host([data-mode="collapsed"]) .chat-sidenav__footer-left > :not(.chat-sidenav__debug) { display: none; } :host([data-mode="collapsed"]) .chat-sidenav__footer-right { diff --git a/libs/chat/src/public-api.ts b/libs/chat/src/public-api.ts index d33020a7d..6656504dc 100644 --- a/libs/chat/src/public-api.ts +++ b/libs/chat/src/public-api.ts @@ -90,18 +90,6 @@ export { ChatComponent } from './lib/compositions/chat/chat.component'; export type { ChatRenderEvent } from './lib/compositions/chat/chat-render-event'; export { ChatPopupComponent } from './lib/compositions/chat-popup/chat-popup.component'; export { ChatSidebarComponent } from './lib/compositions/chat-sidebar/chat-sidebar.component'; -// chat-debug devtools -export { ChatDebugComponent } from './lib/compositions/chat-debug/chat-debug.component'; -export type { DockPosition } from './lib/compositions/chat-debug/chat-debug.component'; -export { ChatDebugControlsDirective } from './lib/compositions/chat-debug/chat-debug-controls.directive'; -export { ChatDebugInspectorDirective } from './lib/compositions/chat-debug/chat-debug-inspector.directive'; -export { ChatDebugSectionComponent } from './lib/compositions/chat-debug/primitives/chat-debug-section.component'; -export { ChatDebugSegmentedComponent } from './lib/compositions/chat-debug/primitives/chat-debug-segmented.component'; -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 { 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 { ChatSidenavComponent } from './lib/compositions/chat-sidenav/chat-sidenav.component'; export type { ChatSidenavMode } from './lib/compositions/chat-sidenav/chat-sidenav.component'; diff --git a/libs/chat/tsconfig.lib.json b/libs/chat/tsconfig.lib.json index 7140f6fa2..a10fdf3f7 100644 --- a/libs/chat/tsconfig.lib.json +++ b/libs/chat/tsconfig.lib.json @@ -8,7 +8,7 @@ "lib": ["es2022", "dom"], "types": [] }, - "include": ["src/**/*.ts"], + "files": ["src/public-api.ts", "debug/public-api.ts", "testing/public-api.ts"], "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"], "references": [ { diff --git a/tools/verify-chat-debug-bundle.mjs b/tools/verify-chat-debug-bundle.mjs new file mode 100644 index 000000000..335ba69ca --- /dev/null +++ b/tools/verify-chat-debug-bundle.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: MIT +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const [root, expected] = process.argv.slice(2); + +if (!root || !['present', 'absent'].includes(expected)) { + console.error('Usage: node tools/verify-chat-debug-bundle.mjs '); + process.exit(2); +} + +const sentinels = [ + 'Checkpoint timeline', + 'Chat Debug', + 'chat-debug-state-inspector', + 'chat-debug-timeline-inspector', + 'ngaf-chat-debug-root-styles', +]; + +function files(dir) { + return readdirSync(dir).flatMap((name) => { + const path = join(dir, name); + return statSync(path).isDirectory() ? files(path) : [path]; + }); +} + +const hits = []; +for (const file of files(root).filter((path) => /\.(m?js|css|html|map)$/.test(path))) { + const contents = readFileSync(file, 'utf8'); + for (const sentinel of sentinels) { + if (contents.includes(sentinel)) hits.push(`${file}: ${sentinel}`); + } +} + +if (expected === 'absent' && hits.length > 0) { + console.error(`Expected chat-debug implementation to be absent, found:\n${hits.join('\n')}`); + process.exit(1); +} + +if (expected === 'present' && hits.length === 0) { + console.error('Expected chat-debug implementation to be present, found no sentinels.'); + process.exit(1); +} + +console.log(`chat-debug bundle check passed: ${expected}`); diff --git a/tsconfig.base.json b/tsconfig.base.json index 453b76c99..0dfa8036b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -19,6 +19,7 @@ "@ngaf/ag-ui": ["libs/ag-ui/src/public-api.ts"], "@ngaf/a2ui": ["libs/a2ui/src/index.ts"], "@ngaf/chat": ["libs/chat/src/public-api.ts"], + "@ngaf/chat/debug": ["libs/chat/debug/public-api.ts"], "@ngaf/chat/testing": ["libs/chat/testing/public-api.ts"], "@ngaf/cockpit-docs": ["libs/cockpit-docs/src/index.ts"], "@ngaf/cockpit-langgraph-streaming-python": [