Skip to content

Commit e234560

Browse files
committed
feat(react): integrate native computed functions preview
1 parent d5cfeda commit e234560

36 files changed

Lines changed: 2327 additions & 33 deletions

‎apps/website/content/AGENTS.md.template‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,5 @@ Component Registry has an experimental local React preview at https://threadplan
109109
Element Rendering has an experimental local React preview at https://threadplane.ai/docs/render/api/render-spec-component?frontend=react. Installed native RenderSpec owns ordered children, nested readonly views and visibility from explicit caller-owned boolean state. Show detail survives playback, seek, Reset and sample changes without modifying JSON; reload shows detail. The bounded projection withholds the conditional node until its exact /showDetail visibility path is available, preventing hidden text from flashing during partial playback. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-element-rendering-react` and verify with `npx nx e2e cockpit-render-element-rendering-react`.
110110

111111
The private React render entry also accepts an optional `ReactRenderFunctions` host map for authored `$computed` expressions. `RenderComputedFunction` receives owned recursively frozen arguments and synchronously returns `RenderValue`; calculations must be pure and can run repeatedly during React rendering. Plain or null-prototype maps admit own callable data entries, including non-enumerable entries, and reject accessors, symbols and non-callable values. Results retain the existing plain-data ownership guards. This adds no actions, watchers, state ownership or asynchronous effect execution, and raw specs still cannot contain executable callbacks.
112+
113+
Computed Functions has an experimental local React preview at https://threadplane.ai/docs/render/api/provide-render?frontend=react. Three authored samples preserve native $computed expressions and use RenderSpec.functions for uppercase/reverse text, products and browser-local dates. Only complete expression nodes calculate; numeric/date prefixes never publish provisional results. Heading/Card/Value display resolved props and native children. Pure synchronous callbacks receive owned frozen arguments and may run repeatedly. Play/Pause, seek, Finish and Reset share one bounded private parser owner; sample changes and page exit revoke old callbacks. It makes no backend or model requests. Build with `npx nx build cockpit-render-computed-functions-react` and verify with `npx nx e2e cockpit-render-computed-functions-react`.

‎apps/website/content/CLAUDE.md.template‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,5 @@ Component Registry has an experimental local React preview at https://threadplan
109109
Element Rendering has an experimental local React preview at https://threadplane.ai/docs/render/api/render-spec-component?frontend=react. Installed native RenderSpec owns ordered children, nested readonly views and visibility from explicit caller-owned boolean state. Show detail survives playback, seek, Reset and sample changes without modifying JSON; reload shows detail. The bounded projection withholds the conditional node until its exact /showDetail visibility path is available, preventing hidden text from flashing during partial playback. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-element-rendering-react` and verify with `npx nx e2e cockpit-render-element-rendering-react`.
110110

111111
The private React render entry also accepts an optional `ReactRenderFunctions` host map for authored `$computed` expressions. `RenderComputedFunction` receives owned recursively frozen arguments and synchronously returns `RenderValue`; calculations must be pure and can run repeatedly during React rendering. Plain or null-prototype maps admit own callable data entries, including non-enumerable entries, and reject accessors, symbols and non-callable values. Results retain the existing plain-data ownership guards. This adds no actions, watchers, state ownership or asynchronous effect execution, and raw specs still cannot contain executable callbacks.
112+
113+
Computed Functions has an experimental local React preview at https://threadplane.ai/docs/render/api/provide-render?frontend=react. Three authored samples preserve native $computed expressions and use RenderSpec.functions for uppercase/reverse text, products and browser-local dates. Only complete expression nodes calculate; numeric/date prefixes never publish provisional results. Heading/Card/Value display resolved props and native children. Pure synchronous callbacks receive owned frozen arguments and may run repeatedly. Play/Pause, seek, Finish and Reset share one bounded private parser owner; sample changes and page exit revoke old callbacks. It makes no backend or model requests. Build with `npx nx build cockpit-render-computed-functions-react` and verify with `npx nx e2e cockpit-render-computed-functions-react`.
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { test, expect, type Page } from '@playwright/test';
2+
3+
const route = '/docs/render/api/provide-render';
4+
const iframe = (page: Page) =>
5+
page.locator(
6+
'iframe[title="Computed Functions (React preview) live example"]'
7+
);
8+
const frame = (page: Page) => iframe(page).contentFrame();
9+
10+
test.beforeEach(async ({ page }) => {
11+
const executions: string[] = [];
12+
const errors: string[] = [];
13+
page.on('request', (request) => {
14+
if (
15+
!['GET', 'HEAD'].includes(request.method()) &&
16+
/\/agent(?:\/|$)|\/api\/native(?:\/|$)|\/api\/threads(?:\/|$)|\/threads(?:\/|$)/.test(
17+
new URL(request.url()).pathname
18+
)
19+
)
20+
executions.push(request.url());
21+
});
22+
page.on('pageerror', (error) => errors.push(error.message));
23+
observers.set(page, { executions, errors });
24+
});
25+
26+
const observers = new WeakMap<
27+
Page,
28+
{ executions: string[]; errors: string[] }
29+
>();
30+
test.afterEach(async ({ page }) => {
31+
const observed = observers.get(page);
32+
expect(observed?.executions).toEqual([]);
33+
expect(observed?.errors).toEqual([]);
34+
});
35+
36+
async function expectFreshRun(page: Page) {
37+
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
38+
'data-hydrated',
39+
'true'
40+
);
41+
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
42+
'data-workspace-mode',
43+
'Run'
44+
);
45+
await expect(iframe(page)).toBeVisible();
46+
await expect(page.getByLabel('Example UI')).toHaveValue('react');
47+
await expect(page).toHaveURL(
48+
(url) =>
49+
url.pathname === route &&
50+
url.searchParams.get('frontend') === 'react' &&
51+
url.searchParams.get('mode') === 'run'
52+
);
53+
await expect(iframe(page)).toHaveAttribute(
54+
'src',
55+
/(?:localhost:4619(?:[/?#]|$)|\/render\/computed-functions\/react\/?(?:[?#]|$))/
56+
);
57+
const example = frame(page);
58+
await expect(example.getByRole('status')).toHaveText('Paused · 0 characters');
59+
await expect(example.getByRole('combobox', { name: 'Sample' })).toHaveValue(
60+
'0'
61+
);
62+
await expect(
63+
example.getByRole('slider', { name: 'Playback position' })
64+
).toHaveValue('0');
65+
await expect(
66+
example.getByRole('region', { name: 'Streaming JSON' }).locator('pre')
67+
).toHaveText('');
68+
await expect(
69+
example.getByRole('region', { name: 'Render output' })
70+
).toContainText('Play a sample to see its view.');
71+
}
72+
73+
async function renderChangedSample(page: Page) {
74+
const example = frame(page);
75+
await example
76+
.getByRole('combobox', { name: 'Sample' })
77+
.selectOption({ label: 'Data Display' });
78+
await example.getByRole('button', { name: 'Finish', exact: true }).click();
79+
await expect(
80+
example.getByRole('heading', { name: 'Formatted Data', exact: true })
81+
).toBeVisible();
82+
await expect(
83+
example.getByRole('region', { name: 'Streaming JSON' }).locator('pre')
84+
).not.toHaveText('');
85+
}
86+
87+
test('React Computed Functions keeps local Docs, Code and Run aligned', async ({
88+
page,
89+
}, testInfo) => {
90+
await page.goto(`${route}?frontend=react`, { waitUntil: 'domcontentloaded' });
91+
await expect(page.getByLabel('Example UI')).toHaveValue('react');
92+
await expect(
93+
page.getByRole('heading', {
94+
name: 'React Computed Functions preview',
95+
exact: true,
96+
})
97+
).toBeVisible();
98+
await expect(
99+
page.locator(
100+
'[data-example-file="cockpit/render/computed-functions/react/src/playback.ts"]'
101+
)
102+
).toBeVisible();
103+
await expect(page.locator('[data-example-file*="/python/"]')).toHaveCount(0);
104+
await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
105+
'data-hydrated',
106+
'true'
107+
);
108+
const navigation = page.locator('[data-workspace-desktop-navigation]');
109+
await navigation.getByRole('button', { name: 'Code', exact: true }).click();
110+
await expect(page).toHaveURL(/frontend=react/);
111+
await expect(page).toHaveURL(/mode=code/);
112+
await navigation.getByRole('button', { name: /^Run(?:,|$)/ }).click();
113+
await expectFreshRun(page);
114+
await page.screenshot({
115+
path: testInfo.outputPath('react-computed-functions-desktop.png'),
116+
});
117+
});
118+
119+
test('React Computed Functions restores fresh playback through frontend history', async ({
120+
page,
121+
}) => {
122+
await page.goto(`${route}?frontend=react&mode=run`, {
123+
waitUntil: 'domcontentloaded',
124+
});
125+
await expectFreshRun(page);
126+
await renderChangedSample(page);
127+
await page.getByLabel('Example UI').selectOption('angular');
128+
await expect(page.getByLabel('Example UI')).toHaveValue('angular');
129+
await expect(page).not.toHaveURL(/frontend=react/);
130+
await expect(page).toHaveURL(/mode=run/);
131+
await expect(page.locator('iframe')).toHaveAttribute(
132+
'src',
133+
/(?:localhost:4406(?:[/?#]|$)|\/render\/computed-functions\/?(?:[?#]|$))/
134+
);
135+
await expect(
136+
page
137+
.locator('iframe')
138+
.contentFrame()
139+
.getByRole('button', { name: 'Play or pause', exact: true })
140+
).toBeVisible();
141+
await page.goBack({ waitUntil: 'domcontentloaded' });
142+
await expectFreshRun(page);
143+
});
144+
145+
test('React Computed Functions reloads fresh playback after rendering another sample', async ({
146+
page,
147+
}) => {
148+
await page.goto(`${route}?frontend=react&mode=run`, {
149+
waitUntil: 'domcontentloaded',
150+
});
151+
await expectFreshRun(page);
152+
await renderChangedSample(page);
153+
await page.reload({ waitUntil: 'domcontentloaded' });
154+
await expectFreshRun(page);
155+
});
156+
157+
test('canonical Computed Functions Run renders local samples without runtime execution', async ({
158+
page,
159+
}) => {
160+
await page.goto(`${route}?frontend=react&mode=run`, {
161+
waitUntil: 'domcontentloaded',
162+
});
163+
await expectFreshRun(page);
164+
const example = frame(page);
165+
const dates = await example
166+
.locator('body')
167+
.evaluate(() =>
168+
['2024-06-15T12:00:00Z', '2025-01-01T00:00:00Z'].map((value) =>
169+
new Date(value).toLocaleDateString()
170+
)
171+
);
172+
for (const [label, heading, values] of [
173+
['Text Transforms', 'Text Transforms', ['HELLO WORLD', 'gnimaerts']],
174+
['Data Display', 'Formatted Data', ['42', dates[0]]],
175+
[
176+
'Mixed Functions',
177+
'Mixed Functions',
178+
['60', 'COMPUTED FUNCTIONS', dates[1]],
179+
],
180+
] as const) {
181+
await example
182+
.getByRole('combobox', { name: 'Sample' })
183+
.selectOption({ label });
184+
await example.getByRole('button', { name: 'Finish', exact: true }).click();
185+
await expect(
186+
example.getByRole('heading', { name: heading, exact: true })
187+
).toBeVisible();
188+
for (const value of values)
189+
await expect(
190+
example
191+
.getByRole('region', { name: 'Render output' })
192+
.getByText(value, { exact: true })
193+
).toBeVisible();
194+
}
195+
await example.getByRole('button', { name: 'Reset', exact: true }).click();
196+
await expect(
197+
example.getByRole('slider', { name: 'Playback position' })
198+
).toHaveValue('0');
199+
await expect(
200+
example.getByRole('region', { name: 'Streaming JSON' }).locator('pre')
201+
).toHaveText('');
202+
});
203+
204+
test('local React Computed Functions remains usable on a narrow screen', async ({
205+
page,
206+
}, testInfo) => {
207+
await page.setViewportSize({ width: 390, height: 844 });
208+
await page.goto(`${route}?frontend=react&mode=run`, {
209+
waitUntil: 'domcontentloaded',
210+
});
211+
await expect(page.getByLabel('Example UI')).toBeVisible();
212+
await expectFreshRun(page);
213+
const example = frame(page);
214+
await example.getByRole('button', { name: 'Finish', exact: true }).click();
215+
await expect(
216+
example.getByRole('heading', {
217+
name: 'Text Transforms',
218+
exact: true,
219+
})
220+
).toBeVisible();
221+
expect(
222+
await page.evaluate(
223+
() => document.documentElement.scrollWidth <= innerWidth
224+
)
225+
).toBe(true);
226+
await page.screenshot({
227+
path: testInfo.outputPath('react-computed-functions-mobile.png'),
228+
});
229+
});

‎apps/website/playwright.config.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ export const createWebsitePlaywrightConfig = (
204204
url: 'http://localhost:4506',
205205
reuseExistingServer,
206206
},
207+
{
208+
command: 'npx nx run cockpit-render-computed-functions-angular:serve:cockpit --port 4406',
209+
cwd: '../..',
210+
url: 'http://localhost:4406',
211+
reuseExistingServer,
212+
timeout: 180_000,
213+
},
207214
]
208215
: []),
209216
{
@@ -360,6 +367,13 @@ export const createWebsitePlaywrightConfig = (
360367
reuseExistingServer,
361368
timeout: 180_000,
362369
},
370+
{
371+
command: 'node scripts/react-cockpit/serve.mjs render-computed-functions --no-parent',
372+
cwd: '../..',
373+
url: 'http://127.0.0.1:4619',
374+
reuseExistingServer,
375+
timeout: 180_000,
376+
},
363377
]
364378
: []),
365379
]

‎apps/website/project.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"cockpit-ag-ui-tool-views-angular",
1313
"cockpit-ag-ui-json-render-angular",
1414
"cockpit-ag-ui-subagents-angular",
15-
"cockpit-chat-threads-angular"
15+
"cockpit-chat-threads-angular",
16+
"cockpit-render-computed-functions-angular"
1617
],
1718
"tags": ["scope:website", "scope:website-e2e", "type:app"],
1819
"targets": {
@@ -92,7 +93,8 @@
9293
"cockpit-render-state-management-react",
9394
"cockpit-render-repeat-loops-react",
9495
"cockpit-render-registry-react",
95-
"cockpit-render-element-rendering-react"
96+
"cockpit-render-element-rendering-react",
97+
"cockpit-render-computed-functions-react"
9698
]
9799
}
98100
],

‎apps/website/public/AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,5 @@ Component Registry has an experimental local React preview at https://threadplan
109109
Element Rendering has an experimental local React preview at https://threadplane.ai/docs/render/api/render-spec-component?frontend=react. Installed native RenderSpec owns ordered children, nested readonly views and visibility from explicit caller-owned boolean state. Show detail survives playback, seek, Reset and sample changes without modifying JSON; reload shows detail. The bounded projection withholds the conditional node until its exact /showDetail visibility path is available, preventing hidden text from flashing during partial playback. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-element-rendering-react` and verify with `npx nx e2e cockpit-render-element-rendering-react`.
110110

111111
The private React render entry also accepts an optional `ReactRenderFunctions` host map for authored `$computed` expressions. `RenderComputedFunction` receives owned recursively frozen arguments and synchronously returns `RenderValue`; calculations must be pure and can run repeatedly during React rendering. Plain or null-prototype maps admit own callable data entries, including non-enumerable entries, and reject accessors, symbols and non-callable values. Results retain the existing plain-data ownership guards. This adds no actions, watchers, state ownership or asynchronous effect execution, and raw specs still cannot contain executable callbacks.
112+
113+
Computed Functions has an experimental local React preview at https://threadplane.ai/docs/render/api/provide-render?frontend=react. Three authored samples preserve native $computed expressions and use RenderSpec.functions for uppercase/reverse text, products and browser-local dates. Only complete expression nodes calculate; numeric/date prefixes never publish provisional results. Heading/Card/Value display resolved props and native children. Pure synchronous callbacks receive owned frozen arguments and may run repeatedly. Play/Pause, seek, Finish and Reset share one bounded private parser owner; sample changes and page exit revoke old callbacks. It makes no backend or model requests. Build with `npx nx build cockpit-render-computed-functions-react` and verify with `npx nx e2e cockpit-render-computed-functions-react`.

‎apps/website/public/CLAUDE.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,5 @@ Component Registry has an experimental local React preview at https://threadplan
109109
Element Rendering has an experimental local React preview at https://threadplane.ai/docs/render/api/render-spec-component?frontend=react. Installed native RenderSpec owns ordered children, nested readonly views and visibility from explicit caller-owned boolean state. Show detail survives playback, seek, Reset and sample changes without modifying JSON; reload shows detail. The bounded projection withholds the conditional node until its exact /showDetail visibility path is available, preventing hidden text from flashing during partial playback. No backend runtime, model request or execution configuration is used. Build with `npx nx build cockpit-render-element-rendering-react` and verify with `npx nx e2e cockpit-render-element-rendering-react`.
110110

111111
The private React render entry also accepts an optional `ReactRenderFunctions` host map for authored `$computed` expressions. `RenderComputedFunction` receives owned recursively frozen arguments and synchronously returns `RenderValue`; calculations must be pure and can run repeatedly during React rendering. Plain or null-prototype maps admit own callable data entries, including non-enumerable entries, and reject accessors, symbols and non-callable values. Results retain the existing plain-data ownership guards. This adds no actions, watchers, state ownership or asynchronous effect execution, and raw specs still cannot contain executable callbacks.
112+
113+
Computed Functions has an experimental local React preview at https://threadplane.ai/docs/render/api/provide-render?frontend=react. Three authored samples preserve native $computed expressions and use RenderSpec.functions for uppercase/reverse text, products and browser-local dates. Only complete expression nodes calculate; numeric/date prefixes never publish provisional results. Heading/Card/Value display resolved props and native children. Pure synchronous callbacks receive owned frozen arguments and may run repeatedly. Play/Pause, seek, Finish and Reset share one bounded private parser owner; sample changes and page exit revoke old callbacks. It makes no backend or model requests. Build with `npx nx build cockpit-render-computed-functions-react` and verify with `npx nx e2e cockpit-render-computed-functions-react`.

0 commit comments

Comments
 (0)