From 2bc452fa848373c6e11d55d1fbdb59ee0b785feb Mon Sep 17 00:00:00 2001 From: Sparkle <2705263765@qq.com> Date: Thu, 3 Sep 2026 16:11:33 +0800 Subject: [PATCH 01/12] docs(web): define React migration MVP Record the frozen PR #352 behavior baseline, Pi-native ownership boundaries, selected frontend stack, static delivery contract, and validation matrix for the local migration experiment. --- docs/design/OPENPI_WEB_REACT_MVP.md | 122 ++++++++++++++++++++++++++++ docs/design/README.md | 1 + 2 files changed, 123 insertions(+) create mode 100644 docs/design/OPENPI_WEB_REACT_MVP.md diff --git a/docs/design/OPENPI_WEB_REACT_MVP.md b/docs/design/OPENPI_WEB_REACT_MVP.md new file mode 100644 index 00000000..fba66e14 --- /dev/null +++ b/docs/design/OPENPI_WEB_REACT_MVP.md @@ -0,0 +1,122 @@ +# OpenPI Web React migration MVP + +- Status: draft +- Created: 2026-09-03 +- Verified through: not yet validated +- Source boundary: OpenPI `72fbba5`, PR #352 head `1b340f2` +- Related issue: none; local validation branch only +- Related PR: [#352](https://github.com/openpi-dev/openpi/pull/352) is the frozen behavior and visual reference, not a dependency merged into this branch + +## Purpose + +Validate that the OpenPI Web browser projection can move from one imperative `app.js` file to React without changing Pi ownership, the Web protocol, or the visible behavior restored by PR #352. The result must work both through the Vite development server with HMR and as built static assets served by `WebHost`. + +This is a framework migration, not a redesign. The browser remains a projection of the Web-owned Pi runtime and must not become a second Session, model, capability, or configuration source of truth. + +## Chosen stack + +- React 19 and TypeScript +- Vite 8 +- Zustand vanilla store with React bindings +- Fetch, authenticated SSE, and `eventsource-parser` +- Existing TypeBox protocol contracts +- Tailwind CSS 4 as the primary CSS system +- Astryx Core and the neutral theme for selected accessible interaction primitives +- Lucide React icons +- `react-markdown`, `remark-gfm`, and `rehype-sanitize` +- i18next and react-i18next +- Vitest, React Testing Library, Playwright, and axe-core + +Astryx owns generic Dialog, Menu, Tooltip, and Select behavior where its API preserves the reference interaction. Tailwind owns layout and visual matching. OpenPI-specific transcript evidence, activity projections, and the animated logo remain local React components. Astryx does not own runtime state or persistence. + +## Runtime boundary + +`PiWebRuntime`, `PiWebAdapter`, the protocol types, command endpoints, authentication token, cursor semantics, and lifecycle remain authoritative and unchanged. The migration adds three browser-side modules: + +1. A typed HTTP client sends the existing bounded commands and distinguishes accepted receipts from terminal runtime evidence. +2. An authenticated Fetch-based SSE client parses records, resumes from the last cursor, and requests a snapshot after invalidation or replay gaps. +3. A Zustand vanilla store reduces snapshots and events into browser projection state. React reads selectors and dispatches commands; it does not infer completion from labels or CSS. + +Only browser-local UI state may persist in `sessionStorage`: the process token and collapsed workspace groups. Theme, language, Sessions, models, and runtime capabilities are not independently persisted by this UI. + +## Component boundaries + +- `AppShell`: desktop grid, narrow-screen drawer, and global connection state. +- `SessionRail`: search, collapse, workspace groups, Session selection, and row actions. +- `WorkspacePicker`: initial workspace selection and native directory chooser command. +- `TranscriptViewport`: ordered persisted/live projections, follow-scroll policy, and turn anchors. +- `MessageBlock`: user and assistant text, copy action, latest-user edit-and-resend flow. +- `ThinkingEvidence`: live and settled thinking duration. +- `ToolEvidence`: call/result pairing, summaries, status, output disclosure, and empty output. +- `CapabilityEvidence`: Subagent and Workflow call/result cards. +- `ToolGroup`: collapses runs of four or more ordinary tool rows. +- `TurnRail`: jumps between user turns without owning transcript state. +- `ActivityBar`: bounded Subagent and Workflow status chips from canonical capability projections. +- `Composer`: active-Session checks, model selection, prompt admission, queued/running states, and auto-resizing input. +- `OpenPiLogo`: PR #352's 16-cell assembly, impact, line-clear, settling animation, click replay, and reduced-motion behavior. + +Components receive typed projected values and callbacks. Protocol messages are normalized before rendering; components do not parse Session files or call Pi APIs directly. + +## Behavior baseline + +The complete baseline is PR #352 exact head `1b340f2`: + +- engineering-grid backdrop, frosted desktop rail, opaque narrow-screen drawer, paper transcript lane, and favicon; +- workspace import, rename, remove, collapse, search, and per-workspace Session creation; +- Session select, rename, archive, active/readonly behavior, and switching feedback; +- model picker and Pi-native model selection; +- prompt admission tokens, queued/running/retrying states, duplicate terminal receipt handling, and edit-and-resend; +- snapshot plus cursor SSE recovery, stale response protection, reconnect status, and invalidation refresh; +- safe GFM Markdown, external links, image-as-link rendering, code, tables, and task lists; +- thinking timers, ordinary tool summaries and evidence, result pairing, error/running states, and four-step grouping; +- Subagent and Workflow cards, custom results, bounded capability chips, omitted counts, message copy/time actions, turn navigation, and follow-scroll behavior; +- responsive sidebar and composer at 390 by 844, keyboard operation, visible focus, and reduced motion; +- animated landing logo with click replay. + +The removed browser-local theme/language settings are deliberately excluded. Language follows browser locale as in the final PR head until canonical package configuration is implemented separately. + +## Build and static delivery + +Vite uses `web/ui` as its source root and writes deterministic, auditable assets to `web/dist`. CSS splitting is disabled for the MVP so `WebHost` can retain a fixed static allowlist. No source map or arbitrary filesystem path is served. + +- `bun run dev:web` starts the source backend plus Vite HMR. +- `bun run build:web` creates production assets. +- `node ./bin/openpi.js web [workspace]` serves those assets without Vite. +- `npm pack` must contain the built browser assets and run without remote CDN dependencies. + +## Error and recovery behavior + +- Missing or rejected auth yields an explicit disconnected/error surface without leaking the token. +- HTTP command errors preserve the current snapshot and show action-scoped feedback. +- A stale Session activation or prompt receipt cannot overwrite a newer selection or prompt state. +- SSE reconnect uses the last accepted cursor. A replay gap, invalid event, or `state_invalidated` event triggers a bounded snapshot refresh. +- Non-active Sessions remain read-only. UI optimism never proves model completion. +- Markdown input is sanitized, raw HTML is rendered as text, unsafe URL schemes are rejected, and images remain outbound links rather than embedded remote loads. +- Reduced-motion users receive the final logo state and nonanimated navigation. + +## Validation + +Automated validation must include: + +- pure reducer tests for snapshot/event ordering, prompt settlement, activation races, reconnect, and invalidation; +- component tests for Session/workspace actions, composer gates, Markdown safety, tool pairing/grouping, copy/edit, turn navigation, activity chips, and Logo replay; +- existing Host/runtime/protocol tests updated to assert the built static asset contract; +- Playwright desktop and 390 by 844 flows against a real local WebHost, including keyboard navigation and axe checks; +- `bun run check`, `bun run test`, `bun run build:web`, and an `npm pack` content/startup smoke. + +Manual acceptance must prove the checkout revision and single OpenPI source before exercising a real provider Session. It then covers landing animation, workspace and Session lifecycle, model selection, normal and queued prompts, streaming, tool evidence, Subagent/Workflow projections, reconnect, refresh recovery, narrow-screen navigation, and production static delivery. + +## Success criteria + +- Every visible and interactive behavior in the frozen PR #352 baseline is present or has an explicit failing test. +- No command, protocol, runtime ownership, persistence, permission, or lifecycle behavior is widened. +- Development HMR and production static delivery both work from the same React source. +- The production page makes no remote asset request and does not require Vite. +- The result passes repository gates and the real-browser acceptance matrix. + +## Non-goals + +- Merging or publishing the migration. +- Adding settings, trust approval, file attachments, interrupt control, transcript search, or other open Web issues. +- Changing Pi Session storage, provider selection semantics, capability schemas, or command endpoints. +- Keeping a second legacy UI route. diff --git a/docs/design/README.md b/docs/design/README.md index 0283a682..945ef370 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -13,5 +13,6 @@ These records predate [`Decision 0001`](../decisions/0001-documentation-and-evid - [`WORKFLOW_INVOCATION_GRAPH.md`](WORKFLOW_INVOCATION_GRAPH.md) — durable invocation facts, same-run handoff refs, reusable operators, and derived graph semantics - [`CHILD_TOOL_ACTIVITY.md`](CHILD_TOOL_ACTIVITY.md) — shared compact and Pi-native expanded evidence projection for Direct Subagent and Workflow child transcripts - [`OPENPI_WEB_ARCHITECTURE.md`](OPENPI_WEB_ARCHITECTURE.md) — draft architecture, protocol boundaries, delivery phases, and visual direction for the local Web workbench +- [`OPENPI_WEB_REACT_MVP.md`](OPENPI_WEB_REACT_MVP.md) — draft local validation design for a behavior-compatible React, Astryx, and Tailwind browser migration 开发与热更新流程见 [`docs/development/OPENPI_WEB_DEVELOPMENT.md`](../development/OPENPI_WEB_DEVELOPMENT.md)。 From 036dd9c8faeb33e1fb277c76ce5693c04296531d Mon Sep 17 00:00:00 2001 From: Sparkle <2705263765@qq.com> Date: Thu, 3 Sep 2026 22:08:46 +0800 Subject: [PATCH 02/12] docs(web): define development port conflict handling Document default port fallback, strict explicit ports, fast startup diagnostics, and TUI error projection for the local React validation branch. --- docs/design/OPENPI_WEB_DEV_PORT_CONFLICTS.md | 85 ++++++++++++++++++++ docs/design/README.md | 1 + 2 files changed, 86 insertions(+) create mode 100644 docs/design/OPENPI_WEB_DEV_PORT_CONFLICTS.md diff --git a/docs/design/OPENPI_WEB_DEV_PORT_CONFLICTS.md b/docs/design/OPENPI_WEB_DEV_PORT_CONFLICTS.md new file mode 100644 index 00000000..73fa8d9f --- /dev/null +++ b/docs/design/OPENPI_WEB_DEV_PORT_CONFLICTS.md @@ -0,0 +1,85 @@ +# OpenPI Web development port-conflict handling + +- Status: draft +- Created: 2026-09-03 +- Last reviewed: 2026-09-03 +- Source boundary: OpenPI `72fbba5` plus the local `codex/web-react-mvp` development launcher +- Related issue: none; local validation branch only +- Related PR: none +- Supersedes: none + +## Context + +`bun run dev:web` starts two loopback listeners: a WebHost backend and a Vite UI. The current launcher fixes them to ports `57107` and `5173`, waits up to 15 seconds for the backend, and starts Vite with `strictPort: true`. + +Two failures currently look similar even though they require different operator actions: + +- an unrelated process owns a preferred port, so another loopback port is safe; +- another OpenPI WebHost owns the shared Web Session directory, so starting a second runtime is unsafe regardless of port. + +A backend started through Node watch can also fail while the watch supervisor remains alive. In that case the launcher waits for the full readiness timeout and reports a secondary timeout after the real startup error. + +The TUI `/web` command inherits child output while the TUI is suspended, but after the terminal is restored its notification reports only the exit code. The actionable child error can therefore disappear from the operator-facing result. + +## Chosen behavior + +### Default ports + +When `OPENPI_WEB_UI_PORT` or `OPENPI_WEB_BACKEND_PORT` is absent, its documented value is a preferred port rather than a required port. The launcher checks loopback availability starting at `5173` for the UI and `57107` for the backend and selects the first available port among at most 100 consecutive candidates, without exceeding `65535`. + +If either preferred port is skipped, startup output names the occupied preferred port and the selected replacement. The selected backend origin and UI origin are the only values passed to the readiness probe, Vite proxy, WebHost `--port`, `OPENPI_WEB_ALLOWED_ORIGIN`, and browser URL. + +Port discovery is a development convenience, not an ownership boundary. The eventual listener bind remains authoritative. If another process wins the port after discovery, startup fails explicitly rather than claiming success. + +### Explicit ports + +When an operator sets either port environment variable, that port is an exact request. The launcher validates it as an integer from 1 through 65535 and fails immediately if it cannot be bound. It does not silently select a different port. + +The error names the occupied port and the matching environment variable. This preserves deterministic CI, scripts, bookmarks, and debugging commands. + +### Existing OpenPI WebHost + +The single WebHost lease remains unchanged. A live `/web`, `openpi web`, `dev:web`, or `dev:web:backend` process cannot be bypassed by selecting another port because all of them own the same Web Session and metadata directory. + +During development startup, the launcher observes up to the latest 8 KiB of backend startup stderr while continuing to forward it to the terminal. A lease conflict or other terminal startup failure ends the launcher promptly and preserves the original error in the final diagnostic instead of waiting for a readiness timeout. It does not reuse the existing Host, persist or recover its token, or terminate it automatically. + +### TUI diagnostics + +`/web` continues to run the packaged CLI in the foreground with the terminal suspended. It forwards normal child output and retains only the latest 8 KiB, terminal-sanitized tail of stderr. If the child exits unsuccessfully, the restored TUI notification includes the actionable final error when available; otherwise it retains the existing exit-code or signal fallback. + +This diagnostic projection is display evidence only. It does not change child lifecycle, Session state, or the canonical process exit result. + +## Implementation boundaries + +- Keep port selection and startup-output handling in small development-script helpers with injected listener/process seams for deterministic tests. +- Do not weaken or duplicate `acquireWebHostLease`. +- Do not add a daemon, registry, persisted development token, or attach-to-existing-Host behavior. +- Do not make the standalone production CLI silently avoid an explicitly requested `--port`. +- Preserve normal `SIGINT` and `SIGTERM` cleanup for Vite, the watch supervisor, WebHost, and its lease. +- Bound both the number of candidate ports and captured stderr bytes. + +## Alternatives considered + +1. Let Vite alone choose another port. Rejected because the backend, proxy, allowed origin, readiness endpoint, and displayed URL would not share one resolved configuration. +2. Bind both services to port `0` and return their assigned ports over a new IPC protocol. This removes the small discovery-to-bind race but requires a larger CLI/watch protocol refactor than the development problem justifies. +3. Fail on every conflict and require manual environment variables. This is deterministic but makes ordinary local development unnecessarily brittle when common frontend ports are occupied. +4. Reuse an existing WebHost. Rejected because the process token is intentionally ephemeral and not persisted, and a second UI cannot infer compatible source, protocol, or runtime ownership from a port alone. + +## Validation + +Automated checks must cover: + +- default UI and backend ports remain unchanged when available; +- occupied default ports select the next available candidates and update every downstream origin; +- explicitly configured occupied ports fail immediately without fallback; +- invalid and out-of-range port values fail before spawning children; +- a backend lease/startup failure rejects immediately with the original cause rather than the readiness timeout; +- a bind race fails explicitly and cleans up any process or listener already started; +- `/web` reports a bounded sanitized child error and preserves signal/code fallbacks; +- successful shutdown still releases both ports and the WebHost lease. + +Repository validation remains `bun run check` and `bun run test`. Manual validation starts an unrelated listener on each preferred port, confirms automatic fallback, then starts an existing OpenPI WebHost and confirms the second launcher fails promptly without selecting a second runtime. + +## Evidence boundary + +Verified facts in the source boundary are the fixed current ports, Vite strict-port configuration, 15-second readiness timeout, Node watch supervision, and the single WebHost lease. The behavior above is approved local design, not shipped behavior or an accepted project Decision. Exact output wording and helper names may change during implementation while preserving the stated operator-visible outcomes. diff --git a/docs/design/README.md b/docs/design/README.md index 945ef370..9b011567 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -14,5 +14,6 @@ These records predate [`Decision 0001`](../decisions/0001-documentation-and-evid - [`CHILD_TOOL_ACTIVITY.md`](CHILD_TOOL_ACTIVITY.md) — shared compact and Pi-native expanded evidence projection for Direct Subagent and Workflow child transcripts - [`OPENPI_WEB_ARCHITECTURE.md`](OPENPI_WEB_ARCHITECTURE.md) — draft architecture, protocol boundaries, delivery phases, and visual direction for the local Web workbench - [`OPENPI_WEB_REACT_MVP.md`](OPENPI_WEB_REACT_MVP.md) — draft local validation design for a behavior-compatible React, Astryx, and Tailwind browser migration +- [`OPENPI_WEB_DEV_PORT_CONFLICTS.md`](OPENPI_WEB_DEV_PORT_CONFLICTS.md) — draft local design for development-port fallback, strict explicit ports, startup diagnostics, and TUI `/web` error projection 开发与热更新流程见 [`docs/development/OPENPI_WEB_DEVELOPMENT.md`](../development/OPENPI_WEB_DEVELOPMENT.md)。 From 645e4b68a567c3c551883d84bba5dc9e52fb678c Mon Sep 17 00:00:00 2001 From: Sparkle <2705263765@qq.com> Date: Fri, 4 Sep 2026 17:48:39 +0800 Subject: [PATCH 03/12] feat(web): migrate workbench UI to React - replace the imperative browser UI with typed React and Zustand projections - add Vite HMR, production asset builds, and bounded port conflict handling - cover the Web store, packaged host, responsive browser flows, and artifact drift in CI --- .github/workflows/ci.yml | 88 +- biome.json | 3 + bun.lock | 469 +++++- docs/design/OPENPI_WEB_REACT_MVP.md | 26 +- docs/development/OPENPI_WEB_DEVELOPMENT.md | 30 +- extensions/web/index.ts | 74 +- package.json | 29 +- scripts/dev-web-support.ts | 170 ++ scripts/dev-web.mjs | 127 +- tests/extensions/web/index.test.ts | 28 +- tests/web/app-render.spec.ts | 171 ++ tests/web/app-render.test.ts | 995 ------------ tests/web/cli.test.ts | 14 +- tests/web/dev-web-support.test.ts | 126 ++ tests/web/openpi-web.e2e.ts | 171 ++ tests/web/playwright.config.ts | 50 + tests/web/web-host.test.ts | 145 +- tests/web/web-store.spec.ts | 778 +++++++++ tsconfig.json | 8 +- web/dist/app.js | 87 + web/dist/favicon.svg | 9 + web/dist/index.html | 15 + web/dist/styles.css | 3 + web/host/static-assets.ts | 4 - web/host/web-host.ts | 15 +- web/ui/app.js | 1398 ----------------- web/ui/index.html | 131 +- web/ui/public/favicon.svg | 9 + web/ui/src/app/App.tsx | 130 ++ web/ui/src/app/providers.tsx | 15 + web/ui/src/components/Markdown.tsx | 52 + web/ui/src/components/OpenPiLogo.tsx | 41 + web/ui/src/features/activity/ActivityBar.tsx | 109 ++ web/ui/src/features/composer/Composer.tsx | 199 +++ .../src/features/sessions/SessionSidebar.tsx | 418 +++++ web/ui/src/features/transcript/Transcript.tsx | 856 ++++++++++ web/ui/src/i18n.ts | 151 ++ web/ui/src/lib/format.ts | 57 + web/ui/src/main.tsx | 16 + web/ui/src/protocol/client.ts | 141 ++ web/ui/src/protocol/event-stream.ts | 55 + web/ui/src/store/web-store.ts | 780 +++++++++ web/ui/src/styles.css | 403 +++++ web/ui/styles.css | 598 ------- web/ui/tsconfig.json | 12 + web/ui/vite-env.d.ts | 1 + web/vite.config.mjs | 22 +- 47 files changed, 5890 insertions(+), 3339 deletions(-) create mode 100644 scripts/dev-web-support.ts create mode 100644 tests/web/app-render.spec.ts delete mode 100644 tests/web/app-render.test.ts create mode 100644 tests/web/dev-web-support.test.ts create mode 100644 tests/web/openpi-web.e2e.ts create mode 100644 tests/web/playwright.config.ts create mode 100644 tests/web/web-store.spec.ts create mode 100644 web/dist/app.js create mode 100644 web/dist/favicon.svg create mode 100644 web/dist/index.html create mode 100644 web/dist/styles.css delete mode 100644 web/host/static-assets.ts delete mode 100644 web/ui/app.js create mode 100644 web/ui/public/favicon.svg create mode 100644 web/ui/src/app/App.tsx create mode 100644 web/ui/src/app/providers.tsx create mode 100644 web/ui/src/components/Markdown.tsx create mode 100644 web/ui/src/components/OpenPiLogo.tsx create mode 100644 web/ui/src/features/activity/ActivityBar.tsx create mode 100644 web/ui/src/features/composer/Composer.tsx create mode 100644 web/ui/src/features/sessions/SessionSidebar.tsx create mode 100644 web/ui/src/features/transcript/Transcript.tsx create mode 100644 web/ui/src/i18n.ts create mode 100644 web/ui/src/lib/format.ts create mode 100644 web/ui/src/main.tsx create mode 100644 web/ui/src/protocol/client.ts create mode 100644 web/ui/src/protocol/event-stream.ts create mode 100644 web/ui/src/store/web-store.ts create mode 100644 web/ui/src/styles.css delete mode 100644 web/ui/styles.css create mode 100644 web/ui/tsconfig.json create mode 100644 web/ui/vite-env.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 718cf5b4..44df5ac5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,14 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - run: bun run check + - name: Verify committed Web build + run: | + changes="$(git status --porcelain=v1 --untracked-files=all -- web/dist)" + if [[ -n "$changes" ]]; then + printf '%s\n' "$changes" + echo "web/dist is missing or stale; run bun run build:web and commit the result" + exit 1 + fi - run: bun run test - name: Smoke-test Pi package discovery run: | @@ -89,12 +97,88 @@ jobs: done origin="$(sed -n 's/^OpenPI Web Workbench is running at //p' "$root/host.log" | tail -n 1)" test -n "$origin" - curl --fail --silent --show-error --output "$root/marked.js" "$origin/marked.js" - grep -q 'marked v18' "$root/marked.js" + curl --fail --silent --show-error --output "$root/index.html" "$origin/" + grep -q '
' "$root/index.html" + grep -Eq ' + + + + + +