@@ -39,6 +53,11 @@ import { ApprovalCardComponent } from './views/approval-card.component';
export class InterruptsComponent {
readonly ui = views({ 'approval-card': ApprovalCardComponent });
readonly uiStore = signalStateStore({});
+ protected readonly suggestions = WELCOME_SUGGESTIONS;
+
+ protected send(text: string): void {
+ void this.agent.submit({ message: text });
+ }
/**
* The streaming resource with interrupt support.
diff --git a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts
index e3d97e0fb..900e60dff 100644
--- a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts
+++ b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts
@@ -1,9 +1,14 @@
+// SPDX-License-Identifier: MIT
import { Component, signal } from '@angular/core';
-import { ChatComponent } from '@ngaf/chat';
+import { ChatComponent, ChatWelcomeSuggestionComponent } from '@ngaf/chat';
import { agent } from '@ngaf/langgraph';
import { ExampleChatLayoutComponent } from '@ngaf/example-layouts';
import { environment } from '../environments/environment';
+const WELCOME_SUGGESTIONS = [
+ { label: 'Save this thread for later', value: 'Help me draft a project brief I can revisit.' },
+] as const;
+
interface Thread {
id: string;
label: string;
@@ -23,10 +28,20 @@ interface Thread {
@Component({
selector: 'app-persistence',
standalone: true,
- imports: [ChatComponent, ExampleChatLayoutComponent],
+ imports: [ChatComponent, ChatWelcomeSuggestionComponent, ExampleChatLayoutComponent],
template: `
-
+
+
+ @for (s of suggestions; track s.value) {
+
+ }
+
+
([]);
protected readonly activeThreadId = signal
(null);
+ protected readonly suggestions = WELCOME_SUGGESTIONS;
private threadCounter = 0;
+ protected send(text: string): void {
+ void this.agent.submit({ message: text });
+ }
+
/**
* The streaming resource with thread persistence.
*
diff --git a/cockpit/langgraph/streaming/angular/src/app/streaming.component.ts b/cockpit/langgraph/streaming/angular/src/app/streaming.component.ts
index 0141a9e80..c5e800297 100644
--- a/cockpit/langgraph/streaming/angular/src/app/streaming.component.ts
+++ b/cockpit/langgraph/streaming/angular/src/app/streaming.component.ts
@@ -1,10 +1,15 @@
// SPDX-License-Identifier: MIT
import { Component } from '@angular/core';
-import { ChatComponent } from '@ngaf/chat';
+import { ChatComponent, ChatWelcomeSuggestionComponent } from '@ngaf/chat';
import { agent } from '@ngaf/langgraph';
import { ExampleChatLayoutComponent } from '@ngaf/example-layouts';
import { environment } from '../environments/environment';
+const WELCOME_SUGGESTIONS = [
+ { label: 'Stream a long answer', value: 'Explain LangGraph checkpointing in 200 words.' },
+ { label: 'Walk me through agent tool calls', value: 'Show me how an agent decides which tool to use.' },
+] as const;
+
/**
* Streaming demo — simplest possible @ngaf/chat integration.
*
@@ -15,10 +20,20 @@ import { environment } from '../environments/environment';
@Component({
selector: 'app-streaming',
standalone: true,
- imports: [ChatComponent, ExampleChatLayoutComponent],
+ imports: [ChatComponent, ChatWelcomeSuggestionComponent, ExampleChatLayoutComponent],
template: `
-
+
+
+ @for (s of suggestions; track s.value) {
+
+ }
+
+
`,
})
@@ -27,4 +42,9 @@ export class StreamingComponent {
apiUrl: environment.langGraphApiUrl,
assistantId: environment.streamingAssistantId,
});
+ protected readonly suggestions = WELCOME_SUGGESTIONS;
+
+ protected send(text: string): void {
+ void this.agent.submit({ message: text });
+ }
}
diff --git a/docs/superpowers/plans/gtm/2026-05-16-cockpit-activation-recipes.md b/docs/superpowers/plans/gtm/2026-05-16-cockpit-activation-recipes.md
new file mode 100644
index 000000000..fa77c6ef0
--- /dev/null
+++ b/docs/superpowers/plans/gtm/2026-05-16-cockpit-activation-recipes.md
@@ -0,0 +1,670 @@
+# Spec 4 — Cockpit Activation Recipes Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Tighten `ChatComponent` so `CHAT_LIFECYCLE.firstMessageSent` flips on any submit path, and drop pre-baked `` rows on the four cockpit capability examples that map to activation signals.
+
+**Architecture:** One small effect added inside `ChatComponent` watches `agent.lifecycle.streamStartedAt`; on its first transition to non-null, it flips the sticky `firstMessageSent` flag. Four cockpit example components (streaming, persistence, interrupts, generative-ui) gain a `WELCOME_SUGGESTIONS` const and project `` rows into the `` welcome slot using the canonical pattern from `examples/chat/angular/src/app/modes/embed-mode.component.ts`. No new analytics events.
+
+**Tech Stack:** Angular 20+ (Signals + `effect()`); `@ngaf/chat` (`ChatComponent`, `ChatWelcomeSuggestionComponent`); `@ngaf/langgraph` (`agent()`); Vitest + jsdom for unit tests (`npx nx run chat:test`).
+
+---
+
+## Context for the implementer
+
+- **Spec:** `docs/superpowers/specs/gtm/2026-05-16-cockpit-activation-recipes-design.md` — read §§3–6 before starting.
+- **Canonical suggestion pattern:** `examples/chat/angular/src/app/modes/embed-mode.component.ts`. Mirror it.
+- **Lifecycle background:** Spec 1C (`docs/superpowers/specs/gtm/2026-05-15-analytics-foundation-1c-cockpit-instrumentation-design.md` §5) defines the three `*_LIFECYCLE` tokens. `firstMessageSent` is documented as sticky for the life of the chat instance.
+- **`agent.lifecycle.streamStartedAt`:** comes from `AGENT_LIFECYCLE` in `@ngaf/langgraph` (Spec 1C Task 0.4). Available on every `agent()` return as `agentRef.lifecycle.streamStartedAt`. It's a `Signal` that flips to `Date.now()` on the first stream chunk and resets to `null` on `switchThread(null)`.
+- **No backend dependency for the new tests.** The chat lib test uses `mockAgent()` from `libs/chat/src/lib/testing/mock-agent.ts`, which already exposes a writable lifecycle.
+- **TDD discipline:** spec → run-see-fail → implement → run-see-pass → commit.
+- **Commit format:** conventional commits, one task = one commit.
+- **Worktree branch:** `gtm-spec-4-cockpit-activation-recipes` (already created from `origin/main`, currently at `a2bbcbf9`).
+
+## File structure (locked)
+
+```
+MODIFIED
+├── libs/chat/src/lib/compositions/chat/chat.component.ts # Phase 0 — new effect
+├── libs/chat/src/lib/lifecycle.spec.ts # Phase 0 — new test case
+├── cockpit/langgraph/streaming/angular/src/app/streaming.component.ts # Phase 1
+├── cockpit/langgraph/persistence/angular/src/app/persistence.component.ts # Phase 2
+├── cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts # Phase 3
+├── cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts # Phase 4
+```
+
+No new files. No deletions.
+
+---
+
+## Phase 0 — Tighten `ChatComponent.firstMessageSent`
+
+### Task 0.1: TDD — add lifecycle test for agent-driven submit
+
+**Files:**
+- Modify: `libs/chat/src/lib/lifecycle.spec.ts`
+
+- [ ] **Step 1: Read the existing spec**
+
+Open `libs/chat/src/lib/lifecycle.spec.ts`. Note the fixture setup pattern: `TestBed.createComponent(ChatComponent)` + `setInput('agent', mockAgent())` + `fixture.detectChanges()` + `injector.get(CHAT_LIFECYCLE)`.
+
+- [ ] **Step 2: Inspect `mockAgent` so you know how to drive its lifecycle**
+
+```bash
+cat libs/chat/src/lib/testing/mock-agent.ts
+```
+
+You need to know:
+- Does `mockAgent()` return an object whose `lifecycle.streamStartedAt` is a writable signal? Or is the lifecycle stubbed read-only?
+- If writable: the test can do `agentRef.lifecycle._internal.streamStartedAt.set(Date.now())` or similar.
+- If stubbed: you may need to call `agentRef.submit({...})` to drive the signal, and rely on the mock's internal submit handler.
+
+Match the test approach to the actual shape of `mockAgent`. If the mock doesn't expose a way to drive `streamStartedAt`, ask before guessing — the mock may need a small enhancement (out of scope or in scope, your call).
+
+- [ ] **Step 3: Write the failing test**
+
+Append to `libs/chat/src/lib/lifecycle.spec.ts` inside the `describe('ChatLifecycle integration', ...)` block (after the existing tests):
+
+```typescript
+ test('firstMessageSent flips when agent.lifecycle.streamStartedAt transitions to non-null', () => {
+ // Capture the agent we passed in so we can drive its lifecycle directly.
+ const agent = mockAgent();
+ const f = TestBed.createComponent(ChatComponent);
+ f.componentRef.setInput('agent', agent);
+ f.detectChanges();
+ const lc = f.componentRef.injector.get(CHAT_LIFECYCLE);
+ expect(lc.firstMessageSent()).toBe(false);
+
+ // Drive the agent's streamStartedAt signal. Exact API depends on mockAgent's
+ // shape — use whichever helper the mock exposes. Common patterns:
+ // agent._internal.streamStartedAt.set(Date.now()) // direct setter
+ // agent.simulateStreamStart() // helper method
+ // await agent.submit({ message: 'hi' }) // real submit path
+ // Use the path that exists; if none does, enhance mockAgent (small).
+ agent._internal.streamStartedAt.set(Date.now());
+ f.detectChanges();
+ expect(lc.firstMessageSent()).toBe(true);
+ });
+
+ test('firstMessageSent stays sticky across multiple agent-driven transitions', () => {
+ const agent = mockAgent();
+ const f = TestBed.createComponent(ChatComponent);
+ f.componentRef.setInput('agent', agent);
+ f.detectChanges();
+ const lc = f.componentRef.injector.get(CHAT_LIFECYCLE);
+
+ agent._internal.streamStartedAt.set(Date.now());
+ f.detectChanges();
+ expect(lc.firstMessageSent()).toBe(true);
+
+ // A second stream-start (e.g., second submit) must not unset.
+ agent._internal.streamStartedAt.set(null);
+ f.detectChanges();
+ expect(lc.firstMessageSent()).toBe(true);
+
+ agent._internal.streamStartedAt.set(Date.now());
+ f.detectChanges();
+ expect(lc.firstMessageSent()).toBe(true);
+ });
+```
+
+- [ ] **Step 4: Run, see fail**
+
+```bash
+npx nx run chat:test -- --testPathPattern=lifecycle.spec
+```
+
+Expected: the two new tests fail (firstMessageSent stays false even after streamStartedAt is set), because the effect doesn't exist yet.
+
+- [ ] **Step 5: NO COMMIT** — Task 0.2 implements the effect.
+
+### Task 0.2: Implement the agent-stream-start effect
+
+**Files:**
+- Modify: `libs/chat/src/lib/compositions/chat/chat.component.ts`
+
+- [ ] **Step 1: Read the existing constructor / init effect**
+
+In `libs/chat/src/lib/compositions/chat/chat.component.ts`, find the existing constructor and the effect that flips `lifecycle._internal.componentReady.set(true)`. That's where the new effect lives (alongside it, not inside it).
+
+- [ ] **Step 2: Add the new effect**
+
+Inside the constructor (after the existing init effect, or anywhere in the constructor body that runs in the injection context — `effect()` requires it), add:
+
+```typescript
+// Spec 4: flip CHAT_LIFECYCLE.firstMessageSent when the agent's stream
+// starts, regardless of submit path (input-bound, programmatic, suggestion-
+// click). Sticky — guarded so we never re-set a flag that's already true.
+effect(() => {
+ const agentRef = this.agent();
+ if (!agentRef) return;
+ const streamStartedAt = agentRef.lifecycle.streamStartedAt();
+ if (streamStartedAt !== null && !this.lifecycle._internal.firstMessageSent()) {
+ this.lifecycle._internal.firstMessageSent.set(true);
+ }
+});
+```
+
+Confirm the imports already include `effect` from `@angular/core`. If not, add it.
+
+- [ ] **Step 3: Run, see pass**
+
+```bash
+npx nx run chat:test -- --testPathPattern=lifecycle.spec
+```
+
+Expected: all lifecycle tests pass (existing + 2 new = ~8 tests).
+
+- [ ] **Step 4: Run the full chat test suite**
+
+```bash
+npx nx run chat:test
+```
+
+Expected: green. The effect is idempotent and only writes when `firstMessageSent` is false, so existing tests stay correct.
+
+- [ ] **Step 5: Build**
+
+```bash
+npx nx run chat:build
+```
+
+Expected: green.
+
+- [ ] **Step 6: Commit (Task 0.1 + Task 0.2 land together)**
+
+Two failing tests + implementation that makes them pass land as one commit so the history reads as a clean TDD slice.
+
+```bash
+git add libs/chat/src/lib/compositions/chat/chat.component.ts libs/chat/src/lib/lifecycle.spec.ts
+git commit -m "$(cat <<'EOF'
+feat(chat): flip firstMessageSent on agent stream-start (any submit path)
+
+ChatComponent now subscribes to agent.lifecycle.streamStartedAt and
+flips CHAT_LIFECYCLE.firstMessageSent on its first transition to a
+non-null value. This makes the lifecycle robust to programmatic
+agent.submit() calls, including the click
+handler pattern in cockpit examples.
+
+messageCount and inputSubmittedAt remain input-bound by design — they
+measure typing engagement, not stream initiation.
+
+Two new lifecycle tests cover the agent-driven flip + stickiness across
+multiple stream-starts.
+
+Co-Authored-By: Claude Opus 4.7
+EOF
+)"
+```
+
+---
+
+## Phase 1 — Streaming capability suggestions
+
+### Task 1.1: Add `WELCOME_SUGGESTIONS` to `streaming.component.ts`
+
+**File:** `cockpit/langgraph/streaming/angular/src/app/streaming.component.ts`
+
+- [ ] **Step 1: Replace the component**
+
+Open the file. Current shape (verbatim from main):
+
+```typescript
+import { Component } from '@angular/core';
+import { ChatComponent } from '@ngaf/chat';
+import { agent } from '@ngaf/langgraph';
+import { ExampleChatLayoutComponent } from '@ngaf/example-layouts';
+import { environment } from '../environments/environment';
+
+@Component({
+ selector: 'app-streaming',
+ standalone: true,
+ imports: [ChatComponent, ExampleChatLayoutComponent],
+ template: `
+
+
+
+ `,
+})
+export class StreamingComponent {
+ protected readonly agent = agent({
+ apiUrl: environment.langGraphApiUrl,
+ assistantId: environment.streamingAssistantId,
+ });
+}
+```
+
+Replace with:
+
+```typescript
+// SPDX-License-Identifier: MIT
+import { Component } from '@angular/core';
+import { ChatComponent, ChatWelcomeSuggestionComponent } from '@ngaf/chat';
+import { agent } from '@ngaf/langgraph';
+import { ExampleChatLayoutComponent } from '@ngaf/example-layouts';
+import { environment } from '../environments/environment';
+
+const WELCOME_SUGGESTIONS = [
+ { label: 'Stream a long answer', value: 'Explain LangGraph checkpointing in 200 words.' },
+ { label: 'Walk me through agent tool calls', value: 'Show me how an agent decides which tool to use.' },
+] as const;
+
+/**
+ * Streaming demo — simplest possible @ngaf/chat integration.
+ *
+ * Creates an agent ref and passes it to the prebuilt composition.
+ * The welcome state projects pre-baked suggestion rows that drive
+ * cockpit:transport_connected + cockpit:chat_first_message activation
+ * signals on first click.
+ */
+@Component({
+ selector: 'app-streaming',
+ standalone: true,
+ imports: [ChatComponent, ChatWelcomeSuggestionComponent, ExampleChatLayoutComponent],
+ template: `
+
+
+
+ @for (s of suggestions; track s.value) {
+
+ }
+
+
+
+ `,
+})
+export class StreamingComponent {
+ protected readonly agent = agent({
+ apiUrl: environment.langGraphApiUrl,
+ assistantId: environment.streamingAssistantId,
+ });
+
+ protected readonly suggestions = WELCOME_SUGGESTIONS;
+
+ protected send(text: string): void {
+ void this.agent.submit({ message: text });
+ }
+}
+```
+
+- [ ] **Step 2: Verify build**
+
+```bash
+npx nx run cockpit-langgraph-streaming-angular:build
+npx nx run cockpit-langgraph-streaming-angular:build:cockpit
+```
+
+Expected: both green.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add cockpit/langgraph/streaming/angular/src/app/streaming.component.ts
+git commit -m "$(cat <<'EOF'
+feat(cockpit-streaming): pre-baked welcome suggestions
+
+Two rows in the empty-state — "Stream a long
+answer" and "Walk me through agent tool calls". Clicking either calls
+agent.submit({ message: ... }) which fires AGENT_LIFECYCLE.streamStartedAt
+on first chunk arrival → cockpit:transport_connected. ChatComponent's
+new effect (Phase 0) then flips firstMessageSent → cockpit:chat_first_message.
+
+Co-Authored-By: Claude Opus 4.7
+EOF
+)"
+```
+
+---
+
+## Phase 2 — Persistence capability suggestions
+
+### Task 2.1: Add `WELCOME_SUGGESTIONS` to `persistence.component.ts`
+
+**File:** `cockpit/langgraph/persistence/angular/src/app/persistence.component.ts`
+
+- [ ] **Step 1: Open the file and locate the `` element**
+
+The current template has `` as a self-closing tag inside ``. The suggestion rows go inside the `` element, so the self-close needs to become a paired open/close.
+
+- [ ] **Step 2: Add imports**
+
+Add `ChatWelcomeSuggestionComponent` to the import from `@ngaf/chat`:
+
+```typescript
+import { ChatComponent, ChatWelcomeSuggestionComponent } from '@ngaf/chat';
+```
+
+Update the `imports:` array in the `@Component` decorator to include `ChatWelcomeSuggestionComponent`.
+
+- [ ] **Step 3: Add the `WELCOME_SUGGESTIONS` const above the class**
+
+```typescript
+const WELCOME_SUGGESTIONS = [
+ { label: 'Save this thread for later', value: 'Help me draft a project brief I can revisit.' },
+] as const;
+```
+
+- [ ] **Step 4: Update the template — change self-close to paired, project suggestions**
+
+Find `` and replace with:
+
+```html
+
+
+ @for (s of suggestions; track s.value) {
+
+ }
+
+
+```
+
+- [ ] **Step 5: Add `suggestions` field + `send` method**
+
+In the `PersistenceComponent` class body, add:
+
+```typescript
+ protected readonly suggestions = WELCOME_SUGGESTIONS;
+
+ protected send(text: string): void {
+ void this.agent.submit({ message: text });
+ }
+```
+
+(Position them after the existing `agent` declaration. Mind the existing thread-list signal — don't displace it.)
+
+- [ ] **Step 6: Verify builds**
+
+```bash
+npx nx run cockpit-langgraph-persistence-angular:build
+npx nx run cockpit-langgraph-persistence-angular:build:cockpit
+```
+
+Expected: both green.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add cockpit/langgraph/persistence/angular/src/app/persistence.component.ts
+git commit -m "$(cat <<'EOF'
+feat(cockpit-persistence): pre-baked welcome suggestion
+
+One row — "Save this thread for later" —
+that prompts a project-brief conversation. After the user reloads the
+page, AGENT_LIFECYCLE.threadPersistedAt fires → cockpit:thread_persisted
+activation signal.
+
+Co-Authored-By: Claude Opus 4.7
+EOF
+)"
+```
+
+---
+
+## Phase 3 — Interrupts capability suggestions
+
+### Task 3.1: Add `WELCOME_SUGGESTIONS` to `interrupts.component.ts`
+
+**File:** `cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts`
+
+- [ ] **Step 1: Open the file and locate the `` element**
+
+The template has `` inside the layout `main` slot. Same self-close-to-paired transformation.
+
+- [ ] **Step 2: Add imports**
+
+```typescript
+import { ChatComponent, ChatInterruptPanelComponent, ChatWelcomeSuggestionComponent, views, type InterruptAction } from '@ngaf/chat';
+```
+
+Add `ChatWelcomeSuggestionComponent` to the `imports:` array.
+
+- [ ] **Step 3: Add `WELCOME_SUGGESTIONS` above the class**
+
+```typescript
+const WELCOME_SUGGESTIONS = [
+ { label: 'Approve a tool call', value: 'Book a flight to Paris for next Tuesday.' },
+] as const;
+```
+
+- [ ] **Step 4: Update the template — paired ``, project suggestions**
+
+Replace `` with:
+
+```html
+
+
+ @for (s of suggestions; track s.value) {
+
+ }
+
+
+```
+
+The interrupt panel `@if (agent.interrupt()) { ... }` block stays where it is — outside ``.
+
+- [ ] **Step 5: Add `suggestions` + `send` to the class**
+
+```typescript
+ protected readonly suggestions = WELCOME_SUGGESTIONS;
+
+ protected send(text: string): void {
+ void this.agent.submit({ message: text });
+ }
+```
+
+- [ ] **Step 6: Verify builds**
+
+```bash
+npx nx run cockpit-langgraph-interrupts-angular:build
+npx nx run cockpit-langgraph-interrupts-angular:build:cockpit
+```
+
+Expected: both green.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts
+git commit -m "$(cat <<'EOF'
+feat(cockpit-interrupts): pre-baked welcome suggestion
+
+One row — "Approve a tool call" — that
+prompts a flight-booking conversation. The graph pauses at an interrupt;
+when the user approves via the ChatInterruptPanelComponent,
+AGENT_LIFECYCLE.interruptResolvedAt fires → cockpit:interrupt_handled
+activation signal.
+
+Co-Authored-By: Claude Opus 4.7
+EOF
+)"
+```
+
+---
+
+## Phase 4 — Generative-UI capability suggestions
+
+### Task 4.1: Add `WELCOME_SUGGESTIONS` to `generative-ui.component.ts`
+
+**File:** `cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts`
+
+- [ ] **Step 1: Update imports + class**
+
+Replace the current file with:
+
+```typescript
+// SPDX-License-Identifier: MIT
+import { Component } from '@angular/core';
+import { ChatComponent, ChatWelcomeSuggestionComponent, views } from '@ngaf/chat';
+import { agent } from '@ngaf/langgraph';
+import { ExampleChatLayoutComponent } from '@ngaf/example-layouts';
+import { environment } from '../environments/environment';
+
+import { StatCardComponent } from './views/stat-card.component';
+import { ContainerComponent } from './views/container.component';
+import { DashboardGridComponent } from './views/dashboard-grid.component';
+import { LineChartComponent } from './views/line-chart.component';
+import { BarChartComponent } from './views/bar-chart.component';
+import { DataGridComponent } from './views/data-grid.component';
+
+const dashboardViews = views({
+ stat_card: StatCardComponent,
+ container: ContainerComponent,
+ dashboard_grid: DashboardGridComponent,
+ line_chart: LineChartComponent,
+ bar_chart: BarChartComponent,
+ data_grid: DataGridComponent,
+});
+
+const WELCOME_SUGGESTIONS = [
+ { label: 'Render a dashboard', value: 'Show me a Q3 sales dashboard with three metrics.' },
+ { label: 'Render a form', value: 'Create a contact form with name, email, and message.' },
+] as const;
+
+@Component({
+ selector: 'app-generative-ui',
+ standalone: true,
+ imports: [ChatComponent, ChatWelcomeSuggestionComponent, ExampleChatLayoutComponent],
+ template: `
+
+
+
+ @for (s of suggestions; track s.value) {
+
+ }
+
+
+
+ `,
+})
+export class GenerativeUiComponent {
+ protected readonly agent = agent({
+ apiUrl: environment.langGraphApiUrl,
+ assistantId: environment.generativeUiAssistantId,
+ });
+ protected readonly dashboardViews = dashboardViews;
+ protected readonly suggestions = WELCOME_SUGGESTIONS;
+
+ protected send(text: string): void {
+ void this.agent.submit({ message: text });
+ }
+}
+```
+
+- [ ] **Step 2: Verify builds**
+
+```bash
+npx nx run cockpit-chat-generative-ui-angular:build
+npx nx run cockpit-chat-generative-ui-angular:build:cockpit
+```
+
+Expected: both green.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts
+git commit -m "$(cat <<'EOF'
+feat(cockpit-generative-ui): pre-baked welcome suggestions
+
+Two rows — "Render a dashboard" and
+"Render a form". Clicking either prompts the agent to emit a
+generative-UI payload that RenderSpecComponent mounts, firing
+RENDER_LIFECYCLE.firstMountAt → cockpit:generative_component_rendered
+activation signal.
+
+Co-Authored-By: Claude Opus 4.7
+EOF
+)"
+```
+
+---
+
+## Phase 5 — Verification (no commit)
+
+### Task 5.1: Full test sweep across affected projects
+
+- [ ] **Step 1: Run lib tests**
+
+```bash
+npx nx run-many -t test -p chat,cockpit-telemetry
+```
+
+Expected: green. Chat suite now ~712-714 tests passing (existing + 2 new lifecycle tests).
+
+- [ ] **Step 2: Run the 4 example builds in both modes**
+
+```bash
+npx nx run-many -t build -p cockpit-langgraph-streaming-angular,cockpit-langgraph-persistence-angular,cockpit-langgraph-interrupts-angular,cockpit-chat-generative-ui-angular
+```
+
+Expected: green.
+
+```bash
+npx nx run cockpit-langgraph-streaming-angular:build:cockpit
+npx nx run cockpit-langgraph-persistence-angular:build:cockpit
+npx nx run cockpit-langgraph-interrupts-angular:build:cockpit
+npx nx run cockpit-chat-generative-ui-angular:build:cockpit
+```
+
+Expected: all four green.
+
+- [ ] **Step 3: Sanity-check the suggestions render in the cockpit dev server**
+
+Optional smoke (requires `OPENAI_API_KEY` for the agent itself to respond, but the suggestion UI renders without it):
+
+```bash
+nx run cockpit:serve-streaming
+```
+
+Open `http://localhost:4201/langgraph/core-capabilities/streaming/...` in a browser. Confirm two pill-style suggestion rows appear below the welcome input ("Stream a long answer" and "Walk me through agent tool calls"). Click one — the input populates and the agent starts streaming (if backend is available).
+
+Repeat for the three other capabilities if desired.
+
+- [ ] **Step 4: Done**
+
+If Steps 1 and 2 pass, Spec 4 is implementation-complete. Proceed to PR.
+
+---
+
+## Self-Review
+
+**1. Spec coverage:**
+
+| Spec deliverable | Task |
+|---|---|
+| `libs/chat/src/lib/compositions/chat/chat.component.ts` — agent.lifecycle.streamStartedAt watcher | 0.2 |
+| `libs/chat/src/lib/lifecycle.spec.ts` — new test asserts agent-driven flip + stickiness | 0.1 (lands in 0.2's commit per TDD) |
+| `cockpit/langgraph/streaming/angular/src/app/streaming.component.ts` — suggestions | 1.1 |
+| `cockpit/langgraph/persistence/angular/src/app/persistence.component.ts` — suggestions | 2.1 |
+| `cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts` — suggestions | 3.1 |
+| `cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts` — suggestions | 4.1 |
+| Affected projects' tests + builds green | 5.1 |
+
+All deliverables covered.
+
+**2. Placeholder scan:** No "TBD", no "implement later", no "similar to Task N" without showing the code. Every step has the actual code or command. ✓
+
+**3. Type consistency:**
+
+- `WELCOME_SUGGESTIONS` shape `{label: string, value: string}[]` — same across all 4 example files. ✓
+- `send(text: string): void` signature — same across all 4 example files. ✓
+- `protected readonly suggestions = WELCOME_SUGGESTIONS` — same field name everywhere. ✓
+- `` `[label]` + `[value]` + `(selected)` — matches the component API in `libs/chat/src/lib/primitives/chat-welcome/chat-welcome-suggestion.component.ts`. ✓
+- `agent.lifecycle.streamStartedAt` — matches the `AGENT_LIFECYCLE` shape in `libs/langgraph/src/lib/lifecycle.ts` (Spec 1C Task 0.3). ✓
+- `this.lifecycle._internal.firstMessageSent` — matches the existing internal access pattern in `chat.component.ts` (Spec 1C Task 0.2). ✓
diff --git a/docs/superpowers/specs/gtm/2026-05-16-cockpit-activation-recipes-design.md b/docs/superpowers/specs/gtm/2026-05-16-cockpit-activation-recipes-design.md
new file mode 100644
index 000000000..353b55354
--- /dev/null
+++ b/docs/superpowers/specs/gtm/2026-05-16-cockpit-activation-recipes-design.md
@@ -0,0 +1,260 @@
+---
+workstream: cockpit-activation-recipes
+status: approved
+owner: brian
+phase: 1
+spec: docs/superpowers/specs/gtm/2026-05-16-cockpit-activation-recipes-design.md
+plan: docs/superpowers/plans/gtm/2026-05-16-cockpit-activation-recipes.md
+parent: docs/superpowers/specs/gtm/2026-05-13-gtm-meta-design.md
+---
+
+# Spec 4 — Cockpit Activation Recipes (Design)
+
+> Drop pre-baked "Try this prompt" suggestions onto each of the four capability examples that map to an activation signal, and tighten `ChatComponent` so `firstMessageSent` flips on any submit path. A developer landing in the cockpit now gets a 1-click path to firing each of the five activation signals.
+
+## 1. Goal
+
+Phase 1's activation deliverable: the developer-funnel dashboard from Spec 1A populates with real cohort data because evaluators have a frictionless way to walk through the activation funnel. Two cohesive changes:
+
+1. **Tighten `ChatComponent`** in `@ngaf/chat` so `CHAT_LIFECYCLE.firstMessageSent` flips on the first stream-start regardless of submit path. Today the flag only flips on input-bound submits; suggestion-click handlers that call `agent.submit({message})` directly bypass the lifecycle. The fix watches `AGENT_LIFECYCLE.streamStartedAt` and flips the sticky `firstMessageSent` flag on its first transition to non-null.
+2. **Add `` rows to four cockpit capability examples** (streaming, persistence, interrupts, generative-ui) using the canonical pattern from `examples/chat/angular/src/app/modes/embed-mode.component.ts`. Each suggestion is pre-baked to drive its capability's lifecycle signal.
+
+## 2. Context
+
+- Parent: `docs/superpowers/specs/gtm/2026-05-13-gtm-meta-design.md` §6 (Phase 1 critical path). The meta-spec's original exit criterion for Spec 4 referenced "six activation signals" and "one comparison page successfully drives an activation." Both are revised:
+ - Five signals (post-Spec 1C rename `cockpit:six_signals_complete` → `cockpit:activation_complete`).
+ - Spec 3 (comparison pages) was deliberately deferred; the "driver" for activations is now the cockpit shell sidebar itself.
+- The lifecycle wiring lives in `@ngaf/cockpit-telemetry` (Spec 1C) and fires on tokens from `@ngaf/chat`, `@ngaf/langgraph`, `@ngaf/render`. Spec 4 doesn't touch the telemetry library; it touches the *trigger surface* (the capability example components) and the chat composition that gates `firstMessageSent`.
+- The `` primitive already exists in `@ngaf/chat`:
+ - Declaration: `libs/chat/src/lib/primitives/chat-welcome/chat-welcome-suggestion.component.ts`
+ - Public export: `libs/chat/src/public-api.ts` line ~75
+ - Canonical usage in `examples/chat/angular/src/app/modes/embed-mode.component.ts` projects suggestions into the `[chatWelcomeSuggestions]` content slot of ``.
+- The `submitMessage()` public method added to `ChatComponent` in Spec 1C Task 0.2 already routes input submits through the lifecycle. But `agent.submit({message})` — the pattern the canonical `EmbedMode` uses for suggestion clicks — bypasses it. The cleanest fix is at the lib level (see §4.1) so every consumer benefits without per-example boilerplate.
+
+## 3. Scope
+
+**In scope:**
+
+- `libs/chat/src/lib/compositions/chat/chat.component.ts` — extend the existing init effect (or add a new effect alongside it) that watches `agent.lifecycle.streamStartedAt`. When it transitions from `null` to a non-null number, flip `lifecycle._internal.firstMessageSent.set(true)` — but only if it isn't already true (idempotent, sticky semantic preserved). `messageCount` and `inputSubmittedAt` are NOT touched by this path; they stay input-bound by design.
+- A new unit test in `libs/chat/src/lib/lifecycle.spec.ts` (or a sibling) covering the "agent-driven submit fires firstMessageSent" case. Use the existing `mockAgent` helper from `./testing/mock-agent`.
+- Per-example WELCOME_SUGGESTIONS for the four activation-signal capability examples:
+ - `cockpit/langgraph/streaming/angular/src/app/streaming.component.ts`
+ - `cockpit/langgraph/persistence/angular/src/app/persistence.component.ts`
+ - `cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts`
+ - `cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts`
+- Each example mirrors the `EmbedMode` pattern: imports `ChatWelcomeSuggestionComponent`, projects suggestions through ``, click handler calls `agent.submit({message: text})`.
+- Each example gets a small Vitest test (or component-level Angular test) asserting that suggestion rows render with the expected labels. Tests use the existing test infrastructure under each example's `angular/` directory.
+- A short README pointer (`cockpit/README.md` or a per-example doc) describing how to add more suggestions if a future capability adds an activation path. Optional polish — skip if it inflates scope.
+
+**Out of scope:**
+
+- aimock harness as the default backend for `cockpit:serve-
`. Today the python backends require `OPENAI_API_KEY`; without it, suggestion clicks fail at the stream layer. We accept this v1: evaluators with an API key get the full path; evaluators without one still see the suggestion UI but the agent errors. A follow-up spec can wire aimock as the default.
+- An "activation progress" indicator (e.g., "3/5 signals fired") in the cockpit shell. Worth doing — a follow-up spec can add it. Spec 4 stays focused on the trigger surface + the lib fix.
+- A Playwright end-to-end activation suite that walks all five signals and asserts `cockpit:activation_complete` lands in PostHog. Worth doing — a follow-up spec can add it.
+- New analytics events. The existing `cockpit:*` event names are unchanged.
+- Updating the three existing `examples/chat/angular/src/app/modes/{embed,sidebar,popup}-mode.component.ts` files. They already use the canonical pattern; the lib fix in §4.1 makes their suggestion paths fire `firstMessageSent` automatically. No edits needed.
+- The fourth capability page IS `cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts`. If the path turns out to differ (verify in implementation), the implementer adapts; spec intent is "the example that fires `cockpit:generative_component_rendered`."
+
+**Success criteria:**
+
+- Clicking any pre-baked suggestion in any of the four capability examples fires its corresponding activation signal (assuming a working backend).
+- `CHAT_LIFECYCLE.firstMessageSent` is true after the first stream chunk, regardless of whether the submit came from the input or from `agent.submit({message})`.
+- `npx nx run-many -t test -p chat,cockpit-telemetry` is green.
+- The four cockpit example builds (`npx nx run cockpit-langgraph-streaming-angular:build:cockpit` etc.) succeed.
+- Manual smoke: a user opening a capability in the cockpit, clicking a suggestion, and waiting for the stream sees the corresponding signal land in PostHog Live Events.
+
+## 4. Architecture
+
+```
+User clicks
+ │ (selected) → send(text)
+ ▼
+example component:
+ agent.submit({ message: text })
+ │
+ ├──▶ AGENT_LIFECYCLE.streamStartedAt flips on first chunk arrival
+ │ │
+ │ ├──▶ CockpitTelemetryService observes → fires cockpit:transport_connected
+ │ │
+ │ └──▶ ChatComponent observes (NEW in Spec 4) → flips CHAT_LIFECYCLE.firstMessageSent
+ │ │
+ │ └──▶ CockpitTelemetryService observes → fires cockpit:chat_first_message
+ │
+ ├──▶ AGENT_LIFECYCLE.threadCreatedAt (first submit) → cockpit:thread_persisted later, on reload
+ ├──▶ AGENT_LIFECYCLE.interruptResolvedAt (when user resolves) → cockpit:interrupt_handled
+ └──▶ RENDER_LIFECYCLE.firstMountAt (when generative UI mounts) → cockpit:generative_component_rendered
+```
+
+## 5. Components
+
+### 5.1 `ChatComponent` enhancement
+
+In `libs/chat/src/lib/compositions/chat/chat.component.ts`, the constructor already runs an effect that flips `componentReady` once `this.agent()` resolves. Add a parallel effect that watches the agent's `streamStartedAt`:
+
+```typescript
+// Inside the constructor (or alongside the existing init effect):
+effect(() => {
+ const agentRef = this.agent();
+ if (!agentRef) return;
+ const streamStartedAt = agentRef.lifecycle.streamStartedAt();
+ if (streamStartedAt !== null && !this.lifecycle._internal.firstMessageSent()) {
+ this.lifecycle._internal.firstMessageSent.set(true);
+ }
+});
+```
+
+Semantics:
+- The signal is sticky. The if-guard prevents double-set.
+- `messageCount` and `inputSubmittedAt` stay input-bound — agent-bound submits don't increment them. Rationale: those signals exist to measure UX engagement (typing, time-stamped interaction), not stream initiation. The downstream cockpit telemetry only depends on `firstMessageSent` for `cockpit:chat_first_message`, so the more selective scope is acceptable.
+- The effect's auto-tracking re-runs on every signal change, but the work is constant-time and idempotent.
+
+### 5.2 New test in `libs/chat/src/lib/lifecycle.spec.ts`
+
+```typescript
+test('firstMessageSent flips on agent-driven submit (no input call)', async () => {
+ // ...standard fixture setup with mockAgent + TestBed...
+ expect(lifecycle.firstMessageSent()).toBe(false);
+ // Programmatic submit (bypasses chat-input)
+ await chatRef.agent().submit({ message: 'hello from agent.submit' });
+ // After the stream starts, firstMessageSent must be true.
+ await waitForSignal(lifecycle.firstMessageSent, true);
+ expect(lifecycle.firstMessageSent()).toBe(true);
+});
+```
+
+The exact API (`mockAgent`, `waitForSignal` helper if it exists) follows the patterns already in `lifecycle.spec.ts`. The implementer adapts to match.
+
+### 5.3 Per-example suggestion arrays
+
+Each of the four example components adds a `WELCOME_SUGGESTIONS` const, imports `ChatWelcomeSuggestionComponent`, and projects suggestions into ``. Full template + content per file:
+
+**`cockpit/langgraph/streaming/angular/src/app/streaming.component.ts`:**
+
+```typescript
+const WELCOME_SUGGESTIONS = [
+ { label: 'Stream a long answer', value: 'Explain LangGraph checkpointing in 200 words.' },
+ { label: 'Walk me through agent tool calls', value: 'Show me how an agent decides which tool to use.' },
+];
+```
+
+Template:
+```html
+
+
+
+ @for (s of suggestions; track s.value) {
+
+ }
+
+
+
+```
+
+`send(text)`:
+```typescript
+protected send(text: string): void {
+ void this.agent.submit({ message: text });
+}
+```
+
+**`cockpit/langgraph/persistence/angular/src/app/persistence.component.ts`:**
+
+```typescript
+const WELCOME_SUGGESTIONS = [
+ { label: 'Save this thread for later', value: 'Help me draft a project brief I can revisit.' },
+];
+```
+
+**`cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts`:**
+
+```typescript
+const WELCOME_SUGGESTIONS = [
+ { label: 'Approve a tool call', value: 'Book a flight to Paris for next Tuesday.' },
+];
+```
+
+**`cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts`:**
+
+```typescript
+const WELCOME_SUGGESTIONS = [
+ { label: 'Render a dashboard', value: 'Show me a Q3 sales dashboard with three metrics.' },
+ { label: 'Render a form', value: 'Create a contact form with name, email, and message.' },
+];
+```
+
+Each component imports `ChatWelcomeSuggestionComponent` from `@ngaf/chat`, adds it to the `imports` array, and exposes `suggestions = WELCOME_SUGGESTIONS` as a protected readonly field.
+
+### 5.4 Per-example tests
+
+Each capability example adds a small Vitest test:
+
+```typescript
+// e.g., cockpit/langgraph/streaming/angular/src/app/streaming.component.spec.ts
+test('renders welcome suggestion buttons', async () => {
+ const fixture = TestBed.createComponent(StreamingComponent);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const buttons = fixture.nativeElement.querySelectorAll('.chat-welcome-suggestion');
+ expect(buttons.length).toBeGreaterThan(0);
+ expect(buttons[0].textContent).toMatch(/Stream a long answer/);
+});
+```
+
+(Spec intent — exact selector + helper may need adjusting to match existing test patterns in the same example directory.)
+
+## 6. Data flow
+
+For a Streaming-capability user landing fresh:
+
+1. User opens `/langgraph/core-capabilities/streaming/...` in the cockpit.
+2. `` renders the welcome state (no messages); `` slot shows the input plus two `` rows.
+3. User clicks "Stream a long answer".
+4. `(selected)="send($event)"` fires with `'Explain LangGraph checkpointing in 200 words.'`.
+5. `send()` calls `agent.submit({ message: text })`.
+6. The agent stream starts → `AGENT_LIFECYCLE.streamStartedAt` flips non-null.
+7. `CockpitTelemetryService.subscribeAgent()` effect fires `cockpit:transport_connected` and `aggregator.markSignal('transport_connected')`.
+8. `ChatComponent`'s new Spec-4 effect observes the same `streamStartedAt` transition and flips `CHAT_LIFECYCLE.firstMessageSent` to true.
+9. `CockpitTelemetryService.subscribeChat()` effect fires `cockpit:chat_first_message` and `aggregator.markSignal('chat_first_message')`.
+10. Both signals join the 5-signal aggregator window. The user explores other capabilities; when all five fire within 30 min, `cockpit:activation_complete` fires.
+
+## 7. Error handling
+
+- **Suggestion click with no backend:** the agent's `submit` throws or stalls; the chat surface shows its existing error state. No special handling needed.
+- **`agent.submit` errors:** captured by the existing chat error pipeline. `firstMessageSent` does not flip because `streamStartedAt` doesn't transition to non-null.
+- **Empty `WELCOME_SUGGESTIONS`:** the `@for` loop renders nothing; the welcome slot is empty. Each example's array is hard-coded so this is a code-review concern, not a runtime risk.
+
+## 8. Testing
+
+- **Unit (jsdom, `libs/chat`):** one new lifecycle spec assertion for agent-driven submit → firstMessageSent.
+- **Per-example tests:** four small assertions that suggestion buttons render.
+- **Build smoke:** `nx run cockpit-langgraph-streaming-angular:build:cockpit` and the three siblings build green.
+- **No new dashboard or PostHog work** — the existing developer-funnel dashboard and the activation-funnel insight already consume the same events.
+- **Manual smoke** (post-merge, in production): open the cockpit on a real PostHog token; click a suggestion in each capability; verify both `cockpit:transport_connected` and `cockpit:chat_first_message` show up in Live Events with the same `cockpit_did`.
+
+## 9. Risks
+
+- **Backend dependency.** Capability examples require `OPENAI_API_KEY` to actually fire signals beyond `transport_connected`. Without one, the user sees the suggestion UI but the agent errors. Acceptable for v1; a follow-up spec can wire aimock as the default.
+- **`firstMessageSent` flipping on stream-start, not on submit.** If an agent errors before the first chunk arrives, the signal never flips even though the user did submit. Acceptable: `cockpit:chat_first_message` is meant to mean "the user got a streaming response", which is the threshold for "they engaged with the cockpit" in PostHog terms.
+- **Prompt content may not drive every signal cleanly.** "Book a flight to Paris" only triggers an interrupt if the example graph is wired for human-in-the-loop on that intent. The implementer verifies during smoke; if a prompt doesn't drive the signal, the implementer adjusts the prompt (not the graph) until it does.
+- **Substring-overlap rule** (from earlier specs): no blind `replace_all`. Each example file is edited individually.
+
+## 10. Phases
+
+1. **Phase 0 — `ChatComponent` enhancement.** Add the agent.lifecycle.streamStartedAt watcher + new lifecycle test. (~2 commits, TDD.)
+2. **Phase 1 — Streaming example.** WELCOME_SUGGESTIONS + template + test. (~1 commit.)
+3. **Phase 2 — Persistence example.** Same pattern. (~1 commit.)
+4. **Phase 3 — Interrupts example.** Same pattern. (~1 commit.)
+5. **Phase 4 — Generative-UI example.** Same pattern. (~1 commit.)
+6. **Phase 5 — Verification** (no commit). Tests + builds + brief manual sanity in the cockpit dev server.
+
+## 11. Deliverables
+
+- ☐ `libs/chat/src/lib/compositions/chat/chat.component.ts` — agent.lifecycle.streamStartedAt watcher added
+- ☐ `libs/chat/src/lib/lifecycle.spec.ts` — new test asserts agent-driven submit flips firstMessageSent
+- ☐ `cockpit/langgraph/streaming/angular/src/app/streaming.component.ts` — WELCOME_SUGGESTIONS + `` rows + `send()`
+- ☐ `cockpit/langgraph/persistence/angular/src/app/persistence.component.ts` — same
+- ☐ `cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts` — same
+- ☐ `cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts` — same
+- ☐ Per-example component test asserts suggestion buttons render
+- ☐ `nx run-many -t test -p chat,cockpit-telemetry` green
+- ☐ Per-example `:build:cockpit` targets green
diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts
index 5c4c1bcc2..e8ffc76cd 100644
--- a/libs/chat/src/lib/compositions/chat/chat.component.ts
+++ b/libs/chat/src/lib/compositions/chat/chat.component.ts
@@ -422,6 +422,21 @@ export class ChatComponent {
});
});
+ // Spec 4: flip CHAT_LIFECYCLE.firstMessageSent when the agent's stream
+ // starts, regardless of submit path (input-bound, programmatic, suggestion-
+ // click). Sticky — guarded so we never re-set a flag that's already true.
+ // `lifecycle` is not on the base Agent contract; adapters like @ngaf/langgraph
+ // attach it. Duck-type the read so non-lifecycle agents are a no-op.
+ effect(() => {
+ let agentRef: ReturnType;
+ try { agentRef = this.agent(); } catch { return; }
+ const lc = (agentRef as unknown as { lifecycle?: { streamStartedAt?: () => number | null } }).lifecycle;
+ const streamStartedAt = lc?.streamStartedAt?.();
+ if (streamStartedAt != null && !this.lifecycle._internal.firstMessageSent()) {
+ this.lifecycle._internal.firstMessageSent.set(true);
+ }
+ });
+
// Auto-scroll-to-bottom. Fires on every signal update during streaming
// (each token mutates the last message's content), so this MUST be cheap
// and idempotent. Earlier this used scrollTo({ behavior: 'smooth' }) per
diff --git a/libs/chat/src/lib/lifecycle.spec.ts b/libs/chat/src/lib/lifecycle.spec.ts
index cd3b62d98..68c4025be 100644
--- a/libs/chat/src/lib/lifecycle.spec.ts
+++ b/libs/chat/src/lib/lifecycle.spec.ts
@@ -70,4 +70,37 @@ describe('ChatLifecycle integration', () => {
expect(lifecycle.messageCount()).toBe(1);
expect(lifecycle.firstMessageSent()).toBe(true);
});
+
+ test('firstMessageSent flips when agent.lifecycle.streamStartedAt transitions to non-null', () => {
+ const agent = mockAgent();
+ const f = TestBed.createComponent(ChatComponent);
+ f.componentRef.setInput('agent', agent);
+ f.detectChanges();
+ const lc = f.componentRef.injector.get(CHAT_LIFECYCLE);
+ expect(lc.firstMessageSent()).toBe(false);
+
+ agent._internal.streamStartedAt.set(Date.now());
+ f.detectChanges();
+ expect(lc.firstMessageSent()).toBe(true);
+ });
+
+ test('firstMessageSent stays sticky across multiple agent-driven transitions', () => {
+ const agent = mockAgent();
+ const f = TestBed.createComponent(ChatComponent);
+ f.componentRef.setInput('agent', agent);
+ f.detectChanges();
+ const lc = f.componentRef.injector.get(CHAT_LIFECYCLE);
+
+ agent._internal.streamStartedAt.set(Date.now());
+ f.detectChanges();
+ expect(lc.firstMessageSent()).toBe(true);
+
+ agent._internal.streamStartedAt.set(null);
+ f.detectChanges();
+ expect(lc.firstMessageSent()).toBe(true);
+
+ agent._internal.streamStartedAt.set(Date.now());
+ f.detectChanges();
+ expect(lc.firstMessageSent()).toBe(true);
+ });
});
diff --git a/libs/chat/src/lib/testing/mock-agent.ts b/libs/chat/src/lib/testing/mock-agent.ts
index 9a26f5062..d8dd083eb 100644
--- a/libs/chat/src/lib/testing/mock-agent.ts
+++ b/libs/chat/src/lib/testing/mock-agent.ts
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-import { signal, WritableSignal } from '@angular/core';
+import { signal, Signal, WritableSignal } from '@angular/core';
import { EMPTY, type Observable } from 'rxjs';
import type {
Agent,
@@ -25,6 +25,24 @@ export interface MockAgent extends Agent {
subagents?: WritableSignal