Skip to content
Merged
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
@@ -1,6 +1,6 @@
---
title: Planning
description: TodoListMiddleware puts a todos array on the graph state, and an Angular panel renders the plan as the agent revises it mid-run.
description: Render evolving todo plans with TodoListMiddleware. Angular is the default; the native React preview shows live and saved plans on the same Python graph.
---

# Planning
Expand Down
227 changes: 227 additions & 0 deletions apps/website/e2e/react-deep-agents-planning-preview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { test, expect, type Page } from '@playwright/test';
import { fromPageOrReactPreview } from './fixtures/react-preview-requests';
const route = '/docs/deep-agents/capabilities/planning';
const reactFrame = /(?:localhost:4628|deep-agents\/planning\/react)/;
const observations = new WeakMap<
Page,
{ requests: string[]; errors: string[] }
>();
test.beforeEach(async ({ page }) => {
const observed = { requests: [] as string[], errors: [] as string[] };
observations.set(page, observed);
page.on('request', (request) => {
if (
['fetch', 'xhr'].includes(request.resourceType()) &&
/\/threads(?:\/|$)/.test(new URL(request.url()).pathname) &&
fromPageOrReactPreview(request, reactFrame)
)
observed.requests.push(request.method());
});
page.on('pageerror', (error) => observed.errors.push(error.message));
});
test.afterEach(async ({ page }) => {
expect(observations.get(page)?.requests).toEqual([]);
expect(observations.get(page)?.errors).toEqual([]);
});
async function hydrated(page: Page, mode: 'docs' | 'code' | 'run') {
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
'data-hydrated',
'true'
);
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
'data-workspace-mode',
{ docs: 'Docs', code: 'Code', run: 'Run' }[mode]
);
}
async function emptyRun(page: Page, draft = '') {
await hydrated(page, 'run');
await expect(page.getByLabel('Example UI')).toHaveValue('react');
await expect(page).toHaveURL(/(?:\?|&)mode=run(?:&|$)/);
await expect(page.locator('iframe')).toBeVisible();
await expect(page.locator('iframe')).toHaveAttribute('src', reactFrame);
const frame = page.frameLocator('iframe');
await expect(frame.getByRole('status')).toHaveText('Ready.');
await expect(
frame
.getByRole('region', { name: 'Conversation', exact: true })
.locator('article')
).toHaveCount(0);
await expect(frame.getByLabel('Message', { exact: true })).toHaveValue(draft);
}
test('native Deep Agents Planning keeps canonical Docs, Code and Run aligned', async ({
page,
}, testInfo) => {
await page.goto(route + '?frontend=react', { waitUntil: 'domcontentloaded' });
await hydrated(page, 'docs');
await expect(page.getByLabel('Example UI')).toHaveValue('react');
await expect(
page.getByRole('heading', {
name: 'React Deep Agents Planning preview',
exact: true,
})
).toBeVisible();
await expect(
page.locator(
'[data-example-file="cockpit/deep-agents/planning/react/src/application.ts"]'
)
).toBeVisible();
await page.screenshot({
path: testInfo.outputPath('react-deep-agents-planning-docs-desktop.png'),
});
await page.setViewportSize({ width: 390, height: 844 });
await page.screenshot({
path: testInfo.outputPath('react-deep-agents-planning-docs-mobile.png'),
});
await page.setViewportSize({ width: 1280, height: 720 });
await page
.locator('[data-workspace-desktop-navigation]')
.getByRole('button', { name: 'Code', exact: true })
.click();
await hydrated(page, 'code');
for (const name of [
'app.tsx',
'application.ts',
'connection.ts',
'plan-state.ts',
'plan-panel.tsx',
'tool-evidence.ts',
'terminal.ts',
'main.tsx',
'styles.css',
])
await expect(
page
.getByRole('complementary', { name: 'File tree' })
.getByRole('button', { name, exact: true })
).toBeVisible();
await expect(page.getByRole('tabpanel')).toContainText(
'createConnectedApplication'
);
const graph = page
.getByRole('complementary', { name: 'File tree' })
.getByRole('button', { name: 'graph.py', exact: true });
await graph.click();
await expect(page.getByRole('tabpanel')).toContainText('TodoListMiddleware');
await expect(page).toHaveURL(/(?:\?|&)mode=code(?:&|$)/);
await page
.locator('[data-workspace-desktop-navigation]')
.getByRole('button', { name: /^Run(?:,|$)/ })
.click();
await emptyRun(page);
await page.screenshot({
path: testInfo.outputPath('react-deep-agents-planning-run-desktop.png'),
});
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.getByLabel('Example UI')).toBeVisible();
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth
)
).toBe(true);
expect(
await page
.frameLocator('iframe')
.locator('html')
.evaluate((element) => element.scrollWidth <= element.clientWidth)
).toBe(true);
await page.screenshot({
path: testInfo.outputPath('react-deep-agents-planning-run-mobile.png'),
});
});
test('the canonical Planning guide keeps Angular as its default frontend', async ({
page,
}) => {
await page.goto(route, { waitUntil: 'domcontentloaded' });
await hydrated(page, 'docs');
await expect(page.getByLabel('Example UI')).toHaveValue('angular');
await expect(
page.getByRole('heading', {
name: 'React Deep Agents Planning preview',
exact: true,
})
).toHaveCount(0);
await page.getByLabel('Example UI').selectOption('react');
await expect(
page.getByRole('heading', {
name: 'React Deep Agents Planning preview',
exact: true,
})
).toBeVisible();
await expect(page).toHaveURL(/frontend=react/);
});
test('a direct React run link mounts an empty conversation without creating a thread', async ({
page,
}) => {
await page.goto(route + '?frontend=react&mode=run', {
waitUntil: 'domcontentloaded',
});
await emptyRun(page);
});
test('history preserves the mounted React owner and unsent draft without runtime requests', async ({
page,
}) => {
await page.goto(route + '?frontend=react&mode=run', {
waitUntil: 'domcontentloaded',
});
await emptyRun(page);
const mountedFrame = await page.locator('iframe').elementHandle();
await page
.frameLocator('iframe')
.getByLabel('Message', { exact: true })
.fill('Unsent history draft');
await expect(
page.frameLocator('iframe').getByLabel('Message', { exact: true })
).toHaveValue('Unsent history draft');
const before = [...(observations.get(page)?.requests ?? [])];
await page
.locator('[data-workspace-desktop-navigation]')
.getByRole('button', { name: 'Code', exact: true })
.click();
await hydrated(page, 'code');
await expect(page).toHaveURL(/(?:\?|&)mode=code(?:&|$)/);
await page.goBack({ waitUntil: 'domcontentloaded' });
await emptyRun(page, 'Unsent history draft');
expect(
await page
.locator('iframe')
.evaluate((frame, original) => frame === original, mountedFrame)
).toBe(true);
expect(observations.get(page)?.requests).toEqual(before);
});
test('reload clears an unsent React draft without runtime requests', async ({
page,
}) => {
await page.goto(route + '?frontend=react&mode=run', {
waitUntil: 'domcontentloaded',
});
await emptyRun(page);
const before = [...(observations.get(page)?.requests ?? [])];
await page
.frameLocator('iframe')
.getByLabel('Message', { exact: true })
.fill('Unsent reload draft');
await expect(
page.frameLocator('iframe').getByLabel('Message', { exact: true })
).toHaveValue('Unsent reload draft');
await page.reload({ waitUntil: 'domcontentloaded' });
await emptyRun(page);
expect(observations.get(page)?.requests).toEqual(before);
});

test('the aviation suggestion fills an exact inert draft', async ({ page }) => {
await page.goto(route + '?frontend=react&mode=run', {
waitUntil: 'domcontentloaded',
});
await emptyRun(page);
const frame = page.frameLocator('iframe');
await frame
.getByRole('button', { name: 'Dispatch brief: KSFO to KASE', exact: true })
.click();
await expect(frame.getByLabel('Message', { exact: true })).toHaveValue(
'Plan a dispatch brief for a flight from KSFO to KASE: check field elevation, runway length, and weather at both ends, then tell me if there is anything the crew should know.'
);
await frame
.getByRole('button', { name: 'New conversation', exact: true })
.click();
await emptyRun(page);
});
7 changes: 7 additions & 0 deletions apps/website/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,13 @@ export const createWebsitePlaywrightConfig = (
reuseExistingServer,
timeout: 180_000,
},
{
command: 'npx nx run cockpit-deep-agents-planning-python:smoke && node scripts/react-cockpit/serve.mjs deep-agents-planning --no-parent',
cwd: '../..',
url: 'http://127.0.0.1:4628',
reuseExistingServer,
timeout: 180_000,
},
{
command: 'npx nx run cockpit-chat-generative-ui-python:smoke && node scripts/react-cockpit/serve.mjs chat-generative-ui --no-parent',
cwd: '../..',
Expand Down
3 changes: 2 additions & 1 deletion apps/website/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@
"cockpit-chat-subagents-react",
"cockpit-chat-threads-react",
"cockpit-chat-timeline-react",
"cockpit-chat-generative-ui-react"
"cockpit-chat-generative-ui-react",
"cockpit-deep-agents-planning-react"
]
}
],
Expand Down
13 changes: 13 additions & 0 deletions apps/website/src/app/docs/[library]/[section]/[slug]/page.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ import { ReactTimeTravelPreview } from '../../../../../components/docs/ReactTime
import { WebsiteWorkspace } from '../../../../../components/workspace/WebsiteWorkspace';
import DocsPage, { generateMetadata } from './page';

it('selects Planning-specific native React Docs and sources while retaining Angular Docs', async () => {
const tree = await route('deep-agents', 'capabilities', 'planning');
const workspace = findElement(tree, WebsiteWorkspace as ComponentType<never>);
const article = workspace?.props.reactDocsSlot as React.ReactElement<ElementProps> | undefined;
expect(article).toBeTruthy();
expect((article?.type as { name?: string })?.name).toBe('ReactDeepAgentsPlanningPreview');
expect(article?.props.exampleCode?.assetPaths).toContain('cockpit/deep-agents/planning/react/src/plan-state.ts');
expect(article?.props.exampleCode?.sources?.['cockpit/deep-agents/planning/react/src/terminal.ts']).toContain('checkpoint');
const angular = findElement(workspace?.props.docsSlot, MdxRenderer as ComponentType<never>);
expect(angular?.props.exampleCode?.assetPaths.some(path => path.includes('/planning/angular/'))).toBe(true);
expect(findElement(workspace?.props.reactDocsSlot, ReactStreamingPreview as ComponentType<never>)).toBeNull();
});

interface ElementProps {
children?: ReactNode;
docsSlot?: ReactNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ReactStateManagementPreview } from '../../../../../components/docs/Reac
import { ReactComputedFunctionsPreview } from '../../../../../components/docs/ReactComputedFunctionsPreview';
import { ReactChatMessagesPreview } from '../../../../../components/docs/ReactChatMessagesPreview';
import { ReactChatGenerativeUiPreview } from '../../../../../components/docs/ReactChatGenerativeUiPreview';
import { ReactDeepAgentsPlanningPreview } from '../../../../../components/docs/ReactDeepAgentsPlanningPreview';
import { ReactChatTimelinePreview } from '../../../../../components/docs/ReactChatTimelinePreview';
import { ReactChatThreadsPreview } from '../../../../../components/docs/ReactChatThreadsPreview';
import { ReactChatSubagentsPreview } from '../../../../../components/docs/ReactChatSubagentsPreview';
Expand Down Expand Up @@ -282,10 +283,13 @@ export default async function DocsPage({ params }: DocsRouteProps) {
reactDocsSlot={
workspacePage.frontendVariants?.react &&
(library === 'langgraph' ||
(library === 'deep-agents' && section === 'capabilities' && slug === 'planning') ||
(library === 'render' && ((section === 'guides' && ['specs','state-store','repeat-loops','registry'].includes(slug)) || (section === 'api' && ['render-spec-component','provide-render'].includes(slug)))) ||
(library === 'ag-ui' && ((section === 'reference' && slug === 'event-mapping') || (section === 'guides' && ['interrupts','tool-views','json-render','subagents'].includes(slug)))) ||
(library === 'chat' && ((section === 'guides' && ['thread-routing', 'generative-ui'].includes(slug)) || slug === 'client-tools' || (section === 'concepts' && slug === 'message-model') || (section === 'components' && ['chat-trace', 'chat-input', 'chat-interrupt-panel', 'chat-tool-calls', 'chat-subagent-card'].includes(slug))))) ? (
library === 'render' ? (
library === 'deep-agents' && section === 'capabilities' && slug === 'planning' ? (
<ReactDeepAgentsPlanningPreview exampleCode={getExampleCodeContext(workspacePage.frontendVariants.react)} />
) : library === 'render' ? (
slug === 'provide-render' ? <ReactComputedFunctionsPreview exampleCode={getExampleCodeContext(workspacePage.frontendVariants.react)} /> :
slug === 'render-spec-component' ? <ReactElementRenderingPreview exampleCode={getExampleCodeContext(workspacePage.frontendVariants.react)} /> :
slug === 'registry' ? <ReactRegistryPreview exampleCode={getExampleCodeContext(workspacePage.frontendVariants.react)} /> :
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { MdxRenderer } from './MdxRenderer';
import type { ExampleCodeContext } from '../../lib/example-code';

const source = `# React Deep Agents Planning preview

This experimental example composes native React chat components with a read-only plan panel. The React, core, content and native LangGraph candidates are private packages built from source. Angular remains the default on this canonical Planning guide. Both frontends use the same Python graph, planning prompt and shared FREE runtime.

## Start a dispatch brief

Choose **Run**, type a message and choose **Send** or press **Enter**. **Dispatch brief: KSFO to KASE** fills the draft with the existing aviation suggestion; it does not send. Airport elevation, runway and weather lookups use fixed tables in the unchanged graph.

The first Send creates the conversation. **New conversation** clears the local view and waits for Send to create another conversation. A failed creation can be retried. Opening Docs, Code or Run makes no thread request. Mode navigation and browser history preserve the mounted frame and unsent draft; reloading clears local state.

<ExampleCode file="cockpit/deep-agents/planning/react/src/app.tsx" />
<ExampleCode file="cockpit/deep-agents/planning/react/src/connection.ts" />

## Watch the current plan

Each valid todos update replaces the whole observed list while the response is running. The panel preserves duplicate tasks and their order, and an empty list clears it. Empty task text has a visible placeholder. Validation allows at most 50 items and 2000 UTF-16 code units per task. An invalid update retains the last valid observed plan and shows a notice; a later valid update clears that notice.

The read-only panel shows the backend's pending, in-progress and completed statuses and derives progress from them. Finishing a response does not mark unfinished tasks completed. **Live observed plan** and **Last saved plan** are separate: a visible streamed update has not yet been confirmed as saved.

<ExampleCode file="cockpit/deep-agents/planning/react/src/plan-state.ts" />
<ExampleCode file="cockpit/deep-agents/planning/react/src/plan-panel.tsx" />

## Confirm what was saved

A tool marked complete means its result was received; it does not prove the plan was applied. Tool arguments, received results and errors remain inspectable beside the conversation. Saved confirmation separately checks the raw tool result status. The example confirms a saved plan only after the actual run succeeds and its exact saved checkpoint agrees with the current human request, retained conversation prefix, canonical native messages and successful write_todos evidence. Tool arguments and checkpoint todos must match exactly, including order and duplicates. Prose and printed Python representations do not supply plan authority.

<ExampleCode file="cockpit/deep-agents/planning/react/src/application.ts" />
<ExampleCode file="cockpit/deep-agents/planning/react/src/tool-evidence.ts" />
<ExampleCode file="cockpit/deep-agents/planning/react/src/terminal.ts" />

## Stop and recover

**Stop** keeps the last saved plan visible and requires **New conversation** before another Send. A first native failure before any saved checkpoint also requires New conversation. A later unconfirmed request keeps the saved plan separate; a later valid write can recover against the current conversation prefix. A response with no write can retain an unchanged, previously confirmed plan, but cannot adopt changed, unconfirmed todos. These are local display and submission rules, not a claim that stopping rolls back server state. There is no automatic replay or resume.

## Build from source

From a Threadplane checkout with dependencies installed:

~~~sh
npx nx build cockpit-deep-agents-planning-react
npx nx e2e cockpit-deep-agents-planning-react
~~~

Browser verification uses the real SDK, compiled Python graph and saved checkpoints with deterministic providers. Verification endpoints are excluded from public output.
`;

export function ReactDeepAgentsPlanningPreview({
exampleCode,
}: {
readonly exampleCode: ExampleCodeContext | null;
}) {
return (
<div className="docs-workspace-article">
<article className="docs-article-layout flex-1 py-8 px-4 sm:px-6 md:px-12 md:max-w-3xl">
<MdxRenderer source={source} exampleCode={exampleCode} />
</article>
</div>
);
}
17 changes: 16 additions & 1 deletion apps/website/src/lib/docs-example-code.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import { capabilityModules } from '@threadplane/cockpit-registry';
import { capabilityModules, getFrontendCapabilityDescriptor } from '@threadplane/cockpit-registry';
import { describe, expect, it } from 'vitest';
import {
resolveExampleFile,
Expand Down Expand Up @@ -28,6 +28,21 @@ const findWorkspaceRoot = (): string => {
const WORKSPACE_ROOT = findWorkspaceRoot();
const CONTENT_ROOT = join(WORKSPACE_ROOT, 'apps/website/content');

it('resolves every native Planning article include from its registered sources and shared backend', () => {
const canonical = capabilityModules.find(entry => entry.id === 'deep-agents-planning-python')!;
const react = getFrontendCapabilityDescriptor(canonical.manifestIdentity as never, 'react')!;
const page = { docsPath: react.docsPath, assetPaths: [...react.codeAssetPaths, ...(react.backendAssetPaths ?? [])] };
const context = contextFor(page);
const article = readFileSync(join(WORKSPACE_ROOT, 'apps/website/src/components/docs/ReactDeepAgentsPlanningPreview.tsx'), 'utf8');
for (const include of includesIn(article)) {
expect(resolveExampleFile(include.file, context)).toBe(include.file);
expect(context.sources[include.file]?.length).toBeGreaterThan(0);
}
expect(includesIn(article)).toHaveLength(7);
expect(page.assetPaths.some(path => /fixture|wire\.py|\.spec\./.test(path))).toBe(false);
for (const path of page.assetPaths) expect(context.sources[path]?.length).toBeGreaterThan(0);
});

interface MappedPage {
readonly docsPath: string;
readonly assetPaths: readonly string[];
Expand Down
Loading
Loading