Below 900px the drawer already overlays the content when open (existing media
query), but it defaulted to open — so on phones the 16rem panel covered the
content + chat on load. Default isOpen from window.innerWidth (> 900) so narrow
screens start at the 3.5rem rail; the user expands to the overlay on demand.
The drawer is client-mounted, so reading window in the initializer is safe.
Applied to the shared threads-drawer.tsx across all examples + north-star.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New thread (header + collapsed rail) now switches to a fresh threadId
(crypto.randomUUID()) instead of setting it to undefined. Setting threadId
undefined after a thread was selected did not reset the V2 chat — it kept
showing the previous conversation. A new id forces the configuration provider
to re-thread to an empty conversation.
- Collapsed-rail buttons use a native title tooltip instead of the styled
::after one, which clipped horizontally against the viewport's left edge in
the narrow rail. The wider expanded row keeps the styled tooltip.
Applied to the shared threads-drawer.tsx across all examples + north-star.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The threads-drawer had drifted into two near-identical .tsx variants (a 1-char
encoding diff) during the bespoke passes. Unify all examples onto a single
shared threads-drawer.tsx, which also propagates the mastra review fixes:
- collapsed rail: hover tooltips on the expand + new-thread buttons, and the
non-interactive decorative icon (read as a dead button) removed;
- tooltip renders below the trigger in each example's themed module.css (the
styled ::after tooltip was clipped when shown above the trigger).
Per-example theming (the module.css token values) is unchanged — only the
shared logic + the tooltip positioning rule are reconciled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Next route handlers only declared GET/POST, so the threads REST API's
DELETE (delete thread) and PATCH (archive/restore) were 405'd by Next before
reaching the runtime — thread deletion and archive were broken in every
example. Export PATCH + DELETE → handle(app) across all 7 routes.
mastra threads-drawer review fixes:
- tooltip renders below the trigger (the styled ::after tooltip was clipped by
the drawer's overflow containers when shown above);
- collapsed rail drops a non-interactive decorative icon (read as a dead button)
and adds hover tooltips to the expand + new-thread buttons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tokenize the last hardcoded literals in threads-drawer.module.css into --threads-*
CSS variables (with fallbacks to the existing design tokens / prior literals, so
the north-star is visually unchanged). Add THEME.md documenting the token contract
so each example can theme the drawer by defining tokens — no per-example CSS edits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add src/components/threads-drawer/** to verbatimFiles (north-star is canonical).
Stage langgraph-js / langgraph-fastapi / strands-python in allowedDivergence for
the threads files (threads-drawer/**, src/app/page.tsx, next.config.ts) so parity
stays green while the frontend rolls out per batch; entries are removed as each
instance is synced. Verified: adds zero new parity failures (pre-existing
example-layout / docker-route-override / next-env.d.ts drift is unrelated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore the threads-drawer components + locked-state gate, page.tsx wiring
(threadId state + CopilotChatConfigurationProvider + two-column shell), and the
next.config build-time derivation of NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED from
the license token. Without a license the gate renders the locked panel; the
drawer is client-mounted (SSR-safe). This is the canonical north-star frontend
that parity:sync distributes to instances per batch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docker-compose (postgres + redis + intelligence/composite) with its load-bearing
init-db SQL, a .env.intelligence fragment (appended on activation), and a README.
Framework-agnostic; consumed by copilotkit init -i / add-intelligence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When COPILOTKIT_LICENSE_TOKEN is set, wire CopilotKitIntelligence + identifyUser +
licenseToken (marker-fenced for opt-out); otherwise InMemoryAgentRunner — today's
behavior, unchanged. Document the optional Intelligence env keys (commented) in
.env.example. No visible change to the example until a license is provided.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-detect resolves the requested transport ("auto") to a concrete value
("rest"/"single") and writes it back to _runtimeTransport. setRuntimeTransport
then compared against that resolved value, so re-applying the same requested
mode — which the provider effect does on every render — compared unequal and
re-ran the entire /info handshake, rebuilding the runtime agents mid-session
and blanking the transcript (and any per-message UI bound to it).
Track the requested mode separately (_requestedTransport) and guard on it, so
re-applying an unchanged requested transport is a no-op.
Adds coverage: re-applying "auto" after auto-detect resolves it does not
refetch /info.
## Summary
- Select generated thread titles only from assistant text messages
returned by the naming run.
- Reject JSON-shaped title output unless it parses to an object with a
string title.
- Add Runtime regression coverage for tool-result suffixes and invalid
JSON title payloads.
## Validation
- pnpm nx run @copilotkit/runtime:test --
src/v2/runtime/__tests__/thread-names.test.ts
src/v2/runtime/__tests__/handle-run.test.ts (passed, 56 tests)
- pnpm nx run @copilotkit/runtime:build (passed)
- Pre-commit package checks passed
- pnpm nx run @copilotkit/runtime:check-types (blocked in dependency
task @copilotkit/shared:check-types: missing
LicenseContextValue/LicenseMode exports from
@copilotkit/license-verifier and telemetry index error)
- NODE_OPTIONS=--max-old-space-size=8192 pnpm nx run
@copilotkit/runtime:check-types --excludeTaskDependencies (failed: tsc
heap out of memory near 8 GB)
Display-only generative UI must use useComponent (not useFrontendTool): its
render is unconditional so the card persists after the tool call completes.
Also pass [transactions, cards] as deps so the renderer re-registers when the
data loads -- otherwise the closure captures the initial empty transactions
and the list renders empty.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The migration mounted the low-level CopilotKitProvider, which omits the
ThreadsProvider that holds the rendered message thread, so the agent ran but
the transcript stayed empty. Use the full CopilotKit provider, and set
useSingleEndpoint={false} to match the multi-endpoint Hono route (the default
single-endpoint transport 404s against it).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- cards PUT handler: stop console.info'ing the request body, which
included the plaintext PIN on every PIN-change request.
- wrapper.tsx: drop the duplicate ./globals.css import (already
imported in app/layout.tsx, which is the canonical place).
- copilot-context.tsx: replace window.location.pathname read during
render with usePathname() from next/navigation, matching how
components/layout.tsx already derives the current route.
- next.config.mjs: remove the eslint.ignoreDuringBuilds block — Next 16
no longer runs ESLint at build, and the key now produces an
"Unrecognized key(s): 'eslint'" warning. Confirmed warning is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
package.json was pinning next@14 / react@18, but the root pnpm.overrides
were already forcing next@16 / react@19 at install time and the code uses
Next 16 async params. Bump the declared ranges to ^16.0.10 / ^19 (and
@types/react{,-dom} to ^19) so the manifest matches reality and matches
the v2 reference demos (mcp-apps, generative-ui-playground).
Also drop examples/showcases/banking/pnpm-lock.yaml: the demo is a
workspace package (listed in root pnpm-workspace.yaml and resolved in the
root pnpm-lock.yaml), so the per-demo lockfile was vestigial v1 cruft
that contradicted the v2 migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace v1-era hooks/data references with the actual v2 surface:
useAgentContext / useFrontendTool / useHumanInTheLoop from
@copilotkit/react-core/v2, the Hono runtime route, the seed.json + store
data layer, and the Northwind identity. Drop mentions of removed
SQL/MSA/ServiceNow/RAG features.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove the /Hydration/i pattern from the ignored-console-errors filter so
Next.js hydration mismatches surface as real failures, and add a
page.on("pageerror", ...) listener that records uncaught exceptions and
asserts none occurred. pageerrors are not filtered — any uncaught
exception fails the smoke test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a CI-safe Playwright smoke test that verifies the banking showcase
boots and the CopilotKit v2 popup opens with its configured suggestion
pills, without invoking the agent (so it runs in CI without secrets).
Covers:
- Document title shows the Northwind Finance brand
- Credit-cards dashboard renders ("Credit Cards" heading)
- CopilotPopup launcher opens the dialog (Northwind Copilot)
- Three suggestion pills render: View transactions, Add a card,
Assign a policy
The dev server gets a dummy OPENAI_API_KEY so the runtime route boots,
but the test never sends a chat message or clicks a suggestion.
.gitignore: ignore test-results/, playwright-report/, and
tsconfig.tsbuildinfo so they don't become clutter.
Banking was on Tailwind v3, which made the pre-compiled v2 stylesheet
(`@copilotkit/react-core/v2/styles.css`, built as Tailwind v4 output)
incompatible with the PostCSS pipeline. A temporary inline-CSS hack in
`layout.tsx` worked around this. Migrate to Tailwind v4 to match the
canonical v2 demos and remove the hack.
- package.json: tailwindcss ^4, add @tailwindcss/postcss ^4, replace
tailwindcss-animate with tw-animate-css (v4 drop-in)
- postcss.config.mjs: switch plugin to @tailwindcss/postcss
- globals.css: use @import "tailwindcss" + @import "tw-animate-css";
add @custom-variant dark for the existing .dark/.light class toggle;
move shadcn color/radius mapping into @theme inline (preserves
bg-background/text-foreground/rounded-{lg,md,sm} semantics)
- tailwind.config.ts: deleted; theme now lives in CSS
- components.json: clear stale tailwind.config reference
- layout.tsx: drop the readFileSync/dangerouslySetInnerHTML inline-CSS
workaround; import "@copilotkit/react-core/v2/styles.css" the normal
way alongside ./globals.css
Verified: tsc clean, next build clean, Playwright-rendered all four
pages (/, /dashboard, /cards, /team) with computed-style checks
(sidebar bg-gray-900 dark, rounded-lg=8px, .border=1px) and opened
the copilot popup which renders fully styled with the v2 stylesheet.
## Summary
Follow-up to #5173 (bucket-(d)) closing three **pre-existing**
browser-pool concurrency defects on the non-release teardown paths. The
browser-pool is a context-pool over a fixed set of long-lived Chromium
processes; these defects biased the hygiene-recycle cadence and could
strand waiters / leak deferred recycles.
## Fixes (each red-green proven)
1. **`serveNextWaiter` orphan-close leaked `servedContexts`.** A waiter
timing out mid-`openContextOn` cleaned up the reservation/context but
never decremented `servedContexts` (which `openContextOn` had already
`++`'d) → every orphaned-by-timeout serve permanently inflated the count
→ premature hygiene recycles. Fix: decrement `servedContexts` in the
orphan-close block.
2. **Deferred `recyclePending` honored only on `release()`.** A recycle
deferred because `pendingOpens > 0` set `recyclePending`, but the
non-release teardown paths (orphan-by-recycle rollback, orphan-close)
returned the entry to idle without re-checking it → the deferred recycle
was dropped and the browser exceeded `recycleAfter` indefinitely. Fix:
shared `maybeFireDeferredRecycle(entry)` helper called on both
non-release teardown paths.
3. **`openContextOn` rollback didn't drain waiters.** The
orphan-by-recycle rollback freed a reservation but never
`scheduleServeNextWaiter()` → queued waiters could stall with free
capacity until an unrelated release. Fix: `scheduleServeNextWaiter()`
after the rollback.
## Verification
- Red-green for all three (reproduced each bug, then green).
- Full harness vitest: **1702–1705 passed**; browser-pool suite
**27/27** (mutation-tested — reverting fix#3 fails its guard). `tsc
--noEmit` exit 0.
- Public API + `BROWSER_POOL_MAX_CONTEXTS` default untouched.
## Reviewed
7-agent unbiased CR (cr-loop): the three fixes confirmed sound; one
ordering concern on the new code investigated and **refuted**
(sole-browser relaunch-failure rejects the waiter rather than stranding
it).
## Known further hardening (separate effort — NOT in this PR)
The CR surfaced additional **pre-existing** browser-pool reliability
bugs that warrant a dedicated hardening pass, independently flagged by
multiple reviewers:
- `shutdown()` vs in-flight `openContextOn` → leaked context (no
`isShutdown` re-check post-`newContext`); recycles added to
`inFlightRecycles` after shutdown's snapshot not awaited.
- crash-reason `recycleBrowser` abandons live contexts without
`.close()` — leaks on the non-dead `acquire`-retry path.
- `acquire` retry treats a transient `newContext` failure as a full
crash → recycles the whole browser, tearing down unrelated live contexts
(correlated flakiness).
- `launchChain` launch gate has no timeout → a single hung
`chromium.launch()` permanently deadlocks all relaunches.
- `parseInt` env parsing silently accepts trailing garbage
(`MAX_CONTEXTS=24x` → 24); no warning.
- context-close failures swallowed via bare `.catch(() => {})`
(inconsistent with `closeBrowser`'s logged path).
- relaunch-failure eviction only rejects waiters when the pool is fully
empty (doesn't redistribute onto surviving browsers); `pickLeastLoaded`
tie-break concentrates load on browser 0.
Bug 1: serveNextWaiter orphan-close (timed-out waiter mid-open) now mirrors
openContextOn's servedContexts++ with a decrement, so an orphaned serve no
longer permanently inflates servedContexts and biases the hygiene recycle to
fire early.
Bug 2: a hygiene recycle deferred via the release-path shouldRecycle&&hadWaiter
guard is now re-checked on the NON-release teardown paths (openContextOn
orphan-by-recycle rollback and serveNextWaiter orphan-close) via a shared
maybeFireDeferredRecycle helper, so the deferred recycle still fires when the
entry's last activity ends without a release() — previously it was dropped and
the browser exceeded recycleAfter indefinitely.
Bug 3: openContextOn's orphan-by-recycle rollback now calls
scheduleServeNextWaiter() so freed capacity immediately drains queued waiters
instead of stalling them until the next unrelated release/recycle handoff.
Adds three red-green regression tests (BUG1/BUG2/BUG3) to the browser-pool
suite. Public API and MAX_CONTEXTS default unchanged.
## Fixes
- **Gate LGP tool-rendering AAPL + Find-flights fixtures on `toolName`**
(not `hasToolResult`): the AAPL tool-rendering and Find-flights
first-leg fixtures now key off `toolName` so the right tool renders.
Local proof: tool-rendering AAPL + Find-flights run **local D6 green**.
- **Restore sandboxed-UI `jsFunctions` in `gen-ui-open-advanced`
fixtures** (agno, crewai-crews, langgraph-fastapi, langgraph-python,
mastra): the sandboxed Calculator/Ping `jsFunctions` were missing, so
the calculator never computed. Local proof: calc **`=` → 4
browser-verified**.
- **Resolve dashboard links to the real shell host via server-threaded
`shellUrl`**: the dashboard tree was entirely `"use client"`, so
`getRuntimeConfig()` returned the `ssr-placeholder.invalid` SSR sentinel
and baked dead hrefs into every Demo/Code link. `page.tsx` is now a
server component that reads the real host server-side and threads it
into the client `DashboardPage`. Local proof: **SSR links click → real
demo, verified**.
- **Fail `verify-deploy` on env-unset config sentinel + robust config
extractor**: when `SHELL_URL` is unset the server config returns the
`about:blank#shell-url-missing` sentinel; the deploy guard now fails
loud on it rather than shipping dead links, with a hardened config
extractor. Local proof: **deploy-guard red-green**.
- **Gate Coverage D6 badge + stats by the depth ladder; gated indicator
only on genuine lower-rung failure**: D6 is the top of the verification
ladder, so a green D6 claim is only valid when the ladder through D5 is
intact. New `d6Effective` collapses to gated (`—`) when a lower rung
genuinely fails (never on no-data), keeping the badge, stat, regression
flag, and chip in agreement. Local proof: **D6-gating full-suite 790
green incl dashboard-color-matrix 54/54**.
- **Raise browser-pool default `MAX_CONTEXTS` to 40 + correct pool
docs**: contexts (not chromium processes) are the scaling knob since the
PID ceiling of 1000 is the binding constraint; D6 peak 32 + D5 peak 8 =
40. Probe cadence/docs corrected to match. Local proof: **pool
MAX_CONTEXTS=40 locally proven, 50 PIDs ≪ 1000**.
## CR
Converged via 4 unbiased 7-agent cr-loop rounds + 2 fix rounds.
## Known follow-ups (not in this PR)
- **(d) browser-pool concurrency hardening** — `servedContexts`
inflation on `serveNextWaiter` orphan-close, `recyclePending` deferral
on non-release teardown, and waiter-drain on `openContextOn` rollback.
These are pre-existing pool internals; separate PR.
- **(b/c) minor cosmetic / naming items** — `DocsRow` unused `shellUrl`
prop; `computeColumnTallyDetail` labels a D6-absent amber as `"e2e"`;
the agno `gen-ui-open-advanced` `_meta` note is misleading but is the
SOLE source of agno Calculator/Ping fixtures (do NOT delete); `API=d3`
vs `d2` naming; `resolveD3` has no effective stale row (pre-existing);
`e2e-deep.yml` stale primary-key comment.
- **react-core consecutive-interrupt run-state fix** — a SEPARATE
pending branch; the `gen-ui-interrupt` cell needs it.
## Summary
Carries [ag-ui PR
#1784](https://github.com/ag-ui-protocol/ag-ui/pull/1784)
("fix(langgraph): skip regeneration check when `command.resume` is set")
into the three langgraph showcase stacks, greening the
`gen-ui-interrupt` D6 cell.
- ag-ui `@ag-ui/langgraph` 0.0.35 (and earlier) incorrectly ran the
regenerate path on a **resumed** run, tripping the regeneration trap and
breaking gen-ui-interrupt. `0.0.36` adds the `command.resume` guard so a
resume skips the regeneration check.
- Pins `@ag-ui/langgraph` `0.0.36` via npm `overrides` (it is a
**transitive** dep of `@copilotkit/runtime`) in `langgraph-python`,
`langgraph-typescript`, and `langgraph-fastapi`.
- Regenerates each integration's `package-lock.json` (python/fastapi
regenerated with `--legacy-peer-deps`, matching their Dockerfile `npm ci
--legacy-peer-deps`).
## Files changed
-
`showcase/integrations/langgraph-typescript/{package.json,package-lock.json}`
-
`showcase/integrations/langgraph-python/{package.json,package-lock.json}`
-
`showcase/integrations/langgraph-fastapi/{package.json,package-lock.json}`
Lockfiles flip `@ag-ui/langgraph` 0.0.34 → 0.0.36 and pull in 0.0.36's
new transitive dep `@ag-ui/a2ui-toolkit@0.0.1-alpha.3`.
## Verification
- `@ag-ui/langgraph@0.0.36` is published to npm `latest`; its
`dist/index.js` contains the `!command?.resume` regeneration guard from
#1784.
- All three regenerated lockfiles resolve
`node_modules/@ag-ui/langgraph` to `0.0.36`.
## Test plan
- [ ] CI green
- [ ] gen-ui-interrupt D6 cell green for langgraph-python,
langgraph-typescript, langgraph-fastapi after showcase rebuild + re-run
The banking showcase was migrated to CopilotKit v2 but `src/app/layout.tsx`
still imported the removed `@copilotkit/react-ui/styles.css`, causing every
route to 500 with "Module not found". Drop the dead import.
Banking is still on Tailwind v3, while the v2 stylesheet
(`@copilotkit/react-core/v2/styles.css`) is pre-compiled Tailwind v4 output
containing bare `@layer base { ... }` rules. Tailwind v3's PostCSS plugin
rejects `@layer base` without a matching `@tailwind base` directive, so any
CSS import routed through the JS/TSX pipeline fails the build. Move the v2
stylesheet load to a server-side `fs.readFileSync` + inline `<style>` tag in
the root layout so the CSS reaches the browser without passing through
PostCSS / Tailwind v3. This avoids a full Tailwind v4 migration of the app.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Carries ag-ui PR #1784 (skip regeneration check when command.resume is
set) into the three langgraph showcase stacks. ag-ui 0.0.35 incorrectly
ran the regenerate path on a resumed run, breaking the gen-ui-interrupt
D6 cell. 0.0.36 adds the command.resume guard.
Pins @ag-ui/langgraph 0.0.36 via npm overrides (transitive dep of
@copilotkit/runtime) in langgraph-python, langgraph-typescript, and
langgraph-fastapi, and regenerates each per-integration package-lock.json.
## Problem
Shell-docs had conflicting v2 guidance around the provider import path.
Some migration/reference/quickstart pages either recommended
`CopilotKitProvider` or kept `CopilotKit` examples on the root
`@copilotkit/react-core` package even though v2 docs should import the
`CopilotKit` component from `@copilotkit/react-core/v2`.
## Why
The correct recommendation is the `CopilotKit` component name, imported
from the v2 entrypoint. Leaving root-package imports in v2-facing docs
makes the migration and reference guidance contradict the v2 package
layout.
## Fix
- Recommend `CopilotKit` from `@copilotkit/react-core/v2`, not
`CopilotKitProvider`.
- Update v2 migration, reference, and quickstart examples to use the v2
provider/style entrypoints.
- Leave root `@copilotkit/react-core` imports only in v1 docs and
explicit migration “Before” examples.
- Add regression coverage for stale provider/style package paths.
- Fix the shell-docs SignupLink SSR test typing exposed by typecheck.
Closes#5153
The D6 cell tool-rendering-custom-catchall for langgraph-python timed out
(deterministic 30s no-response). The probe sends two turns in one thread:
turn 1 "weather in Tokyo" -> get_weather, turn 2 "What's the current price
of AAPL?" -> get_stock_price. The AAPL first-leg fixture was gated on
hasToolResult:false, but aimock implements that gate as
messages.some(m => m.role === "tool") -- i.e. ANY tool message anywhere in
the thread. Turn 1's get_weather leaves a tool result in the thread, so on
turn 2 hasToolResult is permanently true and the false gate can never match
-> no_fixture_match -> 503 -> 30s timeout.
Replace the broken hasToolResult:false gate with toolName:"get_stock_price"
(the same pattern the sibling weather/chain first-leg fixtures already use).
A toolName gate fires whenever the tool is registered, regardless of prior
tool results; the toolCallId follow-up fixture still wins on iteration 2.
Fixture-only change. Verified locally via the D6 Docker path:
tool-rendering-custom-catchall now green (turn 1 get_weather + turn 2
get_stock_price both render, no 503/timeout); tool-rendering-default-catchall
still green (no regression).
The generateSandboxedUi calculator (and the ping-host / inline-evaluator
pills) rendered static HTML with no interactivity — the = and keypad
buttons were no-ops — because the affected per-integration fixtures
omitted the jsFunctions key that the OpenGenerativeUIRenderer injects
into the iframe to wire click handlers to the host bridge
(Websandbox.connection.remote.evaluateExpression / notifyHost).
Copy the jsFunctions VERBATIM from the canonical langgraph-typescript
fixture (apples-to-apples) into agno, crewai-crews, langgraph-fastapi,
and mastra, and create the missing langgraph-python advanced fixture
(Calculator + Ping pills; the inline evaluator already lives in
gen-ui-open.json). Fixture-only change; the renderer is correct.
- Replace toy team names and @example.com emails with credible
professional names and @northwind.example addresses (preserve ids,
roles, team assignments). Default Admin is Alex Morgan.
- Set <title> metadata to IDENTITY.brand (Northwind Finance).
- Add formatCurrency helper in lib/utils.ts and route all money
rendering (transactions list, credit-card details, dashboard +
cards summary tiles) through it so amounts always render as
$10,000.00 / -$1,000.00.
- Instruct the Northwind Copilot prompt to treat the rendered
showTransactions list as the single source of truth so prose no
longer restates counts/totals and contradicts the rendered list.
- Remove stray console.log and tighten == to === in components/layout.tsx.
Replace the in-memory `data` literal in `api/v1/data.ts` with a
file-seeded, module-singleton store:
- `src/data/seed.json` holds the seed (cards, team, policies,
transactions) with refreshed dates (within ~60 days of 2026-06-02)
and valid future card expiries.
- `src/lib/store.ts` deep-clones the seed at module init and exposes
typed read accessors (cards/team/policies/transactions, findCard,
findPolicy, findTransaction) plus the exact mutator set the routes
use (addCard, updateCardPin, assignPolicyToCard, addPolicy,
updateTransaction, addMember, updateMember, removeMember).
- All `src/app/api/v1/**/route.ts` files now read/write through the
store. `data.ts` retains the shared interfaces/enums/CARD_COLORS/
generateUniqueId and drops the literal `data` constant.
- Mutations persist for the running server process and reset to seed
on restart (intentional demo behavior; no disk write-back).
- Domain-data only — conversation threads/memory live in a separate
ticket (FOR-137).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
## Summary
The showcase dashboard's Ops tab fetches `/api/ops/*` as a same-origin
path, which the Route Handler at
`shell-dashboard/src/app/api/ops/[...path]/route.ts` forwards at request
time to `${OPS_BASE_URL}/api/*` on the showcase-harness HTTP origin (the
service that serves `/api/probes`).
In the local compose stack, `OPS_BASE_URL` was set to
`http://localhost:3200` — the dashboard's own host. The proxy therefore
looped back into the dashboard instead of reaching the harness, so the
probe-trigger endpoint failed (self-referential 500/503) and the Ops
live-probe grid could not resolve.
This points `OPS_BASE_URL` at the harness origin over the compose
network: `http://showcase-harness:8080`. The harness `Dockerfile`
EXPOSEs `8080` and `orchestrator.ts` binds `PORT ?? 8080`, so the
dashboard reaches `/api/probes` by container name on the internal port.
This mirrors staging, where the dashboard's `OPS_BASE_URL` likewise
points at the harness origin rather than at itself.
Scope: a single build-arg value in `showcase/docker-compose.local.yml`
(plus an updated explanatory comment). `OPS_BASE_URL` is read at request
time by the Route Handler, so this only seeds the runtime default — no
build-time resolution required.
## Test plan
- [ ] Dashboard Ops tab loads without a 500/503 from the ops proxy
- [ ] Probe-trigger endpoint (`/api/ops/probes` POST) returns 2xx,
forwarded to the harness `/api/probes`
- [ ] Ops live-probe grid renders harness data (harness running on the
compose network as `showcase-harness`)
## Summary
Adds a permanent, env-gated injection seam to the showcase harness
service discovery
(`showcase/harness/src/probes/discovery/railway-services.ts`). When
`LOCAL_SERVICES_JSON` is set, the harness (especially the
d6-all-pills-e2e driver) runs against **LOCAL** backend services instead
of performing Railway discovery — enabling apples-to-apples LOCAL D6
verification without Railway credentials.
- **Zero behavior change when unset/empty.** An unset or empty
`LOCAL_SERVICES_JSON` takes the byte-identical Railway discovery path —
the seam is fully transparent in the default configuration.
- **`demos` plumbed end-to-end (load-bearing).** The injected service
records carry `demos` all the way through. This matters: empty `demos`
would short-circuit the D6 driver into a false 15ms zero-cell "green,"
masking real failures. Plumbing `demos` end-to-end is what makes the
LOCAL path a faithful stand-in for Railway discovery.
- **Enables apples-to-apples LOCAL D6 verification** — run the full pill
suite against local services with the same code path shape as staging.
## Notes
- 8 new tests covering the `LOCAL_SERVICES_JSON injection` path (66
tests total in `railway-services.test.ts`, all passing).
- Env-gated: no Railway credentials required when services are injected.
- The injection seam executes the real discovery code (not mocked).
## Test plan
- [ ] `tsc --noEmit` (typecheck) exits 0
- [ ] `railway-services.test.ts` passes (66 tests, incl. all 8
`LOCAL_SERVICES_JSON injection` tests)
- [ ] Injection seam executes against the real code path (verified via
`discovery.railway-services.local-injection` log emission, not a mock)
- [ ] Unset/empty `LOCAL_SERVICES_JSON` produces a byte-identical
Railway discovery path (zero behavior change)
- [ ] `demos` plumbed end-to-end through injected service records
(guards against false zero-cell D6 green)
- Switch imports from @copilotkit/react-core to /v2 in layout.tsx and actions.ts
- Replace useCopilotReadable with useAgentContext
- Stringify the credit cards/policies/transactions context value to satisfy
the JsonSerializable type
- Switch imports from @copilotkit/react-core to /v2
- Replace useCopilotReadable with useAgentContext (stringify object value)
- Convert navigateToPageAndPerform parameters array to a Zod object schema
## Summary
- **Fix per-cell D6 resolution**: the dashboard's `resolveD6` now reads
per-cell ENUM keys (`d6:<slug>/<featureType>` via `CATALOG_TO_D5_KEY`
fan-out) instead of the integration aggregate — D6 cells render real
per-cell green/red instead of all-gray.
- **Depth + D6 surfaced by default**: `DEFAULT_OVERLAYS = [links,
health, depth]` (the per-cell D6 badge rides on the Health layer; the
depth chip folds D6).
- **Correct the aggregate stats bar** (`page-stats.ts` extraction): D6
cells were dropped from the depth distribution (`dist["d6"]++` → `NaN`,
masked by an `as` cast); gray/no-data cells were counted as green;
`d6Stats` swallowed amber into gray. Now renders the reachable buckets
**D0/D3/D4/D5/D6** (dropped permanently-zero D1/D2), tracks `noData` and
`degraded` distinctly, and validates `parity_tier` fail-loud.
- **Test coverage**: new table-driven cell color/rollup matrix
(`dashboard-color-matrix.test.tsx`) + `page-stats.test.ts` +
order-independence/comment hardening.
- **CR fixes**: effective (stale-downgraded) `.row` from cell-model
resolvers, `WORST_STATE_RANK` rename, composed-cell memo keys,
overlay-types re-export dedup, `setTab` persistence + `window.location`
test stub, `page.tsx` shared-`now` threading + 60s staleness re-render.
## Verification
- **776 tests pass / 1 skip**, `tsc --noEmit` clean, Next.js production
build OK.
- **Local visual proof**: seeded enum D6 rows (30✓/8✗/0 gray) into a
local PocketBase, rebuilt this branch, screenshotted bare `#matrix` —
per-cell D6 green/red renders, and the depth distribution shows `D6:30
D5:8 D4:0 D3:0 D0:588`.
- **7-agent code review converged** over 3 confirmation cycles.
## Notes
- The **#5152 `d6-all-pills` driver must stay on enum keys** (matches
this dashboard's `resolveD6` + the `d5-mapping-drift` test). Do not
revert it to raw catalog keys.
- **Follow-up (separate PR)**: stats-bar single-source-of-truth
hardening (`computeD6Stats`→`buildCellModel`; reconcile
`resolveD6Row`/`resolveD5Row` to the effective row; thread `connection`
into stats for SSE-offline; docs-only stats exclusion); delete
deprecated `composed-cell.tsx` + `deriveDepth`; `useOverlays` mount-hash
deep-link clobber; Notion visualization-doc sync to per-cell
`resolveD6`.
## Test plan
- [ ] CI green
- [ ] After staging deploy, dashboard renders per-cell D6 green/red (not
all-gray) for LangGraph-Python