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
2 changes: 2 additions & 0 deletions apps/website/content/AGENTS.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,5 @@ AG-UI Subagents has an experimental React preview at https://threadplane.ai/docs
Build with `npx nx build cockpit-ag-ui-subagents-react` and verify with `npx nx e2e cockpit-ag-ui-subagents-react`; the existing shared route is `/ag-ui/subagents/agent`. Browser proofs use the actual compiled graph and child bridge with frozen providers.

Render Spec has an experimental local React preview at https://threadplane.ai/docs/render/guides/specs?frontend=react. Three authored samples stream through a private partial JSON parser and installed native RenderSpec. Four registered read-only views receive literal props and native children. Play/Pause, seek, Finish and Reset share one local owner; sample changes and page exit revoke old callbacks. Copied frozen partial trees leave parser state private, and Complete requires the finished output to match the validated source. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-spec-rendering-react` and verify with `npx nx e2e cockpit-render-spec-rendering-react`.

State Management has an experimental local React preview at https://threadplane.ai/docs/render/guides/state-store?frontend=react. The React host owns copied immutable name, age and theme state; installed native RenderSpec resolves the three authored $state paths in read-only views. Edits survive partial playback, seek, Reset and sample selection. Invalid age drafts keep the last valid value, and reload restores the seed. Parser callbacks cannot replace host state; unknown bindings and executable fields are excluded. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-state-management-react` and verify with `npx nx e2e cockpit-render-state-management-react`.
2 changes: 2 additions & 0 deletions apps/website/content/CLAUDE.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,5 @@ AG-UI Subagents has an experimental React preview at https://threadplane.ai/docs
Build with `npx nx build cockpit-ag-ui-subagents-react` and verify with `npx nx e2e cockpit-ag-ui-subagents-react`; the existing shared route is `/ag-ui/subagents/agent`. Browser proofs use the actual compiled graph and child bridge with frozen providers.

Render Spec has an experimental local React preview at https://threadplane.ai/docs/render/guides/specs?frontend=react. Three authored samples stream through a private partial JSON parser and installed native RenderSpec. Four registered read-only views receive literal props and native children. Play/Pause, seek, Finish and Reset share one local owner; sample changes and page exit revoke old callbacks. Copied frozen partial trees leave parser state private, and Complete requires the finished output to match the validated source. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-spec-rendering-react` and verify with `npx nx e2e cockpit-render-spec-rendering-react`.

State Management has an experimental local React preview at https://threadplane.ai/docs/render/guides/state-store?frontend=react. The React host owns copied immutable name, age and theme state; installed native RenderSpec resolves the three authored $state paths in read-only views. Edits survive partial playback, seek, Reset and sample selection. Invalid age drafts keep the last valid value, and reload restores the seed. Parser callbacks cannot replace host state; unknown bindings and executable fields are excluded. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-state-management-react` and verify with `npx nx e2e cockpit-render-state-management-react`.
236 changes: 236 additions & 0 deletions apps/website/e2e/react-state-management-preview.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { test, expect, type Page } from '@playwright/test';

const route = '/docs/render/guides/state-store';
const iframe = (page: Page) =>
page.locator('iframe[title="State Management (React preview) live example"]');
const frame = (page: Page) => iframe(page).contentFrame();

test.beforeEach(async ({ page }) => {
const executions: string[] = [];
const errors: string[] = [];
page.on('request', (request) => {
if (
!['GET', 'HEAD'].includes(request.method()) &&
/\/agent(?:\/|$)|\/api\/native(?:\/|$)|\/api\/threads(?:\/|$)|\/threads(?:\/|$)/.test(
new URL(request.url()).pathname
)
)
executions.push(request.url());
});
page.on('pageerror', (error) => errors.push(error.message));
observers.set(page, { executions, errors });
});

const observers = new WeakMap<
Page,
{ executions: string[]; errors: string[] }
>();
test.afterEach(async ({ page }) => {
const observed = observers.get(page);
expect(observed?.executions).toEqual([]);
expect(observed?.errors).toEqual([]);
});

async function expectFreshRun(page: Page) {
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
'data-hydrated',
'true'
);
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
'data-workspace-mode',
'Run'
);
await expect(iframe(page)).toBeVisible();
await expect(page.getByLabel('Example UI')).toHaveValue('react');
await expect(page).toHaveURL(
(url) =>
url.pathname === route &&
url.searchParams.get('frontend') === 'react' &&
url.searchParams.get('mode') === 'run'
);
await expect(iframe(page)).toHaveAttribute(
'src',
/(?:localhost:4615(?:[/?#]|$)|\/render\/state-management\/react\/?(?:[?#]|$))/
);
const example = frame(page);
await expect(example.getByRole('textbox', { name: 'Name' })).toHaveValue(
'Alice'
);
await expect(example.getByRole('spinbutton', { name: 'Age' })).toHaveValue(
'30'
);
await expect(
example.getByRole('spinbutton', { name: 'Age' })
).toHaveAttribute('aria-invalid', 'false');
await expect(example.getByRole('combobox', { name: 'Theme' })).toHaveValue(
'dark'
);
await expect(example.getByRole('alert')).toHaveCount(0);
await expect(example.getByRole('status')).toHaveText('Paused · 0 characters');
await expect(example.getByRole('combobox', { name: 'Sample' })).toHaveValue(
'0'
);
await expect(
example.getByRole('slider', { name: 'Playback position' })
).toHaveValue('0');
await expect(
example.getByRole('region', { name: 'Streaming JSON' }).locator('pre')
).toHaveText('');
await expect(
example.getByRole('region', { name: 'Render output' })
).toContainText('Play a sample to see its view.');
}

async function renderChangedSample(page: Page) {
const example = frame(page);
await example.getByRole('textbox', { name: 'Name' }).fill('Ada');
await example.getByRole('spinbutton', { name: 'Age' }).fill('42');
await example.getByRole('combobox', { name: 'Theme' }).selectOption('light');
await example
.getByRole('combobox', { name: 'Sample' })
.selectOption({ label: 'Nested Paths' });
await example.getByRole('button', { name: 'Finish', exact: true }).click();
await expect(
example.getByRole('heading', { name: 'Nested State Paths', exact: true })
).toBeVisible();
await expect(
example.getByRole('region', { name: 'Render output' })
).toContainText('Ada');
await expect(
example.getByRole('region', { name: 'Render output' })
).toContainText('light');
await expect(
example.getByRole('region', { name: 'Streaming JSON' }).locator('pre')
).not.toHaveText('');
}

test('React State Management keeps local Docs, Code and Run aligned', async ({
page,
}, testInfo) => {
await page.goto(`${route}?frontend=react`, { waitUntil: 'domcontentloaded' });
await expect(page.getByLabel('Example UI')).toHaveValue('react');
await expect(
page.getByRole('heading', {
name: 'React State Management preview',
exact: true,
})
).toBeVisible();
await expect(
page.locator(
'[data-example-file="cockpit/render/state-management/react/src/state.ts"]'
)
).toBeVisible();
await expect(page.locator('[data-example-file*="/python/"]')).toHaveCount(0);
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
'data-hydrated',
'true'
);
const navigation = page.locator('[data-workspace-desktop-navigation]');
await navigation.getByRole('button', { name: 'Code', exact: true }).click();
await expect(page).toHaveURL(/frontend=react/);
await expect(page).toHaveURL(/mode=code/);
await navigation.getByRole('button', { name: /^Run(?:,|$)/ }).click();
await expectFreshRun(page);
await page.screenshot({
path: testInfo.outputPath('react-state-management-desktop.png'),
});
});

test('React State Management restores fresh playback through frontend history', async ({
page,
}) => {
await page.goto(`${route}?frontend=react&mode=run`, {
waitUntil: 'domcontentloaded',
});
await expectFreshRun(page);
await renderChangedSample(page);
await page.getByLabel('Example UI').selectOption('angular');
await expect(page.getByLabel('Example UI')).toHaveValue('angular');
await expect(page).toHaveURL(`${route}?mode=run`);
await expect(page.locator('iframe')).toBeVisible();
await expect(page.locator('iframe')).toHaveAttribute(
'src',
/(?:localhost:4403(?:[/?#]|$)|\/render\/state-management\/?(?:[?#]|$))/
);
await page.goBack({ waitUntil: 'domcontentloaded' });
await expectFreshRun(page);
});

test('React State Management reloads fresh playback after rendering another sample', async ({
page,
}) => {
await page.goto(`${route}?frontend=react&mode=run`, {
waitUntil: 'domcontentloaded',
});
await expectFreshRun(page);
await renderChangedSample(page);
await frame(page).getByRole('spinbutton', { name: 'Age' }).fill('151');
await expect(
frame(page).getByRole('spinbutton', { name: 'Age' })
).toHaveAttribute('aria-invalid', 'true');
await expect(frame(page).getByRole('alert')).toHaveText(
'Enter a whole age from 0 to 150.'
);
await page.reload({ waitUntil: 'domcontentloaded' });
await expectFreshRun(page);
});

test('canonical State Management Run resolves edited bindings in all samples without execution', async ({
page,
}) => {
await page.goto(`${route}?frontend=react&mode=run`, {
waitUntil: 'domcontentloaded',
});
await expectFreshRun(page);
const frame = page.frameLocator('iframe');
await expect(frame.getByRole('status')).toHaveText('Paused · 0 characters');
await frame.getByRole('textbox', { name: 'Name' }).fill('Ada');
await frame.getByRole('spinbutton', { name: 'Age' }).fill('42');
await frame.getByRole('combobox', { name: 'Theme' }).selectOption('light');
const output = frame.getByRole('region', { name: 'Render output' });
for (const [label, heading] of [
['User Profile', 'User Profile'],
['Nested Paths', 'Nested State Paths'],
['Form Display', 'State-Driven Form'],
]) {
await frame
.getByRole('combobox', { name: 'Sample' })
.selectOption({ label });
await frame.getByRole('button', { name: 'Finish' }).click();
await expect(
frame.getByRole('heading', { name: heading, exact: true })
).toBeVisible();
await expect(output).toContainText('Ada');
await expect(output).toContainText(
label === 'User Profile' ? '42' : 'light'
);
}
await frame.getByRole('button', { name: 'Reset' }).click();
await expect(frame.getByRole('slider')).toHaveValue('0');
await expect(frame.getByRole('textbox', { name: 'Name' })).toHaveValue('Ada');
});

test('local React State Management remains usable on a narrow screen', async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(`${route}?frontend=react&mode=run`, {
waitUntil: 'domcontentloaded',
});
await expectFreshRun(page);
await expect(page.getByLabel('Example UI')).toBeVisible();
const frame = page.frameLocator('iframe');
await frame.getByRole('textbox', { name: 'Name' }).fill('Grace');
await frame.getByRole('button', { name: 'Finish' }).click();
await expect(
frame.getByRole('region', { name: 'Render output' })
).toContainText('Grace');
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth
)
).toBe(true);
await page.screenshot({
path: testInfo.outputPath('react-state-management-mobile.png'),
});
});
9 changes: 8 additions & 1 deletion apps/website/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export const createWebsitePlaywrightConfig = (
? [
{
command:
'npx nx run-many -t build --projects=cockpit-langgraph-streaming-react,cockpit-langgraph-interrupts-react,cockpit-langgraph-memory-react,cockpit-langgraph-client-tools-react,cockpit-langgraph-persistence-react,cockpit-langgraph-durable-execution-react,cockpit-langgraph-subgraphs-react,cockpit-langgraph-time-travel-react,cockpit-langgraph-deployment-runtime-react,cockpit-ag-ui-streaming-react,cockpit-ag-ui-interrupts-react,cockpit-ag-ui-tool-views-react,cockpit-ag-ui-json-render-react,cockpit-ag-ui-subagents-react,cockpit-render-spec-rendering-react --parallel=3 && node scripts/react-cockpit/serve.mjs',
'npx nx run-many -t build --projects=cockpit-langgraph-streaming-react,cockpit-langgraph-interrupts-react,cockpit-langgraph-memory-react,cockpit-langgraph-client-tools-react,cockpit-langgraph-persistence-react,cockpit-langgraph-durable-execution-react,cockpit-langgraph-subgraphs-react,cockpit-langgraph-time-travel-react,cockpit-langgraph-deployment-runtime-react,cockpit-ag-ui-streaming-react,cockpit-ag-ui-interrupts-react,cockpit-ag-ui-tool-views-react,cockpit-ag-ui-json-render-react,cockpit-ag-ui-subagents-react,cockpit-render-spec-rendering-react,cockpit-render-state-management-react --parallel=3 && node scripts/react-cockpit/serve.mjs',
cwd: '../..',
url: 'http://127.0.0.1:4600',
reuseExistingServer,
Expand Down Expand Up @@ -329,6 +329,13 @@ export const createWebsitePlaywrightConfig = (
reuseExistingServer,
timeout: 180_000,
},
{
command: 'node scripts/react-cockpit/serve.mjs render-state-management --no-parent',
cwd: '../..',
url: 'http://127.0.0.1:4615',
reuseExistingServer,
timeout: 180_000,
},
]
: []),
]
Expand Down
2 changes: 2 additions & 0 deletions apps/website/public/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,5 @@ AG-UI Subagents has an experimental React preview at https://threadplane.ai/docs
Build with `npx nx build cockpit-ag-ui-subagents-react` and verify with `npx nx e2e cockpit-ag-ui-subagents-react`; the existing shared route is `/ag-ui/subagents/agent`. Browser proofs use the actual compiled graph and child bridge with frozen providers.

Render Spec has an experimental local React preview at https://threadplane.ai/docs/render/guides/specs?frontend=react. Three authored samples stream through a private partial JSON parser and installed native RenderSpec. Four registered read-only views receive literal props and native children. Play/Pause, seek, Finish and Reset share one local owner; sample changes and page exit revoke old callbacks. Copied frozen partial trees leave parser state private, and Complete requires the finished output to match the validated source. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-spec-rendering-react` and verify with `npx nx e2e cockpit-render-spec-rendering-react`.

State Management has an experimental local React preview at https://threadplane.ai/docs/render/guides/state-store?frontend=react. The React host owns copied immutable name, age and theme state; installed native RenderSpec resolves the three authored $state paths in read-only views. Edits survive partial playback, seek, Reset and sample selection. Invalid age drafts keep the last valid value, and reload restores the seed. Parser callbacks cannot replace host state; unknown bindings and executable fields are excluded. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-state-management-react` and verify with `npx nx e2e cockpit-render-state-management-react`.
2 changes: 2 additions & 0 deletions apps/website/public/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,5 @@ AG-UI Subagents has an experimental React preview at https://threadplane.ai/docs
Build with `npx nx build cockpit-ag-ui-subagents-react` and verify with `npx nx e2e cockpit-ag-ui-subagents-react`; the existing shared route is `/ag-ui/subagents/agent`. Browser proofs use the actual compiled graph and child bridge with frozen providers.

Render Spec has an experimental local React preview at https://threadplane.ai/docs/render/guides/specs?frontend=react. Three authored samples stream through a private partial JSON parser and installed native RenderSpec. Four registered read-only views receive literal props and native children. Play/Pause, seek, Finish and Reset share one local owner; sample changes and page exit revoke old callbacks. Copied frozen partial trees leave parser state private, and Complete requires the finished output to match the validated source. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-spec-rendering-react` and verify with `npx nx e2e cockpit-render-spec-rendering-react`.

State Management has an experimental local React preview at https://threadplane.ai/docs/render/guides/state-store?frontend=react. The React host owns copied immutable name, age and theme state; installed native RenderSpec resolves the three authored $state paths in read-only views. Edits survive partial playback, seek, Reset and sample selection. Invalid age drafts keep the last valid value, and reload restores the seed. Parser callbacks cannot replace host state; unknown bindings and executable fields are excluded. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-state-management-react` and verify with `npx nx e2e cockpit-render-state-management-react`.
11 changes: 11 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 @@ -12,6 +12,7 @@ import { ReactAgUiToolViewsPreview } from '../../../../../components/docs/ReactA
import { ReactAgUiJsonRenderPreview } from '../../../../../components/docs/ReactAgUiJsonRenderPreview';
import { ReactAgUiSubagentsPreview } from '../../../../../components/docs/ReactAgUiSubagentsPreview';
import { ReactRenderSpecPreview } from '../../../../../components/docs/ReactRenderSpecPreview';
import { ReactStateManagementPreview } from '../../../../../components/docs/ReactStateManagementPreview';
import { ReactInterruptsPreview } from '../../../../../components/docs/ReactInterruptsPreview';
import { ReactMemoryPreview } from '../../../../../components/docs/ReactMemoryPreview';
import { ReactClientToolsPreview } from '../../../../../components/docs/ReactClientToolsPreview';
Expand Down Expand Up @@ -65,6 +66,16 @@ const route = (library: string, section: string, slug: string, mode?: string) =>
} as never);

describe('unified docs workspace route', () => {
it('selects React State Management Docs with owned state and readonly view sources', async () => {
const tree = await route('render', 'guides', 'state-store');
const workspace = findElement(tree, WebsiteWorkspace as ComponentType<never>);
const article = findElement(workspace?.props.reactDocsSlot, ReactStateManagementPreview as ComponentType<never>);
expect(article).not.toBeNull();
expect(article?.props.exampleCode?.assetPaths).toContain('cockpit/render/state-management/react/src/state.ts');
expect(article?.props.exampleCode?.assetPaths).toContain('cockpit/render/state-management/react/src/views.tsx');
expect(article?.props.exampleCode?.assetPaths?.some(path => path.includes('/python/'))).toBe(false);
expect(findElement(workspace?.props.reactDocsSlot, ReactRenderSpecPreview as ComponentType<never>)).toBeNull();
});
it('selects the local React Render Spec guide and owned renderer sources without Python assets', async () => {
const tree = await route('render', 'guides', 'specs');
const workspace = findElement(tree, WebsiteWorkspace as ComponentType<never>);
Expand Down
Loading
Loading