From 8bfb35ff425fc38cb7b377938776967d6e163997 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 09:16:54 -0700 Subject: [PATCH 01/12] docs(specs): docs and workspace unification design Co-Authored-By: Claude Fable 5.1 --- ...09-04-docs-workspace-unification-design.md | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-04-docs-workspace-unification-design.md diff --git a/docs/superpowers/specs/2026-09-04-docs-workspace-unification-design.md b/docs/superpowers/specs/2026-09-04-docs-workspace-unification-design.md new file mode 100644 index 000000000..958dc0d42 --- /dev/null +++ b/docs/superpowers/specs/2026-09-04-docs-workspace-unification-design.md @@ -0,0 +1,249 @@ +# Docs and workspace unification — design + +**Date:** 2026-09-04 +**Status:** approved + +## Problem + +The 2026-09 control-plane program folded the standalone Cockpit into the +Website: every docs page now renders through one workspace shell with Docs, +Run, Code, and API modes, and all 41 capability topics have a live runtime. +Three things still keep the surface from reading as one: + +1. **Two route families.** Six topics whose docs page is shared with a + sibling capability have no docs page of their own and live only at + `/workspace//`: `langgraph/durable-execution`, + `ag-ui/client-tools`, `ag-ui/tool-views`, `ag-ui/json-render`, + `ag-ui/subagents`, and `render/repeat-loops`. The pairing is encoded in + `PRIMARY_CAPABILITY_BY_DOCS_PATH` in + `libs/cockpit-registry/src/lib/workspace-resolution.ts` and in the + `/workspace/[product]/[topic]` route. +2. **"Cockpit" on the surface.** The sidebar's aria-label reads "Cockpit + navigation"; about 25 `data-cockpit-*` hooks and `.cockpit-*` classes + drive styling and tests; a screenshot asset is named `cockpit-run.webp`; + two prose mentions in blog and docs use the word as a product name. +3. **A redirect service that only exists to serve a retired host.** + `apps/cockpit` is a 308 table for 47 legacy paths with its own Vercel + project (`threadplane-cockpit`, domains `cockpit.threadplane.ai` and + `cockpit-smoky.vercel.app`, 100+ deployments), a bypass secret, three CI + jobs, cockpit steps inside the deploy job, a 544-line smoke script, and + a `legacyPath` field on every manifest entry. The examples proxies still + trust the retired host in their CORS allowlists. Three libraries + (`cockpit-docs`, `cockpit-testing`, `cockpit-ui`) are empty scaffolds + with no consumer. + +The owner has decided that legacy redirects and inbound links to the old +host no longer matter. + +## Goal + +One route family under `/docs`, a surface that never says "cockpit", and no +infrastructure whose only purpose is the retired host. + +## Non-goals + +- Renaming internal packages (`@threadplane/cockpit-*`, `workspace-react`), + Nx project names, the `cockpit/` example directories, or the `cockpit_*` + CI scope keys and job ids. The word stays in identifiers. +- Building the approved-but-unbuilt "workspace control plane v2" spec. +- Any change to `examples.threadplane.ai`, the assembled Angular examples, + or the runtime bridge. They are the Run-mode runtime, not cockpit + leftovers. +- Writing new long-form guides for the six consolidated topics. Their pages + start as short framing plus the narrative docs the examples already ship. + +## Facts the design rests on + +- The docs route (`apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx`) + requires a registered `DocsPage` in `apps/website/src/lib/docs-config.ts` + and an MDX file under `apps/website/content/docs//
/`; + a missing doc is a 404. +- Every workspace page, docs-backed or not, renders `WebsiteWorkspace` with + a content bundle from `libs/cockpit-shell/src/lib/workspace-content.ts` + that already carries `narrativeDocs` read from the descriptor's + `docsAssetPaths`. The six workspace-only topics all have those. +- `libs/cockpit-registry/src/lib/docs-links.ts` maps each topic to its docs + path; `COCKPIT_TOPICS_WITHOUT_DOCS` is empty. Making the six mappings + unique removes every shared docs path, which is the only reason the + primary-capability override and the `/workspace` route exist. +- `apps/cockpit/scripts/capability-registry.ts` is the single source of + truth for the 41 assembled examples and is imported by + `scripts/assemble-examples.ts`, `scripts/generate-ag-ui-deployment-config.ts`, + two CI path filters, and two guard specs. `serve-example.ts`, + `generate-combined-langgraph.ts`, and `runtime-wiring-audit.ts` (with its + two specs) are developer tooling that happens to live in `apps/cockpit`. +- `resolveLegacyPath`, `getLegacyWebsiteRedirect`, + `resolveLegacyRequestMode`, and the manifest's `legacyPath` field are + consumed only by `apps/cockpit`, its smoke script, and their tests; the + Website references `legacyPath` only in specs and in the production smoke. +- `scripts/ag-ui-proxy.ts:54` and `scripts/examples-middleware.ts:18` list + `https://cockpit.threadplane.ai` in `ALLOWED_ORIGINS`. +- `threadplane.ai` DNS is hosted on Vercel; the `cockpit` record lives in + that zone. The `threadplane-cockpit` project holds env vars nothing else + uses. +- `apps/website/src/lib/cockpit-retirement.spec.ts` scans the Website tree + for the retired host string and pins one blog alt text; it does not + check the bare word. + +## Design + +### Part A — one route family + +For each of the six topics, add a `DocsPage` and an MDX file: + +| topic | new docs path | +| --- | --- | +| `langgraph/durable-execution` | `/docs/langgraph/guides/durable-execution` | +| `ag-ui/client-tools` | `/docs/ag-ui/guides/client-tools` | +| `ag-ui/tool-views` | `/docs/ag-ui/guides/tool-views` | +| `ag-ui/json-render` | `/docs/ag-ui/guides/json-render` | +| `ag-ui/subagents` | `/docs/ag-ui/guides/subagents` | +| `render/repeat-loops` | `/docs/render/guides/repeat-loops` | + +Each MDX page carries frontmatter in the house shape, a one-paragraph +framing of the capability, a pointer to the sibling guide it used to share, +and nothing that duplicates the narrative docs the bundle renders. All +three target sections (`langgraph/guides`, `ag-ui/guides`, `render/guides`) +already exist in `docs-config.ts`; the pages are appended to them. + +Registry changes: `docs-links.ts` maps the six topics to the new paths; +`PRIMARY_CAPABILITY_BY_DOCS_PATH` and `resolveWorkspaceOnlyRoute` are +deleted; `getWorkspaceDestinationPath` always returns the docs path; +`validate-manifest.ts` asserts every entry's docs path is unique and +non-empty instead of asserting the workspace-only invariants. The +`/workspace/[product]/[topic]` route directory is deleted, and +`apps/website/src/app/docs/page.tsx` resolves its default example by docs +path. `WebsiteWorkspace` loses `routeKind="workspace"`. + +Sitemap, docs index, sidebar, prev/next links, and the search index derive +from `docs-config.ts`, so the six pages appear without further wiring; the +public-copy and docs e2e gates cover them. + +### Part B — surface rename + +- `aria-label="Cockpit navigation"` becomes `"Documentation navigation"`, + matching the existing "Documentation control plane" dialog label. +- Every `data-cockpit-*` attribute becomes `data-workspace-*` and every + `.cockpit-*` class becomes `.workspace-*`, across `libs/workspace-react`, + `apps/website/src/styles`, and the Playwright specs that select on them + (`apps/website/e2e/*.spec.ts`). The rename is mechanical and lands in one + commit so selectors and markup never disagree. +- `apps/website/public/screenshots/cockpit-run.webp` becomes + `workspace-run.webp`; its one reference is updated. +- The two prose mentions (`content/blog/2026-05-17-*.mdx:287` link text and + `content/docs/chat/getting-started/introduction.mdx:47`) are reworded to + "workspace" or "debug panel". +- `cockpit-retirement.spec.ts` gains a second guard: the bare word + "cockpit" (case-insensitive) must not appear in Website-served copy — + MDX prose, JSX text, aria-labels, titles — with an explicit allowlist for + repository paths (`cockpit/...`, `apps/cockpit/...` until Part C removes + them) and package names. The guard reads the same file set as the host + guard. + +### Part C — retire the redirect service in the repo + +Delete: + +- `apps/cockpit/` except the four relocated scripts, `vercel.cockpit.json`, + `apps/cockpit/scripts/deploy-smoke.ts` and `.spec.ts`, + `apps/cockpit/scripts/vercel-config.spec.ts`, and the `cockpit` Nx app. +- `legacyPath` from `manifest.types.ts`, `manifest.ts`, and + `validate-manifest.ts`; `resolveLegacyPath`, `resolveLegacyRequestMode`, + and `getLegacyWebsiteRedirect` with their tests; the legacy assertions in + `platform-production-smoke.spec.ts`, `WebsiteWorkspace.spec.tsx`, + `workspace-provider.spec.tsx`, and `workspace-shell.spec.tsx`. +- `libs/cockpit-docs`, `libs/cockpit-testing`, `libs/cockpit-ui`, their + tsconfig path aliases, and their entries in the CI `run-many` list. +- `https://cockpit.threadplane.ai` from both proxies' `ALLOWED_ORIGINS`. + +Relocate, updating every importer, path filter, guard spec, quickstart, and +README that names the old path: + +| from | to | +| --- | --- | +| `apps/cockpit/scripts/capability-registry.ts` | `libs/cockpit-registry/src/lib/capability-registry.ts` (exported from the package index) | +| `apps/cockpit/scripts/serve-example.ts` (+spec) | `scripts/examples/serve-example.ts` | +| `apps/cockpit/scripts/generate-combined-langgraph.ts` | `scripts/examples/generate-combined-langgraph.ts` | +| `apps/cockpit/runtime-wiring-audit.ts` (+ `cockpit-capability-wiring.spec.ts`, `cockpit-e2e-wiring.spec.ts`) | `scripts/examples/runtime-wiring-audit.ts` and specs alongside | + +CI (`.github/workflows/ci.yml` and `scripts/ci-workflow.spec.mjs`): + +- Delete jobs `cockpit-deploy-smoke` and `cockpit-preview-smoke`, their + scope gating, their entries in `required-pr-checks` (needs, env, + `require_scoped` / `require_preview` lines), and the guard tests that + describe them. +- Delete the deploy job's cockpit steps: prepare, build, deploy, + exhaustive verify, promotion freshness, promote, production redirects; + and the guard tests that order and pin them. The + `VERCEL_COCKPIT_*` secrets are no longer referenced. +- The `cockpit` job stops building `nx build cockpit`; it keeps the libs + `run-many` test target minus the three deleted scaffolds. +- The examples deploy step and `examples_changed` detection are unchanged. +- `PREVIEW_LANES_ELIGIBLE` and `require_preview` remain for the Website + preview lane. + +The `cockpit_*` scope keys in `scripts/ci-scope.mjs` stay, per the +non-goals; `cockpit_deploy_smoke` becomes unused and is removed from the +key list with its guard, since nothing consumes it. + +### Part D — external cleanup (after Part C is on main) + +1. Delete the Vercel project `threadplane-cockpit` + (`prj_nVbpDgli7yjZxOaLKh2C2SBARJQd`) through the API with the team + token. This removes its domains, deployments, env, and bypass secret. +2. Delete the `cockpit` DNS record from the `threadplane.ai` zone on + Vercel. +3. Delete the repository secrets `VERCEL_COCKPIT_PROJECT_ID` and + `VERCEL_COCKPIT_AUTOMATION_BYPASS_SECRET`. +4. Confirm `https://cockpit.threadplane.ai/` no longer resolves. + +Memory notes that record the cockpit project id and bypass are updated in +the same session. + +## Sequencing + +Three pull requests in order, each green through CI including the +PR-side preview lanes, then Part D by hand: + +1. Part A. +2. Part B. +3. Part C. + +Part A before Part C so that the docs pages exist before the legacy table +that once pointed at their workspace paths disappears. Part B before Part C +so the word-guard's allowlist for `apps/cockpit` paths can be dropped in +Part C rather than added and removed. + +## Error handling + +- Part A: a topic whose new MDX is missing fails `docs.spec.ts` and the + public-copy gate at build; the manifest validator fails the build if two + entries resolve to one docs path. +- Part B: the word-guard fails the Website unit suite on any user-facing + regression; e2e selector drift fails `website-e2e`. +- Part C: the guard suite fails if any deleted job or step is still + referenced; `ci-scope.spec.mjs` fails if a removed key is still emitted. +- Part D: each API deletion is confirmed by a follow-up GET returning 404 + before the next step. + +## Testing + +- Part A: `npx nx test website`, `npx nx test cockpit-registry`, + `npx nx e2e website` locally with the six new pages visited; the + `website-preview-e2e` lane on the PR exercises Run mode on each. +- Part B: unit guard plus `npx nx e2e website`; the PR lane confirms the + renamed selectors against a deployed preview. +- Part C: `node --test scripts/ci-workflow.spec.mjs scripts/ci-scope.spec.mjs`, + `npx nx run-many -t test --projects=cockpit-registry,cockpit-shell,cockpit-runtime-bridge,workspace-react`, + `npx tsx scripts/assemble-examples.ts` completes, the three quickstarts' + commands run from the new path. +- Part D: `curl -sI https://cockpit.threadplane.ai/` fails to resolve; + `gh secret list` no longer lists the two secrets. + +## Cost + +Part A adds six thin pages and removes one route and one override table. +Part B is a mechanical rename. Part C removes roughly 3,500 lines +(redirect app, smoke script, legacy resolution, scaffolds, CI steps) and +about six minutes from every main push's deploy job. Part D removes one +Vercel project and its 100-plus deployments. From cb15e896c57df9bc34d1d2d133ee3f065c000295 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 09:24:59 -0700 Subject: [PATCH 02/12] docs(plans): docs and workspace unification implementation plan Co-Authored-By: Claude Fable 5.1 --- .../2026-09-04-docs-workspace-unification.md | 905 ++++++++++++++++++ 1 file changed, 905 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-04-docs-workspace-unification.md diff --git a/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md b/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md new file mode 100644 index 000000000..d4312402a --- /dev/null +++ b/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md @@ -0,0 +1,905 @@ +# Docs and workspace unification Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** One route family under `/docs`, a Website surface that never says "cockpit", and no infrastructure whose only purpose is the retired `cockpit.threadplane.ai` host. + +**Architecture:** Three pull requests in order. Part A gives the six workspace-only capabilities their own docs pages so every manifest entry has a unique docs path, then deletes the `/workspace` route and the primary-capability override. Part B mechanically renames `data-cockpit-*`/`.cockpit-*` hooks to `data-workspace-*`/`.workspace-*`, fixes the remaining user-facing copy, and adds a word-guard. Part C deletes the redirect app, its Vercel config, smoke script, legacy-path resolution, three empty scaffold libraries, and the CI jobs and deploy-job steps that served it, relocating the four load-bearing scripts. Part D is a manual external cleanup after Part C is on main. + +**Tech Stack:** Next.js app router (`apps/website`), React workspace shell (`libs/workspace-react`), registry (`libs/cockpit-registry`), vitest, Playwright, GitHub Actions with `node:test` workflow guards, Vercel REST API. + +**Spec:** `docs/superpowers/specs/2026-09-04-docs-workspace-unification-design.md` + +**Conventions the engineer must know:** + +- Work in `/Users/blove/repos/angular-agent-framework/.claude/worktrees/pr-963-production-deploy-e76853`. Each part starts a fresh branch from `origin/main` (`git fetch origin main && git checkout -B origin/main`). Never `git stash`. +- Website unit tests: `npx nx test website --skip-nx-cache` from the root (targeted: `cd apps/website && npx vitest run `; never `--root`; running from `apps/website` falsely fails `cockpit-retirement.spec.ts`, so always confirm from the root). +- Registry / shell unit tests: `npx nx run-many -t test --projects=cockpit-registry,cockpit-shell,cockpit-runtime-bridge,workspace-react --skip-nx-cache`. +- Workflow guards: `node --test --test-reporter=tap scripts/ci-workflow.spec.mjs scripts/ci-scope.spec.mjs 2>&1 | grep -E "^not ok|^# (pass|fail)"`. +- Website e2e locally: `npx nx e2e website --skip-nx-cache` (starts the dev server, three example apps, and the fixture; free ports 4300/4308/4321/4399/4506 first). Against a deployed target: `BASE_URL=https://threadplane.ai npx nx e2e website --skip-nx-cache`. +- Commit messages end with `Co-Authored-By: Claude Fable 5.1 `. No SPDX headers. Run `npx prettier --write` on touched TS/TSX/MDX files before committing. +- PRs: `gh pr create --base main`, wait for `CI — required` plus the two preview lanes (`Website — e2e (deployed preview)`, `Cockpit — immutable preview smoke`), check `gh api repos/cacheplane/angular-agent-framework/pulls//comments` for AI review findings, then `gh pr merge --squash`. + +--- + +## File structure + +| Part | File | Responsibility | +| --- | --- | --- | +| A | `libs/cockpit-registry/src/lib/docs-links.ts` | topic → docs path table; six entries change | +| A | `libs/cockpit-registry/src/lib/content-descriptors.ts` | per-capability `docsPath`; six entries change | +| A | `libs/cockpit-registry/src/lib/workspace-resolution.ts` | drop `PRIMARY_CAPABILITY_BY_DOCS_PATH`; destination is always the docs path | +| A | `libs/cockpit-registry/src/lib/validate-manifest.ts` | duplicate docs path is an error | +| A | `apps/website/src/lib/docs-config.ts` | six new `DocsPage` entries | +| A | `apps/website/content/docs/{langgraph,ag-ui,render}/guides/*.mdx` | six new pages | +| A | `apps/website/src/app/workspace/` | deleted | +| A | `apps/website/src/components/workspace/WebsiteWorkspace.tsx` | drop `routeKind` prop; documentation labels only | +| A | `apps/website/src/app/docs/page.tsx` | default example resolved by docs path | +| A | `apps/website/e2e/workspace-shell.spec.ts`, `apps/website/e2e/platform-production-smoke.spec.ts` | no workspace-only cases | +| B | `libs/workspace-react/src/**`, `apps/website/src/styles/docs.css`, `apps/website/e2e/*.spec.ts`, `apps/website/scripts/capture-screenshots*.ts` | mechanical hook/class rename | +| B | `apps/website/src/lib/cockpit-retirement.spec.ts` | word-guard for user-facing copy | +| C | `libs/cockpit-registry/src/lib/capability-registry.ts` (moved) | single source of truth for assembled examples | +| C | `scripts/examples/{serve-example,generate-combined-langgraph,runtime-wiring-audit}.ts` (moved) | developer tooling | +| C | `.github/workflows/ci.yml`, `scripts/ci-workflow.spec.mjs`, `scripts/ci-scope.mjs`, `scripts/ci-scope.spec.mjs` | cockpit jobs and steps removed | + +--- + +# Part A — one route family + +Branch: `blove/docs-one-route-family` from `origin/main`. + +### Task A1: Give the six topics unique docs paths in the registry + +**Files:** +- Modify: `libs/cockpit-registry/src/lib/docs-links.ts` +- Modify: `libs/cockpit-registry/src/lib/content-descriptors.ts` (lines 134, 314, 340, 367, 421, 656) +- Modify: `libs/cockpit-registry/src/lib/workspace-resolution.ts` +- Modify: `libs/cockpit-registry/src/lib/validate-manifest.ts` +- Modify: `libs/cockpit-registry/src/lib/workspace-resolution.spec.ts` +- Modify: `libs/cockpit-registry/src/lib/validate-manifest.spec.ts` + +- [ ] **Step 1: Write the failing tests** + +In `libs/cockpit-registry/src/lib/workspace-resolution.spec.ts`, replace the test `'uses an explicit primary capability for duplicate Docs paths, not manifest order'` (starts at line 72) with: + +```ts + it('gives every manifest entry a unique docs path so no override table is needed', () => { + const seen = new Map(); + for (const entry of cockpitManifest) { + expect(entry.docsPath, entry.id).not.toBe(''); + const previous = seen.get(entry.docsPath); + expect(previous, `${entry.id} shares ${entry.docsPath} with ${previous}`).toBeUndefined(); + seen.set(entry.docsPath, entry.id); + } + expect( + resolveDocsWorkspace('/docs/langgraph/guides/durable-execution', 'Durable Execution') + ).toMatchObject({ + kind: 'mapped', + identity: { + id: 'langgraph:core-capabilities:durable-execution:overview:python', + }, + }); + expect( + getWorkspaceDestinationPath({ + id: 'langgraph:core-capabilities:durable-execution:overview:python', + docsPath: '/docs/langgraph/guides/durable-execution', + workspacePath: '/workspace/langgraph/durable-execution', + }) + ).toBe('/docs/langgraph/guides/durable-execution'); + }); +``` + +Remove `PRIMARY_CAPABILITY_BY_DOCS_PATH` from that file's import list and add `getWorkspaceDestinationPath` if it is not already imported. + +Replace the test `'omits Docs mode on a canonical Docs path and includes it on a secondary workspace path'` (starts at line 199) with: + +```ts + it('omits Docs mode on every canonical Docs path', () => { + const primary = resolveLegacyPath( + '/langgraph/core-capabilities/persistence/overview/python' + ); + const secondary = resolveLegacyPath( + '/langgraph/core-capabilities/durable-execution/overview/python' + ); + expect(primary).not.toBeNull(); + expect(secondary).not.toBeNull(); + if (!primary || !secondary) return; + + expect(getCanonicalWebsiteWorkspaceHref(primary, 'Docs')).toBe( + '/docs/langgraph/guides/persistence' + ); + expect(getCanonicalWebsiteWorkspaceHref(secondary, 'Docs')).toBe( + '/docs/langgraph/guides/durable-execution' + ); + expect(getCanonicalWebsiteWorkspaceHref(secondary, 'Run')).toBe( + '/docs/langgraph/guides/durable-execution?mode=run' + ); + expect(getCanonicalWebsiteWorkspaceHref(primary, 'Code')).toBe( + '/docs/langgraph/guides/persistence?mode=code' + ); + }); +``` + +In `libs/cockpit-registry/src/lib/validate-manifest.spec.ts`, replace the test `'rejects ambiguous reverse Docs mappings without an explicit primary mapping'` (starts at line 125) with: + +```ts + it('rejects two entries that publish the same Docs path', () => { + const first = getLangGraphEntry('streaming'); + const second = getLangGraphEntry('interrupts'); + const invalidManifest = [first, { ...second, docsPath: first.docsPath }]; + + expect(validateManifest(invalidManifest)).toContain( + `Duplicate Docs path: ${first.docsPath}` + ); + }); + + it('rejects an entry without a Docs path', () => { + const entry = getLangGraphEntry('streaming'); + + expect(validateManifest([{ ...entry, docsPath: '' }])).toContain( + `Missing docsPath for ${entry.id}` + ); + }); +``` + +Remove `PRIMARY_CAPABILITY_BY_DOCS_PATH` from that file's imports. + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `npx nx test cockpit-registry --skip-nx-cache 2>&1 | grep -E "×|✓ .*unique docs|Tests |FAIL" | head` +Expected: the new tests fail (duplicate docs paths exist; `Duplicate Docs path` is not emitted). + +- [ ] **Step 3: Point the six topics at their own pages** + +In `libs/cockpit-registry/src/lib/docs-links.ts`, change these six values (keep the keys; delete the comments that explained the sharing): + +```ts + 'langgraph/core-capabilities/durable-execution': + '/docs/langgraph/guides/durable-execution', + ... + 'ag-ui/core-capabilities/tool-views': '/docs/ag-ui/guides/tool-views', + 'ag-ui/core-capabilities/json-render': '/docs/ag-ui/guides/json-render', + 'ag-ui/core-capabilities/client-tools': '/docs/ag-ui/guides/client-tools', + 'ag-ui/core-capabilities/subagents': '/docs/ag-ui/guides/subagents', + ... + 'render/core-capabilities/repeat-loops': '/docs/render/guides/repeat-loops', +``` + +In `libs/cockpit-registry/src/lib/content-descriptors.ts`, change the `docsPath` of the six descriptors to the same six values (`langgraph-durable-execution-python` line 134, `ag-ui-tool-views-python` line 314, `ag-ui-json-render-python` line 340, `ag-ui-client-tools-python` line 367, `ag-ui-subagents-python` line 421, `render-repeat-loops-python` line 656). + +- [ ] **Step 4: Remove the override table** + +In `libs/cockpit-registry/src/lib/workspace-resolution.ts`: + +Delete the `PRIMARY_CAPABILITY_BY_DOCS_PATH` constant and its doc comment. + +Replace `getWorkspaceDestinationPath` with: + +```ts +export const getWorkspaceDestinationPath = ( + identity: Pick +): string => { + if (!identity.docsPath) { + throw new Error(`Manifest entry without a docs path: ${identity.id}`); + } + return identity.docsPath; +}; +``` + +Replace `resolveDocsWorkspace` with: + +```ts +export const resolveDocsWorkspace = ( + docsPath: string, + title: string, + manifest: readonly CockpitManifestEntry[] = cockpitManifest +): WorkspaceResolution => { + const matches = manifest.filter((entry) => entry.docsPath === docsPath); + if (matches.length === 1) return mapped(matches[0]); + if (matches.length > 1) { + throw new Error( + `Docs path ${docsPath} is published by ${matches.length} manifest entries` + ); + } + + return { + kind: 'docs-only', + docsPath, + title, + unavailableReason: 'no-workspace-capability', + }; +}; +``` + +In `libs/cockpit-registry/src/lib/validate-manifest.ts`: + +- Delete the `import { PRIMARY_CAPABILITY_BY_DOCS_PATH }` line, the `ValidateManifestOptions` interface, the `options` parameter, and the `primaryDocsMappings` constant. +- Replace the block from `const docsPathEntries = new Map` through the closing of the `for (const [docsPath, entries] ...)` loop with: + +```ts + const docsPaths = new Set(); + for (const entry of manifest) { + if (!entry.docsPath) { + errors.push(`Missing docsPath for ${entry.id}`); + continue; + } + if (docsPaths.has(entry.docsPath)) { + errors.push(`Duplicate Docs path: ${entry.docsPath}`); + } else { + docsPaths.add(entry.docsPath); + } + } +``` + +Keep the signature `validateManifest(manifest)`; grep for callers passing a second argument (`git grep -n "validateManifest(" -- libs apps scripts`) and drop the argument where present. + +- [ ] **Step 5: Run the registry tests** + +Run: `npx nx test cockpit-registry --skip-nx-cache 2>&1 | tail -4` +Expected: `Successfully ran target test for project cockpit-registry`. + +- [ ] **Step 6: Commit** + +```bash +git add libs/cockpit-registry +git commit -m "feat(registry): every capability publishes a unique docs path + +Co-Authored-By: Claude Fable 5.1 " +``` + +### Task A2: Register and author the six docs pages + +**Files:** +- Modify: `apps/website/src/lib/docs-config.ts` +- Create: `apps/website/content/docs/langgraph/guides/durable-execution.mdx` +- Create: `apps/website/content/docs/ag-ui/guides/client-tools.mdx` +- Create: `apps/website/content/docs/ag-ui/guides/tool-views.mdx` +- Create: `apps/website/content/docs/ag-ui/guides/json-render.mdx` +- Create: `apps/website/content/docs/ag-ui/guides/subagents.mdx` +- Create: `apps/website/content/docs/render/guides/repeat-loops.mdx` + +- [ ] **Step 1: Write the failing test** + +Append to `apps/website/src/lib/docs.spec.ts` (inside its top-level `describe`; if the file has none, wrap in `describe('docs pages', ...)`): + +```ts + it('publishes a docs page for every capability the registry maps', () => { + const configured = new Set( + docsLibraries.flatMap((library) => + library.sections.flatMap((section) => + section.pages.map( + (page) => `/docs/${library.id}/${section.id}/${page.slug}` + ) + ) + ) + ); + for (const entry of cockpitManifest) { + expect(configured.has(entry.docsPath), entry.docsPath).toBe(true); + const file = path.join( + process.cwd(), + 'apps/website/content', + `${entry.docsPath}.mdx` + ); + expect(fs.existsSync(file), file).toBe(true); + } + }); +``` + +Add the imports the file lacks: `import fs from 'node:fs'; import path from 'node:path'; import { cockpitManifest } from '@threadplane/cockpit-registry'; import { docsLibraries } from './docs-config';` (check the actual export name of the libraries array at the top of `docs-config.ts` and use it). + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `npx nx test website --skip-nx-cache 2>&1 | grep -E "publishes a docs page|Tests " | head -3` +Expected: the new test fails on `/docs/langgraph/guides/durable-execution`. + +- [ ] **Step 3: Register the pages** + +In `apps/website/src/lib/docs-config.ts`: + +- LangGraph `guides` section, after `{ title: 'Persistence', slug: 'persistence', section: 'guides' },`: + `{ title: 'Durable Execution', slug: 'durable-execution', section: 'guides' },` +- AG-UI `guides` section, after `{ title: 'Interrupts', slug: 'interrupts', section: 'guides' },`: + ```ts + { title: 'Client Tools', slug: 'client-tools', section: 'guides' }, + { title: 'Tool Views', slug: 'tool-views', section: 'guides' }, + { title: 'JSON Render', slug: 'json-render', section: 'guides' }, + { title: 'Subagents', slug: 'subagents', section: 'guides' }, + ``` +- Render `guides` section, after `{ title: 'Specs & Elements', slug: 'specs', section: 'guides' },`: + `{ title: 'Repeat Loops', slug: 'repeat-loops', section: 'guides' },` + +- [ ] **Step 4: Author the pages** + +Existing guides start with an H1 and prose (no frontmatter is required; `docs.ts` strips one if present). Create each file with this shape, substituting the bracketed parts: + +`apps/website/content/docs/langgraph/guides/durable-execution.mdx`: + +```mdx +# Durable Execution + +Durable execution keeps a LangGraph run alive across process restarts and long waits. The checkpointer records every super-step, so a run that is interrupted, redeployed, or paused for a human can resume from its last checkpoint instead of starting over. + +This page is the live example for durable execution. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The checkpointer configuration the example relies on is written up in the [Persistence guide](/docs/langgraph/guides/persistence). +``` + +`apps/website/content/docs/ag-ui/guides/client-tools.mdx`: + +```mdx +# Client Tools + +An AG-UI agent can call tools that live in the browser. The Angular app declares the tool, the adapter forwards the agent's call, and the result flows back into the run without a server round trip. + +This page is the live example for client tools over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The browser-side tool contract is documented in the [Chat client tools guide](/docs/chat/guides/client-tools). +``` + +`apps/website/content/docs/ag-ui/guides/tool-views.mdx`: + +```mdx +# Tool Views + +Tool views render an AG-UI tool call as a purpose-built component instead of a generic card: a map for a location lookup, a table for a query, a form for a confirmation. + +This page is the live example for tool views. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The tool-call component surface is documented in [ChatToolCalls](/docs/chat/components/chat-tool-calls). +``` + +`apps/website/content/docs/ag-ui/guides/json-render.mdx`: + +```mdx +# JSON Render + +An AG-UI agent can stream a declarative UI specification that `@threadplane/render` turns into a live Angular component tree, so the agent shapes the interface rather than only its text. + +This page is the live example for JSON rendering over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The rendering engine is introduced in the [Render introduction](/docs/render/getting-started/introduction). +``` + +`apps/website/content/docs/ag-ui/guides/subagents.mdx`: + +```mdx +# Subagents + +AG-UI carries subagent activity as first-class events, so a delegating agent's child runs show up as attributed cards with their own tool calls and messages instead of being folded into the parent transcript. + +This page is the live example for subagents over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The card component is documented in [ChatSubagentCard](/docs/chat/components/chat-subagent-card). +``` + +`apps/website/content/docs/render/guides/repeat-loops.mdx`: + +```mdx +# Repeat Loops + +A repeat loop renders one element per item of a bound collection, with `$item` and `$index` available to child expressions, so a spec can describe a list without enumerating its rows. + +This page is the live example for repeat loops. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The spec format, including the repeat element, is documented in [Specs & Elements](/docs/render/guides/specs). +``` + +- [ ] **Step 5: Run the Website unit tests** + +Run: `npx nx test website --skip-nx-cache 2>&1 | tail -3` +Expected: `Successfully ran target test for project website`. If `public-copy.spec.ts` or `angular-support-copy.spec.ts` flags a phrase in the new pages, reword to satisfy it; do not touch the guard. + +- [ ] **Step 6: Commit** + +```bash +git add apps/website/src/lib/docs-config.ts apps/website/src/lib/docs.spec.ts apps/website/content/docs +git commit -m "docs: give the six workspace-only capabilities their own guides + +Co-Authored-By: Claude Fable 5.1 " +``` + +### Task A3: Delete the `/workspace` route and the workspace route kind in the Website + +**Files:** +- Delete: `apps/website/src/app/workspace/[product]/[topic]/page.tsx`, `apps/website/src/app/workspace/[product]/[topic]/page.spec.tsx` +- Modify: `apps/website/src/components/workspace/WebsiteWorkspace.tsx` +- Modify: `apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx` +- Modify: `apps/website/src/lib/workspace-page.ts` (remove `getWebsiteWorkspaceRoutePage` if nothing else uses it) +- Modify: `apps/website/src/app/docs/page.tsx` +- Modify: `apps/website/e2e/workspace-shell.spec.ts` +- Modify: `apps/website/e2e/platform-production-smoke.spec.ts` + +- [ ] **Step 1: Update the Website unit tests first** + +In `apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx`: + +- Delete the tests `'uses host-neutral labels and no fabricated Docs slot on workspace routes'` (line 382), `'restores the canonical workspace Docs mode on reload'` (line 510), and `'uses workspace routes only when Docs are absent or would lose identity'` (line 538). +- In `'serializes Docs mode explicitly on workspace routes and keeps the selected mode synchronized'` (line 475): rename to `'keeps the selected mode synchronized on a docs route'`; change the fourth `mappedResolution` argument to `'/docs/langgraph/guides/durable-execution'`; replace every `/workspace/langgraph/durable-execution` with `/docs/langgraph/guides/durable-execution`; delete the `routeKind: 'workspace',` line; change the expected push to `'/docs/langgraph/guides/durable-execution'` (Docs mode on a docs route serializes without a query) and the expected `requestedMode` to `null`. +- In `'keeps workspace mode state aligned across Back and Forward history entries'` (line 788): same path substitutions, delete `routeKind: 'workspace',`, and replace the two `?mode=docs` URLs with the bare docs path. +- Add, after the deleted line-538 test's former position: + +```ts + it('always links a capability to its docs path', () => { + renderWorkspace(); + const resolveHref = mocks.latestProviderProps?.resolveIdentityHref; + if (!resolveHref) throw new Error('Expected Website identity resolver'); + + expect( + resolveHref({ + id: 'langgraph:core-capabilities:durable-execution:overview:python', + docsPath: '/docs/langgraph/guides/durable-execution', + workspacePath: '/workspace/langgraph/durable-execution', + } as never) + ).toBe('/docs/langgraph/guides/durable-execution'); + }); +``` + +- Anywhere else in the file that passes `routeKind: 'workspace'` or asserts `routeKind` on provider props, remove it. + +- [ ] **Step 2: Run to verify they fail** + +Run: `cd apps/website && npx vitest run WebsiteWorkspace 2>&1 | grep -E "×|Tests " | head` +Expected: the edited tests fail (the component still labels workspace routes and pushes `?mode=docs` on the old path). + +- [ ] **Step 3: Simplify `WebsiteWorkspace.tsx`** + +- Delete `readonly routeKind?: 'docs' | 'workspace';` (line 54) and `routeKind = 'docs',` (line 175) and `const isWorkspaceRoute = routeKind === 'workspace';` (line 182). +- Replace `routeKind={routeKind}` (line 319) with `routeKind="docs"`. +- Replace the five conditional labels (lines 340–353) with their documentation branch: `'Documentation workspace'`, `'Documentation modes'`, `'Documentation context'`, `'Documentation control plane'`, `'Documentation'`. +- Remove `props.routeKind,` from the memo dependency list (line 481). + +- [ ] **Step 4: Delete the route and its helper** + +```bash +git rm -r "apps/website/src/app/workspace" +``` + +In `apps/website/src/lib/workspace-page.ts`, delete `getWebsiteWorkspaceRoutePage` (lines 40–53) if `git grep -n getWebsiteWorkspaceRoutePage -- apps libs` shows no remaining caller. + +In `apps/website/src/app/docs/page.tsx`, replace the `DEFAULT_EXAMPLE_RESOLUTION` line and its comment with: + +```ts +/** + * The example the index's Run rail item opens, resolved through the registry + * so a renamed or removed capability yields null and Run falls back to + * disabled rather than to a dead link. + */ +const DEFAULT_EXAMPLE_RESOLUTION = resolveDocsWorkspace( + '/docs/langgraph/guides/streaming', + 'Streaming' +); +``` + +and update the import from `@threadplane/cockpit-registry` (`resolveWorkspacePath` → `resolveDocsWorkspace`). `getCanonicalWebsiteWorkspaceHref` accepts either resolution kind, so the following lines stay. + +- [ ] **Step 5: Update the e2e specs** + +In `apps/website/e2e/workspace-shell.spec.ts`: change line 7 to `const durableExecutionDocsPath = '/docs/langgraph/guides/durable-execution';` and replace the test `'uses workspace fallbacks only when a shared Docs path would lose identity'` (line 412) with: + +```ts + test('serves the formerly workspace-only capabilities as docs pages with Run available', async ({ + page, + }) => { + const response = await page.goto(durableExecutionDocsPath); + expect(response?.status()).toBe(200); + await expect(page).toHaveURL(durableExecutionDocsPath); + await expect(page.locator('[data-workspace-shell]')).toHaveAttribute( + 'aria-label', + 'Documentation workspace' + ); + await expectMode(page, 'Docs'); + await modeButton(page, 'Run').click(); + await expect(page).toHaveURL(`${durableExecutionDocsPath}?mode=run`); + await expect( + page.locator('iframe[title="LangGraph Durable Execution live example"]') + ).toBeAttached(); + + const missing = await page.goto('/workspace/langgraph/durable-execution'); + expect(missing?.status()).toBe(404); + }); +``` + +(Confirm the iframe title by reading the entry's `title` in `libs/cockpit-registry/src/lib/manifest.ts`; the frame title is `${entryTitle} live example`.) + +In `apps/website/e2e/platform-production-smoke.spec.ts`: delete the `workspaceOnly` lookup (lines 143–145), change the throw to `if (!docsBacked) throw new Error('Production smoke requires a Docs-backed route');`, and delete the `'workspace-only production redirect'` case (lines 165–170). Leave the rest; Part C removes the whole redirect block. + +- [ ] **Step 6: Verify** + +Run: `npx nx test website --skip-nx-cache 2>&1 | tail -3` → success. +Run: `npx nx run-many -t test --projects=cockpit-registry,cockpit-shell,workspace-react --skip-nx-cache 2>&1 | tail -3` → success. +Run: `npx nx lint website --skip-nx-cache 2>&1 | grep problems` → `0 errors`. +Run: `npx nx build website --skip-nx-cache 2>&1 | tail -3` → success (static params for the six pages generate). +Run: `npx nx e2e website --skip-nx-cache 2>&1 | grep -E "passed|failed"` → all passed. + +- [ ] **Step 7: Commit and open the PR** + +```bash +git add -A apps/website +git commit -m "feat(website): retire the /workspace route; every capability is a docs page + +Co-Authored-By: Claude Fable 5.1 " +git push -u origin blove/docs-one-route-family +gh pr create --base main --title "feat: one route family — every capability is a docs page" --body "Part A of docs/superpowers/specs/2026-09-04-docs-workspace-unification-design.md. Six formerly workspace-only capabilities get their own guides; the /workspace route and the primary-capability override table are deleted. Follow-ups: Part B (surface rename), Part C (redirect service retirement)." +``` + +Wait for green (including `Website — e2e (deployed preview)`), address review comments, merge with `gh pr merge --squash`. + +--- + +# Part B — surface rename + +Branch: `blove/workspace-surface-rename` from `origin/main` (after Part A merged). + +### Task B1: Word-guard for user-facing copy (test first) + +**Files:** +- Modify: `apps/website/src/lib/cockpit-retirement.spec.ts` + +- [ ] **Step 1: Add the guard** + +Append inside `describe('Cockpit surface retirement', ...)`: + +```ts + it('keeps the retired product name out of user-facing Website copy', () => { + const USER_FACING = /\.(?:mdx|md|tsx)$/; + const ALLOWED_TOKENS = [ + /cockpit\/[a-z-]+\/[a-z-]+/g, // example repository paths + /apps\/cockpit\//g, // tooling paths (removed by Part C) + /@threadplane\/cockpit-[a-z-]+/g, // package names + /cockpit-[a-z-]+-angular/g, // Nx project names + /cockpit-(?:registry|shell|runtime-bridge|telemetry|docs|testing|ui)/g, + /cockpit_(?:did|cap|phk|host)/g, // runtime bridge query params + /Cockpit(?:ManifestEntry|ManifestIdentity)/g, // type names + /cockpit-retirement/g, // this guard's own file name + ]; + const sources = sourceFiles(WEBSITE_ROOT) + .filter((path) => USER_FACING.test(path)) + .map((path) => ({ + relativePath: relative(WEBSITE_ROOT, path), + content: readFileSync(path, 'utf8'), + })); + + const violations = sources.flatMap(({ relativePath, content }) => + content.split('\n').flatMap((line, index) => { + let scrubbed = line; + for (const token of ALLOWED_TOKENS) scrubbed = scrubbed.replace(token, ''); + return /cockpit/i.test(scrubbed) ? [`${relativePath}:${index + 1}`] : []; + }) + ); + + expect(violations).toEqual([]); + }); +``` + +- [ ] **Step 2: Run it to see the current violations** + +Run: `cd apps/website && npx vitest run cockpit-retirement 2>&1 | grep -A20 "user-facing Website copy" | head -30` +Expected: FAIL listing at least `content/blog/2026-05-17-...mdx:287`, `content/docs/chat/getting-started/introduction.mdx:47`, `src/app/pilot-to-prod/page.tsx:111`, and any TSX carrying `data-cockpit-`/`cockpit-` class strings. Keep this list; Tasks B2 and B3 clear it. + +- [ ] **Step 3: Commit the failing guard** + +```bash +git add apps/website/src/lib/cockpit-retirement.spec.ts +git commit -m "test(website): guard user-facing copy against the retired product name + +Co-Authored-By: Claude Fable 5.1 " +``` + +### Task B2: Mechanical hook and class rename + +**Files:** every file listed by `git grep -l "data-cockpit-\|\.cockpit-\|cockpit-\(control-plane\|prose\|shell\|file-tree\|nav-\|tab-trigger\|mobile-\|tablet-\|runtime-status\|code-pane\|api-heading\)" -- apps/website libs/workspace-react`, excluding `package.json`, `tsconfig*.json`, `src/index.ts` (those match package names, not hooks). + +- [ ] **Step 1: Apply the rename with an explicit token map** + +Save as `/private/tmp/claude-501/rename-cockpit-hooks.py` and run from the repo root with `python3`: + +```python +import re, subprocess, pathlib +files = subprocess.check_output(['git','grep','-l','-E', + r'data-cockpit-|\.cockpit-|cockpit-(control-plane|prose|shell|file-tree|nav-|tab-trigger|mobile-|tablet-|runtime-status|code-pane|api-heading)', + '--','apps/website','libs/workspace-react']).decode().split() +skip = re.compile(r'(package\.json|tsconfig[^/]*\.json|src/index\.ts)$') +attrs = ['context-content','desktop-navigation','activity-attention','activity-icon','utility','control-plane'] +classes = ['control-plane-theme','control-plane-scope','control-plane','prose--wide','prose--code','prose', + 'file-tree','shell-navigation','shell','mobile-control-plane-header','mobile-control-plane-close', + 'mobile-control-plane-panel','mobile-control-plane','mobile-navigation-trigger','tablet-context-trigger', + 'tablet-context-surface','nav-caret--open','nav-caret','nav-item','tab-trigger','runtime-status-loader', + 'code-pane','api-heading'] +for f in files: + if skip.search(f): continue + p = pathlib.Path(f); s = p.read_text(); o = s + s = s.replace('data-cockpit-workspace', 'data-workspace-surface') + for a in attrs: s = s.replace(f'data-cockpit-{a}', f'data-workspace-{a}') + for c in classes: s = re.sub(rf'(?&1 | grep -E "×|Tests "` → all passing (the guard is green). +Run: `npx nx test website --skip-nx-cache 2>&1 | tail -3` → success (`style-contracts.spec.ts` now reads `.workspace-shell`). +Run: `npx nx run-many -t test --projects=workspace-react --skip-nx-cache 2>&1 | tail -3` → success. +Run: `npx nx lint website --skip-nx-cache 2>&1 | grep problems` → `0 errors`. +Run: `npx nx e2e website --skip-nx-cache 2>&1 | grep -E "passed|failed"` → all passed (selectors were rewritten with the markup). + +- [ ] **Step 5: Commit and open the PR** + +```bash +git add -A apps/website libs/workspace-react +git commit -m "refactor(website): the workspace surface no longer says cockpit + +Co-Authored-By: Claude Fable 5.1 " +git push -u origin blove/workspace-surface-rename +gh pr create --base main --title "refactor: workspace surface rename — hooks, classes, labels, assets" --body "Part B of docs/superpowers/specs/2026-09-04-docs-workspace-unification-design.md. Mechanical data-cockpit-* → data-workspace-* and .cockpit-* → .workspace-* rename with e2e selectors, 'Documentation navigation' aria-label, screenshot assets, two prose fixes, and a word-guard in cockpit-retirement.spec.ts. Internal package, project, and CI names unchanged." +``` + +Wait for green (the preview lane proves the renamed selectors against a deployed preview), address comments, merge. + +--- + +# Part C — retire the redirect service in the repo + +Branch: `blove/retire-cockpit-redirect` from `origin/main` (after Part B merged). + +### Task C1: Relocate the load-bearing scripts + +**Files:** +- Move: `apps/cockpit/scripts/capability-registry.ts` → `libs/cockpit-registry/src/lib/capability-registry.ts` +- Move: `apps/cockpit/scripts/serve-example.ts`, `serve-example.spec.ts`, `generate-combined-langgraph.ts` → `scripts/examples/` +- Move: `apps/cockpit/runtime-wiring-audit.ts`, `cockpit-capability-wiring.spec.ts`, `cockpit-e2e-wiring.spec.ts` → `scripts/examples/` +- Modify: `libs/cockpit-registry/src/index.ts`, `scripts/assemble-examples.ts`, `scripts/generate-ag-ui-deployment-config.ts`, `scripts/cockpit-runtime-bridge-coverage.spec.mjs`, `scripts/ci-scope.spec.mjs`, `.github/workflows/deploy-ag-ui.yml`, `.github/workflows/deploy-langgraph.yml`, `scripts/vite.config.mts`, three quickstart MDX files, three `cockpit/runtimes/**/docs/guide.md`, `deployments/ag-ui-dev/README.md`, two `deployments/ag-ui-dev/deps/**/docs/guide.md`, `deployments/ag-ui-mastra/README.md` + +- [ ] **Step 1: Move the files** + +```bash +git mv apps/cockpit/scripts/capability-registry.ts libs/cockpit-registry/src/lib/capability-registry.ts +mkdir -p scripts/examples +git mv apps/cockpit/scripts/serve-example.ts scripts/examples/serve-example.ts +git mv apps/cockpit/scripts/serve-example.spec.ts scripts/examples/serve-example.spec.ts +git mv apps/cockpit/scripts/generate-combined-langgraph.ts scripts/examples/generate-combined-langgraph.ts +git mv apps/cockpit/runtime-wiring-audit.ts scripts/examples/runtime-wiring-audit.ts +git mv apps/cockpit/cockpit-capability-wiring.spec.ts scripts/examples/capability-wiring.spec.ts +git mv apps/cockpit/cockpit-e2e-wiring.spec.ts scripts/examples/e2e-wiring.spec.ts +``` + +- [ ] **Step 2: Fix imports and exports** + +- `libs/cockpit-registry/src/lib/capability-registry.ts`: change `import type { RuntimeAdapter } from '@threadplane/cockpit-registry';` to `import type { RuntimeAdapter } from './manifest.types';` (confirm `RuntimeAdapter` is declared there with `git grep -n "export type RuntimeAdapter" libs/cockpit-registry`). +- `libs/cockpit-registry/src/index.ts`: add `export * from './lib/capability-registry';`. +- `scripts/examples/serve-example.ts`, `serve-example.spec.ts`, `generate-combined-langgraph.ts`, `capability-wiring.spec.ts`, `e2e-wiring.spec.ts`: replace `from './capability-registry'` / `from './scripts/capability-registry'` with `from '@threadplane/cockpit-registry'`; in `e2e-wiring.spec.ts` keep `from '../../cockpit/ports.mjs'` (same depth as before); in the two wiring specs replace `from './runtime-wiring-audit'` with `from './runtime-wiring-audit'` (unchanged, now siblings) and check any `resolve(__dirname, '../..')`-style repo-root computations still land on the repo root (both `apps/cockpit` and `scripts/examples` are two levels deep). +- `scripts/assemble-examples.ts:21` and `scripts/generate-ag-ui-deployment-config.ts:3`: import from `'@threadplane/cockpit-registry'` (these run under `tsx`; confirm `tsconfig.base.json` paths resolve for tsx by running the script in Step 5). +- `scripts/cockpit-runtime-bridge-coverage.spec.mjs:10`: `'libs/cockpit-registry/src/lib/capability-registry.ts'`. +- `scripts/ci-scope.spec.mjs:440`: `'libs/cockpit-registry/src/lib/capability-registry.ts'`, and change that test's project fixture from `{ name: 'cockpit', tags: COCKPIT_APP_TAGS }` to the registry lib's tags: `{ name: 'cockpit-registry', tags: ['scope:cockpit', 'scope:cockpit-e2e', 'scope:cockpit-examples'] }` and drop the `cockpit_deploy_smoke` assertion. +- `.github/workflows/deploy-ag-ui.yml` and `deploy-langgraph.yml` path filters: `- 'libs/cockpit-registry/src/lib/capability-registry.ts'`. +- `scripts/vite.config.mts`: ensure `include` covers `examples/**/*.spec.ts` (read the file; add the glob if the existing patterns do not match `scripts/examples/*.spec.ts`). +- Docs and READMEs: replace `apps/cockpit/scripts/serve-example.ts` with `scripts/examples/serve-example.ts` in `apps/website/content/docs/runtimes/{aws-strands,mastra,microsoft-agent-framework}/quickstart.mdx`, `cockpit/runtimes/{aws-strands/python,mastra/angular,microsoft-agent-framework/python}/docs/guide.md`, `deployments/ag-ui-dev/deps/{aws_strands,microsoft_agent_framework}/docs/guide.md`, `deployments/ag-ui-mastra/README.md`; and `apps/cockpit/scripts/capability-registry.ts` with `libs/cockpit-registry/src/lib/capability-registry.ts` in `deployments/ag-ui-dev/README.md`. + +- [ ] **Step 3: Verify the moves** + +Run: `git grep -n "apps/cockpit/scripts\|apps/cockpit/runtime-wiring-audit" -- . ':!docs/superpowers'` → no output. +Run: `npx nx test scripts --skip-nx-cache 2>&1 | tail -3` → success, and confirm the three moved specs ran: `npx nx test scripts --skip-nx-cache 2>&1 | grep -E "serve-example|capability-wiring|e2e-wiring"`. +Run: `npx nx test cockpit-registry --skip-nx-cache 2>&1 | tail -3` → success. +Run: `node --test --test-reporter=tap scripts/ci-scope.spec.mjs scripts/cockpit-runtime-bridge-coverage.spec.mjs 2>&1 | grep -E "^not ok|^# (pass|fail)"` → `# fail 0` (the `apps/cockpit/project.json` tests still pass because the app still exists at this step). +Run: `npx tsx scripts/examples/serve-example.ts --help 2>&1 | head -3` → usage text, no module-resolution error. +Run: `npx tsx scripts/generate-ag-ui-deployment-config.ts --help 2>&1 | head -3` or the script's dry-run flag → runs. + +- [ ] **Step 4: Commit** + +```bash +git add -A +git commit -m "refactor: move the capability registry into cockpit-registry and example tooling into scripts/examples + +Co-Authored-By: Claude Fable 5.1 " +``` + +### Task C2: Delete the redirect app, legacy-path resolution, scaffolds, and the retired CORS origin + +**Files:** +- Delete: `apps/cockpit/` (everything remaining), `vercel.cockpit.json`, `libs/cockpit-docs/`, `libs/cockpit-testing/`, `libs/cockpit-ui/` +- Modify: `tsconfig.base.json` (lines 26, 38, 39), `apps/website/tsconfig.json` (line 23) +- Modify: `libs/cockpit-registry/src/lib/manifest.types.ts`, `manifest.ts`, `validate-manifest.ts`, `validate-manifest.spec.ts`, `workspace-resolution.ts`, `workspace-resolution.spec.ts` +- Modify: `libs/workspace-react/src/lib/workspace-provider.tsx`, `workspace-provider.spec.tsx`, `workspace-shell.spec.tsx` +- Modify: `apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx:106`, `apps/website/e2e/platform-production-smoke.spec.ts` +- Modify: `scripts/ag-ui-proxy.ts:54`, `scripts/examples-middleware.ts:18` + +- [ ] **Step 1: Write the failing tests** + +In `libs/cockpit-registry/src/lib/validate-manifest.spec.ts`: remove the `['legacy path', 'legacyPath', 'Duplicate legacy path']` row from the `it.each` at line 111 and the `['legacyPath', ...]` row from the `it.each` at line 143. + +In `libs/cockpit-registry/src/lib/workspace-resolution.spec.ts`: delete the tests that call `resolveLegacyPath` or `resolveLegacyRequestMode` (`'round-trips canonical workspace and legacy paths'` line 26 — keep only its workspace half if it has one, else delete; `'applies the legacy Cockpit default…'` line 135; `'resolves valid legacy modes…'` line 163; `'omits Docs mode on every canonical Docs path'` from Task A1 — rewrite it to take resolutions from `resolveDocsWorkspace('/docs/langgraph/guides/persistence', 'Persistence')` and `resolveDocsWorkspace('/docs/langgraph/guides/durable-execution', 'Durable Execution')`). Change `'uses Docs for Docs and docs-only routes and Run only for runnable workspace routes'` (line 103) to: + +```ts + it('defaults every route to Docs', () => { + const mappedDocs = resolveDocsWorkspace('/docs/langgraph/guides/streaming', 'Streaming'); + const docsOnly = resolveDocsWorkspace('/docs/langgraph/api/inject-agent', 'Inject an agent into Angular'); + expect(getRouteDefaultMode(mappedDocs)).toBe('Docs'); + expect(getRouteDefaultMode(docsOnly)).toBe('Docs'); + }); +``` + +In `libs/workspace-react/src/lib/workspace-provider.spec.tsx`: delete `legacyPath` from the identity fixture (line 34); replace `path: identity.legacyPath` (line 84) and `identity.legacyPath` (line 334) with `identity.docsPath`; delete every `routeKind: 'workspace',` (lines 202, 220, 262) — the tests then exercise the docs default, so change any expectation that relied on the Run default to `'Docs'` (the assertions near lines 210–215 expect `'Docs'` already; re-read each after the edit). +In `libs/workspace-react/src/lib/workspace-shell.spec.tsx`: replace `routeKind="workspace"` (line 339) with `routeKind="docs"` and `routePath={identity.legacyPath}` (line 340) with `routePath={identity.docsPath ?? '/docs'}`. +In `apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx:106`: delete the `legacyPath:` line from the fixture. + +- [ ] **Step 2: Run to verify they fail** + +Run: `npx nx run-many -t test --projects=cockpit-registry,workspace-react --skip-nx-cache 2>&1 | grep -E "×|FAIL|Tests " | head` +Expected: type or assertion failures referencing `legacyPath` / `getRouteDefaultMode` arity. + +- [ ] **Step 3: Remove legacy-path resolution** + +In `libs/cockpit-registry/src/lib/manifest.types.ts`: delete `legacyPath: string;` from both `WorkspaceIdentity` (line 38) and `CockpitManifestEntry` (line 101). +In `manifest.ts`: delete the `legacyPath:` line of `createEntry` (line 202). +In `validate-manifest.ts`: delete the `legacyPaths` set, its duplicate check, and the `expectedLegacyPath` block. +In `workspace-resolution.ts`: delete `legacyPath: entry.legacyPath,` in `toWorkspaceIdentity`, delete `resolveLegacyPath`, `LEGACY_REQUEST_MODES`, and `resolveLegacyRequestMode`; replace `getRouteDefaultMode` with: + +```ts +export const getRouteDefaultMode = ( + _resolution: WorkspaceResolution | null +): WorkspaceMode => 'Docs'; +``` + +In `libs/workspace-react/src/lib/workspace-provider.tsx`: narrow `routeKind` to `'docs'` in the props interface (line 62) and in `normalizedMode` (line 136), and call `getRouteDefaultMode(resolution)`; then remove `routeKind` entirely (props, destructuring at line 160, the `normalizedMode` argument at 183, the dependency at 230/250) and fix the three callers (`WebsiteWorkspace.tsx`, `public-api.spec.tsx:95`, `workspace-shell.spec.tsx:94/339/412`) by dropping the prop. + +- [ ] **Step 4: Delete the app, its config, and the scaffolds** + +```bash +git rm -r apps/cockpit vercel.cockpit.json libs/cockpit-docs libs/cockpit-testing libs/cockpit-ui +``` + +- `tsconfig.base.json`: delete the `@threadplane/cockpit-docs`, `@threadplane/cockpit-testing`, `@threadplane/cockpit-ui` path entries (lines 26, 38, 39). `apps/website/tsconfig.json`: delete the `@threadplane/cockpit-docs` entry (line 23). +- `apps/website/e2e/platform-production-smoke.spec.ts`: delete `expectedRedirect`, `docsBacked`, `COCKPIT_REDIRECT_CASES`, and the test(s) that iterate them; delete the now-unused imports (`resolveLegacyPath`, `resolveLegacyRequestMode`, `getCanonicalWebsiteWorkspaceHref` if unused, `COCKPIT_URL` env reads). Keep `WEBSITE_DESTINATIONS` and every Website-facing test. +- `scripts/ag-ui-proxy.ts:54` and `scripts/examples-middleware.ts:18`: delete the `'https://cockpit.threadplane.ai',` line. +- `git grep -n "cockpit.threadplane.ai" -- . ':!docs/superpowers' ':!CONTRIBUTING.md'` → only `apps/website/src/lib/cockpit-retirement.spec.ts` (the guard's own constant) and `.github/workflows/ci.yml` (removed in Task C3) may remain. + +- [ ] **Step 5: Verify** + +Run: `npx nx run-many -t test --projects=cockpit-registry,cockpit-shell,cockpit-runtime-bridge,workspace-react,scripts --skip-nx-cache 2>&1 | tail -3` → success. +Run: `npx nx test website --skip-nx-cache 2>&1 | tail -3` → success. +Run: `npx nx lint website --skip-nx-cache 2>&1 | grep problems` → `0 errors`; `npx nx lint workspace-react --skip-nx-cache 2>&1 | grep problems` → `0 errors`. +Run: `npx nx show projects 2>/dev/null | grep -E "^cockpit(-docs|-testing|-ui)?$"` → no output. +Run: `npx tsx scripts/assemble-examples.ts 2>&1 | tail -3` → completes (this also proves the registry import path). + +- [ ] **Step 6: Commit** + +```bash +git add -A +git commit -m "chore: delete the cockpit redirect service, legacy-path resolution, and empty scaffold libraries + +Co-Authored-By: Claude Fable 5.1 " +``` + +### Task C3: Remove the cockpit jobs and deploy steps from CI + +**Files:** +- Modify: `.github/workflows/ci.yml` +- Modify: `scripts/ci-workflow.spec.mjs` +- Modify: `scripts/ci-scope.mjs`, `scripts/ci-scope.spec.mjs` +- Modify: `CONTRIBUTING.md` (lines mentioning `VERCEL_COCKPIT_AUTOMATION_BYPASS_SECRET` and the cockpit preview lane) + +- [ ] **Step 1: Update the guard tests first** + +In `scripts/ci-workflow.spec.mjs`: +- Delete the tests `'deploys examples before the Cockpit redirect artifact and preserves fail-fast ordering'` (line 394), `'smokes one immutable Cockpit deployment before promoting that exact URL'` (line 454), `'smokes a throwaway cockpit preview on same-repo PRs and queue candidates'` (line 594), and `'gates Cockpit deployment on the production Website smoke even for Cockpit-only changes'` (line 751). +- In `'verifies every protected immutable preview with its own automation bypass'` (line 559): delete the `cockpitStep` lookup and every assertion on it, keep the Website assertions. +- In `'requires both PR-side preview verifications through the scoped gate'` (line 721): rename to `'requires the Website preview verification through the scoped gate'`; delete the `cockpit-preview-smoke` needs assertion, the `RESULT_COCKPIT_PREVIEW_SMOKE` assertion, and the cockpit `require_preview` assertion. +- In the exact-`needs` fixture (line ~1150): delete `'cockpit-deploy-smoke'` and `'cockpit-preview-smoke'`. +- In `'runs the cockpit sibling libraries that own vitest specs'` (line 933): the expected `run-many` project list becomes `cockpit-registry,workspace-react` (drop `cockpit` and `cockpit-docs`); read the test and adjust its assertion accordingly. +- In `'binds Vercel deploys to the renamed Threadplane projects'` (line ~732): delete the `threadplane-cockpit` assertion. +- Add: + +```js + it('carries no cockpit redirect deployment anywhere', async () => { + const workflow = await readWorkflow(); + assert.doesNotMatch(workflow, /vercel\.cockpit\.json/); + assert.doesNotMatch(workflow, /threadplane-cockpit/); + assert.doesNotMatch(workflow, /VERCEL_COCKPIT_/); + assert.doesNotMatch(workflow, /deploy-smoke\.ts/); + assert.doesNotMatch(workflow, /cockpit\.threadplane\.ai/); + }); +``` + +In `scripts/ci-scope.spec.mjs`: delete the tests that read `apps/cockpit/project.json` (`'the Cockpit Vercel gate selects redirect build and deploy smoke'`, `'apps/cockpit is not tagged scope:cockpit-e2e'`, and the `cockpit` half of `'a website-only change leaves cockpit_e2e false'` — replace its `cockpit` project fixture with `libs/cockpit-registry/project.json`), and delete every `scope.cockpit_deploy_smoke` assertion. + +- [ ] **Step 2: Run the guards to verify they fail** + +Run: `node --test --test-reporter=tap scripts/ci-workflow.spec.mjs scripts/ci-scope.spec.mjs 2>&1 | grep -E "^not ok|^# (pass|fail)"` +Expected: `carries no cockpit redirect deployment` fails, plus any test that now references removed fixtures. + +- [ ] **Step 3: Edit `ci.yml`** + +- Delete jobs `cockpit-deploy-smoke` and `cockpit-preview-smoke` entirely. +- `cockpit` job: delete `- run: npx nx build cockpit --skip-nx-cache`; change the `run-many` projects to `cockpit-registry,workspace-react`; rename the job's `name` to `Workspace libraries — lint / test`. +- `ci-scope` job outputs: delete `cockpit_deploy_smoke: ...`. +- `required-pr-checks`: delete `- cockpit-deploy-smoke` and `- cockpit-preview-smoke` from `needs`; delete `RESULT_COCKPIT_DEPLOY_SMOKE`, `RESULT_COCKPIT_PREVIEW_SMOKE`, `SCOPE_COCKPIT_DEPLOY_SMOKE` from env; delete the `require_scoped "cockpit_deploy_smoke" ...` and `require_preview "cockpit_deploy_smoke" ...` lines. Keep `require_preview` and `PREVIEW_LANES_ELIGIBLE` for the Website lane. +- `deploy` job: in `Detect deploy-relevant changes`, remove the `cockpit_changed` computation and the `cockpit=` output line; remove `|| steps.affected.outputs.cockpit == 'true'` from the `Cache Playwright browsers`, `Install Playwright browsers`, and `Verify deployed website` conditions; delete the seven steps from `Prepare cockpit Vercel project` through `Verify production cockpit redirects` (lines 1288–1347). +- `scripts/ci-scope.mjs`: delete `'cockpit_deploy_smoke'` from `SCOPE_KEYS`; delete the `apps/cockpit/vite.config.mts` comment at line 85 if it is now misleading. +- `CONTRIBUTING.md`: in the "PR-side deploy verification" subsection, delete the cockpit bullet and the `VERCEL_COCKPIT_AUTOMATION_BYPASS_SECRET` mention; elsewhere, remove any line describing the cockpit redirect deploy. + +- [ ] **Step 4: Verify** + +Run: `node --test --test-reporter=tap scripts/ci-workflow.spec.mjs scripts/ci-scope.spec.mjs 2>&1 | grep -E "^not ok|^# (pass|fail)"` → `# fail 0`. +Run: `python3 -c "import yaml;yaml.safe_load(open('.github/workflows/ci.yml'));print('yaml ok')"` → `yaml ok`. +Run: `git grep -n "cockpit_deploy_smoke\|cockpit-deploy-smoke\|cockpit-preview-smoke\|VERCEL_COCKPIT" -- . ':!docs/superpowers'` → no output. +Run: `node scripts/ci-scope.mjs --event push --output /dev/stdout 2>/dev/null | head` → lists keys without `cockpit_deploy_smoke`. + +- [ ] **Step 5: Commit and open the PR** + +```bash +git add -A +git commit -m "ci: drop the cockpit redirect jobs and deploy steps + +Co-Authored-By: Claude Fable 5.1 " +git push -u origin blove/retire-cockpit-redirect +gh pr create --base main --title "chore: retire the cockpit redirect service" --body "Part C of docs/superpowers/specs/2026-09-04-docs-workspace-unification-design.md. Deletes apps/cockpit, vercel.cockpit.json, the deploy smoke, legacy-path resolution, three empty scaffold libraries, and every CI job and deploy step that served cockpit.threadplane.ai. Relocates the capability registry into @threadplane/cockpit-registry and example tooling into scripts/examples. Part D (Vercel project, DNS record, repository secrets) follows by hand once this is on main." +``` + +Wait for green, address comments, merge. Then watch the main run's deploy job: it must promote the Website and pass production smoke with the cockpit steps gone. + +--- + +# Part D — external cleanup (manual, after Part C is on main) + +Token: `VERCEL_API_TOKEN` in the primary checkout's root `.env` (never print it). Team `team_RWMT2bzjj1nkSXI3N3arQ6CP`. Cockpit project `prj_nVbpDgli7yjZxOaLKh2C2SBARJQd`. Run the steps in one shell so `$tok` carries over. + +- [ ] **Step 1: Confirm main no longer references the project** + +```bash +git fetch origin main && git grep -c "threadplane-cockpit\|VERCEL_COCKPIT" origin/main -- .github CONTRIBUTING.md || echo "no references on main" +``` + +Expected: `no references on main`. + +- [ ] **Step 2: Delete the Vercel project** + +```bash +tok=$(grep -E "^VERCEL_API_TOKEN=" /Users/blove/repos/angular-agent-framework/.env | cut -d= -f2- | tr -d '"'"'"' ') +tid=team_RWMT2bzjj1nkSXI3N3arQ6CP +curl -s -o /dev/null -w "delete project: %{http_code}\n" -X DELETE -H "Authorization: Bearer $tok" "https://api.vercel.com/v9/projects/prj_nVbpDgli7yjZxOaLKh2C2SBARJQd?teamId=$tid" +curl -s -o /dev/null -w "project after delete: %{http_code}\n" -H "Authorization: Bearer $tok" "https://api.vercel.com/v9/projects/prj_nVbpDgli7yjZxOaLKh2C2SBARJQd?teamId=$tid" +``` + +Expected: `delete project: 204` then `project after delete: 404`. + +- [ ] **Step 3: Delete the DNS record** + +```bash +rid=$(curl -s -H "Authorization: Bearer $tok" "https://api.vercel.com/v4/domains/threadplane.ai/records?teamId=$tid&limit=100" | python3 -c 'import sys,json;print(next(r["id"] for r in json.load(sys.stdin)["records"] if r.get("name")=="cockpit"))') +curl -s -o /dev/null -w "delete record: %{http_code}\n" -X DELETE -H "Authorization: Bearer $tok" "https://api.vercel.com/v2/domains/threadplane.ai/records/$rid?teamId=$tid" +``` + +Expected: `delete record: 200`. + +- [ ] **Step 4: Delete the repository secrets** + +```bash +gh secret delete VERCEL_COCKPIT_PROJECT_ID --repo cacheplane/angular-agent-framework +gh secret delete VERCEL_COCKPIT_AUTOMATION_BYPASS_SECRET --repo cacheplane/angular-agent-framework +gh secret list | grep -c VERCEL_COCKPIT || echo "cockpit secrets gone" +``` + +Expected: `cockpit secrets gone`. + +- [ ] **Step 5: Confirm the host is gone** + +```bash +sleep 60; curl -sI --max-time 10 https://cockpit.threadplane.ai/ 2>&1 | head -1 || echo "does not resolve" +``` + +Expected: a resolution error or `does not resolve` (DNS caches may take longer; re-check later). + +- [ ] **Step 6: Update memory** + +Edit the memory notes that record the cockpit project id and bypass (`project_deploy_verify_remote_target_guard.md`, `project_release_process_gotchas.md`) to say the project, domain, record, and secrets were deleted on the date of this step. From 02d38ab46bef70ca1b99e6b3e515bca10175668e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 09:31:33 -0700 Subject: [PATCH 03/12] feat(registry): every capability publishes a unique docs path Six capabilities used to share a docs page with a sibling (durable-execution/persistence, ag-ui tool-views/json-render/ client-tools/subagents, render repeat-loops/specs) and relied on PRIMARY_CAPABILITY_BY_DOCS_PATH to pick a primary owner for the route. Give each its own docs path instead, so the override table and its validate-manifest ambiguity check can go away. The six new docs paths (e.g. /docs/langgraph/guides/ durable-execution) do not have pages yet - that lands in a follow-up task alongside removing the /workspace route. Co-Authored-By: Claude Fable 5.1 --- .../ag-ui/client-tools/python/src/index.ts | 27 ++++++++-- cockpit/ag-ui/json-render/python/src/index.ts | 2 +- cockpit/ag-ui/subagents/python/src/index.ts | 7 ++- cockpit/ag-ui/tool-views/python/src/index.ts | 2 +- .../durable-execution/python/src/index.ts | 6 ++- .../render/repeat-loops/python/src/index.ts | 10 ++-- .../src/lib/content-descriptors.ts | 12 ++--- libs/cockpit-registry/src/lib/docs-links.ts | 16 +++--- .../src/lib/validate-manifest.spec.ts | 47 ++++++----------- .../src/lib/validate-manifest.ts | 32 ++++-------- .../src/lib/workspace-resolution.spec.ts | 50 +++++++++++-------- .../src/lib/workspace-resolution.ts | 43 ++++------------ 12 files changed, 113 insertions(+), 141 deletions(-) diff --git a/cockpit/ag-ui/client-tools/python/src/index.ts b/cockpit/ag-ui/client-tools/python/src/index.ts index eb1c8161c..3188f408d 100644 --- a/cockpit/ag-ui/client-tools/python/src/index.ts +++ b/cockpit/ag-ui/client-tools/python/src/index.ts @@ -1,6 +1,12 @@ export interface CockpitCapabilityModule { id: string; - manifestIdentity: { product: 'ag-ui'; section: 'core-capabilities'; topic: 'client-tools'; page: 'overview'; language: 'python'; }; + manifestIdentity: { + product: 'ag-ui'; + section: 'core-capabilities'; + topic: 'client-tools'; + page: 'overview'; + language: 'python'; + }; title: string; docsPath: string; promptAssetPaths: string[]; @@ -13,17 +19,28 @@ export interface CockpitCapabilityModule { export const agUiClientToolsPythonModule: CockpitCapabilityModule = { id: 'ag-ui-client-tools-python', - manifestIdentity: { product: 'ag-ui', section: 'core-capabilities', topic: 'client-tools', page: 'overview', language: 'python' }, + manifestIdentity: { + product: 'ag-ui', + section: 'core-capabilities', + topic: 'client-tools', + page: 'overview', + language: 'python', + }, title: 'AG-UI Client Tools (Python)', - docsPath: '/docs/chat/guides/client-tools', - promptAssetPaths: ['cockpit/ag-ui/client-tools/python/prompts/client-tools.md'], + docsPath: '/docs/ag-ui/guides/client-tools', + promptAssetPaths: [ + 'cockpit/ag-ui/client-tools/python/prompts/client-tools.md', + ], codeAssetPaths: [ 'cockpit/ag-ui/client-tools/angular/src/app/client-tools.component.ts', 'cockpit/ag-ui/client-tools/angular/src/app/weather-card.component.ts', 'cockpit/ag-ui/client-tools/angular/src/app/confirm-booking.component.ts', 'cockpit/ag-ui/client-tools/angular/src/app/app.config.ts', ], - backendAssetPaths: ['cockpit/ag-ui/client-tools/python/src/graph.py', 'cockpit/ag-ui/client-tools/python/src/server.py'], + backendAssetPaths: [ + 'cockpit/ag-ui/client-tools/python/src/graph.py', + 'cockpit/ag-ui/client-tools/python/src/server.py', + ], docsAssetPaths: ['cockpit/ag-ui/client-tools/python/docs/guide.md'], runtimeUrl: 'ag-ui/client-tools', devPort: 4325, diff --git a/cockpit/ag-ui/json-render/python/src/index.ts b/cockpit/ag-ui/json-render/python/src/index.ts index fcc9a1093..65c8a377b 100644 --- a/cockpit/ag-ui/json-render/python/src/index.ts +++ b/cockpit/ag-ui/json-render/python/src/index.ts @@ -27,7 +27,7 @@ export const agUiJsonRenderPythonModule: CockpitCapabilityModule = { language: 'python', }, title: 'AG-UI JSON Render (Python)', - docsPath: '/docs/render/getting-started/introduction', + docsPath: '/docs/ag-ui/guides/json-render', promptAssetPaths: ['cockpit/ag-ui/json-render/python/prompts/json-render.md'], codeAssetPaths: [ 'cockpit/ag-ui/json-render/angular/src/app/json-render.component.ts', diff --git a/cockpit/ag-ui/subagents/python/src/index.ts b/cockpit/ag-ui/subagents/python/src/index.ts index f27777000..d68be0aa3 100644 --- a/cockpit/ag-ui/subagents/python/src/index.ts +++ b/cockpit/ag-ui/subagents/python/src/index.ts @@ -27,13 +27,16 @@ export const agUiSubagentsPythonModule: CockpitCapabilityModule = { language: 'python', }, title: 'AG-UI Subagents (Python)', - docsPath: '/docs/chat/components/chat-subagent-card', + docsPath: '/docs/ag-ui/guides/subagents', promptAssetPaths: ['cockpit/ag-ui/subagents/python/prompts/subagents.md'], codeAssetPaths: [ 'cockpit/ag-ui/subagents/angular/src/app/subagents.component.ts', 'cockpit/ag-ui/subagents/angular/src/app/app.config.ts', ], - backendAssetPaths: ['cockpit/ag-ui/subagents/python/src/graph.py', 'cockpit/ag-ui/subagents/python/src/server.py'], + backendAssetPaths: [ + 'cockpit/ag-ui/subagents/python/src/graph.py', + 'cockpit/ag-ui/subagents/python/src/server.py', + ], docsAssetPaths: ['cockpit/ag-ui/subagents/python/docs/guide.md'], runtimeUrl: 'ag-ui/subagents', devPort: 4326, diff --git a/cockpit/ag-ui/tool-views/python/src/index.ts b/cockpit/ag-ui/tool-views/python/src/index.ts index db8fccd4a..33b1576ba 100644 --- a/cockpit/ag-ui/tool-views/python/src/index.ts +++ b/cockpit/ag-ui/tool-views/python/src/index.ts @@ -27,7 +27,7 @@ export const agUiToolViewsPythonModule: CockpitCapabilityModule = { language: 'python', }, title: 'AG-UI Tool Views (Python)', - docsPath: '/docs/chat/components/chat-tool-calls', + docsPath: '/docs/ag-ui/guides/tool-views', promptAssetPaths: ['cockpit/ag-ui/tool-views/python/prompts/tool-views.md'], codeAssetPaths: [ 'cockpit/ag-ui/tool-views/angular/src/app/tool-views.component.ts', diff --git a/cockpit/langgraph/durable-execution/python/src/index.ts b/cockpit/langgraph/durable-execution/python/src/index.ts index 38f1b8454..db8677c0e 100644 --- a/cockpit/langgraph/durable-execution/python/src/index.ts +++ b/cockpit/langgraph/durable-execution/python/src/index.ts @@ -27,8 +27,10 @@ export const langgraphDurableExecutionPythonModule: CockpitCapabilityModule = { language: 'python', }, title: 'LangGraph Durable Execution (Python)', - docsPath: '/docs/langgraph/guides/persistence', - promptAssetPaths: ['cockpit/langgraph/durable-execution/python/prompts/durable-execution.md'], + docsPath: '/docs/langgraph/guides/durable-execution', + promptAssetPaths: [ + 'cockpit/langgraph/durable-execution/python/prompts/durable-execution.md', + ], codeAssetPaths: [ 'cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts', 'cockpit/langgraph/durable-execution/angular/src/app/app.config.ts', diff --git a/cockpit/render/repeat-loops/python/src/index.ts b/cockpit/render/repeat-loops/python/src/index.ts index 2acdfb0ed..ce0ffc500 100644 --- a/cockpit/render/repeat-loops/python/src/index.ts +++ b/cockpit/render/repeat-loops/python/src/index.ts @@ -27,15 +27,15 @@ export const renderRepeatLoopsPythonModule: CockpitCapabilityModule = { language: 'python', }, title: 'Render Repeat Loops (Python)', - docsPath: '/docs/render/guides/specs', - promptAssetPaths: ['cockpit/render/repeat-loops/python/prompts/repeat-loops.md'], + docsPath: '/docs/render/guides/repeat-loops', + promptAssetPaths: [ + 'cockpit/render/repeat-loops/python/prompts/repeat-loops.md', + ], codeAssetPaths: [ 'cockpit/render/repeat-loops/angular/src/app/repeat-loops.component.ts', 'cockpit/render/repeat-loops/angular/src/app/app.config.ts', ], - backendAssetPaths: [ - 'cockpit/render/repeat-loops/python/src/graph.py', - ], + backendAssetPaths: ['cockpit/render/repeat-loops/python/src/graph.py'], docsAssetPaths: ['cockpit/render/repeat-loops/python/docs/guide.md'], runtimeUrl: 'render/repeat-loops', devPort: 4405, diff --git a/libs/cockpit-registry/src/lib/content-descriptors.ts b/libs/cockpit-registry/src/lib/content-descriptors.ts index 2d6b7734d..8a045fc06 100644 --- a/libs/cockpit-registry/src/lib/content-descriptors.ts +++ b/libs/cockpit-registry/src/lib/content-descriptors.ts @@ -131,7 +131,7 @@ const capabilityModuleData: RegisteredCapabilityModule[] = [ language: 'python', }, title: 'LangGraph Durable Execution (Python)', - docsPath: '/docs/langgraph/guides/persistence', + docsPath: '/docs/langgraph/guides/durable-execution', promptAssetPaths: [ 'cockpit/langgraph/durable-execution/python/prompts/durable-execution.md', ], @@ -311,7 +311,7 @@ const capabilityModuleData: RegisteredCapabilityModule[] = [ language: 'python', }, title: 'AG-UI Tool Views (Python)', - docsPath: '/docs/chat/components/chat-tool-calls', + docsPath: '/docs/ag-ui/guides/tool-views', promptAssetPaths: ['cockpit/ag-ui/tool-views/python/prompts/tool-views.md'], codeAssetPaths: [ 'cockpit/ag-ui/tool-views/angular/src/app/tool-views.component.ts', @@ -337,7 +337,7 @@ const capabilityModuleData: RegisteredCapabilityModule[] = [ language: 'python', }, title: 'AG-UI JSON Render (Python)', - docsPath: '/docs/render/getting-started/introduction', + docsPath: '/docs/ag-ui/guides/json-render', promptAssetPaths: [ 'cockpit/ag-ui/json-render/python/prompts/json-render.md', ], @@ -364,7 +364,7 @@ const capabilityModuleData: RegisteredCapabilityModule[] = [ language: 'python', }, title: 'AG-UI Client Tools (Python)', - docsPath: '/docs/chat/guides/client-tools', + docsPath: '/docs/ag-ui/guides/client-tools', promptAssetPaths: [ 'cockpit/ag-ui/client-tools/python/prompts/client-tools.md', ], @@ -418,7 +418,7 @@ const capabilityModuleData: RegisteredCapabilityModule[] = [ language: 'python', }, title: 'AG-UI Subagents (Python)', - docsPath: '/docs/chat/components/chat-subagent-card', + docsPath: '/docs/ag-ui/guides/subagents', promptAssetPaths: ['cockpit/ag-ui/subagents/python/prompts/subagents.md'], codeAssetPaths: [ 'cockpit/ag-ui/subagents/angular/src/app/subagents.component.ts', @@ -653,7 +653,7 @@ const capabilityModuleData: RegisteredCapabilityModule[] = [ language: 'python', }, title: 'Render Repeat Loops (Python)', - docsPath: '/docs/render/guides/specs', + docsPath: '/docs/render/guides/repeat-loops', promptAssetPaths: [ 'cockpit/render/repeat-loops/python/prompts/repeat-loops.md', ], diff --git a/libs/cockpit-registry/src/lib/docs-links.ts b/libs/cockpit-registry/src/lib/docs-links.ts index 04eefa8dc..59055e4f8 100644 --- a/libs/cockpit-registry/src/lib/docs-links.ts +++ b/libs/cockpit-registry/src/lib/docs-links.ts @@ -53,9 +53,8 @@ export const COCKPIT_DOCS_LINKS: Readonly> = { '/docs/langgraph/getting-started/introduction', 'langgraph/core-capabilities/persistence': '/docs/langgraph/guides/persistence', - // Durable execution is the checkpointer story; persistence is where it is written up. 'langgraph/core-capabilities/durable-execution': - '/docs/langgraph/guides/persistence', + '/docs/langgraph/guides/durable-execution', 'langgraph/core-capabilities/streaming': '/docs/langgraph/guides/streaming', 'langgraph/core-capabilities/interrupts': '/docs/langgraph/guides/interrupts', 'langgraph/core-capabilities/memory': '/docs/langgraph/guides/memory', @@ -72,13 +71,11 @@ export const COCKPIT_DOCS_LINKS: Readonly> = { // AG-UI has no streaming guide; event mapping is where token streaming is specified. 'ag-ui/core-capabilities/streaming': '/docs/ag-ui/reference/event-mapping', 'ag-ui/core-capabilities/interrupts': '/docs/ag-ui/guides/interrupts', - 'ag-ui/core-capabilities/tool-views': '/docs/chat/components/chat-tool-calls', - 'ag-ui/core-capabilities/json-render': - '/docs/render/getting-started/introduction', - 'ag-ui/core-capabilities/client-tools': '/docs/chat/guides/client-tools', + 'ag-ui/core-capabilities/tool-views': '/docs/ag-ui/guides/tool-views', + 'ag-ui/core-capabilities/json-render': '/docs/ag-ui/guides/json-render', + 'ag-ui/core-capabilities/client-tools': '/docs/ag-ui/guides/client-tools', 'ag-ui/core-capabilities/a2ui': '/docs/a2ui/getting-started/introduction', - 'ag-ui/core-capabilities/subagents': - '/docs/chat/components/chat-subagent-card', + 'ag-ui/core-capabilities/subagents': '/docs/ag-ui/guides/subagents', // render 'render/getting-started/overview': @@ -89,8 +86,7 @@ export const COCKPIT_DOCS_LINKS: Readonly> = { 'render/core-capabilities/state-management': '/docs/render/guides/state-store', 'render/core-capabilities/registry': '/docs/render/guides/registry', - // Repeat loops are a spec feature, documented under "Repeat Loops" in the specs guide. - 'render/core-capabilities/repeat-loops': '/docs/render/guides/specs', + 'render/core-capabilities/repeat-loops': '/docs/render/guides/repeat-loops', // `$computed` resolves against the `functions` map registered by provideRender(). 'render/core-capabilities/computed-functions': '/docs/render/api/provide-render', diff --git a/libs/cockpit-registry/src/lib/validate-manifest.spec.ts b/libs/cockpit-registry/src/lib/validate-manifest.spec.ts index 8a234c289..f95eb5dbf 100644 --- a/libs/cockpit-registry/src/lib/validate-manifest.spec.ts +++ b/libs/cockpit-registry/src/lib/validate-manifest.spec.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; import { cockpitManifest } from './manifest'; -import { PRIMARY_CAPABILITY_BY_DOCS_PATH } from './workspace-resolution'; import { validateCockpitManifest, validateManifest } from './validate-manifest'; import type { CockpitManifestEntry } from './manifest.types'; @@ -14,25 +13,8 @@ const getLangGraphEntry = (topic: string): CockpitManifestEntry => { }; describe('validateCockpitManifest', () => { - it('accepts the authoritative manifest and explicit reverse mappings', () => { + it('accepts the authoritative manifest', () => { expect(validateManifest(cockpitManifest)).toEqual([]); - - const docsPathCounts = new Map(); - for (const entry of cockpitManifest) { - if (!entry.docsPath) continue; - docsPathCounts.set( - entry.docsPath, - (docsPathCounts.get(entry.docsPath) ?? 0) + 1 - ); - } - const duplicateDocsPaths = [...docsPathCounts] - .filter(([, count]) => count > 1) - .map(([docsPath]) => docsPath) - .sort(); - - expect(Object.keys(PRIMARY_CAPABILITY_BY_DOCS_PATH).sort()).toEqual( - duplicateDocsPaths - ); }); it('rejects duplicate canonical identities', () => { @@ -122,22 +104,21 @@ describe('validateCockpitManifest', () => { ); }); - it('rejects ambiguous reverse Docs mappings without an explicit primary mapping', () => { - const persistenceEntries = cockpitManifest.filter( - (entry) => entry.docsPath === '/docs/langgraph/guides/persistence' - ); + it('rejects two entries that publish the same Docs path', () => { + const first = getLangGraphEntry('streaming'); + const second = getLangGraphEntry('interrupts'); + const invalidManifest = [first, { ...second, docsPath: first.docsPath }]; - expect( - validateManifest(persistenceEntries, { primaryDocsMappings: {} }) - ).toContain( - 'Ambiguous Docs path without an explicit primary capability: /docs/langgraph/guides/persistence' + expect(validateManifest(invalidManifest)).toContain( + `Duplicate Docs path: ${first.docsPath}` ); - expect( - validateManifest(persistenceEntries, { - primaryDocsMappings: PRIMARY_CAPABILITY_BY_DOCS_PATH, - }) - ).not.toContain( - 'Ambiguous Docs path without an explicit primary capability: /docs/langgraph/guides/persistence' + }); + + it('rejects an entry without a Docs path', () => { + const entry = getLangGraphEntry('streaming'); + + expect(validateManifest([{ ...entry, docsPath: '' }])).toContain( + `Missing docsPath for ${entry.id}` ); }); diff --git a/libs/cockpit-registry/src/lib/validate-manifest.ts b/libs/cockpit-registry/src/lib/validate-manifest.ts index 152a11707..75142bee8 100644 --- a/libs/cockpit-registry/src/lib/validate-manifest.ts +++ b/libs/cockpit-registry/src/lib/validate-manifest.ts @@ -2,7 +2,6 @@ import type { CockpitManifestEntry, CockpitManifestIdentity, } from './manifest.types'; -import { PRIMARY_CAPABILITY_BY_DOCS_PATH } from './workspace-resolution'; const identityKey = ({ product, @@ -13,21 +12,14 @@ const identityKey = ({ }: CockpitManifestIdentity): string => `${product}/${section}/${topic}/${page}/${language}`; -export interface ValidateManifestOptions { - primaryDocsMappings?: Readonly>; -} - export const validateManifest = ( - manifest: readonly CockpitManifestEntry[], - options: ValidateManifestOptions = {} + manifest: readonly CockpitManifestEntry[] ): string[] => { const errors: string[] = []; const identities = new Set(); const stableIds = new Set(); const workspacePaths = new Set(); const legacyPaths = new Set(); - const primaryDocsMappings = - options.primaryDocsMappings ?? PRIMARY_CAPABILITY_BY_DOCS_PATH; for (const entry of manifest) { const key = identityKey(entry); @@ -64,22 +56,16 @@ export const validateManifest = ( } } - const docsPathEntries = new Map(); + const docsPaths = new Set(); for (const entry of manifest) { - if (entry.docsPath) { - const entries = docsPathEntries.get(entry.docsPath) ?? []; - entries.push(entry); - docsPathEntries.set(entry.docsPath, entries); + if (!entry.docsPath) { + errors.push(`Missing docsPath for ${entry.id}`); + continue; } - } - - for (const [docsPath, entries] of docsPathEntries) { - if (entries.length < 2) continue; - const primaryId = primaryDocsMappings[docsPath]; - if (!primaryId || !entries.some((entry) => entry.id === primaryId)) { - errors.push( - `Ambiguous Docs path without an explicit primary capability: ${docsPath}` - ); + if (docsPaths.has(entry.docsPath)) { + errors.push(`Duplicate Docs path: ${entry.docsPath}`); + } else { + docsPaths.add(entry.docsPath); } } diff --git a/libs/cockpit-registry/src/lib/workspace-resolution.spec.ts b/libs/cockpit-registry/src/lib/workspace-resolution.spec.ts index 21011781e..7143aa1d0 100644 --- a/libs/cockpit-registry/src/lib/workspace-resolution.spec.ts +++ b/libs/cockpit-registry/src/lib/workspace-resolution.spec.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; import { cockpitManifest } from './manifest'; import { - PRIMARY_CAPABILITY_BY_DOCS_PATH, getCanonicalWebsiteWorkspaceHref, getWorkspaceDestinationPath, getRouteDefaultMode, @@ -69,21 +68,35 @@ describe('workspace identity resolution', () => { } }); - it('uses an explicit primary capability for duplicate Docs paths, not manifest order', () => { - const docsPath = '/docs/render/guides/specs'; - const reversedManifest = [...cockpitManifest].reverse(); - - expect(PRIMARY_CAPABILITY_BY_DOCS_PATH[docsPath]).toBe( - 'render:core-capabilities:spec-rendering:overview:python' - ); + it('gives every manifest entry a unique docs path so no override table is needed', () => { + const seen = new Map(); + for (const entry of cockpitManifest) { + expect(entry.docsPath, entry.id).not.toBe(''); + const previous = seen.get(entry.docsPath); + expect( + previous, + `${entry.id} shares ${entry.docsPath} with ${previous}` + ).toBeUndefined(); + seen.set(entry.docsPath, entry.id); + } expect( - resolveDocsWorkspace(docsPath, 'Specs', reversedManifest) + resolveDocsWorkspace( + '/docs/langgraph/guides/durable-execution', + 'Durable Execution' + ) ).toMatchObject({ kind: 'mapped', identity: { - id: 'render:core-capabilities:spec-rendering:overview:python', + id: 'langgraph:core-capabilities:durable-execution:overview:python', }, }); + expect( + getWorkspaceDestinationPath({ + id: 'langgraph:core-capabilities:durable-execution:overview:python', + docsPath: '/docs/langgraph/guides/durable-execution', + workspacePath: '/workspace/langgraph/durable-execution', + }) + ).toBe('/docs/langgraph/guides/durable-execution'); }); it('returns a discriminated docs-only resolution for an unmapped valid Docs page', () => { @@ -139,9 +152,9 @@ describe('canonical Website workspace destinations', () => { if (!resolution) continue; const expectedDefault = getRouteDefaultMode(resolution, 'workspace'); - const unavailableMode = ( - ['Run', 'Code', 'API', 'Docs'] as const - ).find((mode) => !entry.availableModes.includes(mode)); + const unavailableMode = (['Run', 'Code', 'API', 'Docs'] as const).find( + (mode) => !entry.availableModes.includes(mode) + ); expect(resolveLegacyRequestMode(undefined, resolution)).toBe( expectedDefault @@ -196,7 +209,7 @@ describe('canonical Website workspace destinations', () => { } }); - it('omits Docs mode on a canonical Docs path and includes it on a secondary workspace path', () => { + it('omits Docs mode on every canonical Docs path', () => { const primary = resolveLegacyPath( '/langgraph/core-capabilities/persistence/overview/python' ); @@ -211,17 +224,14 @@ describe('canonical Website workspace destinations', () => { '/docs/langgraph/guides/persistence' ); expect(getCanonicalWebsiteWorkspaceHref(secondary, 'Docs')).toBe( - '/workspace/langgraph/durable-execution?mode=docs' + '/docs/langgraph/guides/durable-execution' ); - expect(getCanonicalWebsiteWorkspaceHref(primary, 'Run')).toBe( - '/docs/langgraph/guides/persistence?mode=run' + expect(getCanonicalWebsiteWorkspaceHref(secondary, 'Run')).toBe( + '/docs/langgraph/guides/durable-execution?mode=run' ); expect(getCanonicalWebsiteWorkspaceHref(primary, 'Code')).toBe( '/docs/langgraph/guides/persistence?mode=code' ); - expect(getCanonicalWebsiteWorkspaceHref(primary, 'API')).toBe( - '/docs/langgraph/guides/persistence?mode=api' - ); }); it('serializes docs-only resolutions without accepting source query data', () => { diff --git a/libs/cockpit-registry/src/lib/workspace-resolution.ts b/libs/cockpit-registry/src/lib/workspace-resolution.ts index 46c54bced..cabee2ad0 100644 --- a/libs/cockpit-registry/src/lib/workspace-resolution.ts +++ b/libs/cockpit-registry/src/lib/workspace-resolution.ts @@ -6,26 +6,6 @@ import type { WorkspaceResolution, } from './manifest.types'; -/** - * Published Docs paths shared by more than one capability need an explicit - * reverse mapping. This table is deliberately independent of manifest order. - */ -export const PRIMARY_CAPABILITY_BY_DOCS_PATH: Readonly> = - { - '/docs/langgraph/guides/persistence': - 'langgraph:core-capabilities:persistence:overview:python', - '/docs/chat/guides/client-tools': - 'langgraph:core-capabilities:client-tools:overview:python', - '/docs/chat/components/chat-tool-calls': - 'chat:core-capabilities:tool-calls:overview:python', - '/docs/render/getting-started/introduction': - 'render:getting-started:overview:overview:python', - '/docs/chat/components/chat-subagent-card': - 'chat:core-capabilities:subagents:overview:python', - '/docs/render/guides/specs': - 'render:core-capabilities:spec-rendering:overview:python', - }; - export const toWorkspaceIdentity = ( entry: CockpitManifestEntry ): WorkspaceIdentity => ({ @@ -46,11 +26,10 @@ export const toWorkspaceIdentity = ( export const getWorkspaceDestinationPath = ( identity: Pick ): string => { - if (!identity.docsPath) return identity.workspacePath; - const primaryId = PRIMARY_CAPABILITY_BY_DOCS_PATH[identity.docsPath]; - return primaryId && primaryId !== identity.id - ? identity.workspacePath - : identity.docsPath; + if (!identity.docsPath) { + throw new Error(`Manifest entry without a docs path: ${identity.id}`); + } + return identity.docsPath; }; const mapped = (entry: CockpitManifestEntry): WorkspaceResolution => ({ @@ -64,14 +43,12 @@ export const resolveDocsWorkspace = ( manifest: readonly CockpitManifestEntry[] = cockpitManifest ): WorkspaceResolution => { const matches = manifest.filter((entry) => entry.docsPath === docsPath); - const primaryId = PRIMARY_CAPABILITY_BY_DOCS_PATH[docsPath]; - const primary = primaryId - ? matches.find((entry) => entry.id === primaryId) - : matches.length === 1 - ? matches[0] - : undefined; - - if (primary) return mapped(primary); + if (matches.length === 1) return mapped(matches[0]); + if (matches.length > 1) { + throw new Error( + `Docs path ${docsPath} is published by ${matches.length} manifest entries` + ); + } return { kind: 'docs-only', From dfc8f2cf1d75aa61aa77081185f4ae52c14bc577 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 09:32:50 -0700 Subject: [PATCH 04/12] test(cockpit-shell): expect the moved capabilities' own docs paths The durable-execution capability fixture in workspace-presentation.spec.ts still pinned the old shared docs path (/docs/langgraph/guides/persistence) that the registry no longer publishes for it. Update the expectation to /docs/langgraph/guides/durable-execution, its own path. No other cockpit-shell/workspace-react fixture pins one of the other five moved capabilities' old shared paths (the one remaining hit on /docs/render/guides/specs belongs to spec-rendering, the primary sibling, and is unaffected). Co-Authored-By: Claude Fable 5.1 --- libs/cockpit-shell/src/lib/workspace-presentation.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/cockpit-shell/src/lib/workspace-presentation.spec.ts b/libs/cockpit-shell/src/lib/workspace-presentation.spec.ts index 6ef3cf167..15185c4c1 100644 --- a/libs/cockpit-shell/src/lib/workspace-presentation.spec.ts +++ b/libs/cockpit-shell/src/lib/workspace-presentation.spec.ts @@ -257,7 +257,7 @@ describe('getCapabilityPresentation', () => { expect(presentation).toMatchObject({ kind: 'capability', - docsPath: '/docs/langgraph/guides/persistence', + docsPath: '/docs/langgraph/guides/durable-execution', docsAssetPaths: [ 'cockpit/langgraph/durable-execution/python/docs/guide.md', ], From 6a72864b176cd80b31fb86e2e99871ae40a0667a Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 09:36:12 -0700 Subject: [PATCH 05/12] fix(examples): mirror the moved capabilities' docs paths in the Angular descriptors The Python siblings for the six capabilities that got their own docs path (durable-execution, ag-ui tool-views/json-render/client-tools/ subagents, render repeat-loops) were updated, but each Angular mirror in cockpit/*/*/angular/src/index.ts still carried the old shared path. The Website drift guard (apps/website/src/lib/cockpit-docs-links.spec.ts) scans every cockpit/**/src/index.ts, so the Angular copies needed the same update. Co-Authored-By: Claude Fable 5.1 --- cockpit/ag-ui/client-tools/angular/src/index.ts | 2 +- cockpit/ag-ui/json-render/angular/src/index.ts | 2 +- cockpit/ag-ui/subagents/angular/src/index.ts | 2 +- cockpit/ag-ui/tool-views/angular/src/index.ts | 2 +- cockpit/langgraph/durable-execution/angular/src/index.ts | 2 +- cockpit/render/repeat-loops/angular/src/index.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cockpit/ag-ui/client-tools/angular/src/index.ts b/cockpit/ag-ui/client-tools/angular/src/index.ts index 7bd3dffc0..05b3fa0f3 100644 --- a/cockpit/ag-ui/client-tools/angular/src/index.ts +++ b/cockpit/ag-ui/client-tools/angular/src/index.ts @@ -12,7 +12,7 @@ export const agUiClientToolsAngularModule: CockpitCapabilityModule = { id: 'ag-ui-client-tools-angular', manifestIdentity: { product: 'ag-ui', section: 'core-capabilities', topic: 'client-tools', page: 'overview', language: 'angular' }, title: 'AG-UI Client Tools (Angular)', - docsPath: '/docs/chat/guides/client-tools', + docsPath: '/docs/ag-ui/guides/client-tools', promptAssetPaths: ['cockpit/ag-ui/client-tools/angular/prompts/client-tools.md'], codeAssetPaths: [ 'cockpit/ag-ui/client-tools/angular/src/app/client-tools.component.ts', diff --git a/cockpit/ag-ui/json-render/angular/src/index.ts b/cockpit/ag-ui/json-render/angular/src/index.ts index 8eba3da09..b834fb3f9 100644 --- a/cockpit/ag-ui/json-render/angular/src/index.ts +++ b/cockpit/ag-ui/json-render/angular/src/index.ts @@ -24,7 +24,7 @@ export const agUiJsonRenderAngularModule: CockpitCapabilityModule = { language: 'angular', }, title: 'AG-UI JSON Render (Angular)', - docsPath: '/docs/render/getting-started/introduction', + docsPath: '/docs/ag-ui/guides/json-render', promptAssetPaths: ['cockpit/ag-ui/json-render/python/prompts/json-render.md'], codeAssetPaths: [ 'cockpit/ag-ui/json-render/angular/src/app/json-render.component.ts', diff --git a/cockpit/ag-ui/subagents/angular/src/index.ts b/cockpit/ag-ui/subagents/angular/src/index.ts index 710ece36a..8cca72f97 100644 --- a/cockpit/ag-ui/subagents/angular/src/index.ts +++ b/cockpit/ag-ui/subagents/angular/src/index.ts @@ -24,7 +24,7 @@ export const agUiSubagentsAngularModule: CockpitCapabilityModule = { language: 'angular', }, title: 'AG-UI Subagents (Angular)', - docsPath: '/docs/chat/components/chat-subagent-card', + docsPath: '/docs/ag-ui/guides/subagents', promptAssetPaths: [ 'cockpit/ag-ui/subagents/angular/prompts/subagents.md', ], diff --git a/cockpit/ag-ui/tool-views/angular/src/index.ts b/cockpit/ag-ui/tool-views/angular/src/index.ts index 191a052af..2f21b6070 100644 --- a/cockpit/ag-ui/tool-views/angular/src/index.ts +++ b/cockpit/ag-ui/tool-views/angular/src/index.ts @@ -24,7 +24,7 @@ export const agUiToolViewsAngularModule: CockpitCapabilityModule = { language: 'angular', }, title: 'AG-UI Tool Views (Angular)', - docsPath: '/docs/chat/components/chat-tool-calls', + docsPath: '/docs/ag-ui/guides/tool-views', promptAssetPaths: ['cockpit/ag-ui/tool-views/angular/prompts/tool-views.md'], codeAssetPaths: [ 'cockpit/ag-ui/tool-views/angular/src/app/tool-views.component.ts', diff --git a/cockpit/langgraph/durable-execution/angular/src/index.ts b/cockpit/langgraph/durable-execution/angular/src/index.ts index 01741978f..2113085ee 100644 --- a/cockpit/langgraph/durable-execution/angular/src/index.ts +++ b/cockpit/langgraph/durable-execution/angular/src/index.ts @@ -23,7 +23,7 @@ export const langgraphDurableExecutionAngularModule: CockpitCapabilityModule = { language: 'angular', }, title: 'LangGraph Durable Execution (Angular)', - docsPath: '/docs/langgraph/guides/persistence', + docsPath: '/docs/langgraph/guides/durable-execution', promptAssetPaths: [ 'cockpit/langgraph/durable-execution/angular/prompts/durable-execution.md', ], diff --git a/cockpit/render/repeat-loops/angular/src/index.ts b/cockpit/render/repeat-loops/angular/src/index.ts index 4953500a0..e85395949 100644 --- a/cockpit/render/repeat-loops/angular/src/index.ts +++ b/cockpit/render/repeat-loops/angular/src/index.ts @@ -23,7 +23,7 @@ export const renderRepeatLoopsAngularModule: CockpitCapabilityModule = { language: 'angular', }, title: 'Render Repeat Loops (Angular)', - docsPath: '/docs/render/guides/specs', + docsPath: '/docs/render/guides/repeat-loops', promptAssetPaths: ['cockpit/render/repeat-loops/angular/prompts/repeat-loops.md'], codeAssetPaths: ['cockpit/render/repeat-loops/angular/src/app/repeat-loops.component.ts'], }; From 4408e724334adfc49abca98616a205210d926bbd Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 09:38:39 -0700 Subject: [PATCH 06/12] chore(deployments): regenerate ag-ui-dev artifacts after the docs path move deployments/ag-ui-dev/deps/** is a generated mirror of the cockpit/ag-ui/*/python sources. The docsPath moves for tool-views, json-render, client-tools, and subagents (plus the earlier prettier reflow of two of those sources) left the generated copies stale, which .github/workflows/deploy-ag-ui.yml would have caught post-merge via `git diff --exit-code -- deployments/ag-ui-dev/`. Regenerated with scripts/generate-ag-ui-deployment-config.ts. Co-Authored-By: Claude Fable 5.1 --- .../ag-ui-dev/deps/client_tools/src/index.ts | 27 +++++++++++++++---- .../ag-ui-dev/deps/json_render/src/index.ts | 2 +- .../ag-ui-dev/deps/subagents/src/index.ts | 7 +++-- .../ag-ui-dev/deps/tool_views/src/index.ts | 2 +- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/deployments/ag-ui-dev/deps/client_tools/src/index.ts b/deployments/ag-ui-dev/deps/client_tools/src/index.ts index eb1c8161c..3188f408d 100644 --- a/deployments/ag-ui-dev/deps/client_tools/src/index.ts +++ b/deployments/ag-ui-dev/deps/client_tools/src/index.ts @@ -1,6 +1,12 @@ export interface CockpitCapabilityModule { id: string; - manifestIdentity: { product: 'ag-ui'; section: 'core-capabilities'; topic: 'client-tools'; page: 'overview'; language: 'python'; }; + manifestIdentity: { + product: 'ag-ui'; + section: 'core-capabilities'; + topic: 'client-tools'; + page: 'overview'; + language: 'python'; + }; title: string; docsPath: string; promptAssetPaths: string[]; @@ -13,17 +19,28 @@ export interface CockpitCapabilityModule { export const agUiClientToolsPythonModule: CockpitCapabilityModule = { id: 'ag-ui-client-tools-python', - manifestIdentity: { product: 'ag-ui', section: 'core-capabilities', topic: 'client-tools', page: 'overview', language: 'python' }, + manifestIdentity: { + product: 'ag-ui', + section: 'core-capabilities', + topic: 'client-tools', + page: 'overview', + language: 'python', + }, title: 'AG-UI Client Tools (Python)', - docsPath: '/docs/chat/guides/client-tools', - promptAssetPaths: ['cockpit/ag-ui/client-tools/python/prompts/client-tools.md'], + docsPath: '/docs/ag-ui/guides/client-tools', + promptAssetPaths: [ + 'cockpit/ag-ui/client-tools/python/prompts/client-tools.md', + ], codeAssetPaths: [ 'cockpit/ag-ui/client-tools/angular/src/app/client-tools.component.ts', 'cockpit/ag-ui/client-tools/angular/src/app/weather-card.component.ts', 'cockpit/ag-ui/client-tools/angular/src/app/confirm-booking.component.ts', 'cockpit/ag-ui/client-tools/angular/src/app/app.config.ts', ], - backendAssetPaths: ['cockpit/ag-ui/client-tools/python/src/graph.py', 'cockpit/ag-ui/client-tools/python/src/server.py'], + backendAssetPaths: [ + 'cockpit/ag-ui/client-tools/python/src/graph.py', + 'cockpit/ag-ui/client-tools/python/src/server.py', + ], docsAssetPaths: ['cockpit/ag-ui/client-tools/python/docs/guide.md'], runtimeUrl: 'ag-ui/client-tools', devPort: 4325, diff --git a/deployments/ag-ui-dev/deps/json_render/src/index.ts b/deployments/ag-ui-dev/deps/json_render/src/index.ts index fcc9a1093..65c8a377b 100644 --- a/deployments/ag-ui-dev/deps/json_render/src/index.ts +++ b/deployments/ag-ui-dev/deps/json_render/src/index.ts @@ -27,7 +27,7 @@ export const agUiJsonRenderPythonModule: CockpitCapabilityModule = { language: 'python', }, title: 'AG-UI JSON Render (Python)', - docsPath: '/docs/render/getting-started/introduction', + docsPath: '/docs/ag-ui/guides/json-render', promptAssetPaths: ['cockpit/ag-ui/json-render/python/prompts/json-render.md'], codeAssetPaths: [ 'cockpit/ag-ui/json-render/angular/src/app/json-render.component.ts', diff --git a/deployments/ag-ui-dev/deps/subagents/src/index.ts b/deployments/ag-ui-dev/deps/subagents/src/index.ts index f27777000..d68be0aa3 100644 --- a/deployments/ag-ui-dev/deps/subagents/src/index.ts +++ b/deployments/ag-ui-dev/deps/subagents/src/index.ts @@ -27,13 +27,16 @@ export const agUiSubagentsPythonModule: CockpitCapabilityModule = { language: 'python', }, title: 'AG-UI Subagents (Python)', - docsPath: '/docs/chat/components/chat-subagent-card', + docsPath: '/docs/ag-ui/guides/subagents', promptAssetPaths: ['cockpit/ag-ui/subagents/python/prompts/subagents.md'], codeAssetPaths: [ 'cockpit/ag-ui/subagents/angular/src/app/subagents.component.ts', 'cockpit/ag-ui/subagents/angular/src/app/app.config.ts', ], - backendAssetPaths: ['cockpit/ag-ui/subagents/python/src/graph.py', 'cockpit/ag-ui/subagents/python/src/server.py'], + backendAssetPaths: [ + 'cockpit/ag-ui/subagents/python/src/graph.py', + 'cockpit/ag-ui/subagents/python/src/server.py', + ], docsAssetPaths: ['cockpit/ag-ui/subagents/python/docs/guide.md'], runtimeUrl: 'ag-ui/subagents', devPort: 4326, diff --git a/deployments/ag-ui-dev/deps/tool_views/src/index.ts b/deployments/ag-ui-dev/deps/tool_views/src/index.ts index db8fccd4a..33b1576ba 100644 --- a/deployments/ag-ui-dev/deps/tool_views/src/index.ts +++ b/deployments/ag-ui-dev/deps/tool_views/src/index.ts @@ -27,7 +27,7 @@ export const agUiToolViewsPythonModule: CockpitCapabilityModule = { language: 'python', }, title: 'AG-UI Tool Views (Python)', - docsPath: '/docs/chat/components/chat-tool-calls', + docsPath: '/docs/ag-ui/guides/tool-views', promptAssetPaths: ['cockpit/ag-ui/tool-views/python/prompts/tool-views.md'], codeAssetPaths: [ 'cockpit/ag-ui/tool-views/angular/src/app/tool-views.component.ts', From 1e4a632549a75f6260e425d4d4327f9f3ef85132 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 09:45:13 -0700 Subject: [PATCH 07/12] docs(plans): A3 also retires the workspace-only cases in the deploy smoke and wires cockpit-shell into CI Co-Authored-By: Claude Fable 5.1 --- .../plans/2026-09-04-docs-workspace-unification.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md b/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md index d4312402a..4087188bf 100644 --- a/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md +++ b/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md @@ -491,10 +491,17 @@ In `apps/website/e2e/workspace-shell.spec.ts`: change line 7 to `const durableEx In `apps/website/e2e/platform-production-smoke.spec.ts`: delete the `workspaceOnly` lookup (lines 143–145), change the throw to `if (!docsBacked) throw new Error('Production smoke requires a Docs-backed route');`, and delete the `'workspace-only production redirect'` case (lines 165–170). Leave the rest; Part C removes the whole redirect block. +In `apps/cockpit/scripts/deploy-smoke.ts` (still deployed until Part C): `buildPreviewCases` (around line 239) and `buildProductionCases` (around line 288) each look up a workspace-only entry with `getWorkspaceDestinationPath(entry).startsWith('/workspace/')` and throw when none exists. Delete those lookups and the cases built from them (`'workspace Docs serialization'` in preview mode; `'workspace-only production redirect'` in production mode), and change the production guard to `if (!docsBacked) throw new Error('Redirect smoke requires a Docs-backed route');`. Update `apps/cockpit/scripts/deploy-smoke.spec.ts` so no assertion expects those two cases (the `'workspace Docs serialization'` `expect(...some(...))` and any `workspace-only` label check), then run `cd apps/cockpit && npx vitest run deploy-smoke` → all passing, and `npx tsx apps/cockpit/scripts/deploy-smoke.ts --url https://cockpit.threadplane.ai --mode preview --dry-run` → prints a `dry-run:preview:…` line. + +Also in this task: +- `libs/cockpit-registry/src/lib/manifest.types.ts`: change `WorkspaceIdentity.docsPath` from `string | null` to `string`, and in `workspace-resolution.ts` `toWorkspaceIdentity` use `docsPath: entry.docsPath` (drop `|| null`). Fix any type errors this surfaces (they will be sites that handled `null`; delete the null branch). +- `.github/workflows/ci.yml`, job `cockpit`: add `cockpit-shell` to the `run-many` project list (`--projects=cockpit,cockpit-docs,cockpit-registry,cockpit-shell,workspace-react`) so the fixture updated in A1 is actually verified by CI; update the guard test `'runs the cockpit sibling libraries that own vitest specs'` in `scripts/ci-workflow.spec.mjs` to expect it. + - [ ] **Step 6: Verify** Run: `npx nx test website --skip-nx-cache 2>&1 | tail -3` → success. -Run: `npx nx run-many -t test --projects=cockpit-registry,cockpit-shell,workspace-react --skip-nx-cache 2>&1 | tail -3` → success. +Run: `npx nx run-many -t test --projects=cockpit,cockpit-registry,cockpit-shell,workspace-react --skip-nx-cache 2>&1 | tail -3` → success (`cockpit` covers the deploy smoke). +Run: `node --test --test-reporter=tap scripts/ci-workflow.spec.mjs 2>&1 | grep -E "^not ok|^# (pass|fail)"` → `# fail 0`. Run: `npx nx lint website --skip-nx-cache 2>&1 | grep problems` → `0 errors`. Run: `npx nx build website --skip-nx-cache 2>&1 | tail -3` → success (static params for the six pages generate). Run: `npx nx e2e website --skip-nx-cache 2>&1 | grep -E "passed|failed"` → all passed. From 2124294632385a957e226a1e46bd4ea4c54d60fd Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 09:53:39 -0700 Subject: [PATCH 08/12] docs: give the six workspace-only capabilities their own guides Co-Authored-By: Claude Fable 5.1 --- .../docs/ag-ui/guides/client-tools.mdx | 5 + .../content/docs/ag-ui/guides/json-render.mdx | 5 + .../content/docs/ag-ui/guides/subagents.mdx | 5 + .../content/docs/ag-ui/guides/tool-views.mdx | 5 + .../langgraph/guides/durable-execution.mdx | 5 + .../docs/render/guides/repeat-loops.mdx | 5 + apps/website/src/lib/docs-config.ts | 376 +++++++++++++++--- apps/website/src/lib/docs.spec.ts | 222 +++++++++-- 8 files changed, 525 insertions(+), 103 deletions(-) create mode 100644 apps/website/content/docs/ag-ui/guides/client-tools.mdx create mode 100644 apps/website/content/docs/ag-ui/guides/json-render.mdx create mode 100644 apps/website/content/docs/ag-ui/guides/subagents.mdx create mode 100644 apps/website/content/docs/ag-ui/guides/tool-views.mdx create mode 100644 apps/website/content/docs/langgraph/guides/durable-execution.mdx create mode 100644 apps/website/content/docs/render/guides/repeat-loops.mdx diff --git a/apps/website/content/docs/ag-ui/guides/client-tools.mdx b/apps/website/content/docs/ag-ui/guides/client-tools.mdx new file mode 100644 index 000000000..13a9fb681 --- /dev/null +++ b/apps/website/content/docs/ag-ui/guides/client-tools.mdx @@ -0,0 +1,5 @@ +# Client Tools + +An AG-UI agent can call tools that live in the browser. The Angular app declares the tool, the adapter forwards the agent's call, and the result flows back into the run without a server round trip. + +This page is the live example for client tools over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The browser-side tool contract is documented in the [Chat client tools guide](/docs/chat/guides/client-tools). diff --git a/apps/website/content/docs/ag-ui/guides/json-render.mdx b/apps/website/content/docs/ag-ui/guides/json-render.mdx new file mode 100644 index 000000000..9fbef0e61 --- /dev/null +++ b/apps/website/content/docs/ag-ui/guides/json-render.mdx @@ -0,0 +1,5 @@ +# JSON Render + +An AG-UI agent can stream a declarative UI specification that `@threadplane/render` turns into a live Angular component tree, so the agent shapes the interface rather than only its text. + +This page is the live example for JSON rendering over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The rendering engine is introduced in the [Render introduction](/docs/render/getting-started/introduction). diff --git a/apps/website/content/docs/ag-ui/guides/subagents.mdx b/apps/website/content/docs/ag-ui/guides/subagents.mdx new file mode 100644 index 000000000..c008fa70a --- /dev/null +++ b/apps/website/content/docs/ag-ui/guides/subagents.mdx @@ -0,0 +1,5 @@ +# Subagents + +AG-UI carries subagent activity as first-class events, so a delegating agent's child runs show up as attributed cards with their own tool calls and messages instead of being folded into the parent transcript. + +This page is the live example for subagents over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The card component is documented in [ChatSubagentCard](/docs/chat/components/chat-subagent-card). diff --git a/apps/website/content/docs/ag-ui/guides/tool-views.mdx b/apps/website/content/docs/ag-ui/guides/tool-views.mdx new file mode 100644 index 000000000..3758f4190 --- /dev/null +++ b/apps/website/content/docs/ag-ui/guides/tool-views.mdx @@ -0,0 +1,5 @@ +# Tool Views + +Tool views render an AG-UI tool call as a purpose-built component instead of a generic card: a map for a location lookup, a table for a query, a form for a confirmation. + +This page is the live example for tool views. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The tool-call component surface is documented in [ChatToolCalls](/docs/chat/components/chat-tool-calls). diff --git a/apps/website/content/docs/langgraph/guides/durable-execution.mdx b/apps/website/content/docs/langgraph/guides/durable-execution.mdx new file mode 100644 index 000000000..957caebae --- /dev/null +++ b/apps/website/content/docs/langgraph/guides/durable-execution.mdx @@ -0,0 +1,5 @@ +# Durable Execution + +Durable execution keeps a LangGraph run alive across process restarts and long waits. The checkpointer records every super-step, so a run that is interrupted, redeployed, or paused for a human can resume from its last checkpoint instead of starting over. + +This page is the live example for durable execution. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The checkpointer configuration the example relies on is written up in the [Persistence guide](/docs/langgraph/guides/persistence). diff --git a/apps/website/content/docs/render/guides/repeat-loops.mdx b/apps/website/content/docs/render/guides/repeat-loops.mdx new file mode 100644 index 000000000..6169c6705 --- /dev/null +++ b/apps/website/content/docs/render/guides/repeat-loops.mdx @@ -0,0 +1,5 @@ +# Repeat Loops + +A repeat loop renders one element per item of a bound collection, with `$item` and `$index` available to child expressions, so a spec can describe a list without enumerating its rows. + +This page is the live example for repeat loops. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The spec format, including the repeat element, is documented in [Specs & Elements](/docs/render/guides/specs). diff --git a/apps/website/src/lib/docs-config.ts b/apps/website/src/lib/docs-config.ts index 2286eeb16..0bdca0ac0 100644 --- a/apps/website/src/lib/docs-config.ts +++ b/apps/website/src/lib/docs-config.ts @@ -67,7 +67,8 @@ export const specialDocsPages: SpecialDocsPage[] = [ path: '/docs/choosing-an-adapter', contentPath: 'choosing-an-adapter/index.mdx', title: 'Choosing an adapter', - description: 'Decide between @threadplane/langgraph and @threadplane/ag-ui for your Angular agent UI.', + description: + 'Decide between @threadplane/langgraph and @threadplane/ag-ui for your Angular agent UI.', }, ]; @@ -84,9 +85,21 @@ export const docsConfig: DocsLibrary[] = [ id: 'getting-started', color: 'blue', pages: [ - { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, - { title: 'Quick Start', slug: 'quickstart', section: 'getting-started' }, - { title: 'Installation', slug: 'installation', section: 'getting-started' }, + { + title: 'Introduction', + slug: 'introduction', + section: 'getting-started', + }, + { + title: 'Quick Start', + slug: 'quickstart', + section: 'getting-started', + }, + { + title: 'Installation', + slug: 'installation', + section: 'getting-started', + }, ], }, { @@ -96,6 +109,11 @@ export const docsConfig: DocsLibrary[] = [ pages: [ { title: 'Streaming', slug: 'streaming', section: 'guides' }, { title: 'Persistence', slug: 'persistence', section: 'guides' }, + { + title: 'Durable Execution', + slug: 'durable-execution', + section: 'guides', + }, { title: 'Interrupts', slug: 'interrupts', section: 'guides' }, { title: 'Memory', slug: 'memory', section: 'guides' }, { title: 'Time Travel', slug: 'time-travel', section: 'guides' }, @@ -110,11 +128,31 @@ export const docsConfig: DocsLibrary[] = [ id: 'concepts', color: 'red', pages: [ - { title: 'Agent Contract', slug: 'agent-contract', section: 'concepts' }, - { title: 'Angular Signals', slug: 'angular-signals', section: 'concepts' }, - { title: 'LangGraph Basics', slug: 'langgraph-basics', section: 'concepts' }, - { title: 'Agent Architecture', slug: 'agent-architecture', section: 'concepts' }, - { title: 'State Management', slug: 'state-management', section: 'concepts' }, + { + title: 'Agent Contract', + slug: 'agent-contract', + section: 'concepts', + }, + { + title: 'Angular Signals', + slug: 'angular-signals', + section: 'concepts', + }, + { + title: 'LangGraph Basics', + slug: 'langgraph-basics', + section: 'concepts', + }, + { + title: 'Agent Architecture', + slug: 'agent-architecture', + section: 'concepts', + }, + { + title: 'State Management', + slug: 'state-management', + section: 'concepts', + }, ], }, { @@ -124,9 +162,21 @@ export const docsConfig: DocsLibrary[] = [ pages: [ { title: 'injectAgent()', slug: 'inject-agent', section: 'api' }, { title: 'provideAgent()', slug: 'provide-agent', section: 'api' }, - { title: 'FetchStreamTransport', slug: 'fetch-stream-transport', section: 'api' }, - { title: 'MockAgentTransport', slug: 'mock-stream-transport', section: 'api' }, - { title: 'LangGraphThreadsAdapter', slug: 'langgraph-threads-adapter', section: 'api' }, + { + title: 'FetchStreamTransport', + slug: 'fetch-stream-transport', + section: 'api', + }, + { + title: 'MockAgentTransport', + slug: 'mock-stream-transport', + section: 'api', + }, + { + title: 'LangGraphThreadsAdapter', + slug: 'langgraph-threads-adapter', + section: 'api', + }, ], }, ], @@ -144,9 +194,21 @@ export const docsConfig: DocsLibrary[] = [ id: 'getting-started', color: 'blue', pages: [ - { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, - { title: 'Quick Start', slug: 'quickstart', section: 'getting-started' }, - { title: 'Installation', slug: 'installation', section: 'getting-started' }, + { + title: 'Introduction', + slug: 'introduction', + section: 'getting-started', + }, + { + title: 'Quick Start', + slug: 'quickstart', + section: 'getting-started', + }, + { + title: 'Installation', + slug: 'installation', + section: 'getting-started', + }, ], }, { @@ -157,6 +219,7 @@ export const docsConfig: DocsLibrary[] = [ { title: 'Component Registry', slug: 'registry', section: 'guides' }, { title: 'State Store', slug: 'state-store', section: 'guides' }, { title: 'Specs & Elements', slug: 'specs', section: 'guides' }, + { title: 'Repeat Loops', slug: 'repeat-loops', section: 'guides' }, { title: 'Events & Handlers', slug: 'events', section: 'guides' }, { title: 'Lifecycle Signals', slug: 'lifecycle', section: 'guides' }, ], @@ -166,7 +229,11 @@ export const docsConfig: DocsLibrary[] = [ id: 'concepts', color: 'red', pages: [ - { title: 'JSON Render vs A2UI', slug: 'json-render-vs-a2ui', section: 'concepts' }, + { + title: 'JSON Render vs A2UI', + slug: 'json-render-vs-a2ui', + section: 'concepts', + }, ], }, { @@ -174,10 +241,22 @@ export const docsConfig: DocsLibrary[] = [ id: 'api', color: 'blue', pages: [ - { title: 'RenderSpecComponent', slug: 'render-spec-component', section: 'api' }, - { title: 'defineAngularRegistry()', slug: 'define-angular-registry', section: 'api' }, + { + title: 'RenderSpecComponent', + slug: 'render-spec-component', + section: 'api', + }, + { + title: 'defineAngularRegistry()', + slug: 'define-angular-registry', + section: 'api', + }, { title: 'views()', slug: 'views', section: 'api' }, - { title: 'signalStateStore()', slug: 'signal-state-store', section: 'api' }, + { + title: 'signalStateStore()', + slug: 'signal-state-store', + section: 'api', + }, { title: 'provideRender()', slug: 'provide-render', section: 'api' }, ], }, @@ -211,12 +290,28 @@ export const docsConfig: DocsLibrary[] = [ { title: 'Markdown Rendering', slug: 'markdown', section: 'guides' }, { title: 'Generative UI', slug: 'generative-ui', section: 'guides' }, { title: 'Client Tools', slug: 'client-tools', section: 'guides' }, - { title: 'Custom A2UI Catalogs', slug: 'custom-catalogs', section: 'guides' }, + { + title: 'Custom A2UI Catalogs', + slug: 'custom-catalogs', + section: 'guides', + }, { title: 'Streaming', slug: 'streaming', section: 'guides' }, { title: 'Configuration', slug: 'configuration', section: 'guides' }, - { title: 'Error Handling', slug: 'error-handling', section: 'guides' }, - { title: 'Writing an Adapter', slug: 'writing-an-adapter', section: 'guides' }, - { title: 'Thread Routing', slug: 'thread-routing', section: 'guides' }, + { + title: 'Error Handling', + slug: 'error-handling', + section: 'guides', + }, + { + title: 'Writing an Adapter', + slug: 'writing-an-adapter', + section: 'guides', + }, + { + title: 'Thread Routing', + slug: 'thread-routing', + section: 'guides', + }, { title: 'Lifecycle Signals', slug: 'lifecycle', section: 'guides' }, ], }, @@ -225,8 +320,16 @@ export const docsConfig: DocsLibrary[] = [ id: 'concepts', color: 'red', pages: [ - { title: 'Primitives vs Compositions', slug: 'primitives-vs-compositions', section: 'concepts' }, - { title: 'Message Model', slug: 'message-model', section: 'concepts' }, + { + title: 'Primitives vs Compositions', + slug: 'primitives-vs-compositions', + section: 'concepts', + }, + { + title: 'Message Model', + slug: 'message-model', + section: 'concepts', + }, ], }, { @@ -238,15 +341,43 @@ export const docsConfig: DocsLibrary[] = [ { title: 'ChatPopup', slug: 'chat-popup', section: 'components' }, { title: 'ChatSidebar', slug: 'chat-sidebar', section: 'components' }, { title: 'ChatSidenav', slug: 'chat-sidenav', section: 'components' }, - { title: 'ChatMessageList', slug: 'chat-message-list', section: 'components' }, + { + title: 'ChatMessageList', + slug: 'chat-message-list', + section: 'components', + }, { title: 'ChatTrace', slug: 'chat-trace', section: 'components' }, { title: 'ChatInput', slug: 'chat-input', section: 'components' }, - { title: 'ChatReasoning', slug: 'chat-reasoning', section: 'components' }, - { title: 'ChatInterruptPanel', slug: 'chat-interrupt-panel', section: 'components' }, - { title: 'ChatToolCalls', slug: 'chat-tool-calls', section: 'components' }, - { title: 'chatToolCallTemplate', slug: 'chat-tool-call-template', section: 'components' }, - { title: 'ChatToolCallCard', slug: 'chat-tool-call-card', section: 'components' }, - { title: 'ChatSubagentCard', slug: 'chat-subagent-card', section: 'components' }, + { + title: 'ChatReasoning', + slug: 'chat-reasoning', + section: 'components', + }, + { + title: 'ChatInterruptPanel', + slug: 'chat-interrupt-panel', + section: 'components', + }, + { + title: 'ChatToolCalls', + slug: 'chat-tool-calls', + section: 'components', + }, + { + title: 'chatToolCallTemplate', + slug: 'chat-tool-call-template', + section: 'components', + }, + { + title: 'ChatToolCallCard', + slug: 'chat-tool-call-card', + section: 'components', + }, + { + title: 'ChatSubagentCard', + slug: 'chat-subagent-card', + section: 'components', + }, { title: 'ChatDebug', slug: 'chat-debug', section: 'components' }, { title: 'ChatSelect', slug: 'chat-select', section: 'components' }, ], @@ -257,8 +388,16 @@ export const docsConfig: DocsLibrary[] = [ color: 'red', pages: [ { title: 'Overview', slug: 'overview', section: 'a2ui' }, - { title: 'A2uiSurfaceComponent', slug: 'surface-component', section: 'a2ui' }, - { title: 'createA2uiSurfaceStore()', slug: 'surface-store', section: 'a2ui' }, + { + title: 'A2uiSurfaceComponent', + slug: 'surface-component', + section: 'a2ui', + }, + { + title: 'createA2uiSurfaceStore()', + slug: 'surface-store', + section: 'a2ui', + }, { title: 'Component Catalog', slug: 'catalog', section: 'a2ui' }, ], }, @@ -270,8 +409,16 @@ export const docsConfig: DocsLibrary[] = [ { title: 'provideChat()', slug: 'provide-chat', section: 'api' }, { title: 'ChatConfig', slug: 'chat-config', section: 'api' }, { title: 'mockAgent()', slug: 'mock-agent', section: 'api' }, - { title: 'createContentClassifier()', slug: 'content-classifier', section: 'api' }, - { title: 'createParseTreeStore()', slug: 'parse-tree-store', section: 'api' }, + { + title: 'createContentClassifier()', + slug: 'content-classifier', + section: 'api', + }, + { + title: 'createParseTreeStore()', + slug: 'parse-tree-store', + section: 'api', + }, ], }, ], @@ -279,7 +426,8 @@ export const docsConfig: DocsLibrary[] = [ { id: 'ag-ui', title: 'AG-UI', - description: 'Adapter for AG-UI-compatible backends including CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, and AWS Strands', + description: + 'Adapter for AG-UI-compatible backends including CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, and AWS Strands', group: 'adapter', tagline: 'Any AG-UI backend', demoUrl: 'https://ag-ui.threadplane.ai', @@ -289,9 +437,21 @@ export const docsConfig: DocsLibrary[] = [ id: 'getting-started', color: 'blue', pages: [ - { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, - { title: 'Quick Start', slug: 'quickstart', section: 'getting-started' }, - { title: 'Installation', slug: 'installation', section: 'getting-started' }, + { + title: 'Introduction', + slug: 'introduction', + section: 'getting-started', + }, + { + title: 'Quick Start', + slug: 'quickstart', + section: 'getting-started', + }, + { + title: 'Installation', + slug: 'installation', + section: 'getting-started', + }, ], }, { @@ -311,8 +471,16 @@ export const docsConfig: DocsLibrary[] = [ { title: 'Citations', slug: 'citations', section: 'guides' }, { title: 'Custom Events', slug: 'custom-events', section: 'guides' }, { title: 'Interrupts', slug: 'interrupts', section: 'guides' }, + { title: 'Client Tools', slug: 'client-tools', section: 'guides' }, + { title: 'Tool Views', slug: 'tool-views', section: 'guides' }, + { title: 'JSON Render', slug: 'json-render', section: 'guides' }, + { title: 'Subagents', slug: 'subagents', section: 'guides' }, { title: 'Testing', slug: 'testing', section: 'guides' }, - { title: 'Troubleshooting', slug: 'troubleshooting', section: 'guides' }, + { + title: 'Troubleshooting', + slug: 'troubleshooting', + section: 'guides', + }, ], }, { @@ -320,7 +488,11 @@ export const docsConfig: DocsLibrary[] = [ id: 'reference', color: 'blue', pages: [ - { title: 'Event Mapping', slug: 'event-mapping', section: 'reference' }, + { + title: 'Event Mapping', + slug: 'event-mapping', + section: 'reference', + }, ], }, { @@ -347,8 +519,16 @@ export const docsConfig: DocsLibrary[] = [ id: 'getting-started', color: 'blue', pages: [ - { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, - { title: 'Quick Start', slug: 'quickstart', section: 'getting-started' }, + { + title: 'Introduction', + slug: 'introduction', + section: 'getting-started', + }, + { + title: 'Quick Start', + slug: 'quickstart', + section: 'getting-started', + }, ], }, { @@ -356,9 +536,17 @@ export const docsConfig: DocsLibrary[] = [ id: 'guides', color: 'blue', pages: [ - { title: 'Message Protocol', slug: 'message-protocol', section: 'guides' }, + { + title: 'Message Protocol', + slug: 'message-protocol', + section: 'guides', + }, { title: 'Data Model', slug: 'data-model', section: 'guides' }, - { title: 'Validating & Adapting', slug: 'adapters-and-validation', section: 'guides' }, + { + title: 'Validating & Adapting', + slug: 'adapters-and-validation', + section: 'guides', + }, ], }, { @@ -367,7 +555,11 @@ export const docsConfig: DocsLibrary[] = [ color: 'blue', pages: [ { title: 'Schema', slug: 'schema', section: 'reference' }, - { title: 'Parser, Resolver, and Guards', slug: 'parser-resolver-guards', section: 'reference' }, + { + title: 'Parser, Resolver, and Guards', + slug: 'parser-resolver-guards', + section: 'reference', + }, ], }, ], @@ -383,8 +575,16 @@ export const docsConfig: DocsLibrary[] = [ id: 'getting-started', color: 'blue', pages: [ - { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, - { title: 'Quick Start', slug: 'quickstart', section: 'getting-started' }, + { + title: 'Introduction', + slug: 'introduction', + section: 'getting-started', + }, + { + title: 'Quick Start', + slug: 'quickstart', + section: 'getting-started', + }, ], }, { @@ -392,8 +592,16 @@ export const docsConfig: DocsLibrary[] = [ id: 'guides', color: 'blue', pages: [ - { title: 'LangGraph.js Client Tools', slug: 'langgraph-client-tools', section: 'guides' }, - { title: 'Python LangGraph Middleware', slug: 'python-langgraph', section: 'guides' }, + { + title: 'LangGraph.js Client Tools', + slug: 'langgraph-client-tools', + section: 'guides', + }, + { + title: 'Python LangGraph Middleware', + slug: 'python-langgraph', + section: 'guides', + }, ], }, { @@ -424,7 +632,8 @@ export const docsConfig: DocsLibrary[] = [ { id: 'runtimes', title: 'Runtimes', - description: 'Measured AG-UI runtime integrations behind @threadplane/ag-ui', + description: + 'Measured AG-UI runtime integrations behind @threadplane/ag-ui', // Reference material *behind* the AG-UI adapter, not an adapter you pick. group: 'library', sections: [ @@ -433,7 +642,11 @@ export const docsConfig: DocsLibrary[] = [ id: 'getting-started', color: 'blue', pages: [ - { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, + { + title: 'Introduction', + slug: 'introduction', + section: 'getting-started', + }, ], }, { @@ -443,7 +656,11 @@ export const docsConfig: DocsLibrary[] = [ pages: [ { title: 'Overview', slug: 'overview', section: 'aws-strands' }, { title: 'Quickstart', slug: 'quickstart', section: 'aws-strands' }, - { title: 'How It Connects', slug: 'how-it-connects', section: 'aws-strands' }, + { + title: 'How It Connects', + slug: 'how-it-connects', + section: 'aws-strands', + }, ], }, { @@ -451,9 +668,21 @@ export const docsConfig: DocsLibrary[] = [ id: 'microsoft-agent-framework', color: 'red', pages: [ - { title: 'Overview', slug: 'overview', section: 'microsoft-agent-framework' }, - { title: 'Quickstart', slug: 'quickstart', section: 'microsoft-agent-framework' }, - { title: 'How It Connects', slug: 'how-it-connects', section: 'microsoft-agent-framework' }, + { + title: 'Overview', + slug: 'overview', + section: 'microsoft-agent-framework', + }, + { + title: 'Quickstart', + slug: 'quickstart', + section: 'microsoft-agent-framework', + }, + { + title: 'How It Connects', + slug: 'how-it-connects', + section: 'microsoft-agent-framework', + }, ], }, { @@ -463,7 +692,11 @@ export const docsConfig: DocsLibrary[] = [ pages: [ { title: 'Overview', slug: 'overview', section: 'mastra' }, { title: 'Quickstart', slug: 'quickstart', section: 'mastra' }, - { title: 'How It Connects', slug: 'how-it-connects', section: 'mastra' }, + { + title: 'How It Connects', + slug: 'how-it-connects', + section: 'mastra', + }, ], }, ], @@ -482,7 +715,11 @@ export const docsConfig: DocsLibrary[] = [ id: 'getting-started', color: 'blue', pages: [ - { title: 'Introduction', slug: 'introduction', section: 'getting-started' }, + { + title: 'Introduction', + slug: 'introduction', + section: 'getting-started', + }, ], }, { @@ -528,11 +765,21 @@ export const allDocsPages: DocsPage[] = docsConfig.flatMap((l) => l.sections.flatMap((s) => s.pages) ); -export function findDocsPage(library: string, section: string, slug: string): DocsPage | undefined { - return getLibraryPages(library).find((p) => p.section === section && p.slug === slug); +export function findDocsPage( + library: string, + section: string, + slug: string +): DocsPage | undefined { + return getLibraryPages(library).find( + (p) => p.section === section && p.slug === slug + ); } -export function getPrevNextPages(library: string, section: string, slug: string): { prev: DocsPage | null; next: DocsPage | null } { +export function getPrevNextPages( + library: string, + section: string, + slug: string +): { prev: DocsPage | null; next: DocsPage | null } { const pages = getLibraryPages(library); const idx = pages.findIndex((p) => p.section === section && p.slug === slug); return { @@ -541,7 +788,10 @@ export function getPrevNextPages(library: string, section: string, slug: string) }; } -export function getDocsSection(library: string, sectionId: string): DocsSection | undefined { +export function getDocsSection( + library: string, + sectionId: string +): DocsSection | undefined { const lib = getLibraryConfig(library); return lib?.sections.find((s) => s.id === sectionId); } diff --git a/apps/website/src/lib/docs.spec.ts b/apps/website/src/lib/docs.spec.ts index cb84f027f..173fd6b5a 100644 --- a/apps/website/src/lib/docs.spec.ts +++ b/apps/website/src/lib/docs.spec.ts @@ -2,22 +2,45 @@ import { describe, expect, it } from 'vitest'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { getAllDocSlugs, getDocBySlug, getDocMetadata, stripFrontmatter } from './docs'; -import { allDocsPages, docsConfig, findDocsPage, libraryIntroPath, specialDocsPages } from './docs-config'; +import { cockpitManifest } from '@threadplane/cockpit-registry'; +import { + getAllDocSlugs, + getDocBySlug, + getDocMetadata, + stripFrontmatter, +} from './docs'; +import { + allDocsPages, + docsConfig, + findDocsPage, + libraryIntroPath, + specialDocsPages, +} from './docs-config'; import { getCanonicalUrl, getSitemapRoutes } from './site-metadata'; -const internalDocsLinkPattern = /(?:href=["']|\]\()(?\/docs\/[^"')#\s]+)/g; -const mdxLinkPattern = /(?:href=["']|\]\()(?[^"')\s]+\.mdx(?:#[^"')\s]+)?)/g; +const internalDocsLinkPattern = + /(?:href=["']|\]\()(?\/docs\/[^"')#\s]+)/g; +const mdxLinkPattern = + /(?:href=["']|\]\()(?[^"')\s]+\.mdx(?:#[^"')\s]+)?)/g; function findInternalDocsLinks(content: string): string[] { - return Array.from(content.matchAll(internalDocsLinkPattern), (match) => match.groups?.href) + return Array.from( + content.matchAll(internalDocsLinkPattern), + (match) => match.groups?.href + ) .filter((href): href is string => Boolean(href)) .map((href) => href.split('?')[0]); } // Resolved relative to this spec file so the path stays correct regardless of // the runner's cwd (apps/website/ vs workspace root). -const contentRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'content', 'docs'); +const contentRoot = path.join( + path.dirname(fileURLToPath(import.meta.url)), + '..', + '..', + 'content', + 'docs' +); function walkMdxFiles(dir: string): string[] { return fs.readdirSync(dir).flatMap((entry) => { @@ -28,11 +51,23 @@ function walkMdxFiles(dir: string): string[] { }); } -function getConfiguredDocPath({ library, section, slug }: { library: string; section: string; slug: string }): string { +function getConfiguredDocPath({ + library, + section, + slug, +}: { + library: string; + section: string; + slug: string; +}): string { return path.join(contentRoot, library, section, `${slug}.mdx`); } -function getAllConfiguredDocFiles(): Array<{ id: string; filePath: string; content: string }> { +function getAllConfiguredDocFiles(): Array<{ + id: string; + filePath: string; + content: string; +}> { const configured = getAllDocSlugs().map(({ library, section, slug }) => { const filePath = getConfiguredDocPath({ library, section, slug }); return { @@ -53,21 +88,53 @@ function getAllConfiguredDocFiles(): Array<{ id: string; filePath: string; conte } function findPackageImports(content: string): string[] { - const imports = Array.from(content.matchAll(/from\s+['"](?@threadplane\/[^'"]+)['"]/g), (match) => match.groups?.pkg); - const dynamicImports = Array.from(content.matchAll(/import\(\s*['"](?@threadplane\/[^'"]+)['"]\s*\)/g), (match) => match.groups?.pkg); - return [...imports, ...dynamicImports].filter((pkg): pkg is string => Boolean(pkg)); + const imports = Array.from( + content.matchAll(/from\s+['"](?@threadplane\/[^'"]+)['"]/g), + (match) => match.groups?.pkg + ); + const dynamicImports = Array.from( + content.matchAll(/import\(\s*['"](?@threadplane\/[^'"]+)['"]\s*\)/g), + (match) => match.groups?.pkg + ); + return [...imports, ...dynamicImports].filter((pkg): pkg is string => + Boolean(pkg) + ); } describe('website docs bindings', () => { it('lists all doc slugs from config', () => { const slugs = getAllDocSlugs(); expect(slugs.length).toBe(allDocsPages.length); - expect(slugs).toContainEqual({ library: 'langgraph', section: 'getting-started', slug: 'introduction' }); - expect(slugs).toContainEqual({ library: 'langgraph', section: 'guides', slug: 'streaming' }); - expect(slugs).toContainEqual({ library: 'render', section: 'getting-started', slug: 'introduction' }); - expect(slugs).toContainEqual({ library: 'chat', section: 'getting-started', slug: 'introduction' }); - expect(slugs).toContainEqual({ library: 'ag-ui', section: 'concepts', slug: 'architecture' }); - expect(slugs).toContainEqual({ library: 'a2ui', section: 'getting-started', slug: 'introduction' }); + expect(slugs).toContainEqual({ + library: 'langgraph', + section: 'getting-started', + slug: 'introduction', + }); + expect(slugs).toContainEqual({ + library: 'langgraph', + section: 'guides', + slug: 'streaming', + }); + expect(slugs).toContainEqual({ + library: 'render', + section: 'getting-started', + slug: 'introduction', + }); + expect(slugs).toContainEqual({ + library: 'chat', + section: 'getting-started', + slug: 'introduction', + }); + expect(slugs).toContainEqual({ + library: 'ag-ui', + section: 'concepts', + slug: 'architecture', + }); + expect(slugs).toContainEqual({ + library: 'a2ui', + section: 'getting-started', + slug: 'introduction', + }); }); it('loads every configured doc page', () => { @@ -77,7 +144,9 @@ describe('website docs bindings', () => { }); it('does not leave tracked MDX docs outside the configured docs inventory', () => { - const configuredPaths = new Set(getAllDocSlugs().map((slug) => getConfiguredDocPath(slug))); + const configuredPaths = new Set( + getAllDocSlugs().map((slug) => getConfiguredDocPath(slug)) + ); for (const page of specialDocsPages) { configuredPaths.add(path.join(contentRoot, page.contentPath)); } @@ -113,7 +182,7 @@ describe('website docs bindings', () => { }); expect(metadata?.description).toContain('AG-UI protocol events'); expect(metadata?.description).not.toBe( - 'Adapter for AG-UI-compatible backends including CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, and AWS Strands', + 'Adapter for AG-UI-compatible backends including CrewAI, Mastra, Microsoft AF, AG2, Pydantic AI, and AWS Strands' ); }); @@ -126,17 +195,28 @@ describe('website docs bindings', () => { for (const { library, section, slug } of slugs) { const description = getDocMetadata(library, section, slug)?.description; expect(description, `${library}/${section}/${slug}`).toBeTruthy(); - expect(description!.length, `${library}/${section}/${slug}`).toBeLessThanOrEqual(160); - expect(description!.endsWith('...'), `${library}/${section}/${slug}`).toBe(false); + expect( + description!.length, + `${library}/${section}/${slug}` + ).toBeLessThanOrEqual(160); + expect( + description!.endsWith('...'), + `${library}/${section}/${slug}` + ).toBe(false); } }); it('derives mostly unique descriptions from page content', () => { const descriptions = getAllDocSlugs() - .map(({ library, section, slug }) => getDocMetadata(library, section, slug)?.description) + .map( + ({ library, section, slug }) => + getDocMetadata(library, section, slug)?.description + ) .filter((description): description is string => Boolean(description)); - const duplicateDescriptions = descriptions.filter((description, index) => descriptions.indexOf(description) !== index); + const duplicateDescriptions = descriptions.filter( + (description, index) => descriptions.indexOf(description) !== index + ); expect(duplicateDescriptions).toHaveLength(0); }); @@ -148,14 +228,17 @@ describe('website docs bindings', () => { const metadata = getDocMetadata('chat', 'guides', 'custom-catalogs'); expect(metadata?.description).toBe( - 'Compose custom component catalogs for generative UI using ViewRegistry composition.', + 'Compose custom component catalogs for generative UI using ViewRegistry composition.' ); }); it('never leaks frontmatter keys into a derived description', () => { for (const { library, section, slug } of getAllDocSlugs()) { - const description = getDocMetadata(library, section, slug)?.description ?? ''; - expect(description, `/docs/${library}/${section}/${slug}`).not.toMatch(/^title:/); + const description = + getDocMetadata(library, section, slug)?.description ?? ''; + expect(description, `/docs/${library}/${section}/${slug}`).not.toMatch( + /^title:/ + ); } }); @@ -163,12 +246,16 @@ describe('website docs bindings', () => { for (const { library, section, slug } of getAllDocSlugs()) { const doc = getDocBySlug(library, section, slug); - expect(doc?.body.startsWith('---'), `/docs/${library}/${section}/${slug}`).toBe(false); + expect( + doc?.body.startsWith('---'), + `/docs/${library}/${section}/${slug}` + ).toBe(false); } }); it('strips a frontmatter block before the body is handed to MDX', () => { - const source = '---\ntitle: X\ndescription: D.\n---\n\n# Heading\n\nBody.\n'; + const source = + '---\ntitle: X\ndescription: D.\n---\n\n# Heading\n\nBody.\n'; expect(stripFrontmatter(source)).toBe('# Heading\n\nBody.\n'); }); @@ -189,11 +276,16 @@ describe('website docs bindings', () => { }); it('resolves canonical URLs against the production origin', () => { - expect(getCanonicalUrl('/docs/langgraph/guides/streaming')).toBe('https://threadplane.ai/docs/langgraph/guides/streaming'); + expect(getCanonicalUrl('/docs/langgraph/guides/streaming')).toBe( + 'https://threadplane.ai/docs/langgraph/guides/streaming' + ); }); it('does not contain stale or broken internal docs links', () => { - const validDocsRoutes = new Set(['/docs', ...getSitemapRoutes().filter((route) => route.startsWith('/docs/'))]); + const validDocsRoutes = new Set([ + '/docs', + ...getSitemapRoutes().filter((route) => route.startsWith('/docs/')), + ]); const brokenLinks: string[] = []; for (const { library, section, slug } of getAllDocSlugs()) { @@ -256,12 +348,26 @@ describe('website docs bindings', () => { }); it('has generated API docs for every documented package surface', () => { - const librariesWithApiDocs = ['langgraph', 'chat', 'render', 'ag-ui', 'a2ui', 'middleware']; + const librariesWithApiDocs = [ + 'langgraph', + 'chat', + 'render', + 'ag-ui', + 'a2ui', + 'middleware', + ]; const missingApiDocs = librariesWithApiDocs.filter((library) => { - const apiDocsPath = path.join(contentRoot, library, 'api', 'api-docs.json'); + const apiDocsPath = path.join( + contentRoot, + library, + 'api', + 'api-docs.json' + ); if (!fs.existsSync(apiDocsPath)) return true; - const entries = JSON.parse(fs.readFileSync(apiDocsPath, 'utf8')) as unknown[]; + const entries = JSON.parse( + fs.readFileSync(apiDocsPath, 'utf8') + ) as unknown[]; return entries.length === 0; }); @@ -329,8 +435,15 @@ describe('website docs bindings', () => { const doc = getDocBySlug(library, section, slug); if (!doc?.content.includes('Auto-rendered from api-docs.json')) continue; - const apiDocsPath = path.join(contentRoot, library, 'api', 'api-docs.json'); - const entries = JSON.parse(fs.readFileSync(apiDocsPath, 'utf8')) as Array<{ name: string }>; + const apiDocsPath = path.join( + contentRoot, + library, + 'api', + 'api-docs.json' + ); + const entries = JSON.parse( + fs.readFileSync(apiDocsPath, 'utf8') + ) as Array<{ name: string }>; const names = new Set(entries.map((entry) => entry.name)); // Group pages declare the exports they cover; every one must exist. @@ -338,7 +451,9 @@ describe('website docs bindings', () => { if (configured) { const missing = configured.filter((name) => !names.has(name)); if (missing.length > 0) { - unresolvedApiPages.push(`${library}/api/${slug} → ${missing.join(', ')}`); + unresolvedApiPages.push( + `${library}/api/${slug} → ${missing.join(', ')}` + ); } continue; } @@ -357,12 +472,31 @@ describe('website docs bindings', () => { }); it('returns null for non-existent library', () => { - expect(getDocBySlug('nonexistent', 'getting-started', 'introduction')).toBeNull(); + expect( + getDocBySlug('nonexistent', 'getting-started', 'introduction') + ).toBeNull(); }); it('returns null metadata for non-existent docs', () => { expect(getDocMetadata('langgraph', 'guides', 'nonexistent')).toBeNull(); }); + + it('publishes a docs page for every capability the registry maps', () => { + const configured = new Set( + docsConfig.flatMap((library) => + library.sections.flatMap((section) => + section.pages.map( + (page) => `/docs/${library.id}/${section.id}/${page.slug}` + ) + ) + ) + ); + for (const entry of cockpitManifest) { + expect(configured.has(entry.docsPath), entry.docsPath).toBe(true); + const file = path.join(contentRoot, '..', `${entry.docsPath}.mdx`); + expect(fs.existsSync(file), file).toBe(true); + } + }); }); describe('docs breadcrumb routes', () => { @@ -375,8 +509,14 @@ describe('docs breadcrumb routes', () => { const routes = new Set(getSitemapRoutes()); for (const library of docsConfig) { - expect([library.id, routes.has(libraryIntroPath(library.id))]).toEqual([library.id, true]); - expect([library.id, routes.has(`/docs/${library.id}`)]).toEqual([library.id, false]); + expect([library.id, routes.has(libraryIntroPath(library.id))]).toEqual([ + library.id, + true, + ]); + expect([library.id, routes.has(`/docs/${library.id}`)]).toEqual([ + library.id, + false, + ]); } }); }); @@ -395,7 +535,9 @@ describe('retired telemetry docs library', () => { it('contributes no searchable page', () => { const pages = docsConfig.flatMap((library) => library.sections.flatMap((section) => - section.pages.map((page) => `${library.id}/${page.section}/${page.slug}`) + section.pages.map( + (page) => `${library.id}/${page.section}/${page.slug}` + ) ) ); expect(pages.filter((page) => page.startsWith('telemetry/'))).toEqual([]); From d4c4d5449be0299ec89b817a5fd2a3b8ca6abb84 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 4 Sep 2026 10:08:50 -0700 Subject: [PATCH 09/12] =?UTF-8?q?docs:=20correct=20the=20six=20new=20guide?= =?UTF-8?q?s=20=E2=80=94=20accurate=20framing,=20descriptions,=20generativ?= =?UTF-8?q?e=20UI=20title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .../content/docs/ag-ui/guides/client-tools.mdx | 6 +++++- .../content/docs/ag-ui/guides/json-render.mdx | 10 +++++++--- .../website/content/docs/ag-ui/guides/subagents.mdx | 6 +++++- .../content/docs/ag-ui/guides/tool-views.mdx | 8 ++++++-- .../docs/langgraph/guides/durable-execution.mdx | 8 ++++++-- .../content/docs/render/guides/repeat-loops.mdx | 8 ++++++-- apps/website/src/lib/docs-config.ts | 2 +- apps/website/src/lib/docs.spec.ts | 13 +++++++++++-- 8 files changed, 47 insertions(+), 14 deletions(-) diff --git a/apps/website/content/docs/ag-ui/guides/client-tools.mdx b/apps/website/content/docs/ag-ui/guides/client-tools.mdx index 13a9fb681..10fb80868 100644 --- a/apps/website/content/docs/ag-ui/guides/client-tools.mdx +++ b/apps/website/content/docs/ag-ui/guides/client-tools.mdx @@ -1,5 +1,9 @@ +--- +description: Declare a tool in the Angular app, let an AG-UI agent call it, and submit the result back to continue the run with no server-side implementation. +--- + # Client Tools -An AG-UI agent can call tools that live in the browser. The Angular app declares the tool, the adapter forwards the agent's call, and the result flows back into the run without a server round trip. +An AG-UI agent can call tools that live in the browser. The Angular app declares the tool, the backend ends its turn when the agent calls it, the browser runs it, and the result is submitted back as a tool message so the run continues. The tool has no server-side implementation. This page is the live example for client tools over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The browser-side tool contract is documented in the [Chat client tools guide](/docs/chat/guides/client-tools). diff --git a/apps/website/content/docs/ag-ui/guides/json-render.mdx b/apps/website/content/docs/ag-ui/guides/json-render.mdx index 9fbef0e61..505199da2 100644 --- a/apps/website/content/docs/ag-ui/guides/json-render.mdx +++ b/apps/website/content/docs/ag-ui/guides/json-render.mdx @@ -1,5 +1,9 @@ -# JSON Render +--- +description: Stream a json-render UI spec with AG-UI shared state and let @threadplane/render resolve $state bindings into a live Angular component tree. +--- -An AG-UI agent can stream a declarative UI specification that `@threadplane/render` turns into a live Angular component tree, so the agent shapes the interface rather than only its text. +# Generative UI -This page is the live example for JSON rendering over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The rendering engine is introduced in the [Render introduction](/docs/render/getting-started/introduction). +An AG-UI agent can stream a declarative json-render specification together with shared state. `STATE_SNAPSHOT` and `STATE_DELTA` events keep that state current, and `@threadplane/render` resolves the spec's `$state` bindings against it, so the agent shapes the interface and its data rather than only its text. + +This page is the live example for generative UI over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The rendering engine is introduced in the [Render introduction](/docs/render/getting-started/introduction), and the AG-UI state events in the [event mapping reference](/docs/ag-ui/reference/event-mapping). diff --git a/apps/website/content/docs/ag-ui/guides/subagents.mdx b/apps/website/content/docs/ag-ui/guides/subagents.mdx index c008fa70a..ab418dc64 100644 --- a/apps/website/content/docs/ag-ui/guides/subagents.mdx +++ b/apps/website/content/docs/ag-ui/guides/subagents.mdx @@ -1,5 +1,9 @@ +--- +description: Show an AG-UI agent's delegated child runs as attributed subagent cards with their own tool calls and messages. +--- + # Subagents -AG-UI carries subagent activity as first-class events, so a delegating agent's child runs show up as attributed cards with their own tool calls and messages instead of being folded into the parent transcript. +AG-UI carries subagent activity as first-class events. When an agent delegates, each child run appears as an attributed card with its own tool calls and messages instead of being folded into the parent transcript. This page is the live example for subagents over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The card component is documented in [ChatSubagentCard](/docs/chat/components/chat-subagent-card). diff --git a/apps/website/content/docs/ag-ui/guides/tool-views.mdx b/apps/website/content/docs/ag-ui/guides/tool-views.mdx index 3758f4190..ac7780a5b 100644 --- a/apps/website/content/docs/ag-ui/guides/tool-views.mdx +++ b/apps/website/content/docs/ag-ui/guides/tool-views.mdx @@ -1,5 +1,9 @@ +--- +description: Render an AG-UI tool call's plain data through a component the frontend owns, keyed by tool name, with no UI spec crossing the wire. +--- + # Tool Views -Tool views render an AG-UI tool call as a purpose-built component instead of a generic card: a map for a location lookup, a table for a query, a form for a confirmation. +A tool view renders an AG-UI tool call's result through a component the frontend owns, keyed by the tool's name. The agent returns plain data, such as a weather reading, and the Angular app decides how it looks. No UI specification crosses the wire. -This page is the live example for tool views. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The tool-call component surface is documented in [ChatToolCalls](/docs/chat/components/chat-tool-calls). +This page is the live example for tool views over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The tool-call component surface is documented in [ChatToolCalls](/docs/chat/components/chat-tool-calls). diff --git a/apps/website/content/docs/langgraph/guides/durable-execution.mdx b/apps/website/content/docs/langgraph/guides/durable-execution.mdx index 957caebae..a8680a176 100644 --- a/apps/website/content/docs/langgraph/guides/durable-execution.mdx +++ b/apps/website/content/docs/langgraph/guides/durable-execution.mdx @@ -1,5 +1,9 @@ +--- +description: Resume a LangGraph run after a crash, restart, or redeploy from its last checkpoint, and offer retry from the Angular UI. +--- + # Durable Execution -Durable execution keeps a LangGraph run alive across process restarts and long waits. The checkpointer records every super-step, so a run that is interrupted, redeployed, or paused for a human can resume from its last checkpoint instead of starting over. +Persistence keeps a thread's history; durable execution keeps a _run_ alive through failure. When a step crashes, the process restarts, or a deployment rolls, the checkpointer lets the run resume from its last completed super-step instead of starting over, and the UI can offer a retry rather than a blank transcript. -This page is the live example for durable execution. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The checkpointer configuration the example relies on is written up in the [Persistence guide](/docs/langgraph/guides/persistence). +This page is the live example for durable execution over LangGraph. Use **Run** to interrupt and retry a run, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The checkpointer configuration it relies on is written up in the [Persistence guide](/docs/langgraph/guides/persistence). diff --git a/apps/website/content/docs/render/guides/repeat-loops.mdx b/apps/website/content/docs/render/guides/repeat-loops.mdx index 6169c6705..d9adf2c3d 100644 --- a/apps/website/content/docs/render/guides/repeat-loops.mdx +++ b/apps/website/content/docs/render/guides/repeat-loops.mdx @@ -1,5 +1,9 @@ +--- +description: Render one element per item of a bound collection with $item and $index in a json-render spec, backed by a signal state store. +--- + # Repeat Loops -A repeat loop renders one element per item of a bound collection, with `$item` and `$index` available to child expressions, so a spec can describe a list without enumerating its rows. +A repeat loop renders one element per item of a bound collection, with `$item` and `$index` available to child expressions, so a spec can describe a list without enumerating its rows. The example binds the loop to a signal-backed state store and updates the list live. -This page is the live example for repeat loops. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The spec format, including the repeat element, is documented in [Specs & Elements](/docs/render/guides/specs). +This page is the live example for repeat loops. Use **Run** to try the example live (it has no agent), **Code** to read the Angular source, and **API** for the extracted reference. The spec format, including the repeat element, is documented in [Specs & Elements](/docs/render/guides/specs). diff --git a/apps/website/src/lib/docs-config.ts b/apps/website/src/lib/docs-config.ts index 0bdca0ac0..4bae443f0 100644 --- a/apps/website/src/lib/docs-config.ts +++ b/apps/website/src/lib/docs-config.ts @@ -473,7 +473,7 @@ export const docsConfig: DocsLibrary[] = [ { title: 'Interrupts', slug: 'interrupts', section: 'guides' }, { title: 'Client Tools', slug: 'client-tools', section: 'guides' }, { title: 'Tool Views', slug: 'tool-views', section: 'guides' }, - { title: 'JSON Render', slug: 'json-render', section: 'guides' }, + { title: 'Generative UI', slug: 'json-render', section: 'guides' }, { title: 'Subagents', slug: 'subagents', section: 'guides' }, { title: 'Testing', slug: 'testing', section: 'guides' }, { diff --git a/apps/website/src/lib/docs.spec.ts b/apps/website/src/lib/docs.spec.ts index 173fd6b5a..8d812ae30 100644 --- a/apps/website/src/lib/docs.spec.ts +++ b/apps/website/src/lib/docs.spec.ts @@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { cockpitManifest } from '@threadplane/cockpit-registry'; +import { + NO_COCKPIT_DOCS_LINK, + cockpitManifest, +} from '@threadplane/cockpit-registry'; import { getAllDocSlugs, getDocBySlug, @@ -492,8 +495,14 @@ describe('website docs bindings', () => { ) ); for (const entry of cockpitManifest) { + if (entry.docsPath === NO_COCKPIT_DOCS_LINK) continue; expect(configured.has(entry.docsPath), entry.docsPath).toBe(true); - const file = path.join(contentRoot, '..', `${entry.docsPath}.mdx`); + // contentRoot already points at apps/website/content/docs, so strip the + // leading /docs/ segment before joining. + const file = path.join( + contentRoot, + `${entry.docsPath.replace(/^\/docs\//, '')}.mdx` + ); expect(fs.existsSync(file), file).toBe(true); } }); From ff53e697afe6661cc2975dce622be004838fe3c9 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 5 Sep 2026 09:19:51 -0700 Subject: [PATCH 10/12] feat(website): retire the /workspace route; every capability is a docs page Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 2 +- apps/cockpit/scripts/deploy-smoke.spec.ts | 8 -- apps/cockpit/scripts/deploy-smoke.ts | 32 +---- apps/cockpit/src/lib/cockpit-page.spec.ts | 17 +-- .../e2e/platform-production-smoke.spec.ts | 29 ++--- apps/website/e2e/workspace-shell.spec.ts | 24 ++-- apps/website/src/app/docs/page.tsx | 18 ++- .../workspace/[product]/[topic]/page.spec.tsx | 76 ----------- .../app/workspace/[product]/[topic]/page.tsx | 91 -------------- .../workspace/WebsiteWorkspace.spec.tsx | 118 ++++-------------- .../components/workspace/WebsiteWorkspace.tsx | 26 +--- apps/website/src/lib/workspace-page.ts | 16 --- .../src/lib/manifest.types.ts | 2 +- .../src/lib/workspace-resolution.ts | 2 +- scripts/ci-workflow.spec.mjs | 7 +- 15 files changed, 72 insertions(+), 396 deletions(-) delete mode 100644 apps/website/src/app/workspace/[product]/[topic]/page.spec.tsx delete mode 100644 apps/website/src/app/workspace/[product]/[topic]/page.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b260bb73..7b41407a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -293,7 +293,7 @@ jobs: # `scope:shared`, which is not a SCOPE_KEY — adding it to LIBS would not # have run it, because a workspace-react change never flips `library`. - run: npx nx lint workspace-react - - run: npx nx run-many -t test --projects=cockpit,cockpit-docs,cockpit-registry,workspace-react --skip-nx-cache + - run: npx nx run-many -t test --projects=cockpit,cockpit-docs,cockpit-registry,cockpit-shell,workspace-react --skip-nx-cache cockpit-examples-build: name: Cockpit — build all examples needs: ci-scope diff --git a/apps/cockpit/scripts/deploy-smoke.spec.ts b/apps/cockpit/scripts/deploy-smoke.spec.ts index 0510b937b..1b76ce1a1 100644 --- a/apps/cockpit/scripts/deploy-smoke.spec.ts +++ b/apps/cockpit/scripts/deploy-smoke.spec.ts @@ -128,13 +128,6 @@ describe('redirect deploy smoke contract', () => { expect( cases.some((smokeCase) => smokeCase.name.includes('unavailable mode')) ).toBe(true); - expect( - cases.some( - (smokeCase) => - smokeCase.name.includes('workspace Docs serialization') && - smokeCase.expectedLocation?.includes('?mode=docs') - ) - ).toBe(true); expect( cases.some( (smokeCase) => @@ -201,7 +194,6 @@ describe('redirect deploy smoke contract', () => { for (const label of [ 'root', 'Docs-backed', - 'workspace-only', 'unknown', 'favicon', 'raw malformed', diff --git a/apps/cockpit/scripts/deploy-smoke.ts b/apps/cockpit/scripts/deploy-smoke.ts index bb0828f72..e2e55c68c 100644 --- a/apps/cockpit/scripts/deploy-smoke.ts +++ b/apps/cockpit/scripts/deploy-smoke.ts @@ -236,20 +236,6 @@ const buildPreviewCases = (): RedirectSmokeCase[] => { ); } - const workspaceOnly = cockpitManifest.find((entry) => - getWorkspaceDestinationPath(entry).startsWith('/workspace/') - ); - if (!workspaceOnly) - throw new Error('Expected a workspace-only manifest entry'); - cases.push( - redirectCase( - 'workspace Docs serialization', - `${workspaceOnly.legacyPath}?mode=docs`, - entryResolution(workspaceOnly), - 'docs' - ) - ); - cases.push( notFoundCase('unknown path 404', '/unknown'), notFoundCase('partial path 404', '/langgraph/core-capabilities/streaming'), @@ -285,13 +271,8 @@ const buildProductionCases = (): RedirectSmokeCase[] => { const docsBacked = cockpitManifest.find((entry) => getWorkspaceDestinationPath(entry).startsWith('/docs/') ); - const workspaceOnly = cockpitManifest.find((entry) => - getWorkspaceDestinationPath(entry).startsWith('/workspace/') - ); - if (!docsBacked || !workspaceOnly) { - throw new Error( - 'Redirect smoke requires Docs-backed and workspace-only routes' - ); + if (!docsBacked) { + throw new Error('Redirect smoke requires a Docs-backed route'); } return [ redirectCase('root production redirect', '/', root, 'run'), @@ -300,11 +281,6 @@ const buildProductionCases = (): RedirectSmokeCase[] => { docsBacked.legacyPath, entryResolution(docsBacked) ), - redirectCase( - 'workspace-only production redirect', - workspaceOnly.legacyPath, - entryResolution(workspaceOnly) - ), notFoundCase('unknown production 404', '/unknown'), { name: 'favicon production redirect', @@ -442,9 +418,7 @@ const verifyCase = ( if (response.status !== smokeCase.expectedStatus) { const protectionHint = response.status === 302 && - (response.headers.location ?? '').startsWith( - 'https://vercel.com/sso-api' - ) + (response.headers.location ?? '').startsWith('https://vercel.com/sso-api') ? ` The deployment answered with Vercel deployment protection, not the redirect service; supply the owning project's automation bypass secret via ${BYPASS_SECRET_ENV}.` : ''; throw new RedirectContractError( diff --git a/apps/cockpit/src/lib/cockpit-page.spec.ts b/apps/cockpit/src/lib/cockpit-page.spec.ts index dcf21c361..6ccf75ee5 100644 --- a/apps/cockpit/src/lib/cockpit-page.spec.ts +++ b/apps/cockpit/src/lib/cockpit-page.spec.ts @@ -266,15 +266,12 @@ describe('registry-derived legacy Website redirects', () => { ).toBe(expectedHref(docsOnly, 'Docs')); }); - it('serializes Docs mode truthfully for docs and workspace destinations', () => { + it('serializes Docs mode truthfully for docs destinations', () => { const docsDestination = cockpitManifest.find((entry) => getWorkspaceDestinationPath(entry).startsWith('/docs/') ); - const workspaceDestination = cockpitManifest.find((entry) => - getWorkspaceDestinationPath(entry).startsWith('/workspace/') - ); - if (!docsDestination || !workspaceDestination) { - throw new Error('Expected docs and workspace fixtures'); + if (!docsDestination) { + throw new Error('Expected a docs fixture'); } expect( @@ -284,14 +281,6 @@ describe('registry-derived legacy Website redirects', () => { productionEnvironment ) ).toBe(expectedHref(docsDestination, 'Docs')); - expect( - getLegacyWebsiteRedirect( - workspaceDestination.legacyPath, - ['docs'], - productionEnvironment - ) - ).toBe(expectedHref(workspaceDestination, 'Docs')); - expect(expectedHref(workspaceDestination, 'Docs')).toContain('?mode=docs'); }); it('returns null for unknown, partial, extra, malformed, and trailing paths', () => { diff --git a/apps/website/e2e/platform-production-smoke.spec.ts b/apps/website/e2e/platform-production-smoke.spec.ts index 9e6f5d67a..03f9172ce 100644 --- a/apps/website/e2e/platform-production-smoke.spec.ts +++ b/apps/website/e2e/platform-production-smoke.spec.ts @@ -50,7 +50,10 @@ const expectedRuntimeParentOrigins = validateRuntimeParentOrigins([ ...(baseRuntimeParentOrigins ?? []), ...runtimeParentPreviewOrigins, ]); -if (baseRuntimeParentOrigins === null || expectedRuntimeParentOrigins === null) { +if ( + baseRuntimeParentOrigins === null || + expectedRuntimeParentOrigins === null +) { throw new Error('Invalid runtime parent origin smoke policy'); } @@ -140,12 +143,8 @@ const expectedRedirect = (legacyPath: string): string => { const docsBacked = cockpitManifest.find((entry) => getWorkspaceDestinationPath(entry).startsWith('/docs/') ); -const workspaceOnly = cockpitManifest.find((entry) => - getWorkspaceDestinationPath(entry).startsWith('/workspace/') -); -if (!docsBacked || !workspaceOnly) { - throw new Error('Production smoke requires Docs-backed and workspace routes'); -} +if (!docsBacked) + throw new Error('Production smoke requires a Docs-backed route'); const COCKPIT_REDIRECT_CASES = [ { @@ -162,12 +161,6 @@ const COCKPIT_REDIRECT_CASES = [ status: 308, location: expectedRedirect(docsBacked.legacyPath), }, - { - name: 'workspace-only production redirect', - path: workspaceOnly.legacyPath, - status: 308, - location: expectedRedirect(workspaceOnly.legacyPath), - }, { name: 'unknown production 404', path: '/unknown', @@ -259,13 +252,9 @@ test.describe('Production: unified runtime embedding policy', () => { .find((directive) => directive.trim().startsWith('frame-ancestors')); expect(response.status()).toBe(200); - const actualFrameAncestors = frameAncestors - ?.trim() - .split(/\s+/) - .slice(1); - const validatedFrameAncestors = validateRuntimeParentOrigins( - actualFrameAncestors - ); + const actualFrameAncestors = frameAncestors?.trim().split(/\s+/).slice(1); + const validatedFrameAncestors = + validateRuntimeParentOrigins(actualFrameAncestors); expect(validatedFrameAncestors).not.toBeNull(); if (runtimeParentPreviewOrigins.length > 0) { expect(validatedFrameAncestors).toEqual(expectedRuntimeParentOrigins); diff --git a/apps/website/e2e/workspace-shell.spec.ts b/apps/website/e2e/workspace-shell.spec.ts index 4728b70f0..cda048129 100644 --- a/apps/website/e2e/workspace-shell.spec.ts +++ b/apps/website/e2e/workspace-shell.spec.ts @@ -4,8 +4,7 @@ const streamingDocsPath = '/docs/langgraph/guides/streaming'; const persistenceDocsPath = '/docs/langgraph/guides/persistence'; const mappedDocsOnlyPath = '/docs/langgraph/getting-started/introduction'; const unmappedDocsOnlyPath = '/docs/langgraph/getting-started/installation'; -const workspaceOnlyPath = '/workspace/langgraph/durable-execution'; -const deepAgentsDocsPath = '/docs/deep-agents/capabilities/planning'; +const durableExecutionDocsPath = '/docs/langgraph/guides/durable-execution'; const RUN_RAIL_ITEM = /^Run(?:,|$)/; declare global { @@ -409,28 +408,25 @@ test.describe('workspace shell', () => { await expect(page.locator('[data-workspace-shell]')).toBeVisible(); }); - test('uses workspace fallbacks only when a shared Docs path would lose identity', async ({ + test('serves the formerly workspace-only capabilities as docs pages with Run available', async ({ page, }) => { - const response = await page.goto(workspaceOnlyPath); + const response = await page.goto(durableExecutionDocsPath); expect(response?.status()).toBe(200); - await expect(page).toHaveURL(workspaceOnlyPath); - await expect(page.locator('[data-workspace-shell]')).toHaveAttribute( - 'aria-label', - 'Website workspace' - ); - await expectMode(page, 'Run'); - - await page.goto(deepAgentsDocsPath); - await expect(page).toHaveURL(deepAgentsDocsPath); + await expect(page).toHaveURL(durableExecutionDocsPath); await expect(page.locator('[data-workspace-shell]')).toHaveAttribute( 'aria-label', 'Documentation workspace' ); await expectMode(page, 'Docs'); + await modeButton(page, 'Run').click(); + await expect(page).toHaveURL(`${durableExecutionDocsPath}?mode=run`); await expect( - page.locator('iframe[title="Deep Agents Planning live example"]') + page.locator('iframe[title="LangGraph Durable Execution live example"]') ).toBeAttached(); + + const missing = await page.goto('/workspace/langgraph/durable-execution'); + expect(missing?.status()).toBe(404); }); test('renders the full desktop rail and context at the 64rem breakpoint', async ({ diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index fb9e1f8f7..de479bbfa 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -11,7 +11,7 @@ import { DocsSearchFooter } from '../../components/docs/DocsSearchFooter'; import { createPageMetadata } from '../../lib/site-metadata'; import { getCanonicalWebsiteWorkspaceHref, - resolveWorkspacePath, + resolveDocsWorkspace, } from '@threadplane/cockpit-registry'; export const metadata = createPageMetadata({ @@ -23,16 +23,14 @@ export const metadata = createPageMetadata({ }); /** - * The example the index's Run rail item opens. - * - * Resolved through the registry rather than written as a path: this - * capability publishes a `docsPath`, so `getWorkspaceDestinationPath()` makes - * its canonical destination the docs route and `/workspace/langgraph/streaming` - * 404s. Today this yields `/docs/langgraph/guides/streaming?mode=run`, and it - * stays correct if that docs path moves. A renamed or removed capability - * resolves to null, and Run falls back to disabled rather than to a dead link. + * The example the index's Run rail item opens, resolved through the registry + * so a renamed or removed capability yields null and Run falls back to + * disabled rather than to a dead link. */ -const DEFAULT_EXAMPLE_RESOLUTION = resolveWorkspacePath('/workspace/langgraph/streaming'); +const DEFAULT_EXAMPLE_RESOLUTION = resolveDocsWorkspace( + '/docs/langgraph/guides/streaming', + 'Streaming' +); const DEFAULT_EXAMPLE_RUN_HREF = DEFAULT_EXAMPLE_RESOLUTION ? getCanonicalWebsiteWorkspaceHref(DEFAULT_EXAMPLE_RESOLUTION, 'Run') : undefined; diff --git a/apps/website/src/app/workspace/[product]/[topic]/page.spec.tsx b/apps/website/src/app/workspace/[product]/[topic]/page.spec.tsx deleted file mode 100644 index c6ef7772c..000000000 --- a/apps/website/src/app/workspace/[product]/[topic]/page.spec.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - cockpitManifest, - getWorkspaceDestinationPath, -} from '@threadplane/cockpit-registry'; -import WorkspacePage, { generateMetadata, generateStaticParams } from './page'; - -const route = (product: string, topic: string) => - WorkspacePage({ params: Promise.resolve({ product, topic }) }); - -describe('workspace-only Website routes', () => { - it('statically exposes only usable identity-preserving workspace routes', () => { - const params = generateStaticParams(); - const expected = new Set( - cockpitManifest - .filter( - (entry) => - getWorkspaceDestinationPath(entry) === entry.workspacePath && - entry.availableModes.length > 0 - ) - .map((entry) => `${entry.product}/${entry.topic}`) - ); - - expect( - new Set(params.map(({ product, topic }) => `${product}/${topic}`)) - ).toEqual(expected); - expect(params).toHaveLength(expected.size); - expect(expected).not.toContain('deep-agents/planning'); - expect(expected).not.toContain('deep-agents/overview'); - expect(expected).not.toContain('langgraph/streaming'); - expect(expected).toContain('langgraph/durable-execution'); - }); - - it('rejects Docs-backed aliases and workspace identities with no usable mode', async () => { - await expect(route('langgraph', 'streaming')).rejects.toMatchObject({ - digest: 'NEXT_HTTP_ERROR_FALLBACK;404', - }); - await expect(route('deep-agents', 'planning')).rejects.toMatchObject({ - digest: 'NEXT_HTTP_ERROR_FALLBACK;404', - }); - await expect(route('deep-agents', 'overview')).rejects.toMatchObject({ - digest: 'NEXT_HTTP_ERROR_FALLBACK;404', - }); - }); - - it('publishes a workspace route when a shared Docs path would lose identity', async () => { - const page = await route('langgraph', 'durable-execution'); - - expect(page.props).toMatchObject({ - routePath: '/workspace/langgraph/durable-execution', - routeKind: 'workspace', - resolution: { - kind: 'mapped', - identity: { - id: 'langgraph:core-capabilities:durable-execution:overview:python', - }, - }, - }); - }); - - it('does not publish workspace metadata for a Docs-backed capability', async () => { - await expect( - generateMetadata({ - params: Promise.resolve({ product: 'deep-agents', topic: 'planning' }), - }) - ).rejects.toMatchObject({ digest: 'NEXT_HTTP_ERROR_FALLBACK;404' }); - }); - - it('uses Next not-found for an invalid workspace path', async () => { - await expect( - route('deep-agents', 'not-a-capability') - ).rejects.toMatchObject({ - digest: 'NEXT_HTTP_ERROR_FALLBACK;404', - }); - }); -}); diff --git a/apps/website/src/app/workspace/[product]/[topic]/page.tsx b/apps/website/src/app/workspace/[product]/[topic]/page.tsx deleted file mode 100644 index 6932f312d..000000000 --- a/apps/website/src/app/workspace/[product]/[topic]/page.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import type { Metadata } from 'next'; -import { - cockpitManifest, - getWorkspaceDestinationPath, - resolveWorkspacePath, - type WorkspaceResolution, -} from '@threadplane/cockpit-registry'; -import { notFound } from 'next/navigation'; -import { WebsiteWorkspace } from '../../../../components/workspace/WebsiteWorkspace'; -import { createPageMetadata } from '../../../../lib/site-metadata'; -import { getWebsiteWorkspaceRoutePage } from '../../../../lib/workspace-page'; - -interface WorkspaceRouteProps { - readonly params: Promise<{ product: string; topic: string }>; -} - -function resolveWorkspaceOnlyRoute( - routePath: string -): WorkspaceResolution | null { - const resolution = resolveWorkspacePath(routePath); - if ( - !resolution || - resolution.kind !== 'mapped' || - getWorkspaceDestinationPath(resolution.identity) !== routePath || - resolution.identity.availableModes.length === 0 - ) { - return null; - } - return resolution; -} - -export function generateStaticParams() { - const params = new Map(); - - for (const entry of cockpitManifest) { - if ( - getWorkspaceDestinationPath(entry) !== entry.workspacePath || - entry.availableModes.length === 0 - ) { - continue; - } - const segments = entry.workspacePath.split('/').filter(Boolean); - const [prefix, product, topic, ...remainder] = segments; - if (prefix !== 'workspace' || !product || !topic || remainder.length > 0) { - continue; - } - params.set(entry.workspacePath, { product, topic }); - } - - return [...params.values()]; -} - -export async function generateMetadata({ - params, -}: WorkspaceRouteProps): Promise { - const { product, topic } = await params; - const routePath = `/workspace/${product}/${topic}`; - const resolution = resolveWorkspaceOnlyRoute(routePath); - if (!resolution || resolution.kind !== 'mapped') notFound(); - - const { identity } = resolution; - return createPageMetadata({ - title: `${identity.title} — Threadplane Workspace`, - description: `Explore ${ - identity.title - } in the Threadplane workspace across its available ${identity.availableModes.join( - ', ' - )} views.`, - pathname: identity.workspacePath, - type: 'website', - }); -} - -export default async function WorkspacePage({ params }: WorkspaceRouteProps) { - const { product, topic } = await params; - const routePath = `/workspace/${product}/${topic}`; - if (!resolveWorkspaceOnlyRoute(routePath)) notFound(); - const workspacePage = await getWebsiteWorkspaceRoutePage(routePath); - if (!workspacePage) notFound(); - - return ( - - ); -} diff --git a/apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx b/apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx index 71e866520..fc27b190e 100644 --- a/apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx +++ b/apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx @@ -379,35 +379,6 @@ describe('WebsiteWorkspace', () => { ); }); - it('uses host-neutral labels and no fabricated Docs slot on workspace routes', () => { - const resolution = mappedResolution( - 'deep-agents:core-capabilities:planning:overview:python', - 'planning', - ['Docs', 'Run', 'Code', 'API'] - ); - renderWorkspace({ - resolution, - presentation: mappedPresentation(resolution), - routePath: '/workspace/deep-agents/planning', - routeKind: 'workspace', - docsSlot: undefined, - }); - - expect(mocks.latestProviderProps).toMatchObject({ - routeKind: 'workspace', - routePath: '/workspace/deep-agents/planning', - }); - expect(mocks.latestProviderProps?.docsSlot).toBeUndefined(); - expect(activeWorkspaceMode()).toBe('Run'); - expect(mocks.latestShellProps).toMatchObject({ - ariaLabel: 'Website workspace', - modeNavigationLabel: 'Workspace modes', - contextPaneLabel: 'Workspace context', - mobileDialogLabel: 'Workspace control plane', - mobileTitle: 'Workspace', - }); - }); - it('normalizes a valid but unavailable mode to the canonical Docs URL', async () => { window.history.replaceState( {}, @@ -472,17 +443,17 @@ describe('WebsiteWorkspace', () => { ); }); - it('serializes Docs mode explicitly on workspace routes and keeps the selected mode synchronized', async () => { + it('keeps the selected mode synchronized on a docs route', async () => { const resolution = mappedResolution( 'langgraph:core-capabilities:durable-execution:overview:python', 'durable-execution', ['Docs', 'Run', 'Code', 'API'], - '/docs/langgraph/guides/persistence' + '/docs/langgraph/guides/durable-execution' ); window.history.replaceState( {}, '', - '/workspace/langgraph/durable-execution?mode=run&keep=1' + '/docs/langgraph/guides/durable-execution?mode=run&keep=1' ); mocks.push.mockImplementation((href: string) => { window.history.pushState({}, '', href); @@ -490,8 +461,7 @@ describe('WebsiteWorkspace', () => { renderWorkspace({ resolution, presentation: mappedPresentation(resolution), - routePath: '/workspace/langgraph/durable-execution', - routeKind: 'workspace', + routePath: '/docs/langgraph/guides/durable-execution', docsSlot: undefined, }); await waitFor(() => expect(activeWorkspaceMode()).toBe('Run')); @@ -500,65 +470,25 @@ describe('WebsiteWorkspace', () => { await waitFor(() => { expect(mocks.push).toHaveBeenCalledWith( - '/workspace/langgraph/durable-execution?mode=docs' + '/docs/langgraph/guides/durable-execution' ); - expect(mocks.latestProviderProps?.requestedMode).toBe('docs'); + expect(mocks.latestProviderProps?.requestedMode).toBe(null); expect(activeWorkspaceMode()).toBe('Docs'); }); }); - it('restores the canonical workspace Docs mode on reload', async () => { - const resolution = mappedResolution( - 'langgraph:core-capabilities:durable-execution:overview:python', - 'durable-execution', - ['Docs', 'Run', 'Code', 'API'], - '/docs/langgraph/guides/persistence' - ); - window.history.replaceState( - {}, - '', - '/workspace/langgraph/durable-execution?mode=docs' - ); - - renderWorkspace({ - resolution, - presentation: mappedPresentation(resolution), - routePath: '/workspace/langgraph/durable-execution', - routeKind: 'workspace', - docsSlot: undefined, - }); - - await waitFor(() => { - expect(activeWorkspaceMode()).toBe('Docs'); - expect(mocks.latestProviderProps?.requestedMode).toBe('docs'); - }); - expect(mocks.replace).not.toHaveBeenCalled(); - }); - - it('uses workspace routes only when Docs are absent or would lose identity', () => { + it('always links a capability to its docs path', () => { renderWorkspace(); const resolveHref = mocks.latestProviderProps?.resolveIdentityHref; if (!resolveHref) throw new Error('Expected Website identity resolver'); - expect( - resolveHref({ - docsPath: '/docs/langgraph/guides/streaming', - workspacePath: '/workspace/langgraph/streaming', - } as never) - ).toBe('/docs/langgraph/guides/streaming'); - expect( - resolveHref({ - docsPath: '', - workspacePath: '/workspace/deep-agents/planning', - } as never) - ).toBe('/workspace/deep-agents/planning'); expect( resolveHref({ id: 'langgraph:core-capabilities:durable-execution:overview:python', - docsPath: '/docs/langgraph/guides/persistence', + docsPath: '/docs/langgraph/guides/durable-execution', workspacePath: '/workspace/langgraph/durable-execution', } as never) - ).toBe('/workspace/langgraph/durable-execution'); + ).toBe('/docs/langgraph/guides/durable-execution'); }); it('records cross-route focus intent before navigating', () => { @@ -790,18 +720,17 @@ describe('WebsiteWorkspace', () => { 'langgraph:core-capabilities:durable-execution:overview:python', 'durable-execution', ['Docs', 'Run', 'Code', 'API'], - '/docs/langgraph/guides/persistence' + '/docs/langgraph/guides/durable-execution' ); window.history.replaceState( {}, '', - '/workspace/langgraph/durable-execution?mode=docs' + '/docs/langgraph/guides/durable-execution' ); renderWorkspace({ resolution: source, presentation: mappedPresentation(source), - routePath: '/workspace/langgraph/durable-execution', - routeKind: 'workspace', + routePath: '/docs/langgraph/guides/durable-execution', docsSlot: undefined, }); await waitFor(() => expect(activeWorkspaceMode()).toBe('Docs')); @@ -809,7 +738,7 @@ describe('WebsiteWorkspace', () => { window.history.pushState( {}, '', - '/workspace/langgraph/durable-execution?mode=code' + '/docs/langgraph/guides/durable-execution?mode=code' ); act(() => window.dispatchEvent(new PopStateEvent('popstate'))); await waitFor(() => expect(activeWorkspaceMode()).toBe('Code')); @@ -817,7 +746,7 @@ describe('WebsiteWorkspace', () => { window.history.replaceState( {}, '', - '/workspace/langgraph/durable-execution?mode=docs' + '/docs/langgraph/guides/durable-execution' ); act(() => window.dispatchEvent(new PopStateEvent('popstate'))); await waitFor(() => expect(activeWorkspaceMode()).toBe('Docs')); @@ -825,7 +754,7 @@ describe('WebsiteWorkspace', () => { window.history.replaceState( {}, '', - '/workspace/langgraph/durable-execution?mode=code' + '/docs/langgraph/guides/durable-execution?mode=code' ); act(() => window.dispatchEvent(new PopStateEvent('popstate'))); await waitFor(() => expect(activeWorkspaceMode()).toBe('Code')); @@ -836,18 +765,17 @@ describe('WebsiteWorkspace', () => { 'langgraph:core-capabilities:durable-execution:overview:python', 'durable-execution', ['Docs', 'Run', 'Code', 'API'], - '/docs/langgraph/guides/persistence' + '/docs/langgraph/guides/durable-execution' ); window.history.replaceState( {}, '', - '/workspace/langgraph/durable-execution?mode=code' + '/docs/langgraph/guides/durable-execution?mode=code' ); renderWorkspace({ resolution: source, presentation: mappedPresentation(source), - routePath: '/workspace/langgraph/durable-execution', - routeKind: 'workspace', + routePath: '/docs/langgraph/guides/durable-execution', docsSlot: undefined, }); await waitFor(() => expect(activeWorkspaceMode()).toBe('Code')); @@ -856,20 +784,22 @@ describe('WebsiteWorkspace', () => { window.history.pushState( {}, '', - '/workspace/langgraph/durable-execution?mode=api&mode=docs&keep=1' + '/docs/langgraph/guides/durable-execution?mode=api&mode=docs&keep=1' ); act(() => window.dispatchEvent(new PopStateEvent('popstate'))); await waitFor(() => { - expect(activeWorkspaceMode()).toBe('Run'); + expect(activeWorkspaceMode()).toBe('Docs'); expect(mocks.replace).toHaveBeenCalledOnce(); }); const replacement = new URL( String(mocks.replace.mock.calls[0]?.[0]), window.location.origin ); - expect(replacement.pathname).toBe('/workspace/langgraph/durable-execution'); - expect(replacement.searchParams.getAll('mode')).toEqual(['run']); + expect(replacement.pathname).toBe( + '/docs/langgraph/guides/durable-execution' + ); + expect(replacement.searchParams.getAll('mode')).toEqual([]); expect(replacement.searchParams.has('keep')).toBe(false); }); diff --git a/apps/website/src/components/workspace/WebsiteWorkspace.tsx b/apps/website/src/components/workspace/WebsiteWorkspace.tsx index ea988f351..46edf7b7e 100644 --- a/apps/website/src/components/workspace/WebsiteWorkspace.tsx +++ b/apps/website/src/components/workspace/WebsiteWorkspace.tsx @@ -51,7 +51,6 @@ export interface WebsiteWorkspaceProps { readonly contentBundle: ContentBundle; readonly navigationTree: NavigationProduct[]; readonly routePath: string; - readonly routeKind?: 'docs' | 'workspace'; /** Test/alternate-host override. Website routes normally read this in-browser. */ readonly requestedMode?: string | null; readonly docsSlot?: ReactNode; @@ -172,14 +171,12 @@ function WebsiteWorkspaceSurface({ contentBundle, navigationTree, routePath, - routeKind = 'docs', requestedMode, docsSlot, docsContext, contextTrail, }: WebsiteWorkspaceProps) { const router = useRouter(); - const isWorkspaceRoute = routeKind === 'workspace'; const routerRef = useRef(router); routerRef.current = router; const [discoveredRouteMode, setDiscoveredRouteMode] = @@ -316,7 +313,7 @@ function WebsiteWorkspaceSurface({ resolution={resolution} presentation={presentation} contentBundle={contentBundle} - routeKind={routeKind} + routeKind="docs" routePath={routePath} requestedMode={routeMode} docsSlot={docsSlot} @@ -336,21 +333,11 @@ function WebsiteWorkspaceSurface({ rootElement="section" navigationTree={navigationTree} contextTrail={contextTrail} - ariaLabel={ - isWorkspaceRoute ? 'Website workspace' : 'Documentation workspace' - } - modeNavigationLabel={ - isWorkspaceRoute ? 'Workspace modes' : 'Documentation modes' - } - contextPaneLabel={ - isWorkspaceRoute ? 'Workspace context' : 'Documentation context' - } - mobileDialogLabel={ - isWorkspaceRoute - ? 'Workspace control plane' - : 'Documentation control plane' - } - mobileTitle={isWorkspaceRoute ? 'Workspace' : 'Documentation'} + ariaLabel="Documentation workspace" + modeNavigationLabel="Documentation modes" + contextPaneLabel="Documentation context" + mobileDialogLabel="Documentation control plane" + mobileTitle="Documentation" renderContextPane={docsContext ? renderContextPane : undefined} onContextAction={handleContextAction} onMobileModalPresenceChange={handleMobileModalPresenceChange} @@ -478,7 +465,6 @@ export function WebsiteWorkspace(props: WebsiteWorkspaceProps) { props.contentBundle, props.navigationTree, props.routePath, - props.routeKind, props.requestedMode, props.docsSlot, props.docsContext, diff --git a/apps/website/src/lib/workspace-page.ts b/apps/website/src/lib/workspace-page.ts index c89d99374..ba78ddb0f 100644 --- a/apps/website/src/lib/workspace-page.ts +++ b/apps/website/src/lib/workspace-page.ts @@ -2,7 +2,6 @@ import { cockpitManifest, getWorkspaceDestinationPath, resolveDocsWorkspace, - resolveWorkspacePath, type CockpitManifestEntry, type WorkspaceResolution, } from '@threadplane/cockpit-registry'; @@ -37,21 +36,6 @@ export async function getWebsiteWorkspacePage(options: { }; } -export async function getWebsiteWorkspaceRoutePage( - workspacePath: string -): Promise { - const resolution = resolveWorkspacePath(workspacePath); - if (!resolution) return null; - const presentation = getWorkspacePresentation(resolution); - - return { - resolution, - presentation, - contentBundle: await getContentBundle(presentation), - navigationTree: buildNavigationTree(cockpitManifest), - }; -} - export function getWebsiteWorkspaceHref(entry: CockpitManifestEntry): string { return getWorkspaceDestinationPath(entry); } diff --git a/libs/cockpit-registry/src/lib/manifest.types.ts b/libs/cockpit-registry/src/lib/manifest.types.ts index 0c14f86ae..615000c83 100644 --- a/libs/cockpit-registry/src/lib/manifest.types.ts +++ b/libs/cockpit-registry/src/lib/manifest.types.ts @@ -33,7 +33,7 @@ export interface WorkspaceIdentity { page: string; language: CockpitLanguage; title: string; - docsPath: string | null; + docsPath: string; workspacePath: string; legacyPath: string; runtimeAdapter: RuntimeAdapter; diff --git a/libs/cockpit-registry/src/lib/workspace-resolution.ts b/libs/cockpit-registry/src/lib/workspace-resolution.ts index cabee2ad0..91031c1a2 100644 --- a/libs/cockpit-registry/src/lib/workspace-resolution.ts +++ b/libs/cockpit-registry/src/lib/workspace-resolution.ts @@ -16,7 +16,7 @@ export const toWorkspaceIdentity = ( page: entry.page, language: entry.language, title: entry.title, - docsPath: entry.docsPath || null, + docsPath: entry.docsPath, workspacePath: entry.workspacePath, legacyPath: entry.legacyPath, runtimeAdapter: entry.runtimeAdapter, diff --git a/scripts/ci-workflow.spec.mjs b/scripts/ci-workflow.spec.mjs index 7b83beb43..76f88e6f8 100644 --- a/scripts/ci-workflow.spec.mjs +++ b/scripts/ci-workflow.spec.mjs @@ -942,7 +942,12 @@ describe('CI workflow', () => { assert.ok(runMany, 'cockpit job should run tests via nx run-many'); const projects = runMany[1].split(','); - for (const project of ['cockpit', 'cockpit-docs', 'cockpit-registry']) { + for (const project of [ + 'cockpit', + 'cockpit-docs', + 'cockpit-registry', + 'cockpit-shell', + ]) { assert.ok( projects.includes(project), `cockpit job should run \`nx test ${project}\`` From af4127d2c477c4e07dad24b7b91b5786cabf6964 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 5 Sep 2026 09:30:55 -0700 Subject: [PATCH 11/12] fix(website): keep the docs index Run link honest after the resolver swap; drop dead workspace helpers - resolveDocsWorkspace never returns null (a miss resolves docs-only), so the docs index's Run href guard is now gated on resolution.kind === 'mapped' instead of a dead null check, moved into a lib module with a unit test that would have caught the regression - drop dead ?? fallbacks on the now-nonnullable WorkspaceIdentity.docsPath in workspace-presentation.ts - delete the unused getWebsiteWorkspaceHref helper - fix WebsiteWorkspace.spec.tsx tests that override docsSlot: undefined even though the docs route they model always supplies a slot Co-Authored-By: Claude Fable 5.1 --- apps/website/src/app/docs/page.tsx | 18 +--------------- .../workspace/WebsiteWorkspace.spec.tsx | 3 --- .../src/lib/docs-index-example.spec.ts | 14 +++++++++++++ apps/website/src/lib/docs-index-example.ts | 21 +++++++++++++++++++ apps/website/src/lib/workspace-page.ts | 6 ------ .../src/lib/workspace-presentation.ts | 4 ++-- 6 files changed, 38 insertions(+), 28 deletions(-) create mode 100644 apps/website/src/lib/docs-index-example.spec.ts create mode 100644 apps/website/src/lib/docs-index-example.ts diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index de479bbfa..718612dea 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -9,10 +9,7 @@ import { DocsControlPlane } from '../../components/docs/DocsControlPlane'; import { DocsSearch } from '../../components/docs/DocsSearch'; import { DocsSearchFooter } from '../../components/docs/DocsSearchFooter'; import { createPageMetadata } from '../../lib/site-metadata'; -import { - getCanonicalWebsiteWorkspaceHref, - resolveDocsWorkspace, -} from '@threadplane/cockpit-registry'; +import { DEFAULT_EXAMPLE_RUN_HREF } from '../../lib/docs-index-example'; export const metadata = createPageMetadata({ title: 'Documentation — Threadplane', @@ -22,19 +19,6 @@ export const metadata = createPageMetadata({ type: 'website', }); -/** - * The example the index's Run rail item opens, resolved through the registry - * so a renamed or removed capability yields null and Run falls back to - * disabled rather than to a dead link. - */ -const DEFAULT_EXAMPLE_RESOLUTION = resolveDocsWorkspace( - '/docs/langgraph/guides/streaming', - 'Streaming' -); -const DEFAULT_EXAMPLE_RUN_HREF = DEFAULT_EXAMPLE_RESOLUTION - ? getCanonicalWebsiteWorkspaceHref(DEFAULT_EXAMPLE_RESOLUTION, 'Run') - : undefined; - interface Backend { title: string; blurb: string; diff --git a/apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx b/apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx index fc27b190e..16d7fa4bd 100644 --- a/apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx +++ b/apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx @@ -462,7 +462,6 @@ describe('WebsiteWorkspace', () => { resolution, presentation: mappedPresentation(resolution), routePath: '/docs/langgraph/guides/durable-execution', - docsSlot: undefined, }); await waitFor(() => expect(activeWorkspaceMode()).toBe('Run')); @@ -731,7 +730,6 @@ describe('WebsiteWorkspace', () => { resolution: source, presentation: mappedPresentation(source), routePath: '/docs/langgraph/guides/durable-execution', - docsSlot: undefined, }); await waitFor(() => expect(activeWorkspaceMode()).toBe('Docs')); @@ -776,7 +774,6 @@ describe('WebsiteWorkspace', () => { resolution: source, presentation: mappedPresentation(source), routePath: '/docs/langgraph/guides/durable-execution', - docsSlot: undefined, }); await waitFor(() => expect(activeWorkspaceMode()).toBe('Code')); mocks.replace.mockClear(); diff --git a/apps/website/src/lib/docs-index-example.spec.ts b/apps/website/src/lib/docs-index-example.spec.ts new file mode 100644 index 000000000..2d2d1c43a --- /dev/null +++ b/apps/website/src/lib/docs-index-example.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { + DEFAULT_EXAMPLE_RESOLUTION, + DEFAULT_EXAMPLE_RUN_HREF, +} from './docs-index-example'; + +describe('docs index default example', () => { + it('resolves the default example to a mapped capability so the Run link cannot be dead', () => { + expect(DEFAULT_EXAMPLE_RESOLUTION.kind).toBe('mapped'); + expect(DEFAULT_EXAMPLE_RUN_HREF).toBe( + '/docs/langgraph/guides/streaming?mode=run' + ); + }); +}); diff --git a/apps/website/src/lib/docs-index-example.ts b/apps/website/src/lib/docs-index-example.ts new file mode 100644 index 000000000..ed817ab78 --- /dev/null +++ b/apps/website/src/lib/docs-index-example.ts @@ -0,0 +1,21 @@ +import { + getCanonicalWebsiteWorkspaceHref, + resolveDocsWorkspace, +} from '@threadplane/cockpit-registry'; + +/** + * The example the index's Run rail item opens, resolved through the + * registry. `resolveDocsWorkspace` never returns null; a renamed or removed + * capability resolves as docs-only, so the href is derived only from a + * mapped resolution and Run falls back to disabled rather than to a dead + * link. + */ +export const DEFAULT_EXAMPLE_RESOLUTION = resolveDocsWorkspace( + '/docs/langgraph/guides/streaming', + 'Streaming' +); + +export const DEFAULT_EXAMPLE_RUN_HREF = + DEFAULT_EXAMPLE_RESOLUTION.kind === 'mapped' + ? getCanonicalWebsiteWorkspaceHref(DEFAULT_EXAMPLE_RESOLUTION, 'Run') + : undefined; diff --git a/apps/website/src/lib/workspace-page.ts b/apps/website/src/lib/workspace-page.ts index ba78ddb0f..b1e440e77 100644 --- a/apps/website/src/lib/workspace-page.ts +++ b/apps/website/src/lib/workspace-page.ts @@ -1,8 +1,6 @@ import { cockpitManifest, - getWorkspaceDestinationPath, resolveDocsWorkspace, - type CockpitManifestEntry, type WorkspaceResolution, } from '@threadplane/cockpit-registry'; import { @@ -35,7 +33,3 @@ export async function getWebsiteWorkspacePage(options: { navigationTree: buildNavigationTree(cockpitManifest), }; } - -export function getWebsiteWorkspaceHref(entry: CockpitManifestEntry): string { - return getWorkspaceDestinationPath(entry); -} diff --git a/libs/cockpit-shell/src/lib/workspace-presentation.ts b/libs/cockpit-shell/src/lib/workspace-presentation.ts index 666f20892..dd4a1f078 100644 --- a/libs/cockpit-shell/src/lib/workspace-presentation.ts +++ b/libs/cockpit-shell/src/lib/workspace-presentation.ts @@ -242,7 +242,7 @@ export const getWorkspacePresentation = ( if (!descriptor) { return { kind: 'docs-only', - docsPath: resolution.identity.docsPath ?? '', + docsPath: resolution.identity.docsPath, title: resolution.identity.title, runnable: false, }; @@ -251,7 +251,7 @@ export const getWorkspacePresentation = ( return { kind: 'capability', identity: cloneWorkspaceIdentity(resolution.identity), - docsPath: resolution.identity.docsPath ?? descriptor.docsPath, + docsPath: resolution.identity.docsPath, promptAssetPaths: [...descriptor.promptAssetPaths], codeAssetPaths: [...descriptor.codeAssetPaths], backendAssetPaths: [...(descriptor.backendAssetPaths ?? [])], From 66e4a52f5e8c095797da7f556114f601acc02dd5 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 5 Sep 2026 09:32:10 -0700 Subject: [PATCH 12/12] docs(plans): Part C also removes resolveWorkspacePath and workspacePath; docsPath is non-nullable Co-Authored-By: Claude Fable 5.1 --- .../superpowers/plans/2026-09-04-docs-workspace-unification.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md b/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md index 4087188bf..7cbceda44 100644 --- a/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md +++ b/docs/superpowers/plans/2026-09-04-docs-workspace-unification.md @@ -737,7 +737,7 @@ In `libs/cockpit-registry/src/lib/workspace-resolution.spec.ts`: delete the test ``` In `libs/workspace-react/src/lib/workspace-provider.spec.tsx`: delete `legacyPath` from the identity fixture (line 34); replace `path: identity.legacyPath` (line 84) and `identity.legacyPath` (line 334) with `identity.docsPath`; delete every `routeKind: 'workspace',` (lines 202, 220, 262) — the tests then exercise the docs default, so change any expectation that relied on the Run default to `'Docs'` (the assertions near lines 210–215 expect `'Docs'` already; re-read each after the edit). -In `libs/workspace-react/src/lib/workspace-shell.spec.tsx`: replace `routeKind="workspace"` (line 339) with `routeKind="docs"` and `routePath={identity.legacyPath}` (line 340) with `routePath={identity.docsPath ?? '/docs'}`. +In `libs/workspace-react/src/lib/workspace-shell.spec.tsx`: replace `routeKind="workspace"` (line 339) with `routeKind="docs"` and `routePath={identity.legacyPath}` (line 340) with `routePath={identity.docsPath}`. In `apps/website/src/components/workspace/WebsiteWorkspace.spec.tsx:106`: delete the `legacyPath:` line from the fixture. - [ ] **Step 2: Run to verify they fail** @@ -767,6 +767,7 @@ git rm -r apps/cockpit vercel.cockpit.json libs/cockpit-docs libs/cockpit-testin ``` - `tsconfig.base.json`: delete the `@threadplane/cockpit-docs`, `@threadplane/cockpit-testing`, `@threadplane/cockpit-ui` path entries (lines 26, 38, 39). `apps/website/tsconfig.json`: delete the `@threadplane/cockpit-docs` entry (line 23). +- `resolveWorkspacePath` in `libs/cockpit-registry/src/lib/workspace-resolution.ts` and the `workspacePath` field on `WorkspaceIdentity` / `CockpitManifestEntry` (`manifest.types.ts`, `manifest.ts`, `toWorkspaceIdentity`, and the `Duplicate workspace path` / `Invalid workspacePath` checks in `validate-manifest.ts`), plus every spec case and fixture that references them (`workspace-resolution.spec.ts`, `validate-manifest.spec.ts`, `cockpit-shell/**/*.spec.ts`, `workspace-react/**/sidebar/*.spec.tsx`, `WebsiteWorkspace.spec.tsx`). Nothing outside tests reads either after Part A. - `apps/website/e2e/platform-production-smoke.spec.ts`: delete `expectedRedirect`, `docsBacked`, `COCKPIT_REDIRECT_CASES`, and the test(s) that iterate them; delete the now-unused imports (`resolveLegacyPath`, `resolveLegacyRequestMode`, `getCanonicalWebsiteWorkspaceHref` if unused, `COCKPIT_URL` env reads). Keep `WEBSITE_DESTINATIONS` and every Website-facing test. - `scripts/ag-ui-proxy.ts:54` and `scripts/examples-middleware.ts:18`: delete the `'https://cockpit.threadplane.ai',` line. - `git grep -n "cockpit.threadplane.ai" -- . ':!docs/superpowers' ':!CONTRIBUTING.md'` → only `apps/website/src/lib/cockpit-retirement.spec.ts` (the guard's own constant) and `.github/workflows/ci.yml` (removed in Task C3) may remain.