+ Click the launcher button (right edge) to slide in the chat panel.
+
+
+
+
+ @for (s of suggestions; track s.value) {
+
+ }
+
+
+ `,
+ styles: [`
+ :host { display: block; height: 100%; }
+ .sidebar-mode__background {
+ display: grid;
+ place-items: center;
+ height: 100%;
+ color: #8a92a3;
+ font-size: 14px;
+ }
+ `],
+})
+export class SidebarMode {
+ protected readonly agent = inject(DEMO_AGENT);
+ protected readonly model = inject(DEMO_MODEL) as ReturnType>;
+ protected readonly suggestions = WELCOME_SUGGESTIONS;
+ protected readonly modelOptions = signal([
+ { value: 'gpt-5', label: 'gpt-5' },
+ { value: 'gpt-5-mini', label: 'gpt-5-mini' },
+ { value: 'gpt-5-nano', label: 'gpt-5-nano' },
+ ]);
+
+ protected send(text: string): void {
+ void this.agent.submit({ message: text });
+ }
+}
+```
+
+- [ ] **Step 4: Commit (build still incomplete; Phase 3 wires DEMO_AGENT)**
+
+```bash
+git add examples/chat/angular/src/app/modes/embed-mode.component.ts \
+ examples/chat/angular/src/app/modes/popup-mode.component.ts \
+ examples/chat/angular/src/app/modes/sidebar-mode.component.ts
+git commit -m "feat(examples-chat-angular): three mode components (embed, popup, sidebar)"
+```
+
+---
+
+# Phase 3 — Wire shared agent + threadId persistence + model passthrough + debug overlay
+
+### Task 3.1: Update `DemoShell` to create + provide the shared agent
+
+**Files:**
+- Modify: `examples/chat/angular/src/app/shell/demo-shell.component.ts`
+- Modify: `examples/chat/angular/src/app/shell/demo-shell.component.html`
+
+- [ ] **Step 1: Replace `DemoShell` with the agent-providing version**
+
+Path: `examples/chat/angular/src/app/shell/demo-shell.component.ts`
+
+```ts
+// SPDX-License-Identifier: MIT
+import {
+ Component,
+ ChangeDetectionStrategy,
+ signal,
+ inject,
+} from '@angular/core';
+import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
+import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
+import { filter, map, startWith } from 'rxjs/operators';
+import { agent } from '@ngaf/langgraph';
+import { ChatDebugComponent } from '@ngaf/chat';
+import { ControlPalette } from './control-palette.component';
+import { PalettePersistence } from './palette-persistence.service';
+import { DEMO_AGENT, DEMO_MODEL } from './shell-tokens';
+
+export type DemoMode = 'embed' | 'popup' | 'sidebar';
+
+const MODES: readonly DemoMode[] = ['embed', 'popup', 'sidebar'] as const;
+
+function modeFromUrl(url: string): DemoMode {
+ const seg = url.split('?')[0].split('/').filter(Boolean)[0];
+ return (MODES as readonly string[]).includes(seg) ? (seg as DemoMode) : 'embed';
+}
+
+@Component({
+ selector: 'demo-shell',
+ standalone: true,
+ imports: [RouterOutlet, ControlPalette, ChatDebugComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ templateUrl: './demo-shell.component.html',
+ styleUrl: './demo-shell.component.css',
+ providers: [
+ { provide: DEMO_AGENT, useFactory: () => inject(DemoShell).agent },
+ { provide: DEMO_MODEL, useFactory: () => inject(DemoShell).model },
+ ],
+})
+export class DemoShell {
+ private readonly router = inject(Router);
+ private readonly persistence = inject(PalettePersistence);
+
+ protected readonly mode = toSignal(
+ this.router.events.pipe(
+ filter((e): e is NavigationEnd => e instanceof NavigationEnd),
+ map((e) => modeFromUrl(e.urlAfterRedirects)),
+ startWith(modeFromUrl(this.router.url)),
+ takeUntilDestroyed(),
+ ),
+ { initialValue: modeFromUrl(this.router.url) },
+ );
+
+ /** Source of truth for the model picker. Mode components read it via DEMO_MODEL. */
+ readonly model = signal(this.persistence.read('model') ?? 'gpt-5-mini');
+
+ protected readonly debugOpen = signal(this.persistence.read('debug') ?? false);
+
+ protected readonly modelOptions = signal([
+ { value: 'gpt-5', label: 'gpt-5' },
+ { value: 'gpt-5-mini', label: 'gpt-5-mini' },
+ { value: 'gpt-5-nano', label: 'gpt-5-nano' },
+ ]);
+
+ /** Persisted thread id (null on first run). Reactive so reload reconnects to the same thread. */
+ private readonly threadIdSignal = signal(this.persistence.read('threadId') ?? null);
+
+ /**
+ * Shared agent instance. Patched submit injects state.model on every
+ * submission so the graph picks up the latest model selection without
+ * a reconnect.
+ */
+ readonly agent = (() => {
+ const a = agent({
+ apiUrl: 'http://localhost:2024',
+ assistantId: 'chat',
+ threadId: this.threadIdSignal,
+ onThreadId: (id: string) => {
+ this.threadIdSignal.set(id);
+ this.persistence.write('threadId', id);
+ },
+ });
+ const orig = a.submit.bind(a);
+ (a as { submit: typeof a.submit }).submit = ((
+ input: Parameters[0],
+ opts?: Parameters[1],
+ ) =>
+ orig(
+ { ...(input ?? {}), state: { ...((input as { state?: Record })?.state ?? {}), model: this.model() } },
+ opts,
+ )) as typeof a.submit;
+ return a;
+ })();
+
+ protected onModeChange(next: DemoMode): void {
+ void this.router.navigate(['/' + next]);
+ }
+
+ protected onModelChange(next: string): void {
+ this.model.set(next);
+ this.persistence.write('model', next);
+ }
+
+ protected onDebugChange(next: boolean): void {
+ this.debugOpen.set(next);
+ this.persistence.write('debug', next);
+ }
+
+ /**
+ * Clear persisted thread id and drop the signal. The next submit
+ * causes the SDK to create a fresh thread server-side; onThreadId
+ * fires and re-persists it.
+ */
+ protected onNewConversation(): void {
+ this.persistence.write('threadId', null);
+ this.threadIdSignal.set(null);
+ }
+}
+```
+
+- [ ] **Step 2: Update demo-shell.component.html to mount ``**
+
+```html
+
+
+
+
+
+ @if (debugOpen()) {
+
+
+
+ }
+
+```
+
+- [ ] **Step 3: Update demo-shell.component.css to position the debug drawer**
+
+Path: `examples/chat/angular/src/app/shell/demo-shell.component.css`
+
+```css
+:host {
+ display: block;
+ height: 100dvh;
+}
+
+.demo-shell {
+ position: relative;
+ display: block;
+ height: 100%;
+}
+
+.demo-shell__debug {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 30vh;
+ background: #0f1116;
+ border-top: 1px solid #303540;
+ overflow: auto;
+ z-index: 999;
+}
+```
+
+- [ ] **Step 4: Run lint to catch type issues**
+
+Run: `npx nx run examples-chat-angular:lint --skip-nx-cache`
+Expected: passes (warnings OK; no errors).
+
+- [ ] **Step 5: Run tests**
+
+Run: `npx nx run examples-chat-angular:test --skip-nx-cache`
+Expected: PalettePersistence tests still pass.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add examples/chat/angular/src/app/shell/demo-shell.component.ts \
+ examples/chat/angular/src/app/shell/demo-shell.component.html \
+ examples/chat/angular/src/app/shell/demo-shell.component.css
+git commit -m "feat(examples-chat-angular): wire shared agent, threadId persistence, debug overlay"
+```
+
+### Task 3.2: Sanity-build the whole Angular app
+
+- [ ] **Step 1: Build (development)**
+
+Run: `npx nx run examples-chat-angular:build --skip-nx-cache --configuration=development`
+Expected: build succeeds. Bundle warnings about CommonJS deps are OK.
+
+- [ ] **Step 2: If errors:** triage type imports (e.g. `ChatDebugComponent` may be exported under a different name — check `libs/chat/src/index.ts` if needed) and fix inline. Re-run build.
+
+- [ ] **Step 3: Commit any inline fixes (if needed)**
+
+```bash
+git add -u examples/chat/angular/
+git commit -m "fix(examples-chat-angular): build-time corrections from sanity build"
+```
+*(Skip if no fixes needed.)*
+
+### Task 3.3: Spec for `DemoShell` — mode signal tracks router URL
+
+**Files:**
+- Create: `examples/chat/angular/src/app/shell/demo-shell.component.spec.ts`
+
+- [ ] **Step 1: Write the spec**
+
+```ts
+import { describe, it, expect, beforeEach } from 'vitest';
+import { TestBed } from '@angular/core/testing';
+import { provideRouter } from '@angular/router';
+import { Router } from '@angular/router';
+import { DemoShell } from './demo-shell.component';
+
+describe('DemoShell — mode signal', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ provideRouter([
+ { path: 'embed', component: DemoShell },
+ { path: 'popup', component: DemoShell },
+ { path: 'sidebar', component: DemoShell },
+ { path: '', pathMatch: 'full', redirectTo: 'embed' },
+ ]),
+ ],
+ });
+ });
+
+ it('defaults to "embed" when URL is /', async () => {
+ const fixture = TestBed.createComponent(DemoShell);
+ fixture.detectChanges();
+ const cmp = fixture.componentInstance as unknown as { mode: () => string };
+ expect(cmp.mode()).toBe('embed');
+ });
+
+ it('resolves "popup" when navigating to /popup', async () => {
+ const router = TestBed.inject(Router);
+ await router.navigateByUrl('/popup');
+ const fixture = TestBed.createComponent(DemoShell);
+ fixture.detectChanges();
+ const cmp = fixture.componentInstance as unknown as { mode: () => string };
+ expect(cmp.mode()).toBe('popup');
+ });
+
+ it('falls back to "embed" for unknown segments', async () => {
+ const router = TestBed.inject(Router);
+ await router.navigateByUrl('/bogus');
+ const fixture = TestBed.createComponent(DemoShell);
+ fixture.detectChanges();
+ const cmp = fixture.componentInstance as unknown as { mode: () => string };
+ expect(cmp.mode()).toBe('embed');
+ });
+});
+```
+
+- [ ] **Step 2: Run tests**
+
+Run: `npx nx run examples-chat-angular:test --skip-nx-cache`
+Expected: 3 DemoShell tests pass + the existing PalettePersistence tests pass.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add examples/chat/angular/src/app/shell/demo-shell.component.spec.ts
+git commit -m "test(examples-chat-angular): DemoShell mode-signal spec"
+```
+
+### Task 3.4: Live smoke — manual `npx nx run examples-chat:serve`
+
+- [ ] **Step 1: Ensure `OPENAI_API_KEY` is in `examples/chat/python/.env`**
+
+Run: `cat examples/chat/python/.env 2>/dev/null | head -1` — should show `OPENAI_API_KEY=sk-...`. If missing, copy from `.env.example` and fill.
+
+- [ ] **Step 2: Sync python deps if not done**
+
+Run: `cd examples/chat/python && uv sync && cd -`
+
+- [ ] **Step 3: Start the aggregate target**
+
+Run: `npx nx run examples-chat:serve`
+Expected: Python on :2024 (`{"ok":true}` from /ok), Angular on :4200 (page loads, palette visible top-right).
+
+- [ ] **Step 4: Manual sanity check** (browser at http://localhost:4200)
+- Page redirects to `/embed`
+- Palette is visible top-right
+- Welcome suggestions render
+- Click a suggestion — message streams
+- Click "Regenerate response" — assistant replaced cleanly (1u/1a)
+- Switch to /popup — same conversation visible inside popup
+- Switch to /sidebar — same conversation visible inside sidebar
+- Toggle Debug ON — overlay appears at bottom
+- Click "↻ New conversation" — welcome state returns
+- Reload page — agent reconnects (or fresh state if no conversation yet)
+
+- [ ] **Step 5: Stop the serve target** (`Ctrl+C` in the terminal running it).
+
+- [ ] **Step 6: Commit any sanity fixes** if anything broke (component selectors, imports, etc.).
+
+---
+
+# Phase 4 — Smoke CLI generator
+
+### Task 4.1: Smoke template — Angular CLI scaffold (no `src/app/`)
+
+**Files:**
+- Create: `examples/chat/smoke/template/package.json`
+- Create: `examples/chat/smoke/template/angular.json`
+- Create: `examples/chat/smoke/template/tsconfig.json`
+- Create: `examples/chat/smoke/template/tsconfig.app.json`
+- Create: `examples/chat/smoke/template/.gitignore`
+- Create: `examples/chat/smoke/template/public/favicon.ico`
+- Create: `examples/chat/smoke/template/src/main.ts`
+- Create: `examples/chat/smoke/template/src/styles.css`
+- Create: `examples/chat/smoke/template/src/index.html`
+
+- [ ] **Step 1: package.json (placeholder uses `"*"` — valid semver, replaced at gen-time)**
+
+```json
+{
+ "name": "examples-chat-smoke-consumer",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development"
+ },
+ "packageManager": "npm@10.9.2",
+ "dependencies": {
+ "@angular/common": "^21.2.0",
+ "@angular/compiler": "^21.2.0",
+ "@angular/core": "^21.2.0",
+ "@angular/forms": "^21.2.0",
+ "@angular/platform-browser": "^21.2.0",
+ "@angular/router": "^21.2.0",
+ "@ngaf/ag-ui": "*",
+ "@ngaf/chat": "*",
+ "@ngaf/langgraph": "*",
+ "@ngaf/render": "*",
+ "@cacheplane/partial-markdown": "^0.3.0",
+ "@cacheplane/partial-json": "^0.2.0",
+ "@langchain/core": "^1.1.33",
+ "marked": "^16.0.0",
+ "rxjs": "~7.8.0",
+ "tslib": "^2.3.0"
+ },
+ "devDependencies": {
+ "@angular/build": "^21.2.9",
+ "@angular/cli": "^21.2.9",
+ "@angular/compiler-cli": "^21.2.0",
+ "typescript": "~5.9.2"
+ }
+}
+```
+
+- [ ] **Step 2: angular.json**
+
+```json
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "cli": { "packageManager": "npm" },
+ "newProjectRoot": "projects",
+ "projects": {
+ "smoke": {
+ "projectType": "application",
+ "schematics": {},
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular/build:application",
+ "options": {
+ "browser": "src/main.ts",
+ "tsConfig": "tsconfig.app.json",
+ "assets": [{ "glob": "**/*", "input": "public" }],
+ "styles": ["src/styles.css"]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ { "type": "initial", "maximumWarning": "500kB", "maximumError": "2MB" },
+ { "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "16kB" }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "serve": {
+ "builder": "@angular/build:dev-server",
+ "configurations": {
+ "production": { "buildTarget": "smoke:build:production" },
+ "development": { "buildTarget": "smoke:build:development" }
+ },
+ "defaultConfiguration": "development"
+ }
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 3: tsconfig.json**
+
+```json
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": false,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "experimentalDecorators": true,
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "preserve"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": false
+ },
+ "files": [],
+ "references": [{ "path": "./tsconfig.app.json" }]
+}
+```
+
+- [ ] **Step 4: tsconfig.app.json**
+
+```json
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.spec.ts"]
+}
+```
+
+- [ ] **Step 5: .gitignore (consumer-local)**
+
+```
+node_modules/
+dist/
+.angular/
+out-tsc/
+```
+
+- [ ] **Step 6: src/main.ts**
+
+```ts
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import { App } from './app/app';
+
+bootstrapApplication(App, appConfig).catch((err) => console.error(err));
+```
+
+- [ ] **Step 7: src/styles.css**
+
+```css
+@import '@ngaf/chat/chat.css';
+
+html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ font-family: system-ui, -apple-system, sans-serif;
+ background: #0f1116;
+ color: #e6e9ef;
+}
+```
+
+- [ ] **Step 8: src/index.html**
+
+```html
+
+
+
+
+ NGAF chat — smoke consumer
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 9: Copy favicon**
+
+```bash
+mkdir -p examples/chat/smoke/template/public
+cp examples/chat/angular/public/favicon.ico examples/chat/smoke/template/public/favicon.ico
+```
+
+- [ ] **Step 10: Commit**
+
+```bash
+git add examples/chat/smoke/template/
+git commit -m "feat(examples-chat-smoke): scaffold consumer template (Angular CLI bones)"
+```
+
+### Task 4.2: CHECKLIST.md
+
+**Files:**
+- Create: `examples/chat/smoke/CHECKLIST.md`
+
+- [ ] **Step 1: Write CHECKLIST.md** (full content from spec, verbatim)
+
+```markdown
+# NGAF chat smoke checklist
+
+Scope: validates the **published** `@ngaf/*` packages render and behave
+correctly in a fresh consumer. Run after any release or whenever
+changes land in libs/chat, libs/langgraph, libs/render, libs/ag-ui.
+
+## Pre-flight
+
+- [ ] `OPENAI_API_KEY` present in `examples/chat/python/.env`
+- [ ] `nx run examples-chat-python:serve` running on :2024 — `curl localhost:2024/ok` returns `{"ok":true}`
+- [ ] Smoke consumer started — page loads at :4200
+- [ ] No console errors on initial load (license warning OK, telemetry DNS failure OK)
+- [ ] No 4xx/5xx in the network tab on initial load
+
+## Initial render (welcome state)
+
+- [ ] Default route redirects to `/embed`
+- [ ] Welcome heading renders ("How can I help?")
+- [ ] All declared welcome suggestion buttons render with their labels
+- [ ] Control palette renders top-right, fully expanded by default
+- [ ] Palette mode segmented control highlights "Embed"
+- [ ] Palette model dropdown shows the default model
+- [ ] Palette debug toggle shows "off"
+- [ ] Send button disabled when input is empty
+
+## Send & receive (basic streaming)
+
+- [ ] Type any prompt, click Send — input clears, user message renders immediately
+- [ ] Typing indicator appears between send and first token
+- [ ] Tokens stream visibly into the assistant bubble (not all-at-once)
+- [ ] Final message stays after stream completes
+- [ ] Auto-scroll keeps the latest content visible during streaming
+- [ ] Send button re-enables after stream completes
+
+## Stop mid-stream
+
+- [ ] Send a long prompt
+- [ ] Mid-stream, Send button has flipped to "Stop generating"
+- [ ] Click stop — stream halts, partial response remains rendered
+- [ ] No console errors; agent returns to idle; Send button returns
+
+## Markdown surfaces (the partial-markdown render path)
+
+Send a prompt that asks for each of the following. Check that each
+renders correctly both during streaming and after completion.
+
+> **Known regressions** documented in the chat 0.0.20 partial-markdown
+> swap: tables and task lists may not match the previous (marked-based)
+> rendering exactly. If a check fails, file an issue against
+> `libs/chat` rather than skipping — the smoke checklist is the
+> canonical "what should work" list.
+
+- [ ] **Headings** — `# H1`, `## H2`, `### H3` all render at distinct sizes
+- [ ] **Paragraphs** with **bold**, *italic*, and `inline code`
+- [ ] **Bullet lists** including nested (2+ levels)
+- [ ] **Ordered lists** with correct numbering
+- [ ] **Task lists** — `- [ ]` (unchecked) and `- [x]` (checked) render as checkboxes
+- [ ] **Fenced code blocks** with language hint — preserved as `
`
+- [ ] **Tables** with header row + 2+ data rows — column alignment preserved
+- [ ] **Blockquotes** — visually distinct
+- [ ] **Links** — clickable, open in new tab
+- [ ] **Horizontal rules** — render as a line
+- [ ] No raw HTML escapes through (e.g. `