## Summary
Hardens GitHub Actions workflows against the supply chain attack pattern
used in the TanStack/router compromise (May 2026), where a fork PR
poisoned the Actions cache to inject malicious code into release builds.
- **Cache namespacing:** Fork PR cache keys are prefixed with `fork-` so
they can never collide with main-branch cache entries. Applied to
`e2e_dojo` and `e2e_legacy_v1` workflows (explicit `actions/cache`).
Built-in `setup-node` cache already provides fork isolation.
- **Release job separation:** `publish-release.yml` and `prerelease.yml`
now split build and publish into separate GitHub Actions jobs.
`NPM_TOKEN` is only available in the publish job — never in the same
process tree as `pnpm install` or build-time code execution.
- **Fork PR security monitor:** New `security_fork-pr-alert.yml`
workflow detects suspicious patterns on fork PRs: `[skip ci]` commits,
force-pushes to zero files, rapid open/close cycles, and large bundled
file additions.
## Test plan
- [ ] Verify workflow YAML syntax passes GitHub Actions validation on
push
- [ ] Confirm existing CI workflows still trigger correctly on internal
PRs
- [ ] Test that the security alert workflow fires on a simulated fork PR
(can be verified after merge by opening a test fork PR)
- [ ] Verify release workflows still function with the build/publish job
split (next release cycle)
Pre-build lint step that fails if any build config file exists that
is not on the checked-in allowlist. Catches TanStack-style attacks
where a malicious vite_setup.mjs is auto-discovered during build.
Runs before pnpm install — zero npm dependencies.
Build and publish now run in isolated GitHub Actions jobs. NPM_TOKEN
is only available in the publish job, preventing /proc/mem token
extraction from build-time code.
Refs https://linear.app/copilotkit/issue/OSS-96
## What this does
Adds three anonymous interaction events to the inspector: banner viewed,
banner clicked, and threads tab clicked. These events are sent to
`https://telemetry.copilotkit.ai/ingest` via a direct fetch POST from
the browser (no shared dependencies).
Also adds a privacy panel in the inspector settings showing opt-out
status, plus a one-time console message after the runtime connects.
Inspector telemetry documentation is folded into the existing
`/telemetry` page on `main` — no new docs surface.
**Opt-out:** Set `COPILOTKIT_TELEMETRY_DISABLED=true` or
`DO_NOT_TRACK=1` as an environment variable on the runtime process. This
disables all telemetry events, URL parameters, and console disclosures
in the inspector.
## Acceptance criteria walkthrough
| AC | Code | Status |
|---|---|---|
| Inspector emits `banner_viewed` + `banner_clicked` to
`telemetry.copilotkit.ai/ingest` with banner variant identifier |
`lib/telemetry.ts` (`TELEMETRY_INGEST_URL`), `index.ts`
`fetchAnnouncement` (banner_viewed with `banner_id:timestamp`) +
`handleDismissAnnouncement` / `handleAnnouncementContentClick`
(banner_clicked with `cta`+`banner_id`+optional `cta_label`) | ✅ |
| PostHog anon distinct ID set on Inspector load |
`getOrCreateTelemetryDistinctId()` (UUID v4 in localStorage), sent as
`X-CopilotKit-Telemetry-Id` header on every ingest POST (Ben confirmed:
lambda parses this as `telemetry_id`). | ✅ |
| Console log on Inspector load + runtime startup |
`onRuntimeConnectionStatusChanged` (status=connected) →
`maybeShowDisclosure()` (once per browser, skipped when
`core.telemetryDisabled`); `BaseCopilotRuntime` + v1 + v2 ctors →
`logRuntimeTelemetryDisclosure()` (once per process) | ✅ |
| Inspector UI opt-out (location documented) | Privacy panel (cog
settings button) shows ✅ telemetry active or ❌ telemetry disabled based
on `core.telemetryDisabled` — display-only. Opt-out is via
`COPILOTKIT_TELEMETRY_DISABLED=true` env var on the runtime. | ✅ |
| Opt-out blocks all sends | All three `track*()` call sites in
`index.ts` check `core.telemetryDisabled` and short-circuit;
`appendRefParam` skips the URL param; `maybeShowDisclosure()` skips the
console log. | ✅ |
| Doc page collects/doesn't-collect/opt-out, linked from log +
disclosure | Updated
`docs/content/docs/(root)/(other)/telemetry/index.mdx`.
`TELEMETRY_DOCS_URL` referenced from console log + privacy panel. | ✅ |
| No content in payload | Properties limited to event metadata;
**negative test** (`telemetry.test.ts`) greps the wire body for
forbidden patterns
(`messages`/`completion`/`prompt`/`state_snapshot`/`agent_state`/`content`/`user_id`).
Reviewers: please grep call sites in `index.ts` for any unintended
payload. | ✅ |
## Verification
- `nx run @copilotkit/web-inspector:test` — 29/29 pass
- `nx run @copilotkit/web-inspector:build` — clean
- `nx run @copilotkit/runtime:test` — 13/13 pass (includes
`telemetryDisabled` flag propagation for `COPILOTKIT_TELEMETRY_DISABLED`
and `DO_NOT_TRACK`)
- End-to-end playwright: with `COPILOTKIT_TELEMETRY_DISABLED=true`, no
telemetry POST in network, privacy panel shows ❌, console disclosure
suppressed
## Test plan
- [x] **CORS fix (merged):** PR #83 (lambda CORS headers) + PR #84
(OPTIONS route on API Gateway) merged
- [x] **Distinct ID header (confirmed):** sending
`X-CopilotKit-Telemetry-Id` header per Ben Taylor
- [x] verify privacy disclosures (in-product + console + docs page) meet
the non-negotiable bar from the ticket
- [ ] confirm consent-banner / CMP alignment and P2 pixel-review status
before merge
- [x] allowlist the three `oss.inspector.*` events on the lambda
(oss-path-to-production)
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Dx4KgZn51RWG8JvysBdpXK)_
- Defer trackBannerViewed until runtime connection is established via
pendingBannerViewed + flushPendingBannerViewed(), preventing the race
where the CDN response beats the /info handshake and fires the event
before core.telemetryDisabled is known
- Remove dead isTelemetryOptedOut() short-circuit from track(); opt-out
is enforced at call sites via core.telemetryDisabled, not localStorage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per Ben Taylor: leave the CTA link URL param for later. Removing the
paragraph describing it from the docs until the approach is confirmed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Don't fire banner_clicked on copy-button clicks (move track call to
the non-copy-button branch of handleAnnouncementContentClick)
- Defer ensureTelemetryDistinctId() to runtime connection so no UUID
is written to localStorage when COPILOTKIT_TELEMETRY_DISABLED is set
- Remove setTelemetryOptOut re-export from telemetry.ts (no production
caller; tests already import directly from persistence.ts)
- Fix docs: replace Privacy tab toggle description with env var opt-out
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per Ben Taylor: the ingest lambda parses the X-CopilotKit-Telemetry-Id header
as telemetry_id for the distinct ID. Send it as a header in addition to keeping
it in the POST body properties.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add telemetryDisabled to RuntimeInfo from COPILOTKIT_TELEMETRY_DISABLED/DO_NOT_TRACK env vars
- Mirror through AgentRegistry and expose via CopilotKitCore getter
- Guard track calls, URL param appending, and console disclosure on core.telemetryDisabled
- Move maybeShowDisclosure() to onRuntimeConnectionStatusChanged (fires after core attaches)
- Update docs to replace localStorage toggle description with env var approach
- Add telemetryDisabled test suite to get-runtime-info tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove "Privacy" from the nav tab row; move settings access to a cog
button (Settings icon) in the top-right header, between dock controls
and the close button. Clicking the cog toggles the settings panel;
clicking again returns to the previous view.
- Settings panel replaces the privacy panel: shows the same transparency
copy but replaces the checkbox toggle with an "I want to opt out —
show me how →" link to the docs. Opt-out is via COPILOTKIT_TELEMETRY_DISABLED
(env variable, documented at docs.copilotkit.ai/telemetry) rather than
a per-browser localStorage toggle.
- Remove isTelemetryOptedOut / setTelemetryOptOut from index.ts (no longer
used; the toggle UI is gone). Underlying persistence exports remain in
telemetry.ts for future use.
- Fix cpk-tab-icon: add display:inline-flex + flex-shrink:0 + align-items:center
to prevent icon containers from collapsing on the x-axis.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move `package` from properties string to top-level `{ name }` object per
Ben's confirmed IngestPayload schema (telemetry-sink-ingest/index.ts:127-134)
- Add typed per-event helpers trackBannerViewed/trackBannerClicked/trackThreadsTabClicked
to enforce property shapes at call sites and prevent PII leakage under wrong keys
- Add trackBannerClickedOnce guard in index.ts (per-mount Set keyed by
banner_id + cta) to prevent banner_clicked inflation on repeated clicks
- Fix handleTelemetryOptOutToggle: replace ?? true fallback with
instanceof HTMLInputElement guard (wrong fallback was a privacy bug)
- Add threadsTabClicked re-selection guard (skip if already on threads tab)
- Replace getTelemetryDistinctIdForUrl() call on mount with ensureTelemetryDistinctId()
- Add inMemoryFallbackId in persistence.ts for funnel coherence when
localStorage is unavailable (same UUID returned per page load)
- Add _resetTelemetryPersistenceForTesting() for test isolation
- Remove @copilotkit/shared dep from telemetry-disclosure.ts (inline
env-var check; keeps module self-contained and testable in isolation)
- Add clearMocks: true to web-inspector vitest config (fixes spy call
history accumulating across tests)
- Expand telemetry.test.ts to 22 tests covering wire body shape, opt-out,
5 error-resilience paths, typed helpers, distinct ID lifecycle (SSR +
localStorage-throws + funnel coherence), maybeShowDisclosure, and
getTelemetryDistinctIdForUrl
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three V1 funnel events from the inspector — oss.inspector.banner_viewed,
oss.inspector.banner_clicked, oss.inspector.threads_tab_clicked — plus a
privacy panel for opt-out, a first-run console disclosure on inspector
mount and runtime startup, and inspector content added to the canonical
/telemetry docs page on main.
Inspector POSTs directly from the browser to telemetry.copilotkit.ai/ingest
(per ticket: URL is intentionally clearly named for transparency in DevTools).
Inline fetch POST in lib/telemetry.ts — no @copilotkit/shared dep on the
inspector, no dependency on any non-main branch.
Wire body shape (conservative; needs Ben confirmation):
POST https://telemetry.copilotkit.ai/ingest
{ event, properties: { ...caller, distinct_id, package }, ts }
If the lambda expects a richer envelope, update the single JSON.stringify
in lib/telemetry.ts.
Privacy invariants:
- Opt-out toggle short-circuits before any network call (verified by test).
- Properties are scoped to event metadata only — no message content, agent
state, prompts, completions, banner markdown. Negative test pins the wire.
- Anonymous distinct ID (UUID v4 in localStorage) set on inspector load and
propagated onto banner CTA links as ?posthog_distinct_id=<uuid> so the
destination site can posthog.alias() and close the
banner_viewed → banner_clicked → signup_attributed funnel. URL param
suppressed when opted out.
- Console disclosure on first inspector mount and runtime startup. Both
link to https://docs.copilotkit.ai/telemetry.
Plan gaps addressed:
- CTA name on banner_clicked: cta:'body'|'dismiss' (click location) plus
optional cta_label read defensively. Sam: confirm dismiss treatment.
- De-anon opt-out folded into the single toggle. Docs say so explicitly.
- banner_viewed dedup: per-instance Set<string> keyed by timestamp.
- EPIC consent / pixel review: out of scope for this PR; flagged at merge.
Deferred for V1.1:
- Wire body shape (Ben).
- Event-type allowlist for oss.inspector.* (Ben — oss-path-to-production).
- posthog_distinct_id URL-param key name (Ben/Tyler/website team).
Refs https://linear.app/copilotkit/issue/OSS-96
ContextVar-based ambient state + httpx event hook. Incoming
x-aimock-* headers from AG-UI requests are forwarded to outgoing
LLM API calls. Keys normalized to lowercase. Warning emitted when
install_httpx_hook receives an unrecognized client type.
Probes now send X-AIMock-Strict: true on all requests via
extraHTTPHeaders, triggering strict fixture matching in aimock.
Live demo traffic (no header) continues to proxy to real LLMs.
Mirror of #4430 by @MalaikaAbb.
Adds an agentId note for non-default agents, replaces `agent.id` with
`agent.agentId`, corrects the `@copilotkit/shared/v2` import to
`@copilotkit/shared`, and adds the missing `[agent]` dependency to a
useEffect.
## Summary
Multi-pill sessions in the showcase demos were silently breaking because
aimock fixtures gated their tool-emit vs. follow-up branches on global
thread state (`hasToolResult`, `turnIndex`). Once the user clicked a
tool-using pill, the thread already had tool/assistant messages so the
next pill's first-leg fixture got skipped and the matcher fell through
to the wrong branch.
Concrete failures Alem reported:
- **tool-rendering**: Find flights then Roll a d20 rendered only 3 d20
cards instead of 5; Chain tools emitted zero cards and went straight to
"Done — Tokyo is sunny…".
- **frontend-tools-async**: First pill rendered the NOTES DB card;
second and third pills only rendered narration with no card.
- **hitl-in-app**: After approving the first pill the second pill
skipped its approval dialog entirely.
## Fix
- Re-key every follow-up fixture on the prior step's `toolCallId` (the
matcher checks `messages[last].tool_call_id`).
- Drop `hasToolResult: false` from the tool-emitting fixtures so they
fire on any iteration whose last message is the user prompt.
- Reorder so the `toolCallId`-specific fixtures come first under
first-match-wins.
- `userMessage` matchers are unchanged — fixtures still match the same
prompt text they always did.
D20 becomes a linear `toolCallId` graph (`seq_001 → _002 → … → _005`);
Chain tools gets disambiguator fixtures for each of its three parallel
`tool_call_id`s; Weather / AAPL / query_notes / HITL approve+reject
branches all gate on the specific `request_user_approval` /
`get_weather` / `query_notes` / `get_stock_price` id that landed last.
## Regression coverage
Adds a multi-pill Playwright test to each affected spec — clicks every
pill sequentially in one thread and asserts the full card count for
each:
- `tool-rendering-default-catchall`: Find flights → 5 d20 rolls (with 20
last)
- `tool-rendering-custom-catchall`: 1 flights + 5 d20 + 3 chain = 9
wildcard cards
- `frontend-tools-async`: 3 NOTES DB cards with the right keyword per
pill
- `hitl-in-app`: refund approve then escalate, each with its own
approval dialog
## Test plan
- [x] Verified each fix end-to-end via Playwright MCP against local dev
server
- [ ] CI runs the new regression specs against the canonical Docker
stack
- [ ] Existing aimock fixture validation suites still pass (verified
locally: 21/21)
## Summary
In LangGraph API, persistence is handled automatically by the platform,
so providing a custom checkpointer throws an Application Failure error.
Therefore, it had to be removed
## What does this PR do?
- Removed ```checkpointer=MemorySaver()``` from python code snippet in
section (Initialize Your Agent Project, and also removed its relevant
import
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
The tool-rendering, frontend-tools-async, and hitl-in-app fixtures all gated
their first-leg (tool-emitting) vs. follow-up (narration) responses on
`hasToolResult: false/true` and/or `turnIndex`. Those constraints count the
*entire* thread, so once a user clicked a tool-using pill the thread already
contained tool messages and assistant turns and subsequent pill clicks fell
through to the wrong branch — d20 dropped from 5 rolls to 3, Chain tools
emitted no cards, query_notes returned narration without the Notes DB card,
and the second HITL pill never raised an approval dialog.
Re-key every follow-up fixture on the prior step's `toolCallId` (the matcher
checks `messages[last].tool_call_id`), drop the global `hasToolResult` gates
from the tool-emitting fixtures, and reorder so the toolCallId-specific
fixtures come first under first-match-wins. The d20 chain becomes a linear
toolCallId graph (`call_tr_d20_seq_001` → `_002` → … → `_005`), Chain tools
gets disambiguators for each of its three parallel tool_call_ids, and
Weather/AAPL/query_notes/HITL approve+reject branches all gate on the
specific request_user_approval / get_weather / query_notes / get_stock_price
id that landed last. userMessage matchers are unchanged.
Adds Playwright multi-pill regression tests to the four affected demos that
click every pill sequentially in one thread and assert the full card counts:
- tool-rendering-default-catchall: Find flights → 5 d20 rolls (with 20 last)
- tool-rendering-custom-catchall: 1 flights + 5 d20 + 3 chain = 9 cards
- frontend-tools-async: 3 NOTES DB cards with the right keyword per pill
- hitl-in-app: refund approve then escalate, each with its own dialog
## Summary
- **Fix**: the three interactive Open-Generative-UI (Advanced) pills
(Calculator, Ping the host, Inline expression evaluator) shipped HTML +
CSS only in `showcase/aimock/d5-all.json`. The buttons rendered but
every click was a silent no-op because the fixtures never included
`jsFunctions` to wire up the host-bridge calls
(`Websandbox.connection.remote.evaluateExpression` / `notifyHost`).
Restores the missing JS.
- **Two-layer regression guard** so this can't land silently again:
1. `showcase/scripts/__tests__/open-gen-ui-advanced-fixtures.test.ts`
(vitest, runs on every PR in `showcase_validate`) — asserts each
interactive fixture entry references the right host bridge in its
`jsFunctions`.
2.
`showcase/integrations/langgraph-python/tests/e2e/open-gen-ui-advanced.spec.ts`
(playwright, runs in `test_e2e-showcase-on-demand`) — drives the
in-iframe controls, captures the host handler's `console.log`, and
asserts the iframe output reflects the host response.
The existing e2e smoke tests are kept (still useful) but switched from
pill-click to textarea fill+Enter, following the precedent set by commit
15db0bbf3 (`gen-ui-headless-complete`). Combined with
`[data-testid="copilot-chat-textarea"]`, an explicit click, and
`waitForLoadState("networkidle")` in `beforeEach`, the suite is no
longer flaky against React hydration.
## Why fixture-only fix is sufficient
The agent system prompt (`open_gen_ui_advanced_agent.py:71`) tells a
real LLM to emit `jsFunctions`. A real model does. The fixtures simulate
that contract — they just hadn't been updated to. No agent code or
framework code needed to change; the runtime already injects
`jsFunctions` into the Websandbox correctly
(`packages/react-core/src/v2/components/OpenGenerativeUIRenderer.tsx`
Effect 2).
## Test plan
- [x] `pnpm --filter @copilotkit/showcase-scripts test
open-gen-ui-advanced-fixtures` — 3/3 passing
- [x] Full `open-gen-ui-advanced.spec.ts` playwright suite,
aimock-driven, locally — 7/7 passing (4 smoke + 3 round-trip) in ~28s
- [x] Manual: Calculator builds + evaluates, Ping the host updates
output with `host replied at ...`, Inline evaluator returns `= <value>`
- [ ] `/test-aimock langgraph-python` once this PR is open (validates
against the CI aimock stack)
The previous fixture regression (HTML+CSS only, no jsFunctions) slipped
past CI because the e2e suite only asserted "iframe mounts with non-empty
srcdoc" — which passes whether or not the iframe is interactive. Adds
two layers of guard so the same regression cannot land silently:
1. showcase/scripts/__tests__/open-gen-ui-advanced-fixtures.test.ts
(vitest, runs in showcase_validate on every PR): asserts each of the
three interactive fixture entries in d5-all.json ships jsFunctions
referencing the matching host bridge (evaluateExpression / notifyHost).
Catches "someone removed jsFunctions" at PR-time with no
infrastructure dependencies.
2. showcase/integrations/langgraph-python/tests/e2e/open-gen-ui-advanced.spec.ts
(playwright, runs in test_e2e-showcase-on-demand): adds three
round-trip tests that drive the in-iframe controls and assert the
host-side handler ran by capturing its console.log + verifying the
iframe output element reflects the host response. Catches "the
renderer fails to inject jsFunctions into the sandbox" too.
The e2e tests also switch the existing smoke tests off pill-click and
onto a textarea-driven fill+Enter path, following the same precedent as
commit 15db0bbf3 (gen-ui-headless-complete) — chip mounts diverge
between EmptyState and SuggestionBar surfaces, and Playwright's pill
click races React hydration. Using [data-testid="copilot-chat-textarea"]
with an explicit click + waitForLoadState("networkidle") makes the
suite reliable end-to-end (7/7 passing locally against the aimock-driven
stack).
The three interactive Open-Generative-UI (Advanced) pills — Calculator,
Ping the host, and Inline expression evaluator — shipped HTML + CSS
only in d5-all.json. The agent system prompt instructs the LLM to also
emit jsFunctions, but the fixtures didn't, so every in-iframe button
became a silent no-op: the iframe rendered, clicks dispatched no events,
and the sandbox-function bridges to evaluateExpression / notifyHost
were never exercised.
Adds the missing jsFunctions to all three fixtures, using single-quoted
JS so no escaping is needed inside the JSON-stringified tool arguments.
Each handler respects the sandbox="allow-scripts"-only iframe constraints
(no <form>, plain addEventListener) and calls back into the host via
Websandbox.connection.remote.<name>, with the expected return-shape
contract (res.ok + res.value / res.receivedAt).
## Summary
- The chat-slots cell is wired to the neutral `sample_agent` graph
(plain `ChatOpenAI`, no Responses API, no reasoning config), so it never
emits AG-UI `REASONING_MESSAGE_*` events. The "Show reasoning"
suggestion pill therefore had no way to light up the wrapped
`messageView.reasoningMessage` slot.
- Its prompt — `"Think out loud step by step about whether 17 is prime,
then answer."` — also wasn't a substring of any `userMessage` in
`showcase/aimock/d5-all.json`, so aimock-backed runs returned `404 "No
fixture matched"` rather than the intended reasoning fixture.
- `qa/chat-slots.md` and `tests/e2e/chat-slots.spec.ts` already only
assert the two remaining pills ("Write a sonnet" / "Tell me a joke"), so
dropping the third pill aligns the demo with what the QA + e2e already
expect.
The `CustomReasoningMessage` slot wrapping is intentionally left in
place — the chat-slots page is a "Slot Atlas" where every overrideable
slot is wrapped, even if dormant for this cell. Live reasoning rendering
already has its own dedicated demos at `/demos/reasoning-default` and
`/demos/reasoning-custom`, which use the `reasoning_agent` graph
(Responses API + `reasoning={"effort":"medium","summary":"detailed"}`).
## Test plan
- [ ] `/demos/chat-slots` renders exactly two suggestion pills: "Write a
sonnet" and "Tell me a joke"
- [ ] Both pills produce an assistant reply wrapped in the
`CustomAssistantMessage` slot marker
- [ ] No console errors or missing-fixture 404s appear during the flow
- [ ] `/demos/reasoning-default` and `/demos/reasoning-custom` still
light up the reasoning slot (unchanged here, but worth a sanity check)
The chat-slots cell is wired to the neutral sample_agent graph (plain
ChatOpenAI, no Responses API, no reasoning config), so it never emits
AG-UI REASONING_MESSAGE_* events. The pill could never light up the
wrapped messageView.reasoningMessage slot, and its prompt didn't match
any fixture in showcase/aimock/d5-all.json — aimock-backed runs hit
"No fixture matched". Drop the pill (the QA doc and the e2e spec
already only expect "Write a sonnet" and "Tell me a joke") and leave a
note pointing reasoning demos at /demos/reasoning-default and
/demos/reasoning-custom where the dedicated reasoning_agent graph lives.
## Summary
- Widen the `declarative-gen-ui` demo so generated A2UI cards (KPI
dashboards, charts, status reports) actually have room to breathe. Outer
page wrapper goes `max-w-4xl` → `max-w-6xl`, and a scoped attribute
selector lifts the chat's internal `cpk:max-w-3xl` to 64rem **for this
demo only** — no impact on other demos.
- Fix the `InfoRow` trailing-separator artifact. Each row now draws its
own `border-b` with `last:border-b-0 last:pb-0 first:pt-0`, so the final
row in a Card (most visible on the **Status Report** suggestion pill) no
longer leaves a dangling line. Works regardless of whether the agent
wraps rows in a Column or drops them directly into the Card child slot.
- `Card` gains `w-full overflow-hidden` so it stretches into the wider
column; `InfoRow` value gets `text-right tabular-nums` for cleaner
alignment in stacks.
Scoped to the north-star (`showcase/integrations/langgraph-python`) only
— will propagate to the other integrations via `pnpm parity:sync` in a
follow-up.
## Why
Clicking the suggestion pills (KPI dashboard, pie chart, bar chart,
status report) rendered cards inside a 768px column even though the page
wrapped at 1024px. The bottleneck was the chat's internal
`cpk:max-w-3xl`. Status Report also looked busy because every `InfoRow`
rendered a full-width divider after itself, including the last row.
## Test plan
- [ ] `pnpm --filter @copilotkit/showcase-langgraph-python dev`, open
`/demos/declarative-gen-ui`
- [ ] Click each suggestion pill (KPI dashboard, pie chart, bar chart,
status report); confirm cards have noticeably more horizontal room
- [ ] Confirm the last row in **Status Report** has no trailing divider
- [ ] Confirm other demos in the same integration are visually unchanged
(the override is scoped to `.declarative-gen-ui-wide`)
## Summary
- Delete the legacy `threadId`/`setThreadId` docs under
`docs/content/docs/integrations/langgraph/advanced/persistence/` and
`docs/content/docs/integrations/crewai-flows/persistence/` — superseded
by the `useThreads`-based `/langgraph/threads` and
`/crewai-flows/threads` pages that share
`docs/snippets/shared/threads/threads.mdx`.
- Drop `"persistence"` from the CrewAI Flows sidebar
(`docs/content/docs/integrations/crewai-flows/meta.json`); LangGraph's
`advanced/meta.json` auto-lists, so no sidebar edit needed there.
- Redirect every deleted path — plus the legacy
`/langgraph/persistence/*` aliases and the `/coagents/persistence`
middleware entry — to the corresponding `/{framework}/threads` page so
existing inbound links don't 404. The pre-existing `/coagents/:path*`
and `/crewai-crews/:path*` wildcards chain through to these.
No content in `docs/content`, `docs/snippets`, or `docs/src` still
references the removed paths.
## Test plan
- [ ] `pnpm --filter docs build` succeeds
- [ ] `/langgraph/threads` and `/crewai-flows/threads` still render
- [ ] Hitting `/langgraph/advanced/persistence/loading-message-history`,
`/langgraph/persistence/message-persistence`,
`/crewai-flows/persistence/loading-agent-state`, and
`/coagents/persistence` lands on the appropriate `/{framework}/threads`
page
- [ ] CrewAI Flows sidebar no longer shows "Persistence"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The `[&_div[style*='flex-direction:_row']]:gap-4` arbitrary variant
(quotes inside doubly-nested brackets) is the most exotic Tailwind
syntax in this PR and lines up exactly with when the Vercel
form-filling deploy started failing. Tailwind v4's content scanner is
likely choking on the apostrophes in the nested attribute selector.
The Metric `flex-1 min-w-[120px]` and the chart `flex-1 min-w-0`
already give us even distribution inside the basic catalog's gap-less
Row; the auto-injected nested gap was nice-to-have, not load-bearing.
The /langgraph/advanced/persistence/* and /crewai-flows/persistence/* pages
documented the older threadId/setThreadId mechanism. They're replaced by the
useThreads-based /langgraph/threads and /crewai-flows/threads pages backed by
the shared threads snippet.
Adds redirects from the deleted paths (and their legacy /langgraph/persistence
aliases) to the new framework-scoped /threads pages, and drops "persistence"
from the crewai-flows sidebar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the inline <style> override that widened the chat's `cpk:max-w-3xl`
column and the outer max-w-6xl bump. The chat keeps its normal width;
the real bug was that the basic catalog's Row/Column primitives are
bare `display: flex` divs with no gap and no min-width control on
children, so when the agent dropped multiple Metrics or charts into a
Row they collapsed to content width and looked glued together.
Four targeted fixes inside our renderers:
- Metric gains `flex-1 min-w-[120px]` so a row of KPI tiles distributes
the available width evenly inside the gap-less basic Row, instead of
shrinking to content.
- PieChart and BarChart switch from a hardcoded max-w to `flex-1
min-w-0` so two charts side-by-side each take half the card column
with Recharts' ResponsiveContainer doing the rest, instead of one
chart insisting on 640px and overflowing.
- Card's CardContent picks up a Tailwind arbitrary variant
`[&_div[style*='flex-direction:_row']]:gap-4` (plus the column
equivalent) that injects a gap into any nested basic Row/Column the
agent drops in. Underscores in the arbitrary value compile to literal
spaces, matching React's serialized inline `flex-direction: row`.
- Card itself drops the old `min-w-[260px]` floor in favour of
`min-w-0` so it cooperates if the agent ever stacks Cards horizontally
inside a Row.