Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ It is not the whole story.

Three places to look next:

- The [cockpit chat example](/docs/chat/getting-started/quickstart) — a full multi-thread, multi-agent chat shell with real LangGraph graphs behind it.
- The [chat quickstart example](/docs/chat/getting-started/quickstart) — a full multi-thread, multi-agent chat shell with real LangGraph graphs behind it.
- The [persistence guide](/docs/chat/guides/lifecycle) — how to manage threads, runs, and resume state across sessions.
- The [interrupts guide](/docs/chat/guides/streaming) — how to handle human-in-the-loop pauses without breaking the streaming model.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ The `<chat-approval-card>` you build today survives the backend you swap in next

Three pieces:

- **AG-UI-fronted LangGraph backend.** The same compiled graph as the LangGraph post, duplicated under `cockpit/ag-ui/interrupts/python` because cockpit examples are standalone.
- **AG-UI-fronted LangGraph backend.** The same compiled graph as the LangGraph post, duplicated under `cockpit/ag-ui/interrupts/python` because example apps are standalone.
Wrapped with `ag-ui-langgraph`'s `LangGraphAgent(name, graph)` + `add_langgraph_fastapi_endpoint(app, agent, path='/agent')` and served by `uvicorn`.
When `interrupt()` fires inside the graph, `ag-ui-langgraph` emits a `CUSTOM` AG-UI event with `name: 'on_interrupt'` and a `value` carrying the interrupt payload — serialized as a JSON string via `dump_json_safe`.
The uvicorn server needs a `MemorySaver` checkpointer (`langgraph dev` and LangGraph Platform inject one automatically; plain uvicorn does not, and `aget_state` raises "No checkpointer set" without it).
Expand Down Expand Up @@ -166,7 +166,7 @@ def request_approval(state: RefundState) -> dict:
}
```

This file is **duplicated** into `cockpit/ag-ui/interrupts/python/src/graph.py` per the cockpit standalone-examples convention — copy, do not import across examples.
This file is **duplicated** into `cockpit/ag-ui/interrupts/python/src/graph.py` per the standalone-examples convention — copy, do not import across examples.
The graph itself does not know it will be served over AG-UI.

</Step>
Expand Down Expand Up @@ -246,7 +246,7 @@ const WELCOME_SUGGESTIONS = [
] as const;

/**
* Refund authorization cockpit example.
* Refund authorization example.
*
* The LangGraph backend acknowledges the refund draft, then pauses at
* `request_approval` with a structured interrupt payload of the form
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ It cannot tell you that your graph's conditional edge routes correctly, that you
Push the seam out to the provider and all of that is under test, because none of it was replaced.

Replacing the model — and only the model — is also what makes it cheap enough to run everywhere: no API spend, no rate limits, no coin flips.
We have 53 fixture files holding 169 entries across 36 apps — 34 cockpit capabilities and two example apps — all on the same harness.
We have 53 fixture files holding 169 entries across 36 apps — 34 workspace capabilities and two example apps — all on the same harness.

This is the outer tier.
For in-process fakes at the unit level, the [testing guide](/docs/langgraph/guides/testing) covers `provideFakeAgent()`, `mockLangGraphAgent()`, and `MockAgentTransport`, which are a different tool for a different job.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ Anything reaching for `AgentWithHistory` — or for the `langGraph*`-prefixed me

### The test harness forks

Each cockpit pair uses a different end-to-end harness entry point: `createGlobalSetup` for the LangGraph twin, `createAgUiGlobalSetup` for the AG-UI twin.
Each workspace pair uses a different end-to-end harness entry point: `createGlobalSetup` for the LangGraph twin, `createAgUiGlobalSetup` for the AG-UI twin.
Different backend process, different port wiring, different fixture replay.
A runtime swap is a test-infrastructure change, not only an app change.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Compositions are opinionated, styled components that combine primitives into rea
| `ChatInterruptPanelComponent` | `chat-interrupt-panel` | Styled interrupt card with accept/edit/respond/ignore actions |
| `ChatToolCallCardComponent` | `chat-tool-call-card` | Expandable card showing tool name, inputs, and output |
| `ChatSubagentCardComponent` | `chat-subagent-card` | Expandable card showing subagent status and messages |
| `ChatDebugComponent` | `chat-debug` | Debug cockpit from `@threadplane/chat/debug` with timeline, state inspector, and diff viewer |
| `ChatDebugComponent` | `chat-debug` | Debug panel from `@threadplane/chat/debug` with timeline, state inspector, and diff viewer |
| `ChatTimelineSliderComponent` | `chat-timeline-slider` | Slider-based timeline navigation |

## How the Stack Fits Together
Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/choosing-an-adapter/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Three layers of test doubles, smallest scope first:
|---|---|---|---|
| **Contract mock** | `mockAgent()` (chat) / `mockLangGraphAgent()` (langgraph) | nothing — you set `messages()`, `status()` directly | component/unit tests |
| **Fake backend** | `provideFakeAgent({ tokens })` (both adapters) | the real adapter pipeline; canned wire events | adapter-integration tests, in-browser, no server |
| **LLM fixture replay** | aimock (cockpit/examples e2e) | the whole stack incl. a real graph; only the LLM is replayed | full end-to-end |
| **LLM fixture replay** | aimock (workspace examples e2e) | the whole stack incl. a real graph; only the LLM is replayed | full end-to-end |

Reach for the smallest layer that covers your test. Don't stand up aimock when a `mockAgent` unit test suffices; don't hand-roll a transport when `provideFakeAgent` exists.

Expand Down
2 changes: 1 addition & 1 deletion apps/website/e2e/custom-runtime-bfcache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const FIXTURE_ORIGIN = 'http://127.0.0.1:4399';
const FIXTURE_KEY = 'test-key-redact-me';

const desktopControlPlane = (page: Page) =>
page.locator('[data-cockpit-desktop-navigation]');
page.locator('[data-workspace-desktop-navigation]');

async function openSettings(page: Page): Promise<void> {
await desktopControlPlane(page)
Expand Down
2 changes: 1 addition & 1 deletion apps/website/e2e/custom-runtime-targets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function observePrivacyChannels(page: Page): {
}

const desktopControlPlane = (page: Page) =>
page.locator('[data-cockpit-desktop-navigation]');
page.locator('[data-workspace-desktop-navigation]');

const runtimeFrame = (page: Page, title: string): FrameLocator =>
page.frameLocator(`iframe[title="${title} live example"]`);
Expand Down
2 changes: 1 addition & 1 deletion apps/website/e2e/docs-shell.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ test.describe('docs shell layout', () => {
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--nav-h')),
);
const tops = async () => ({
plane: await page.locator('[data-cockpit-desktop-navigation]').evaluate((el) => Math.round(el.getBoundingClientRect().top)),
plane: await page.locator('[data-workspace-desktop-navigation]').evaluate((el) => Math.round(el.getBoundingClientRect().top)),
toc: await page.locator('.docs-toc').evaluate((el) => Math.round(el.getBoundingClientRect().top)),
});

Expand Down
2 changes: 1 addition & 1 deletion apps/website/e2e/docs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ test.describe('Docs slug page', () => {
);

const pane = page.locator(
'[data-cockpit-desktop-navigation] [data-control-plane-pane]',
'[data-workspace-desktop-navigation] [data-control-plane-pane]',
);
// Search moved to the top of the pane (was the bottom Actions-bar item
// this test used to focus), so it can no longer stand in for "something
Expand Down
2 changes: 1 addition & 1 deletion apps/website/e2e/nav-height.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ test('the docs column starts directly under the nav at a tablet width', async ({
.first()
.evaluate((el) => el.getBoundingClientRect().bottom);
const shellTop = await page
.locator('.website-workspace-host .cockpit-shell')
.locator('.website-workspace-host .workspace-shell')
.evaluate((el) => el.getBoundingClientRect().top);

expect(Math.abs(shellTop - navBottom)).toBeLessThanOrEqual(1);
Expand Down
2 changes: 1 addition & 1 deletion apps/website/e2e/platform-production-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ test.describe('Production: unified runtime embedding policy', () => {
});

await page.goto(`${WEBSITE_URL}/docs/langgraph/guides/streaming?mode=run`);
const controls = page.locator('[data-cockpit-desktop-navigation]');
const controls = page.locator('[data-workspace-desktop-navigation]');
await controls
.getByRole('button', { name: 'Settings', exact: true })
.click();
Expand Down
8 changes: 4 additions & 4 deletions apps/website/e2e/website.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ for (const viewport of [
);
await expectNoHorizontalOverflow(page, `Docs at ${viewport.width}px`);

const desktopControlPlane = page.locator('[data-cockpit-desktop-navigation]');
const desktopControlPlane = page.locator('[data-workspace-desktop-navigation]');
await expect(page.getByRole('button', { name: 'Open menu' })).toBeHidden();
if (viewport.width >= 1024) {
await expect(desktopControlPlane).toBeVisible();
Expand Down Expand Up @@ -306,7 +306,7 @@ for (const viewport of [
name: 'Documentation control plane',
});
await expect(dialog).toBeVisible();
await expect(page.locator('[data-cockpit-workspace]')).toHaveAttribute('inert', '');
await expect(page.locator('[data-workspace-surface]')).toHaveAttribute('inert', '');
await expect(page.locator('nav.nav-bar')).toHaveAttribute('inert', '');

const close = dialog.getByRole('button', { name: 'Close navigation' });
Expand Down Expand Up @@ -338,7 +338,7 @@ test('docs forced colors preserve control boundaries and keyboard focus', async
);

const run = page
.locator('[data-cockpit-desktop-navigation]')
.locator('[data-workspace-desktop-navigation]')
.getByRole('button', { name: 'Run', exact: true });
await run.focus();
const styles = await run.evaluate((element) => {
Expand Down Expand Up @@ -371,7 +371,7 @@ test('docs reduced motion disables mobile drawer transitions and animations', as
});
await expect(overlay).toBeVisible();
const motion = await overlay.evaluate((element) => {
const panel = element.querySelector('.cockpit-mobile-control-plane-panel');
const panel = element.querySelector('.workspace-mobile-control-plane-panel');
const overlayStyle = getComputedStyle(element);
const panelStyle = panel ? getComputedStyle(panel) : null;
return {
Expand Down
20 changes: 10 additions & 10 deletions apps/website/e2e/workspace-shell.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ declare global {
}

const modeButton = (page: Page, mode: 'Docs' | 'Run' | 'Code' | 'API') =>
page.locator('[data-cockpit-desktop-navigation]').getByRole('button', {
page.locator('[data-workspace-desktop-navigation]').getByRole('button', {
name: mode === 'Run' ? RUN_RAIL_ITEM : mode,
exact: mode !== 'Run',
});
Expand Down Expand Up @@ -269,15 +269,15 @@ test.describe('workspace shell', () => {
await expectMode(page, 'Docs');
await expect(shell).toHaveAttribute('data-e2e-shell-lifetime', 'original');
await page
.locator('[data-cockpit-desktop-navigation]')
.locator('[data-workspace-desktop-navigation]')
.getByRole('button', { name: 'Activity', exact: true })
.click();
await expect(page.getByText('Mode changed to Code')).toBeVisible();
await expect(
page.locator('[data-activity-capability]').first()
).toContainText('streaming');
await page
.locator('[data-cockpit-desktop-navigation]')
.locator('[data-workspace-desktop-navigation]')
.getByRole('button', { name: 'Activity', exact: true })
.click();
await page.goBack();
Expand Down Expand Up @@ -436,7 +436,7 @@ test.describe('workspace shell', () => {
await page.goto(streamingDocsPath);
await expectNoHorizontalOverflow(page, 'desktop workspace');

const desktop = page.locator('[data-cockpit-desktop-navigation]');
const desktop = page.locator('[data-workspace-desktop-navigation]');
await expect(desktop).toBeVisible();
await expect(desktop.locator('[data-control-plane-rail]')).toBeVisible();
await expect(desktop.locator('[data-control-plane-pane]')).toBeVisible();
Expand All @@ -463,7 +463,7 @@ test.describe('workspace shell', () => {
await expectNoHorizontalOverflow(page, `Website at ${viewport.width}px`);

const desktopNavigation = page.locator(
'[data-cockpit-desktop-navigation]'
'[data-workspace-desktop-navigation]'
);
const mobileTrigger = page.getByRole('button', {
name: 'Open navigation',
Expand Down Expand Up @@ -536,7 +536,7 @@ test.describe('workspace shell', () => {
await page.goto(streamingDocsPath);
await expectNoHorizontalOverflow(page, 'tablet workspace');

const desktop = page.locator('[data-cockpit-desktop-navigation]');
const desktop = page.locator('[data-workspace-desktop-navigation]');
await expect(desktop.locator('[data-control-plane-rail]')).toBeVisible();
await expect(desktop.locator('[data-control-plane-pane]')).toBeHidden();
const contextTrigger = page.getByRole('button', { name: 'Open context' });
Expand Down Expand Up @@ -586,7 +586,7 @@ test.describe('workspace shell', () => {
await expectNoHorizontalOverflow(page, 'mobile workspace');

await expect(
page.locator('[data-cockpit-desktop-navigation]')
page.locator('[data-workspace-desktop-navigation]')
).toBeHidden();
await expect(page.getByRole('button', { name: 'Open menu' })).toBeHidden();
const trigger = page.getByRole('button', { name: 'Open navigation' });
Expand Down Expand Up @@ -629,7 +629,7 @@ test.describe('workspace shell', () => {
});
const lateAnnouncement = page.locator('[data-late-announcement]');
await expect(lateAnnouncement).toBeHidden();
await expect(page.locator('[data-cockpit-workspace]')).toHaveAttribute(
await expect(page.locator('[data-workspace-surface]')).toHaveAttribute(
'inert',
''
);
Expand Down Expand Up @@ -789,7 +789,7 @@ test.describe('workspace shell', () => {
await expect(dialog).toBeVisible();
const motion = await dialog.evaluate((element) => {
const panel = element.querySelector(
'.cockpit-mobile-control-plane-panel'
'.workspace-mobile-control-plane-panel'
);
const overlayStyle = getComputedStyle(element);
const panelStyle = panel ? getComputedStyle(panel) : null;
Expand All @@ -800,7 +800,7 @@ test.describe('workspace shell', () => {
panelTransition: panelStyle?.transitionDuration,
};
});
const loader = dialog.locator('.cockpit-runtime-status-loader');
const loader = dialog.locator('.workspace-runtime-status-loader');
await expect(loader).toBeVisible();
expect(
await loader.evaluate(
Expand Down
10 changes: 5 additions & 5 deletions apps/website/scripts/capture-screenshots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('Website workspace screenshot capture', () => {
expect(WORKSPACE_READY_SELECTOR).toBe(
'[data-workspace-shell][data-hydrated="true"]'
);
expect(WORKSPACE_CONTENT_SELECTOR).toBe('[data-cockpit-workspace]');
expect(WORKSPACE_CONTENT_SELECTOR).toBe('[data-workspace-surface]');
expect(workspaceModeSelector('Code')).toBe(
'[data-workspace-shell][data-workspace-mode="Code"]'
);
Expand All @@ -52,10 +52,10 @@ describe('Website workspace screenshot capture', () => {

it('preserves all four modes and output basenames', () => {
expect(CAPTURE_TARGETS.map(({ name, mode }) => ({ name, mode }))).toEqual([
{ name: 'cockpit-run', mode: 'Run' },
{ name: 'cockpit-code', mode: 'Code' },
{ name: 'cockpit-docs', mode: 'Docs' },
{ name: 'cockpit-api', mode: 'API' },
{ name: 'workspace-run', mode: 'Run' },
{ name: 'workspace-code', mode: 'Code' },
{ name: 'workspace-docs', mode: 'Docs' },
{ name: 'workspace-api', mode: 'API' },
]);
});

Expand Down
10 changes: 5 additions & 5 deletions apps/website/scripts/capture-screenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const DEFAULT_WEBSITE_URL =
'https://threadplane.ai/docs/langgraph/guides/streaming?mode=run';
export const WORKSPACE_READY_SELECTOR =
'[data-workspace-shell][data-hydrated="true"]';
export const WORKSPACE_CONTENT_SELECTOR = '[data-cockpit-workspace]';
export const WORKSPACE_CONTENT_SELECTOR = '[data-workspace-surface]';

export interface CaptureTarget {
/** Output filename (without extension). */
Expand All @@ -48,16 +48,16 @@ export interface CaptureTarget {
export const CAPTURE_TARGETS: readonly CaptureTarget[] = [
// Hero collage back frame + Stream FeatureBlock + Pilot "Build" block.
// The "Run" mode shows the live chat surface — captures real product UI.
{ name: 'cockpit-run', mode: 'Run', settleMs: 4000 },
{ name: 'workspace-run', mode: 'Run', settleMs: 4000 },
// Hero collage front frame replacement — Code mode shows the agent
// source code in a tabbed code panel.
{ name: 'cockpit-code', mode: 'Code', settleMs: 1500 },
{ name: 'workspace-code', mode: 'Code', settleMs: 1500 },
// Render FeatureBlock visual — Docs mode shows narrative documentation
// (well-structured content, looks like rendered output).
{ name: 'cockpit-docs', mode: 'Docs', settleMs: 1500 },
{ name: 'workspace-docs', mode: 'Docs', settleMs: 1500 },
// API mode shows the API reference renderer — useful alternative
// for the Render block visual.
{ name: 'cockpit-api', mode: 'API', settleMs: 1500 },
{ name: 'workspace-api', mode: 'API', settleMs: 1500 },
];

export interface CaptureArgs {
Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/app/pilot-to-prod/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export default function PilotToProdPage() {
visual={
<BrowserFrame url="threadplane.ai/docs · Run" elevation="md">
<img
src="/screenshots/cockpit-run.webp"
src="/screenshots/workspace-run.webp"
alt="Threadplane Website workspace — live chat surface ready to receive a message"
className="pilot-screenshot"
loading="lazy"
Expand Down
36 changes: 36 additions & 0 deletions apps/website/src/lib/cockpit-retirement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,40 @@ describe('Cockpit surface retirement', () => {
expect(post).not.toContain('alt="The cockpit ag-ui/interrupts welcome screen');
expect(post).toContain('alt="The Threadplane docs workspace');
});

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
/cockpit-ag-ui-interrupts/g, // real project/app identifier (cockpit/ag-ui/interrupts)
/cockpit\/ports\.mjs/g, // real repo file path
/cockpit\/deep-agents/g, // real repo directory path
/cockpit\/runtimes\/<runtime>\//g, // real repo directory path (templated)
/serve:cockpit\b/g, // real Nx target name
/\("cockpit", "deep-agents-memory"\)/g, // real LangGraph memory namespace literal
];
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([]);
});
});
4 changes: 2 additions & 2 deletions apps/website/src/styles/docs.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ body:has([data-website-workspace-host]) {
overflow: hidden;
}

.website-workspace-host .cockpit-shell {
.website-workspace-host .workspace-shell {
height: calc(100dvh - var(--nav-h));
}

Expand All @@ -95,7 +95,7 @@ body:has([data-website-workspace-host]) {
}

@media (min-width: 48rem) and (max-width: 63.999rem) {
.website-workspace-host .cockpit-tablet-context-surface {
.website-workspace-host .workspace-tablet-context-surface {
top: var(--nav-h);
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/styles/style-contracts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const CONTRACTS: StyleContract[] = [
},
{
file: 'docs.css',
selector: '.website-workspace-host .cockpit-shell',
selector: '.website-workspace-host .workspace-shell',
why: 'The shared shell uses h-screen by default; the Website host must subtract its fixed nav or the bottom of every workspace panel is clipped.',
requires: {
height: /height:\s*calc\(100dvh\s*-\s*var\(--nav-h\)\)/,
Expand Down
Loading
Loading