Skip to content

Commit 0539b84

Browse files
bloveclaude
andauthored
feat(gtm): Spec 1C — cockpit instrumentation (analytics-foundation 1c) (#351)
* docs(gtm): spec for analytics-foundation-1c (cockpit instrumentation) Spec 1C of the GTM motion. Three-surface cockpit instrumentation: - Outer (React shell): cockpit:recipe_opened, mode_switched, code_copied via posthog-js direct - Inner (Angular iframes, per-example): cockpit:chat_first_message, transport_connected, thread_persisted, interrupt_handled, generative_component_rendered via new @ngaf/cockpit-telemetry private library that subscribes to lifecycle signals on @ngaf/chat, @ngaf/langgraph, @ngaf/render - Cross-frame correlation via session UUID in URL params; memory-only persistence on both frames Key decisions: - Architecture B (lifecycle signals + external adapter) — libraries expose @ngaf/* tokens, adapter is private. Customer apps never emit cockpit:* events. - cockpit-telemetry uses posthog-js directly, not @ngaf/telemetry/browser (cockpit is internal product, different posture from customer libs) - main.cockpit.ts build-time entry override per example, so example reference code (main.ts, app.config.ts, components) stays pristine - Telemetry on by default in production, off on localhost unless NEXT_PUBLIC_COCKPIT_CAPTURE_LOCAL=true; honors DO_NOT_TRACK - Activation funnel = 5 signals (dropped cockpit:install_command_copied; ngaf:postinstall from PR #328 is uncorrelatable to cockpit sessions by design) - Renamed cockpit:six_signals_complete → cockpit:activation_complete - All 32 examples rolled out in batched per-category commits within this plan; canonical example: cockpit/langgraph/streaming/angular - Website docs for the three public *_LIFECYCLE tokens land as Phase 5 Phases: 0. Library lifecycle additions (~21 tests) 1. @ngaf/cockpit-telemetry private library (~24 tests, incl. permanent browser silence test) 2. React shell instrumentation (~17 tests) 3. Canonical streaming example + Chrome MCP smoke 4. 31 remaining examples in 4 category batches 5. Website docs at /docs/<lib>/lifecycle 6. Taxonomy + PostHog dashboard cleanup (drop install_command_copied, rename event + insight + dashboard, posthog:sync) Total ~65 tests, 15 new spec files. Pre-PR-#328 design adjusted to match the new ingest proxy + per-package install telemetry pattern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(gtm): implementation plan for analytics-foundation-1c (cockpit instrumentation) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(chat): add CHAT_LIFECYCLE InjectionToken + interface Public API addition for cockpit-telemetry (and other consumers) to subscribe to per-instance chat lifecycle signals. componentReady, firstMessageSent (sticky), messageCount and inputSubmittedAt (reset on clearThread). Token only; wiring lands in next task. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(chat): wire CHAT_LIFECYCLE in ChatComponent Populates the four lifecycle signals from existing component code paths: componentReady on the first agent-resolved effect, firstMessageSent/messageCount/inputSubmittedAt in a new public submitMessage() (also driven by the chat-input submitted output), and reset (except sticky firstMessageSent) in a new public clearThread(). The token is provided component-scoped via a factory that hands ChatComponent a writable internal handle while consumers see only the readonly Signal<T> surface. Adds 6 tests covering all transitions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(chat): add onUserSubmitted coverage + clarify clearThread doc Addresses code-quality review feedback on Task 0.2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(langgraph): add AGENT_LIFECYCLE InjectionToken + interface 8 lifecycle signals exposing transition timestamps. Wiring lands in agent.fn.ts in the next task. Three signals (interruptResolvedAt, threadCreatedAt, threadPersistedAt) require new hook points; five are derived from existing BehaviorSubjects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(langgraph): wire AGENT_LIFECYCLE in agent.fn.ts Eight signal updates hooked into existing stream subscriptions and the agent's submit/switchThread/load-history paths. Three new hooks (interruptResolvedAt, threadCreatedAt, threadPersistedAt) — five signals derive from existing stream state. All reset on switchThread. Lifecycle surface exposed via a new `lifecycle: AgentLifecycle` field on the returned LangGraphAgent (the factory has no DI scope of its own, so this is the minimal-pollution path; consumers can re-provide the AGENT_LIFECYCLE token via standard Angular providers if needed). Mock agent updated to satisfy the new field. 10 tests cover all transitions; 154/154 langgraph suite passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(render): add RENDER_LIFECYCLE token + service + wiring Service subscribes to the existing render-event stream and reduces to five signals. firstMountAt is sticky; the rest update on each event. Provided via provideRender() so all consumers automatically have access. All RenderEvents flow through a single emitTapped() in RenderSpecComponent, which fans out to the events output AND notifies the lifecycle service (when present). Service is injected optionally so the components remain usable without provideRender(). 5 tests cover all signal transitions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-telemetry): scaffold private Nx library @ngaf/cockpit-telemetry — private (not in publishable group), Angular library, consumed by the 32 Angular examples via main.cockpit.ts build-time entry override. Mirrors @ngaf/cockpit-shell scaffold pattern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-telemetry): config token + typed event names Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-telemetry): readCockpitConfigFromIframe — URL param reader Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-telemetry): ActivationAggregator — 5-signal rollup with 30-min window 6 tests cover the rollup math: pre-complete state, fire-once-when-complete, idempotent signals, 30-min window reset, duration_ms property on emit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-telemetry): CockpitTelemetryService — lifecycle subscribers + posthog init Initializes posthog-js with memory persistence + parent-provided distinct_id, subscribes to CHAT/AGENT/RENDER lifecycle tokens (each optional — graceful no-op if absent), fires cockpit:* events and marks signals on the ActivationAggregator. 6 tests cover init idempotency, capture format, missing-lifecycle gracefulness, capability property stamping. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-telemetry): provideCockpitTelemetry() EnvironmentProviders factory Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-telemetry): bootstrapWithCockpitHarness — main.cockpit.ts entry helper Each cockpit example's main.cockpit.ts calls this with its AppComponent + appConfig. When URL params present, telemetry providers are added; otherwise bootstraps pristine. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(cockpit-telemetry): permanent browser silence contract test When the cockpit harness is not present (no URL params), no eager import of posthog-js. Mirrors @ngaf/telemetry/browser silence pattern. Stays green permanently. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(cockpit-telemetry): declare peer deps + use lib- selector in spec - Add @angular/platform-browser, @ngaf/chat, @ngaf/langgraph, @ngaf/render to peerDependencies (consumed by harness + service). - TestComponent in harness.spec.ts uses lib- prefix per project eslint rules. - Lockfile updated to record posthog-js install for the new private lib. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit): analytics module — distinct-id, properties, events, client Mirrors apps/website/src/lib/analytics/ structure. Memory-only session UUID, shouldCaptureAnalytics guard with localhost gate + DO_NOT_TRACK honoring, typed track() helper. ~10 tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit): posthog-js initialization via instrumentation-client.ts Memory persistence + parent-side session UUID. Off on localhost by default (NEXT_PUBLIC_COCKPIT_CAPTURE_LOCAL=true to override). Honors DO_NOT_TRACK. Three new env vars documented in .env.example. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit): fire cockpit:recipe_opened on sidebar capability click Properties: capability, category, from_capability. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit): fire cockpit:mode_switched on mode tab change Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit): fire cockpit:code_copied on Code mode copy Properties: capability, surface=code_mode, file_path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit): fire cockpit:code_copied on narrative docs copy buttons Two surfaces: docs_code_snippet (inline code blocks) and agentic_prompt (prompt callouts). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit): RunMode appends cockpit_did/cockpit_cap to iframe src The iframe URL now carries the session UUID + capability slug + posthog key + host so the Angular harness can correlate to the parent session. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-streaming): main.cockpit.ts harness entry Three-line harness uses bootstrapWithCockpitHarness from @ngaf/cockpit-telemetry. Pristine main.ts unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-streaming): add cockpit build configuration cockpit/<...>/project.json gains a cockpit build that uses main.cockpit.ts as the entry. apps/cockpit:serve-streaming now invokes serve:cockpit on the example. Production build unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-langgraph): wire 7 examples to cockpit-telemetry harness Add main.cockpit.ts + cockpit build/serve configurations to the remaining LangGraph cockpit examples (memory, durable-execution, subgraphs, deployment-runtime, interrupts, persistence, time-travel). Update apps/cockpit serve-* targets to use the :serve:cockpit config so the iframe loads the harness-enabled build. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-deep-agents): wire 6 examples to cockpit-telemetry harness Add main.cockpit.ts + cockpit build/serve configurations to all Deep Agents cockpit examples (sandboxes, subagents, memory, planning, filesystem, skills). Update apps/cockpit serve-* targets to use the :serve:cockpit config so the iframe loads the harness-enabled build. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-chat): wire 11 examples to cockpit-telemetry harness Add main.cockpit.ts + cockpit build/serve configurations to all chat cockpit examples (tool-calls, messages, subagents, input, a2ui, theming, threads, interrupts, timeline, generative-ui, debug). The timeline harness preserves the installEmbeddedTheme() call before bootstrap. Chat examples are launched via the serve-example.ts script; that script is updated in the render batch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cockpit-render): wire 6 examples to cockpit-telemetry harness Add main.cockpit.ts + cockpit build/serve configurations to all render cockpit examples (computed-functions, element-rendering, repeat-loops, state-management, spec-rendering, registry). Also update the shared serve-example.ts script so both --capability and --all modes launch the harness-enabled :serve:cockpit configuration; this covers the chat + render capabilities that don't have per-capability serve-* targets in apps/cockpit/project.json. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(website): chat/lifecycle.md — CHAT_LIFECYCLE signal docs Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(website): langgraph/lifecycle.md — AGENT_LIFECYCLE signal docs Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(website): render/lifecycle.md — RENDER_LIFECYCLE signal docs Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(website): link lifecycle pages from each lib's landing Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(gtm): drop install_command_copied + rename activation event Activation funnel is 5 signals per Spec 1C. ngaf:postinstall is its own top-of-funnel metric, uncorrelated to cockpit sessions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(posthog): rename six-signal-activation-funnel → activation-funnel 5 steps (dropped install_command_copied), 30-minute window. posthog_id nulled to force create on next sync (PostHog will assign a new id). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(posthog): developer-funnel references activation-funnel insight Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(cockpit): polyfill CSS.escape in jsdom test setup jsdom doesn't implement CSS.escape; code-mode copy handler calls it. Tests passed but vitest flagged an unhandled error. Polyfill restores green nx test target. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(gtm): align taxonomy with implemented cockpit shell events - Rename cockpit:recipe_start → cockpit:recipe_opened (sidebar click) - Add cockpit:mode_switched (Run/Code/Docs tab change) - Add cockpit:code_copied (code mode, doc snippet, agentic prompt) - Update cockpit-recipe-completion insight to use renamed event - Document that shell events are funnel context, not activation steps Addresses code-review finding on Spec 1C. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(langgraph,cockpit-telemetry): wire AGENT_LIFECYCLE via registry Bug: CockpitTelemetryService.subscribeAgent() injected AGENT_LIFECYCLE which is never provided in DI (agent() exposes lifecycle on its return object). Three activation signals (transport_connected, thread_persisted, interrupt_handled) never fired; activation funnel was unreachable. Fix: Add AgentLifecycleRegistry to @ngaf/langgraph as an optional service. agent() registers itself if the registry is provided. provideCockpitTelemetry now provides the registry, and the service subscribes to lifecycles reactively via a signal effect. Addresses code-review item on Spec 1C. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(render): drop providedIn:root on RenderLifecycleService Service is already provided by provideRender(). The redundant providedIn:'root' caused both paths to resolve to the same singleton — removing it makes the scope follow the consumer's provideRender() call (sub-tree-friendly). Addresses code-review minor item on Spec 1C. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(website): regenerate api-docs.json for lifecycle additions Picks up CHAT_LIFECYCLE, AGENT_LIFECYCLE, RENDER_LIFECYCLE tokens + AgentLifecycleRegistry from Spec 1C. CI drift check would fail without this regeneration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5909873 commit 0539b84

146 files changed

Lines changed: 6810 additions & 90 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ POSTHOG_PROJECT_ID=
1313
# NGAF_TELEMETRY_SAMPLE_RATE=1.0
1414
# DO_NOT_TRACK=1 # cross-vendor opt-out
1515
# NGAF_TELEMETRY_DISABLED=1 # package-specific opt-out
16+
17+
# Cockpit shell analytics (apps/cockpit)
18+
NEXT_PUBLIC_COCKPIT_POSTHOG_TOKEN=
19+
NEXT_PUBLIC_COCKPIT_POSTHOG_HOST=https://us.i.posthog.com
20+
NEXT_PUBLIC_COCKPIT_CAPTURE_LOCAL=false
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: MIT
2+
import posthog from 'posthog-js';
3+
import { getCockpitSessionId } from './src/lib/analytics/distinct-id';
4+
import { shouldCaptureAnalytics } from './src/lib/analytics/properties';
5+
6+
const token = process.env.NEXT_PUBLIC_COCKPIT_POSTHOG_TOKEN;
7+
const captureLocal = process.env.NEXT_PUBLIC_COCKPIT_CAPTURE_LOCAL === 'true';
8+
const host = typeof window === 'undefined' ? undefined : window.location.host;
9+
const doNotTrack = typeof navigator !== 'undefined' && navigator.doNotTrack === '1';
10+
11+
if (shouldCaptureAnalytics({ token, captureLocal, host, doNotTrack })) {
12+
posthog.init(token!, {
13+
api_host: process.env.NEXT_PUBLIC_COCKPIT_POSTHOG_HOST ?? 'https://us.i.posthog.com',
14+
persistence: 'memory',
15+
bootstrap: { distinctID: getCockpitSessionId() },
16+
autocapture: false,
17+
capture_pageview: false,
18+
defaults: '2026-01-30',
19+
});
20+
}

apps/cockpit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"clsx": "^2.1.1",
1010
"marked": "^15.0.0",
1111
"next": "~16.1.6",
12+
"posthog-js": "^1.372.6",
1213
"react": "^19.0.0",
1314
"react-dom": "^19.0.0",
1415
"tailwind-merge": "^2.5.0"

apps/cockpit/project.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"options": {
5858
"commands": [
5959
"npx nx serve cockpit --port 4201",
60-
"npx nx serve cockpit-langgraph-streaming-angular --port 4300",
60+
"npx nx serve cockpit-langgraph-streaming-angular:serve:cockpit --port 4300",
6161
"cd cockpit/langgraph/streaming/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
6262
],
6363
"parallel": true
@@ -68,7 +68,7 @@
6868
"options": {
6969
"commands": [
7070
"npx nx serve cockpit --port 4201",
71-
"npx nx serve cockpit-langgraph-persistence-angular --port 4301",
71+
"npx nx serve cockpit-langgraph-persistence-angular:serve:cockpit --port 4301",
7272
"cd cockpit/langgraph/persistence/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
7373
],
7474
"parallel": true
@@ -79,7 +79,7 @@
7979
"options": {
8080
"commands": [
8181
"npx nx serve cockpit --port 4201",
82-
"npx nx serve cockpit-langgraph-interrupts-angular --port 4302",
82+
"npx nx serve cockpit-langgraph-interrupts-angular:serve:cockpit --port 4302",
8383
"cd cockpit/langgraph/interrupts/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
8484
],
8585
"parallel": true
@@ -90,7 +90,7 @@
9090
"options": {
9191
"commands": [
9292
"npx nx serve cockpit --port 4201",
93-
"npx nx serve cockpit-langgraph-memory-angular --port 4303",
93+
"npx nx serve cockpit-langgraph-memory-angular:serve:cockpit --port 4303",
9494
"cd cockpit/langgraph/memory/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
9595
],
9696
"parallel": true
@@ -101,7 +101,7 @@
101101
"options": {
102102
"commands": [
103103
"npx nx serve cockpit --port 4201",
104-
"npx nx serve cockpit-langgraph-durable-execution-angular --port 4304",
104+
"npx nx serve cockpit-langgraph-durable-execution-angular:serve:cockpit --port 4304",
105105
"cd cockpit/langgraph/durable-execution/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
106106
],
107107
"parallel": true
@@ -112,7 +112,7 @@
112112
"options": {
113113
"commands": [
114114
"npx nx serve cockpit --port 4201",
115-
"npx nx serve cockpit-langgraph-subgraphs-angular --port 4305",
115+
"npx nx serve cockpit-langgraph-subgraphs-angular:serve:cockpit --port 4305",
116116
"cd cockpit/langgraph/subgraphs/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
117117
],
118118
"parallel": true
@@ -123,7 +123,7 @@
123123
"options": {
124124
"commands": [
125125
"npx nx serve cockpit --port 4201",
126-
"npx nx serve cockpit-langgraph-time-travel-angular --port 4306",
126+
"npx nx serve cockpit-langgraph-time-travel-angular:serve:cockpit --port 4306",
127127
"cd cockpit/langgraph/time-travel/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
128128
],
129129
"parallel": true
@@ -134,7 +134,7 @@
134134
"options": {
135135
"commands": [
136136
"npx nx serve cockpit --port 4201",
137-
"npx nx serve cockpit-langgraph-deployment-runtime-angular --port 4307",
137+
"npx nx serve cockpit-langgraph-deployment-runtime-angular:serve:cockpit --port 4307",
138138
"cd cockpit/langgraph/deployment-runtime/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
139139
],
140140
"parallel": true
@@ -145,7 +145,7 @@
145145
"options": {
146146
"commands": [
147147
"npx nx serve cockpit --port 4201",
148-
"npx nx serve cockpit-deep-agents-planning-angular --port 4310",
148+
"npx nx serve cockpit-deep-agents-planning-angular:serve:cockpit --port 4310",
149149
"cd cockpit/deep-agents/planning/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
150150
],
151151
"parallel": true
@@ -156,7 +156,7 @@
156156
"options": {
157157
"commands": [
158158
"npx nx serve cockpit --port 4201",
159-
"npx nx serve cockpit-deep-agents-filesystem-angular --port 4311",
159+
"npx nx serve cockpit-deep-agents-filesystem-angular:serve:cockpit --port 4311",
160160
"cd cockpit/deep-agents/filesystem/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
161161
],
162162
"parallel": true
@@ -167,7 +167,7 @@
167167
"options": {
168168
"commands": [
169169
"npx nx serve cockpit --port 4201",
170-
"npx nx serve cockpit-deep-agents-subagents-angular --port 4312",
170+
"npx nx serve cockpit-deep-agents-subagents-angular:serve:cockpit --port 4312",
171171
"cd cockpit/deep-agents/subagents/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
172172
],
173173
"parallel": true
@@ -178,7 +178,7 @@
178178
"options": {
179179
"commands": [
180180
"npx nx serve cockpit --port 4201",
181-
"npx nx serve cockpit-deep-agents-memory-angular --port 4313",
181+
"npx nx serve cockpit-deep-agents-memory-angular:serve:cockpit --port 4313",
182182
"cd cockpit/deep-agents/memory/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
183183
],
184184
"parallel": true
@@ -189,7 +189,7 @@
189189
"options": {
190190
"commands": [
191191
"npx nx serve cockpit --port 4201",
192-
"npx nx serve cockpit-deep-agents-skills-angular --port 4314",
192+
"npx nx serve cockpit-deep-agents-skills-angular:serve:cockpit --port 4314",
193193
"cd cockpit/deep-agents/skills/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
194194
],
195195
"parallel": true
@@ -200,7 +200,7 @@
200200
"options": {
201201
"commands": [
202202
"npx nx serve cockpit --port 4201",
203-
"npx nx serve cockpit-deep-agents-sandboxes-angular --port 4315",
203+
"npx nx serve cockpit-deep-agents-sandboxes-angular:serve:cockpit --port 4315",
204204
"cd cockpit/deep-agents/sandboxes/python && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123"
205205
],
206206
"parallel": true

apps/cockpit/scripts/serve-example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ process.on('SIGTERM', cleanup);
3030
run('cockpit', 'npx nx serve cockpit --port 4201', '36');
3131

3232
if (allMode) {
33-
capabilities.forEach((c) => run(c.id, `npx nx serve ${c.angularProject} --port ${c.port}`, '33'));
33+
capabilities.forEach((c) => run(c.id, `npx nx serve ${c.angularProject}:serve:cockpit --port ${c.port}`, '33'));
3434
console.log('\n🚀 Starting cockpit + all 14 examples\n');
3535
} else {
3636
const cap = findCapability(capabilityArg!);
3737
if (!cap) { console.error(`Unknown: ${capabilityArg}`); process.exit(1); }
38-
run(cap.id, `npx nx serve ${cap.angularProject} --port ${cap.port}`, '33');
38+
run(cap.id, `npx nx serve ${cap.angularProject}:serve:cockpit --port ${cap.port}`, '33');
3939
run(`${cap.id}-py`, `cd ${cap.pythonDir} && source $HOME/.local/bin/env 2>/dev/null; uv sync && uv run langgraph dev --port 8123`, '35');
4040
console.log(`\n🚀 ${cap.id}: cockpit=4201 angular=${cap.port} langgraph=8123\n`);
4141
}

apps/cockpit/src/components/cockpit-shell.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export function CockpitShell({
102102
modes={PRIMARY_MODES}
103103
activeMode={activeMode}
104104
onChange={setActiveMode}
105+
capability={entry.topic}
105106
/>
106107
</div>
107108
</header>
@@ -112,6 +113,7 @@ export function CockpitShell({
112113
<RunMode
113114
entryTitle={entryTitle}
114115
runtimeUrl={contentBundle.runtimeUrl}
116+
capabilitySlug={entry.topic}
115117
/>
116118
</div>
117119
{activeMode === 'Code' ? (
@@ -121,10 +123,11 @@ export function CockpitShell({
121123
backendAssetPaths={backendAssetPaths}
122124
codeFiles={contentBundle.codeFiles}
123125
promptFiles={contentBundle.promptFiles}
126+
capability={entry.topic}
124127
/>
125128
) : null}
126129
{activeMode === 'Docs' ? (
127-
<NarrativeDocs narrativeDocs={contentBundle.narrativeDocs} />
130+
<NarrativeDocs narrativeDocs={contentBundle.narrativeDocs} capability={entry.topic} />
128131
) : null}
129132
{activeMode === 'API' ? (
130133
<ApiMode docSections={contentBundle.docSections} />

apps/cockpit/src/components/code-mode/code-mode.spec.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
import React from 'react';
33
import { act } from 'react';
44
import { createRoot } from 'react-dom/client';
5-
import { afterEach, describe, expect, it } from 'vitest';
5+
import { afterEach, describe, expect, it, vi } from 'vitest';
6+
7+
vi.mock('../../lib/analytics/client', () => ({ track: vi.fn() }));
8+
9+
import { track } from '../../lib/analytics/client';
610
import { CodeMode } from './code-mode';
711

812
describe('CodeMode', () => {
@@ -14,6 +18,7 @@ describe('CodeMode', () => {
1418
root?.unmount();
1519
});
1620
container?.remove();
21+
vi.clearAllMocks();
1722
});
1823

1924
it('renders Shiki-highlighted HTML for the active file', () => {
@@ -111,4 +116,42 @@ describe('CodeMode', () => {
111116

112117
expect(container.textContent).toContain('You are a helpful assistant.');
113118
});
119+
120+
it('fires cockpit:code_copied when the Copy button is clicked', () => {
121+
container = document.createElement('div');
122+
document.body.appendChild(container);
123+
root = createRoot(container);
124+
125+
Object.assign(navigator, {
126+
clipboard: { writeText: vi.fn(() => Promise.resolve()) },
127+
});
128+
129+
act(() => {
130+
root!.render(
131+
<CodeMode
132+
entryTitle="Test"
133+
codeAssetPaths={['src/app.tsx']}
134+
backendAssetPaths={[]}
135+
codeFiles={{ 'src/app.tsx': '<pre class="shiki"><code>const x = 1;</code></pre>' }}
136+
promptFiles={{}}
137+
capability="streaming"
138+
/>,
139+
);
140+
});
141+
142+
const copyBtn = container.querySelector(
143+
'button[aria-label^="Copy"]',
144+
) as HTMLButtonElement | null;
145+
expect(copyBtn).not.toBeNull();
146+
147+
act(() => {
148+
copyBtn!.click();
149+
});
150+
151+
expect(track).toHaveBeenCalledWith('cockpit:code_copied', {
152+
capability: 'streaming',
153+
surface: 'code_mode',
154+
file_path: 'src/app.tsx',
155+
});
156+
});
114157
});

apps/cockpit/src/components/code-mode/code-mode.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,28 @@
22

33
import React from 'react';
44
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
5+
import { track } from '../../lib/analytics/client';
56

67
interface CodeModeProps {
78
entryTitle: string;
89
codeAssetPaths: readonly string[];
910
backendAssetPaths: readonly string[];
1011
codeFiles: Record<string, string>;
1112
promptFiles: Record<string, string>;
13+
capability?: string;
1214
}
1315

1416
const getTabLabel = (path: string): string => path.split('/').pop() ?? path;
1517

16-
function CodeFileContent({ path, content }: { path: string; content: string | undefined }) {
18+
function CodeFileContent({
19+
path,
20+
content,
21+
capability,
22+
}: {
23+
path: string;
24+
content: string | undefined;
25+
capability?: string;
26+
}) {
1727
if (!content) {
1828
return <p className="text-sm text-[var(--ds-text-muted)]">No source available for {getTabLabel(path)}</p>;
1929
}
@@ -38,6 +48,11 @@ function CodeFileContent({ path, content }: { path: string; content: string | un
3848
<button
3949
aria-label={`Copy ${getTabLabel(path)}`}
4050
onClick={() => {
51+
track('cockpit:code_copied', {
52+
capability,
53+
surface: 'code_mode',
54+
file_path: path,
55+
});
4156
const el = document.querySelector(`[data-code-path="${CSS.escape(path)}"] pre code`);
4257
if (el) navigator.clipboard.writeText(el.textContent ?? '');
4358
}}
@@ -57,7 +72,7 @@ function CodeFileContent({ path, content }: { path: string; content: string | un
5772
);
5873
}
5974

60-
export function CodeMode({ entryTitle, codeAssetPaths, backendAssetPaths, codeFiles, promptFiles }: CodeModeProps) {
75+
export function CodeMode({ entryTitle, codeAssetPaths, backendAssetPaths, codeFiles, promptFiles, capability }: CodeModeProps) {
6176
const promptPaths = Object.keys(promptFiles);
6277
const allPaths = [...codeAssetPaths, ...backendAssetPaths, ...promptPaths];
6378

@@ -94,7 +109,7 @@ export function CodeMode({ entryTitle, codeAssetPaths, backendAssetPaths, codeFi
94109

95110
{[...codeAssetPaths, ...backendAssetPaths].map((path) => (
96111
<TabsContent key={path} value={path} className="flex-1 overflow-auto mt-4">
97-
<CodeFileContent path={path} content={codeFiles[path]} />
112+
<CodeFileContent path={path} content={codeFiles[path]} capability={capability} />
98113
</TabsContent>
99114
))}
100115

apps/cockpit/src/components/modes/mode-switcher.spec.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
import React, { useState } from 'react';
33
import { createRoot } from 'react-dom/client';
44
import { act } from 'react';
5-
import { afterEach, describe, expect, it } from 'vitest';
5+
import { afterEach, describe, expect, it, vi } from 'vitest';
6+
7+
vi.mock('../../lib/analytics/client', () => ({ track: vi.fn() }));
8+
9+
import { track } from '../../lib/analytics/client';
610
import { ModeSwitcher } from './mode-switcher';
711

812
const MODES = ['Run', 'Code'] as const;
@@ -31,6 +35,7 @@ describe('ModeSwitcher', () => {
3135
root?.unmount();
3236
});
3337
container?.remove();
38+
vi.clearAllMocks();
3439
});
3540

3641
it('shows mode buttons with Run active by default', () => {
@@ -70,4 +75,40 @@ describe('ModeSwitcher', () => {
7075
expect(container.textContent).toContain('Code content');
7176
expect(container.textContent).not.toContain('Run content');
7277
});
78+
79+
it('fires cockpit:mode_switched when capability prop is set and mode changes', () => {
80+
container = document.createElement('div');
81+
document.body.append(container);
82+
root = createRoot(container);
83+
84+
function Harness() {
85+
const [active, setActive] = useState<(typeof MODES)[number]>('Run');
86+
return (
87+
<ModeSwitcher
88+
modes={MODES}
89+
activeMode={active}
90+
onChange={setActive}
91+
capability="streaming"
92+
/>
93+
);
94+
}
95+
96+
act(() => {
97+
root!.render(<Harness />);
98+
});
99+
100+
const codeButton = Array.from(container.querySelectorAll('[data-mode-btn]')).find(
101+
(b) => b.textContent === 'Code',
102+
) as HTMLElement;
103+
104+
act(() => {
105+
codeButton.click();
106+
});
107+
108+
expect(track).toHaveBeenCalledWith('cockpit:mode_switched', {
109+
capability: 'streaming',
110+
from_mode: 'run',
111+
to_mode: 'code',
112+
});
113+
});
73114
});

0 commit comments

Comments
 (0)