diff --git a/.agents/skills/merge-and-baseline/SKILL.md b/.agents/skills/merge-and-baseline/SKILL.md new file mode 100644 index 000000000..983a05127 --- /dev/null +++ b/.agents/skills/merge-and-baseline/SKILL.md @@ -0,0 +1,139 @@ +--- +name: merge-and-baseline +description: Merge a PR into this repo without contaminating the baseline, then bring the local checkout, the roadmap and the group chat back in sync. Use after any PR is merged or after someone else's PR is merged into `webui` or `main`. +--- + +# Merge, then realign + +A merge is not done when GitHub says merged. It is done when three copies +agree: the remote baseline, the local checkout, and what the team was told. + +This exists because on 2026-10-03 a PR that was meant to carry two test files +was branched from the wrong HEAD and dragged 2 375 lines of unreviewed work +into `webui` with it. GitHub was green throughout. The contamination was only +visible in `git diff `. + +## 1. Before opening or merging + +**Never branch from "wherever HEAD happens to be".** A checkout left on +another branch turns `-b` into a fork of that branch. + +```bash +git fetch origin # know the true baseline +git checkout -B origin/ # -B forces the START POINT +git log --oneline -1 # confirm you are on the new base +``` + +**Check the PR carries what it claims.** A PR that says "6 files" and diffs +twenty is the whole failure mode in one command: + +```bash +git fetch origin +git diff --stat origin/...origin/ | tail -1 +git log --oneline origin/..origin/ +``` + +Every commit in that list should be explainable by the PR's title. If a commit +is not, stop and find out how it got there before merging. + +**Wait for the gate, and read which one failed.** `Verification failed at: +` is an entry point, not an answer. The answer is in that job's log: + +```bash +gh pr checks +JID=$(gh pr checks | grep macos | awk '{print $NF}') +gh run view ${JID%/job/*} --job ${JID##*/job/} --log-failed | grep -E "Verification failed at|× " +``` + +**Never tell "still running" from the `gh pr checks` table.** It truncates +long check names, so a row can end at `verify (macos-latest,` with the status +column simply not rendered — which reads exactly like a check that has not +reported yet. An agent once told someone three PRs were "still running" for +half an hour while one of them had already gone red. Use the rollup and the +aggregate: + +```bash +gh pr view --json mergeStateStatus,statusCheckRollup --jq ' + "mergeState=\(.mergeStateStatus)", + (.statusCheckRollup[] | " \(.name)\t\(.status)/\(.conclusion // "-")")' +``` + +| `mergeStateStatus` | meaning | +| --- | --- | +| `CLEAN` | everything required passed; safe to merge | +| `UNSTABLE` | a required check finished red, or is still queued | +| `BEHIND` | base moved; rebase and re-run | +| `DIRTY` | merge conflict | +| `UNKNOWN` | GitHub has not computed it yet — poll again, do not read it as a verdict | + +**There is no push channel here.** `gh` is a pull, not a subscription: a CI +result reaches an agent only because it asked. If you need to be told, use a +schedule or have a human say so — and never report a check's state you did not +actually read. + +## 2. Merging + +```bash +gh pr merge --merge # merge commit, keeps per-commit attribution +``` + +Never squash a PR whose commits mean different things. This repo's gate order +is part of the review surface: a squashed PR hides which gate a fix belongs to. + +## 3. Immediately after the merge + +```bash +git fetch origin +git checkout +git merge --ff-only origin/ # ff-only: a real merge here means someone merged your branch +``` + +`--ff-only` is deliberate. If it refuses, your local branch is ahead of the +remote base — which is what happens when your branch already got merged +*through another PR*. Find out which one: + +```bash +git log --oneline --merges origin/ -4 +git merge-base --is-ancestor origin/ && echo "已在 baseline 上" +``` + +## 4. Report the baseline change to the team + +The roadmap is the team's shared record. A merge that changes what exists but +not what the roadmap says is a merge that will be re-litigated next week. + +The bot speaks for 莫克 in the group. Send one message that states: what was +merged (PR + title), the baseline commit moved to, and what the roadmap's +"基准线" line now reads. Then update the roadmap itself: + +- Baseline line in `mcode-webui-roadmap` (`BBu7dwPTCo5Ryix2MG7ctlAwndg`) — + the commit under `## 决定`, and its date/author, must match `webui` HEAD. +- The claim table and the ✅/🟡/➖/❌ summary rows follow from what actually + merged. Recompute them from the merged code, not from intent. + +Re-read the document's block structure before editing. Its tables are real +docx `table` blocks; writing a markdown pipe row as text produces a line +nobody can read and a table that silently loses a column. + +## 5. Verify the local checkout really matches + +```bash +git rev-parse --short HEAD origin/ +git diff --stat origin/ # empty means aligned +git status --short # empty tree +``` + +Both commands empty. A non-empty diff against the baseline means you are +testing something the team does not have. + +## Gates + +For this repo, `pnpm verify --profile platform` is what CI runs. Two known +conditions on `webui`, both fixed by merged PRs rather than by anything local: + +- `test:webui` was red on the 83321d0 baseline (four stale tests). +- `test:capabilities` was red on macOS (a `/var` vs `/private/var` comparison + that also made the test vacuous). + +A red gate on your branch is not automatically your regression. Diff the +failing test names against the baseline before you start hunting. diff --git a/.agents/skills/webui-development/SKILL.md b/.agents/skills/webui-development/SKILL.md new file mode 100644 index 000000000..2ea7db6ef --- /dev/null +++ b/.agents/skills/webui-development/SKILL.md @@ -0,0 +1,82 @@ +--- +name: webui-development +description: Develop MiniMax Code packages/webui features, workflows, components and styles using its application ownership, loopback contracts and existing visual language. Use for WebUI implementation, structural refactoring and UI design in this repository; use dedicated audit or documentation workflows for gate-only audits and documentation-only synchronization. +--- + +# MiniMax WebUI development + +Use the existing WebUI as the product and architecture reference. Deliver requested behavior through its real application owners and preserve visual continuity unless a redesign is requested. + +## Establish the current contract + +Locate the repository from the working directory; the original checkout is `/home/fectivnfy/my_data/project/my_project/minimax-code`. Reference paths are repository-relative. Read root `AGENTS.md`, `packages/webui/AGENTS.md`, `packages/webui/docs/README.md`, and relevant terms in `CONTEXT.md`. Inspect HEAD and dirty files before editing. This skill is a source-grounded map, refreshed 2026-10-26, not an authority over current instructions, source or tests. + +For the changed use case, identify its resource identity, state owner, command, binding, rendered surface and verification seam. Trace one real caller through implementation before choosing a home. Directory names and passing dependency gates alone do not prove ownership. If an explicitly approved design conflicts with the tree, surface that conflict before redesigning it. + +For business-code changes, follow [Business maintenance](references/business-maintenance.md): choose the change type, locate the owner, implement through its command and lifecycle, and verify the affected consumer. It includes component placement, prohibited shortcuts and runtime maintenance. + +## Load the relevant reference + +| Task | Read | +| --- | --- | +| Find code, choose an owner, judge imports, structural refactor | [Architecture and source map](references/architecture.md) | +| Browser actions, element references, the external browser bridge | [Architecture and source map](references/architecture.md) and [Loopback and runtime](references/runtime.md) — the semantics live in `@mavis/browser-core`; start from `packages/webui/docs/browser-use/10-browser-core-integration.md` | +| Session creation, send/queue/retry, streaming, async reads, disposal | [Ownership and lifecycle](references/ownership.md) | +| Wire operations, runtime assembly, capabilities, process resources | [Loopback and runtime](references/runtime.md) | +| Add a component or modify component styles/icons | Read [Component design system](references/component-design-system.md) first: token selection, scales, component recipes, Tabler icon usage and state rules | +| Theme, assets, layout, CSS compilation or visual verification | [Visual development](references/visual-design.md); screenshots there are optional context, not the design specification | +| Tests, build, packaging, verification claims or delivery | [Verification and delivery](references/verification.md) | +| Refresh the skill or check the investigation's limits | [Source evidence and refresh](references/source-evidence.md) | + +Load the branches the task needs. Repository documents linked there provide detail; verify the particular implementation rather than treating a copied rule as proof. + +## Implement through existing seams + +- Keep business state and submitted operations with their application owner. Inject capability contracts; bindings subscribe and adapt browser effects; UI renders snapshots and invokes named commands. Local uncommitted forms and ephemeral presentation state can remain local when that is their established lifetime. +- Publish a narrow owner-role module or contract for a real peer consumer. A `.public` suffix grants reachability, not authority to expose writers, teardown or another business's implementation. +- Reuse request/freshness and session-interest mechanisms when results outlive selection or mounting. Capture operation identity before awaiting; preserve navigation, deletion, incarnation and newer-input fences. +- For UI work, apply the component design system: semantic color roles, shared type/spacing/radius scales, existing control recipes, the Tabler icon library and complete interaction states. Reuse an existing primitive when available; do not draw interface icons yourself. Keep business-private styles with their business. Inspect the result in both themes; screenshots assist verification, while source tokens and component contracts define the design. +- Keep file movement deliberate: this is a public source projection with an explicit inventory and upstream synchronization costs. Prefer a cohesive change in its current home over layout churn. + +## Run only relevant verification + +Follow the current `packages/webui/AGENTS.md` gate map and select the smallest checks that cover the +changed files and affected behavior. Do not run aggregate gates or full WebUI suites by default. The +latest direct user instruction overrides older repository or skill wording that asks for broader +verification; run a full gate only when the user explicitly requests it. Report the checks run and +the coverage that remains. Activation never authorizes publication, Git pushes, production changes +or paid/live-provider calls by itself. + +## Verify complete business flows and usability + +For a user-visible WebUI flow, build a source-grounded scenario matrix before writing tests. Trace +each user-reachable entry point and primary control through its real owner, command, service or +transport, runtime consumer, persisted state, and final rendered result where those layers apply. +Include material state combinations such as empty/populated, valid/invalid, loading/error, cancel/ +retry/recovery, selection changes, refresh/resume, and concurrent activity. Cover every flow in the +approved plan or request; do not stop at the named examples if the current UI exposes more actions. + +Prefer tests that prove a user outcome over tests that mirror implementation details. Exercise the +actual browser control for interaction claims: click the button, submit the form, change the tab, +select the option, or use the keyboard path the user uses. An injected demo object, direct service +call, reducer assertion or helper invocation cannot stand in for that interaction. Keep useful unit +tests for local contracts, but label their evidence boundary accurately. + +For end-to-end or integration claims, preserve the real WebUI client/input and production service / +transport / runtime adapter / persistence path through an observable rendered or persisted outcome. +Use isolated temporary workspaces and stores, synthetic files and local origins. Deterministic local +model stubs may control output and timing; they must not replace the path under test. Explicitly +identify mocks, stubs, skips, environment gates, and any live/native boundary that remains untested. + +For layout and usability claims, run the built Chromium client and inspect the actual rendered DOM, +computed geometry, responsive or constrained viewport behavior, overlay placement/stacking, focus +and keyboard operation, scrolling, and representative screenshots for the states affected. A passing +build, selector count or screenshot alone does not prove usability. Assert that dialogs remain +overlays, controls stay visible and reachable, labels align with fields, and actions produce the +expected user-visible result. Review screenshot diffs before accepting Linux CI baselines. + +For critical cross-layer paths, add a narrow break-link or mutation negative control when practical: +the target flow should fail if input is dropped, a stream is not projected, persisted state is not +restored, a runtime update is not rendered, or a browser control does not reach its service. Restore +only the mutation introduced for that check and rerun the target flow. Preserve valid existing tests; +remove one only when behavior is retired or replacement evidence is demonstrated and recorded. diff --git a/.agents/skills/webui-development/agents/openai.yaml b/.agents/skills/webui-development/agents/openai.yaml new file mode 100644 index 000000000..bd7a77cac --- /dev/null +++ b/.agents/skills/webui-development/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "MiniMax WebUI Development" + short_description: "Build WebUI features with architecture and visual continuity" diff --git a/.agents/skills/webui-development/references/architecture.md b/.agents/skills/webui-development/references/architecture.md new file mode 100644 index 000000000..ee0d09d71 --- /dev/null +++ b/.agents/skills/webui-development/references/architecture.md @@ -0,0 +1,93 @@ +# Architecture and source map + +Paths are repository-relative. Start structural work with `packages/webui/docs/architecture/design.md`; use `layered-model.md`, `module-map.md`, and `boundaries.md` for detail. Confirm their claims against touched source and tests. + +## Execution boundaries + +| Boundary | Responsibility | Source under `packages/webui/src/` | +| --- | --- | --- | +| Browser client | Owners, projections, React surfaces, browser IO | `client/main.tsx`, `client/application/create-application.ts` | +| Loopback server | HTTP/WS admission, envelope/body validation, dispatch and delivery | `server/service.ts`, `server/access-policy.ts`, `server/operation/operation-dispatch.ts` | +| Process-local runtime | Host assembly, adapters, authentication/resource lifecycle, PTYs | `runtime/assembly.ts`, `runtime/lifecycle.ts`, `runtime/harness/adapter.ts` | +| Shared contracts | Wire names, DTOs, frames, capability facts | `shared/operation-names.ts`, `shared/contracts/`, `shared/envelope.ts` | + +Browser → authenticated loopback service → `WebuiHarnessPort` → process-local harness. The port call is an in-process adapter seam, not another network hop. WebUI builds on `@mavis/local-runtime-v2`; it does not launch the CLI or import `packages/tui`. TUI may be read for behavior/event semantics. Local `src/shared/` and the workspace package `@mavis/shared` are different homes. + +## The browser's own boundary + +The browser runtime lives in `src/runtime/browser/` and the external bridge in `src/server/browser-bridge/`. Both reach a real browser through `@mavis/browser-core`, a **peer** workspace package (`packages/browser-core/`) rather than a WebUI layer. + +| Concern | Owner | Where | +| --- | --- | --- | +| What a browser action *means* — element references, retained snapshots, action deadlines, effect validation | `@mavis/browser-core` | `packages/browser-core/src/` | +| How a command reaches one page or tab | a `BrowserTransport` adapter, one per surface | `src/runtime/browser/isolated-chromium/core-transport.ts`; `src/server/browser-bridge/core-transport.ts` | +| Whether an action *may* run | the WebUI | `src/runtime/browser/authorization.ts`, `src/runtime/browser/browser-service.ts` | + +**The rule that keeps this honest:** Browser Core decides what an action means, the WebUI decides whether it may run, and a transport decides only how the command travels. A transport never judges an action's meaning, and the core reads no authorization switch. + +When changing a browser action, an element reference, or the bridge's command path, read `packages/webui/docs/browser-use/10-browser-core-integration.md` first. Two consequences are easy to miss: + +- **Do not re-implement an action's semantics in a provider.** A provider that translates a restricted action into raw DevTools calls itself is a second definition of what the action does, and the two will drift. Add the capability to the core, or extend the transport — not the provider's action switch. +- **`src/client/` and `src/shared/` may not import `@mavis/browser-core`.** The dependency gate rejects it, and the reason is structural: the browser bundle must not carry a second, unreachable copy of the core's semantics. This is why the restricted command vocabulary is duplicated in `src/shared/contracts/browser.ts` rather than read from the core; `test/unit/browser-restricted-commands.test.ts` asserts the two lists cannot drift. If you add an action, update both and let the test tell you. + +The two surfaces are not interchangeable, and the differences are properties of the browsers rather than of the WebUI's knowledge: the bridge cannot `open_tab` (`chrome.debugger` cannot open a page in the person's browser) and cannot `upload_files` (`DOM.setFileInputFiles` is deliberately absent from the extension's allowlist). The bridge's screenshot comes from the extension's pushed visible-tab frame, so a tab that is not in front is refused rather than answered with another tab's picture. + +## Client placement + +Paths here are under `packages/webui/src/client/`. + +| Home | Owns | Boundary | +| --- | --- | --- | +| `main.tsx` | Concrete transport/storage/DOM wiring and React mount | Side-effectful entry; do not import it as a library | +| `application/` | Assembly, canonical session record, selectors, epochs, task creation | Framework-free; receive IO capabilities rather than concrete adapters | +| `conversation/` | Freshness, selected slots, interests, stream mechanics, transcript history | Mechanisms do not reach feature implementation, platform or assembly | +| `features//application/` | Business decisions, commands, mutations and resource truth | No React, browser globals or concrete transport | +| `features//contracts/` | Capability slices, commands, view types | Leaf declarations, not implementation forwarding barrels | +| `features//projection/` | Pure classification and view derivation | No IO/React; policy-bearing derivations classify as domain | +| `features//bindings/` | Hooks/subscriptions bridging owners to React | Read owners and invoke commands; do not acquire writer authority | +| `features//ui/` | Presentation and business CSS | No business RPCs, owner construction or generic store writes | +| `bindings/`, `platform/` | React/browser effects; browser IO adapters | Business policy belongs to owners | +| `projection/common/` | Pure shared geometry, routes and value shaping | No business owner | +| `shell/`, `ui/` | Surface composition; business-independent primitives | Shell creates no owners; generic UI reaches no business | + +Create only needed roles. A feature-root `.ts` file without a role directory classifies unknown. A business can own resources without owning a screen. + +## Business routing + +This is a starting map, not a frozen registry. Rediscover files with `rg --files packages/webui/src/client/features` and follow callers. Paths below are within the named feature unless stated otherwise. + +| Business | Change here for | Useful source | +| --- | --- | --- | +| `sessions` | Catalogue, unread, archive/delete/fork/import/export, rail | `application/session-workflows.ts`, `session-catalog.ts`, `unread.ts`; `ui/SessionRail.tsx` | +| `composer` | Draft/history/staging, captured input, slash commands, queue/retry | `application/composer-controller.ts`, `composer-store.ts`, `command-workflows.ts` | +| `turn` | Admission, send/resume/attach, stream generation/phase, process-event policy | `application/turn-commands.ts`, `submission-admission.ts`, `event-coordinator.ts`, `session-fact.ts` | +| `transcript` | Message/tool projection, Markdown, scroll, feedback | `projection/transcript-projection.ts`, `message-projection.ts`; `ui/SessionTranscript.tsx`, `markdown.public.tsx` | +| `interactions` | Permissions, questionnaires, plan approval | `application/interaction-coordinator.ts`, `interaction-session-owners.ts` | +| `goal` | Goal reads, create/patch submission and session ownership | `application/goal-coordinator.ts`, `goal-submission.ts`, `goal-session-owners.ts` | +| `models` | Catalogue selection, providers/discovery/tests, editor | `application/models-workflows.ts`, `provider-editor.ts` | +| `settings` | Personalization/profile/memory, preference surfaces and theme | `application/settings-workflows.ts`, `projection/settings-view.ts`, `ui/SettingsModal.tsx` | +| `account` | Login/status/quota/check-in and polling | `application/account-workflows.ts` | +| `plugins` | Plugin/skill/app/MCP/agent listings and mutations | `application/plugin-workflows.ts`, `composer-catalogue.ts`, `contracts/plugin-actions.ts`, `ui/PluginCatalogue.tsx` | +| `workspace` | Files/environment/archives/canvas, panel choices | `application/workspace-queries.ts`, `workspace-*-owner.ts`, `ui/WorkspacePanels.tsx` | +| `review` | Turn changesets and working-tree snapshot resources | `application/turn-review-owner.ts`, `workspace-review-owner.public.ts` | +| `terminal` | Browser terminal catalogue, commands, watches | `application/terminal-owner.ts`; service owns processes | +| `scheduling` | Scheduled tasks: list, create, edit, enable/disable, delete, run now, run history; whether this client holds a task | `application/scheduling-owner.ts`, `projection/scheduling-panel.ts`, `ui/SchedulingPanel.public.tsx`, `ui/scheduling-editor.tsx`; decision: `docs/adr/0027-the-webui-is-a-scheduler-host-for-scheduled-tasks.md` | +| `browser` | Per-target browser state, authorization switches, the panel host | `application/browser-commands.ts`, `application/browser-authorization.ts`, `projection/browser-state.ts`, `ui/BrowserPanelHost.public.tsx`; semantics are `@mavis/browser-core` | +| `miniapp` | Mini App catalogue, open/stop, the standalone tab and the agent bridge | `application/miniapp-workflows.ts`, `application/miniapp-bridge.ts`, `projection/miniapp-view.ts`, `ui/MiniAppView.public.tsx` | +| `shell` | Boundary errors and surface shaping | `application/boundary-error.ts`, `projection/shell-surface.ts` | + +Settings hosting an editor does not transfer Models ownership. Workspace rendering a PTY does not own its process. Transcript rendering an interaction does not own reply policy. A named use case may coordinate multiple owners, as `client/application/task-session-workflows.ts` does. + +## Executable dependency rules + +Read `scripts/lib/webui-dependency-rules.mjs` and `scripts/check-webui-dependency.mjs` before changing imports/classification. The checker resolves TypeScript references, including erased type-only edges. + +- `classifyLayers` and `classifyRole` are separate. A layer-legal edge can violate privacy, presentation authority or ownership. +- `ALLOWED_EDGES` permits identity edges and allows `view`/`domain` cross-edges. Do not invent a strict descending DAG from a picture. +- `PROJECTION_DOMAIN` decides which projection basenames carry policy; `.public` is stripped before lookup. Classify new policy-bearing projections there. +- Peer `contracts/` and published `.public.ts`/`.public.tsx` are reachable subject to other rules. Private peer implementation is not. +- Publication keeps the role. Explicit owned exports are public; export-star and foreign re-exports/namespace/default/dynamic access cannot launder implementation into an API. +- Server reaches `runtime/port.ts`, not runtime implementation. Browser never reaches that port or Node. Shared declarations reach neither execution side. +- A move updates every importer and removes old forwarding shims. Do not add allowances to conceal ownership changes. + +A green graph proves encoded compliance, not use-case cohesion, temporal correctness, portability or platform behavior. diff --git a/.agents/skills/webui-development/references/business-maintenance.md b/.agents/skills/webui-development/references/business-maintenance.md new file mode 100644 index 000000000..daeb49b5c --- /dev/null +++ b/.agents/skills/webui-development/references/business-maintenance.md @@ -0,0 +1,101 @@ +# Business-code maintenance + +Use this procedure when changing WebUI behavior or structure. Paths beginning with `src/` are relative to `packages/webui/`. Current repository/package instructions remain authoritative; [architecture.md](architecture.md) maps owners and import rules, [ownership.md](ownership.md) defines temporal contracts, and [runtime.md](runtime.md) covers wire and host details. + +## Locate the change before editing + +1. Identify the user-visible behavior being changed and an observable completion condition. For a bug, distinguish intended behavior from current behavior; for a refactor, name the behavior/contracts to preserve. +2. Trace the existing surface or public caller to its binding, command, owner and capability. Search real importers and tests, not only filenames or documentation. Identify who stores the truth, who may mutate it, and what lifetime it has. +3. Choose the change type below. State the intended homes briefly for a substantial change; ordinary local choices do not need a separate approval round. If the requested change conflicts with an approved decision or changes scope, resolve that decision before the dependent edits. + +Before the first behavioral edit, know the owner/command, response-affecting identity and affected consumers. A style-only edit need not invent an application workflow. + +## Put each responsibility in its home + +| Responsibility being added | Home | Placement decision | +| --- | --- | --- | +| Business component | `src/client/features//ui/.tsx` | Choose the business owning its behavior/resource, not whichever page happens to host it | +| Business component CSS | Same business's `ui/.css` | Follow [component design system](component-design-system.md); imports flow through `styles/index.css` | +| Business decision, workflow, command or resource owner | Same business's `application/` | Receive capabilities; own operation outcomes and invalidation | +| Capability/command/read-model declaration | Same business's `contracts/` | Expose the narrow contract the consumer needs | +| Pure policy or view derivation | Same business's `projection/` | Classify policy-bearing basenames in `PROJECTION_DOMAIN` when required | +| React subscription or browser effect | Same business's `bindings/`, or the established shared binding | Adapt to React/browser; business decisions remain in the owner | +| Business-independent presentation primitive | `src/client/ui/` | Extract for actual reuse; it must not depend on a business | +| Composition of existing business surfaces | `src/client/shell/` | Compose already-owned surfaces and commands | +| Named application-wide use case or owner wiring | `src/client/application/` | Coordinate existing owners without duplicating their mutable truth | +| Browser IO adapter | `src/client/platform/` | Adapt browser APIs or concrete transport; inject capabilities into application code | +| Shared lifecycle mechanism | `src/client/conversation/` | Generalize only mechanics with real shared consumers; keep business interpretation out | + +Add a new business only when it has distinct resource/policy ownership that existing businesses cannot cohesively own. Create needed role directories only; there is no feature registration list to invent. Keep existing layout stable unless moving responsibility is part of the task. + +## Modify an existing feature + +1. Change the rule where it is owned: owner/command for business behavior, projection for pure shaping, binding for browser adaptation, component/CSS for presentation. Extend existing commands rather than adding a second path with independent state. +2. Preserve the command's actual contract: success/refusal/failure values, absent-capability handling, captured input and invalidation obligations. If the contract must change, inspect and update every producer, consumer, adapter and fixture that depends on it. +3. For async behavior, capture the operation's resource/tab/session identity before awaiting. Check selection, request generation, freshness, owner incarnation, deletion and disposal where they affect this operation. Reuse established primitives rather than adding a view-local request counter to conceal an owner issue. +4. Connect the rendered consumer to the same owner snapshot and named command. Remove an old path only after its consumers have moved; preserve unrelated user changes. A component refresh must not bypass cache policy with a direct fetch or a generic store clear. +5. Exercise the changed outcome and affected ordering/lifetime cases, then run applicable gates from [verification.md](verification.md). Finish with behavior and evidence, not merely a list of edited files. + +Inspect the touched leaf's real implementation: application-level disposal does not prove all its owners fence pending work. Repair a gap required by the task explicitly; do not assume a guide's architectural intent is already universal behavior. + +## Add a component or new business behavior + +For a presentation-only component, reuse its business's read model/commands and existing shared controls. Keep uncommitted forms and ephemeral display state local when appropriate. Select tokens, geometry and interaction states from [component-design-system.md](component-design-system.md) before styling. + +For new business behavior, start from its command and resource contract, then implement the owner, wire the injected capability, add/update the binding and render it. Do not create directories or abstractions for stages that are unnecessary. When remote capability is needed, complete the operation path in [runtime.md](runtime.md); when an existing capability suffices, reuse it. + +Define pending, empty, error, unavailable and completion behavior as applicable. Register new owners/resources with the actual construction/start/release/dispose path. Check operation survival when the view unmounts or selection changes. Add public exports only for identified consumers, and register new tests/files using the repository's suite and source-inventory rules. + +## Change a cross-business use case + +Identify which business owns each decision and which owner writes each result. Inject a peer's published command/intent/read contract through existing composition. The initiating owner can choose when to call it; the peer keeps its writer and teardown. + +Use an explicitly named application use case when a cohesive operation genuinely spans owners, as task/session creation does. It coordinates their commands and committed outcome rather than collecting their stores into another central service. A `.public` marker makes a module reachable subject to other rules; it does not make every export appropriate or grant cross-business write authority. + +Check both import legality and the caller's knowledge: if a consumer must reconstruct another owner's admission, freshness, state-transition or cleanup protocol, move that responsibility behind the owning command rather than distributing the protocol across callers. + +## Refactor without changing behavior + +Define the ownership/dependency improvement first. Keep behavior-preserving movement distinguishable from contract/behavior corrections so each has reviewable evidence. Change cohesive responsibilities rather than splitting by arbitrary file size. + +For a move, repoint all resolved importers, including relative and type-only edges; remove obsolete forwarding shims and review inventory changes. Compare behavior at the public command/consumer seam. Use AST, wire or effective-CSS comparisons when those contracts need preservation, as described in [verification.md](verification.md). Passing import checks alone does not establish preserved behavior or improved cohesion. + +## Maintain WebUI runtime code + +Choose the actual responsibility before editing `src/runtime/`: + +| Runtime change | Edit and preserve | +| --- | --- | +| Map a host capability to WebUI | `runtime/port.ts` plus matching `runtime/harness/.ts`; DTOs stay in `shared/contracts/`, validators/dispatch stay server-owned | +| Add/change a wire operation | Canonical spec → descriptor/validation → binding or dedicated handler → registry → port/adapter → client slice/command and affected tests; follow [runtime.md](runtime.md) | +| Construct a process resource | `runtime/assembly.ts` wires it; its module encapsulates behavior; `runtime/lifecycle.ts` or the established resource owner handles close/startup failure | +| Change credentials/login/tools/browser-provider lifetime | Inspect acquisition, refresh/watch, successful close and partial-startup failure together; retain errors and ensure each acquired resource is released | +| Change terminals | Preserve service-owned PTYs and independent browser watches/xterm attachments; only explicit process disposal or service shutdown ends the process | +| Change host events or readiness | Trace actual producer and decoder, regenerate host-event catalog when needed, and update capability prerequisites/consumers without inventing support | + +Read `docs/runtime/boot.md` and `owner-identity.md` for changed host policy. Retain `webui` identity and current startup/permission/questionnaire contracts. Assembly wires dependencies; it must not accumulate domain state machines or become a duplicate harness. Server admission, framing and browser selection state do not move into runtime for convenience. + +Test new cleanup on both successful shutdown and failure after partial acquisition. Verify changed capability mapping through real loopback with a controlled leaf when appropriate; production-host/native changes need their corresponding seam. Do not spend on a live provider or claim another platform without authorization/evidence. + +## Prohibited shortcuts + +| Do not | Use instead | +| --- | --- | +| Fetch business data or write generic stores directly from UI | Owner command and subscribed read model | +| Create business owners, permanent business event routes or independent business polling in a mounted presentation | Existing application/session composition and observation ownership | +| Copy session/resource truth into a second mutable map | Canonical owner plus projections or identity-keyed resources | +| Import another business's private implementation | Narrow owned contract or deliberate published surface | +| Hand a peer generic writers or teardown through a public module | Named business intent/command with retained ownership | +| Import React/DOM/storage/concrete transport into pure application/projection code | Inject values/capabilities and adapt browser effects in their proper homes | +| Cross browser→Node, server→runtime implementation, or import TUI implementation | Browser contracts, shared DTOs and the harness port/adapter | +| Turn page/view detach into host abort or PTY destruction | Separate observation cleanup from explicit execution/process commands | +| Add gate exceptions, drop supported behavior, or relax validation/admission just to pass checks | Fix the actual ownership/contract violation; preserve explicit failures | +| Hide failed or absent capability behind empty/default-success data | Existing typed outcome/error and appropriate disabled/error UI | +| Add a new palette, control family or stylesheet entry for a local component | Existing semantic tokens, recipes and stylesheet ownership | +| Hand-edit generated catalogs/inventory or preserve old paths with forwarding barrels | Regenerate reviewed artifacts and update real importers | + +Apply these to the task's affected behavior; they do not authorize an unrelated cleanup sweep. + +## Ready for delivery + +Confirm that the requested outcome has one authoritative command/write path, new files have correct homes, peer APIs expose only intended authority, and affected async/resource cleanup is accounted for. Run relevant declared checks once for the actual inputs and record remaining boundaries. Update the behavior-owning docs and affected development guidance; use `webui-docs-sync` for a broader synchronization when needed. Keep implementation, source inspection, fixture/browser evidence and live/platform verification distinct. diff --git a/.agents/skills/webui-development/references/component-design-system.md b/.agents/skills/webui-development/references/component-design-system.md new file mode 100644 index 000000000..34ca8e788 --- /dev/null +++ b/.agents/skills/webui-development/references/component-design-system.md @@ -0,0 +1,118 @@ +# Component design system + +Read this before adding a WebUI component or changing its appearance. It supplies the immediate choices needed to produce consistent components. Paths below are relative to `packages/webui/`. Current `src/client/styles/tokens.css` defines values; `tailwind.config.cjs` defines available utility mappings. Component recipes below distinguish existing implementations from recommended defaults for new work. + +## Design direction + +Use restrained neutral surfaces, quiet separators, compact readable text, rounded controls and sparse blue accents. Communicate hierarchy through surface, spacing and text roles. Primary actions use the existing neutral high-contrast treatment; links, selected indicators and switches use accent where their existing recipe does. Adding a component is not an invitation to add a new palette, font, gradient, shadow or icon family. Use the existing Tabler icon library for interface glyphs; do not draw new icons yourself. + +For a style modification, retain the component's established size and interaction contract unless the request changes it. For a new component, choose the matching recipe below, then fill its content and behavior. The rules and recipes are the design reference; full-page screenshots are optional context. + +## Select colors by purpose + +Use semantic tokens in component styles. Primitive ramps such as `gray_500` and `blue_400` define the palette underneath themes; do not use them instead of semantic roles simply because a sampled color looks right. Let `.light`/`.dark` rebind semantics rather than adding per-component theme colors. + +| Purpose | CSS token | Tailwind example | +| --- | --- | --- | +| Main canvas/input fill | `--bg_default_primary` | `bg-bg_default_primary` | +| Secondary surface | `--bg_default_secondary` | `bg-bg_default_secondary` | +| Grouped card/surface | `--bg_grouped_secondary` or `--bg_grouped_tertiary` | `bg-bg_grouped_secondary` | +| Floating elevated surface | `--bg_grouped_secondary_elevated` | `bg-bg_grouped_secondary_elevated` | +| Main title/body | `--text_default_primary` | `text-text_default_primary` | +| Description/support text | `--text_default_secondary` | `text-text_default_secondary` | +| Lower emphasis metadata | `--text_default_tertiary` | `text-text_default_tertiary` | +| Accent link/indicator | `--text_default_accent`, `--icon_default_accent` | `text-text_default_accent` | +| Primary/secondary icon | `--icon_default_primary`, `--icon_default_secondary` | `text-icon_default_secondary` | +| Ordinary/subtle separator | `--border_default`, `--border_light` | `border-border_default` | +| Focus/selected outline | `--border_accent` | `border-border_accent` | +| Neutral primary action | `--bg_interaction_primary_default` + `--text_label_primary_default` | `bg-bg_interaction_primary_default text-text_label_primary_default` | +| Secondary action | `--bg_interaction_secondary_default` + `--text_default_primary` | `bg-bg_interaction_secondary_default` | +| Quiet hover/press/selection | `--bg_interaction_tertiary_hover`, `_press`, `_selected` | `hover:bg-bg_interaction_tertiary_hover` | +| Destructive text action | `--text_label_danger_secondary_default` | `text-text_label_danger_secondary_default` | +| Destructive filled action | `--bg_interaction_danger_primary_default` + `--text_label_danger_primary_default` | Match its `_hover`/`_press`/`_inactive` family | +| Error/warning/success surface | `--bg_status_error`, `--bg_status_warning`, `--bg_status_positive` | Pair with corresponding semantic text/icon/border, not color alone | + +Choose a background/foreground pair together. Inverted/static text is for the matching contrast surface, not ordinary card labels. Use the same semantic family's state variants; do not darken a hex value manually for hover. Existing `--webui-color-*` aliases and hardcoded legacy declarations are not a second design system to extend. + +## Typography and geometry + +| Concern | Available scale / preferred starting point | +| --- | --- | +| Fonts | `font-sans` for UI; `font-mono` for code/terminal. Existing HarmonyOS/system/CJK and Hack/system fallbacks remain authoritative | +| Body | `text-body-base`: 14px / 22px; compact form/menu labels often use `--size_14` / `--line_height_20` | +| Caption | `text-caption-base`: 12px / 18px; metadata uses secondary/tertiary text roles | +| Other size tokens | `--size_12/14/16/20/24/32/40/48/64`; use large sizes for actual headings/hero content, not ordinary controls | +| Spacing tokens | `--spacing_0/2/4/6/8/12/16/20/24/32/40/48/64/90/128` | +| Spacing use | 4–8px within an icon/label group; 8–12px between related controls; 12–16px component padding; 20–32px between sections are recommended starting points, adjusted to the existing surface recipe | +| Radius tokens | `--radius_4/8/12/16/20/24/32/full` | +| Radius use | 8px compact controls/inputs; 12px menus/small surfaces; 16–20px larger cards/composer; full for pills/circles. Preserve a reused component's exact recipe | +| Weight | Use existing `font-normal`/`font-medium` utilities (400/500). Baseline `--weight_regular`/`--weight_medium` values contain `px`; do not copy that unit into new `font-weight` rules | +| Icons | Tabler React components from `@tabler/icons-react`; typically 16px inside compact controls. Reuse a matching `WebuiIcon*` wrapper from `src/client/shell/icons.tsx` when available; otherwise import the Tabler glyph directly and size it to the component recipe | + +### Interface icons + +- Use `@tabler/icons-react`, already included by `packages/webui`. Prefer an existing semantic wrapper from `src/client/shell/icons.tsx` when one matches; otherwise import the appropriate named `Icon*` component directly. +- Do not hand-draw interface glyphs as inline SVG paths, CSS shapes/pseudo-elements, or text characters such as arrows, checks, plus signs and crosses. Replace such glyphs with Tabler components and retain the surrounding accessible name, layout and interaction. +- Keep approved brand marks as their supplied assets. Purpose-built illustrations, data visualizations, progress geometry and animations are not interface glyphs; keep their domain-specific rendering when needed instead of forcing them into an icon component. + +Prefer `gap-spacing_8`, `p-spacing_12`, `rounded-radius_8`, `text-body-base` and semantic colors. Stock classes such as `gap-2` and `size-7` are supported too; preserve them in existing recipes. Literal geometry is appropriate when the scale does not describe the required shape. Distinguish documented existing sizes from recommended composition defaults; there is no universal 36px height for every button or 20px radius for every card. + +## Component recipes and reuse + +| Component | Reuse or inspect | Existing recipe / instruction | +| --- | --- | --- | +| Binary switch | `src/client/ui/ToggleSwitch.tsx`; `.webui-toggle-switch` in settings CSS | Reuse component and its `checked`, `label`, `onChange`, `disabled` contract. Existing track is 40×20px with a sliding thumb; keep `role=switch`, `aria-checked`, accent checked state and visible focus | +| Compact icon action | `src/client/ui/form-atoms.tsx` `IconButton` | 28px hit area, 16px glyph, tertiary→secondary icon hover and disabled opacity. Supply a Tabler React icon component as its child; `IconButton` renders the child directly | +| General settings row | `features/settings/ui/SettingsModal.public.tsx`: `SettingRow`, `Section`, `Divider`, `Button` | Reuse inside its owning module: left title/description, right control, grouped card, inset separators. These helpers are private, not a shared package API | +| Settings action button | `.webui-mavis-button` in `features/settings/ui/settings.css` | Existing 36px height, 80px min-width, radius 8, horizontal padding 12, text 14; neutral gray/primary/danger variants. Inspect effective declarations and state rules | +| Form field | `features/models/ui/ProviderFormPanel.public.tsx` and `.webui-provider-form-field` in `models.css` | Existing input 40px high, radius 8, border-default, canvas fill, horizontal padding 12; label 14/20, gap 8; focus border-accent. Textareas preserve multiline behavior | +| Context menu | `src/client/ui/ContextMenu.tsx`, `.webui-context-menu` in shell CSS | Reuse placement/dismissal semantics. Existing surface width 208px, radius 12, padding 6, rows min-height 30px, gap 8, hover fill and danger/disabled states | +| Help hint | `features/settings/ui/settings-atoms.tsx` `InfoHint`/`SectionHeader` | Settings-owned; matches existing bubble and keyboard focus. Do not import a private settings atom from another business | +| Status/empty/error | Neighboring feature component and semantic status tokens | Preserve text explanation, pending/error distinction and retry ownership; do not rely on a badge color to convey state | + +There is no universal public Button/Input/Card library in this baseline. Reuse an existing shared primitive when one exists. Within a business, reuse its private recipe. For a new business-owned control, implement its own semantic-token CSS matching the established recipe rather than importing another business's private CSS class or TSX helper. Extract a shared primitive only when actual reuse justifies it and it can live in `client/ui/` without business dependencies. Do not create a generic component library merely to add one control. + +## State contract for interactive controls + +| State | Required treatment when applicable | +| --- | --- | +| Default | Semantic foreground/background pair, legible label, stable geometry | +| Hover / press | Corresponding semantic state tokens; keep dimensions stable | +| Keyboard focus | Visible `--border_accent` treatment; existing precedent is a 2px outline with 1–2px offset, or the reused component's inset ring | +| Selected / checked | Visible indicator plus `aria-pressed`, `aria-selected` or `aria-checked` as appropriate; selected is not hover | +| Disabled / unavailable | Native disabled semantics where appropriate, muted/inactive treatment and no active hover; explain unavailable capabilities when the user needs the reason | +| Pending | Prevent duplicate action as required by its command contract; retain label/layout, expose progress; pending is not empty | +| Error | Semantic error treatment, readable message and existing recovery command; preserve input | + +Global controls already use a short 120ms response curve and reduced-motion handling in `styles/index.css`. Reuse those defaults rather than introducing decorative movement. Check long labels, shrink/overflow behavior, keyboard navigation and overlays in the actual component. + +## Minimal new-control styling example + +This illustrates a quiet business-owned action, not a new shared component or a prescription to replace existing controls. Use a native `button type="button"`, an accessible name and the owner's command. + +```css +@layer components { + .webui-example-action { + display: inline-flex; + align-items: center; + gap: var(--spacing_8); + border: 1px solid var(--border_default); + border-radius: var(--radius_8); + padding: var(--spacing_6) var(--spacing_12); + background: var(--bg_interaction_tertiary_default); + color: var(--text_default_primary); + font-size: var(--size_14); + line-height: var(--line_height_20); + } + .webui-example-action:not(:disabled):hover { background: var(--bg_interaction_tertiary_hover); } + .webui-example-action:not(:disabled):active { background: var(--bg_interaction_tertiary_press); } + .webui-example-action:focus-visible { outline: 2px solid var(--border_accent); outline-offset: 2px; } + .webui-example-action:disabled { color: var(--text_default_tertiary); cursor: default; } +} +``` + +Replace the example name with the owning business/component name. Place CSS in `src/client/features//ui/.css`, imported through `styles/index.css`. Shared styling belongs in the appropriate existing shared home. Do not add another stylesheet link. + +## Completion check + +Before delivering a component, be able to name its reused primitive/recipe, semantic foreground/background/state tokens, type/spacing/radius choices, and stylesheet owner. Build to confirm utilities/classes are emitted, then inspect the actual result in light/dark and relevant interaction states. Use `visual-design.md` for compiler/cascade/asset and browser-verification details. Screenshots verify the result; they do not replace these design decisions. diff --git a/.agents/skills/webui-development/references/ownership.md b/.agents/skills/webui-development/references/ownership.md new file mode 100644 index 000000000..8e2a7a78b --- /dev/null +++ b/.agents/skills/webui-development/references/ownership.md @@ -0,0 +1,64 @@ +# Ownership, submission and async lifecycle + +Read `packages/webui/docs/client/ownership.md`, `submission.md` and, for input fields, `composer.md`. Source paths below are under `packages/webui/src/client/`. + +## Application lifetime + +Trace `main.tsx` → `application/create-application.ts` → owner. Construction is dormant. `start()` applies initial selection before unread hydration, activates initial reads and attaches one process-event ingress. Start is idempotent while started; disposed instances refuse restart. Application disposal fences its lifecycle first, attempts every cleanup and retains labeled failures. This does not prove that each leaf owner fences its own pending work: inspect the owner being changed. + +One long-lived application `watchEvents` ingress uses `platform/event-channel.ts` through `bindings/event-channel-adapter.ts`. Send/resume delivery and terminal watches are separate; one ingress does not mean one socket for the product. + +Owners are framework-free; snapshots stay stable until changed for `useSyncExternalStore`. Assembly owns the canonical session record and independent business owners. Do not duplicate mutable truth for a panel. + +Most owners live for application/session lifetime. Preserve explicit exceptions: Models' `application/provider-editor.ts` is used once per Usage-page visit, above the custom-source conditional. Draft generation invalidates older test results; operation generation fences close/dispose. Local uncommitted forms and ephemeral presentation state may stay local. + +## Resource identity + +Use `conversation/request-freshness.ts` when cached reads have identity/freshness obligations. + +| Mechanism | Contract | Consequence | +| --- | --- | --- | +| Keyed resource | Answer stored under all response-affecting inputs | Include session/agent/snapshot and opaque credential/configuration epochs when relevant; never secrets | +| Selected slot | Destination has identity and selection generation | A valid A result cannot populate selected B | +| Commit token | Resource, revision and owner incarnation must match | Recreated owners reject old completions | +| Invalidation | Advance freshness before repair scheduling | An older read cannot clear a later dirty revision | +| Coalescing | Match key, revision and incarnation | Pre-invalidation pending reads are not fresh repairs | +| Request outcome | `ready` with token/value, `superseded`, `disposed`, `failed` | Consume this request's outcome rather than guessing from shared state | +| Epoch/release | Retire affected identities and subscriptions | Preserve unrelated resources and monotonic stale-token fences | + +Reuse `runWebuiMutation` where appropriate: submitted writes invalidate affected resources. Invalidation obligations outlive mounted views. Distinguish absent capabilities, stale/disposed results, empty data and successful mutation; keep original errors. + +Read actual examples: Settings summary/body are separate keys; Models acquisition differs from catalogue selection; Review separates turn changesets/workspace snapshots; Workspace has files/environment/archive/canvas owners. + +Existing implementations do not all use the same primitive. At this baseline `workspace-files-owner.ts` uses request versions keyed by operations/tab IDs and its `dispose()` only clears listeners; it does not itself invalidate pending reads. `workspace-queries.ts.releaseSession()` releases canvas state, not every file resource. Treat lifecycle rules as requirements to preserve or establish for touched behavior, not evidence that every leaf already satisfies them. A preview refresh must also account for the current `readFile()` cache short-circuit and `setInlineFile()`/`discardTab()` supersession; add a named owner command instead of a component fetch or generic state clear. + +## Interests and execution + +`conversation/session-scope.ts` tracks independent selected-view, secondary-consumer, delivery-attempt and command-settlement interests. Navigation releases its own interest. `turn/application/stream-lease-controller.ts` and `conversation/stream-lease.ts` fence attempts by generation; one live lease per session owns stream writes. + +Keep `conversation/stream-loop.ts` generic. Turn's `stream-loop-sink.ts` adapter owns Turn/Transcript interpretation. Do not push business transitions into generic delivery to reduce wiring arguments. + +Closing pages, releasing views and detaching delivery end observation. Explicit stop invokes `abortSession`; removing an unstarted queued item is another operation. Terminal owner disposal ends browser watches; `runtime/terminal.ts` owns processes for the service. Hiding xterm or detaching a secondary page must not kill another page's PTY. + +Authoritative deletion revokes commit rights, releases owned detail and detaches late delivery handles. Compact deleted-ID fences survive until application teardown because events have no replay barrier; count them separately from live cache/interest/detail. + +Read `packages/webui/docs/adr/observation-architecture.md` before proposing server snapshot/delta reconciliation; recheck host capabilities against that decision. + +## Creation and input consumption + +`application/task-session-workflows.ts` owns workspace/no-project policy, creation completion, home incarnation and navigation epoch across send/goals/plugins/rail creation. It delegates to existing stores rather than keeping another session map. + +Read `turn/contracts/submission.ts`, `turn/application/submission-admission.ts`, `composer-submission.ts`, `execution-target.ts`, and `composer/application/composer-controller.ts` together. + +- Tickets separate immediate admission, early creation identity/navigation and terminal completion. Adoption must not wait for stream settlement. +- No delivery capability refuses unsupported before creating. An in-flight home creation is busy. Live turns queue only with queue capability; queue support alone does not imply send support. +- Completion represents dispatch/queue/refusal/failure as values. Preserve exact current unions rather than reducing them to a success boolean. +- Creation is single-flight per captured home incarnation. Adopt/select under the initiating navigation claim. Existing destination state wins; purged targets stay deleted; superseded sources are stale. +- Home adoption transfers draft/history/goal read together; failed creation preserves input. +- Capture slot/session identity, draft revision and submitted staging before awaiting. Consume only captured input; newer edits and another selected session survive. + +Cross-business flows receive published intents/commands: Interactions invokes Turn's `WebuiInteractionPhaseIntents`, while Turn keeps its writer and lifecycle. + +## Regression selection + +Choose affected scenarios: delayed A after selecting B; invalidation while reading; release/recreate same key; dispose during settlement; delete before attach; submit twice during creation; edit during submit; navigate before creation; switch away before DONE; secondary-page PTY detach; malformed persisted rows versus real render failure. Assert observable state/outcomes, not helper or testid existence. diff --git a/.agents/skills/webui-development/references/runtime.md b/.agents/skills/webui-development/references/runtime.md new file mode 100644 index 000000000..b438d7490 --- /dev/null +++ b/.agents/skills/webui-development/references/runtime.md @@ -0,0 +1,84 @@ +# Loopback, contracts and runtime + +Read `packages/webui/docs/runtime/harness-boundary.md`, `boot.md` and `owner-identity.md` as relevant. Source paths below are under `packages/webui/src/`. + +## Operation changes + +Trace the complete producer/consumer path. The package guide prescribes the declaration sequence: + +1. Canonical name and `OperationSpec` row in `shared/operation-names.ts`; shared DTOs in `shared/contracts/.ts`. +2. Descriptor plus actual body validator in `server/operation/.ts`. Pin rejection predicate, stable code and exact message when changing validators. +3. Capability binding in `server/operation/bind-handlers.ts`. `binding-mechanism.ts` checks group/method/argument tuple/result mapper. Dedicated `handlers/` remain for stream/terminal/command shapes, response shaping or capability degradation; inspect `DEDICATED_OPERATION_NAMES`. +4. Registration in `server/operation/operations.ts`. Order is observable: append rather than reorder existing entries. +5. Capability in `runtime/port.ts`, adapter in `runtime/harness/`, and affected stubs/fixtures. For browser use, extend feature contract, `client/contracts/transport.ts`, `client/platform/transport.ts`, injected slice and owner command. + +Both sides use canonical names, not independent literals. The runtime port references shared DTOs rather than defining them. Type correctness is only one check: prove registration, wiring, caller reachability and real-envelope behavior. + +## Capabilities + +Read `shared/capability-manifest.ts`, `server/service.ts` (`withRegistryDerivedAvailability`) and `runtime/harness/adapter.ts` (`readHostPrerequisites`). Support comes from registered operations, callable bindings and actual host prerequisites. + +`supported`, `absent`, `not-ready` are distinct. Missing manifest on an older handshake means client-side unknown, retaining the unverified attempt path. A complete browser method table proves only protocol knowledge. Permission denial remains a call failure. + +## Wire and delivery + +`server/operation/operation-dispatch.ts` validates envelopes before trusting identity, rejects non-request frames, honors shutdown, validates bodies and dispatches. Preserve stable capability error codes; uncoded errors become harness errors. Frame identity, payload bytes and order can be compatibility contracts. + +Acknowledgement is opt-in. Iterator finalization shares one idempotent gate between abort and normal cleanup so unresolved pulls close without double-return. Client cancellation ends delivery, not host execution. + +`client/platform/wire.ts` owns socket/unary/stream mechanics; `transport.ts` maps domain methods to operation names; `event-channel.ts` owns persistent event connection/reconnect. Business interpretation stays with owners. + +## Process resources + +`server/access-policy.ts` owns loopback Host/Origin/URL-host and upgrade admission; `server/credentials.ts` owns the per-start credential. Retain admission guarantees when diagnosing dev/browser failures. + +`runtime/assembly.ts` constructs resources; `runtime/lifecycle.ts` owns startup-failure cleanup and close. Auth, host, mcode-tools broker and external browser provider close in order; all cleanup is attempted and failures reported. Service shutdown stops admission, closes connections and closes harness/resources. Register new process resources with the actual close owner and failure path. + +The **browser bridge** is a `server/` resource that is *supplied* to the service rather than created by the assembly, and the service closes it (`server/service.ts`). It is the one `server/` **area** — `src/server/browser-bridge/` — whose modules may import +`@mavis/browser-core`; `src/client/` and `src/shared/` may not. See +[`architecture.md`](architecture.md) for the browser's own boundary and +`packages/webui/docs/browser-use/10-browser-core-integration.md` for the layer itself. + +## Browser actions and element references + +The browser's action semantics belong to `@mavis/browser-core`. A restricted action reaches it through +a `BrowserTransport` adapter, and the WebUI's own gate decides whether it may run at all. + +| Concern | Where | +| --- | --- | +| The restricted vocabulary (28 names) and the gate that reads it | `runtime/browser/authorization.ts` (`isBrowserCommandAllowed`); the list itself is `shared/contracts/browser.ts` | +| The Agent's tool path | `runtime/browser/tool-adapter.ts` — forwards to the same service the panel uses | +| The isolated Chromium's core transport | `runtime/browser/isolated-chromium/core-transport.ts` | +| The bridge's core transport | `server/browser-bridge/core-transport.ts` | + +When you add, change, or remove a browser action: + +- **Change the semantics in the core**, not in a provider. A provider that translates an action into + raw DevTools calls is a second definition of what it does. +- **Update the vocabulary in both places.** `shared/contracts/browser.ts` holds the WebUI's copy and + `@mavis/browser-core` holds the core's; `test/unit/browser-restricted-commands.test.ts` asserts they + cannot drift. The shared layer cannot import the core, which is why the copy exists. +- **Keep the surfaces' refusals named.** `open_tab` is refused on the bridge, `return_to_previous_tab` + on the isolated Chromium, and `upload_files` on the bridge. Each refusal says `browser_capability_denied` + and each names a fact about the browser rather than a policy. + +Element references are opaque (`browser-element:-`) and the core holds the element +map and the retained snapshot. A reference whose page has navigated is refused with +a stale-reference refusal (`STALE_ELEMENT_REF` or `STALE_SNAPSHOT`); a reference on a page that did not navigate stays usable. Do not add a path that +turns a reference back into a coordinate outside the core. + +Two error vocabularies meet at the core's edge and are deliberately **not** flattened: the core's +action errors (`STALE_ELEMENT_REF`, `STALE_SNAPSHOT`) tell the Agent what to do next, and the WebUI's +codes (`browser_target_stale`, `browser_capability_denied`) tell the panel and the client what to +render. The one translation point is the panel boundary, +`client/features/browser/ui/BrowserPanelHost.public.tsx`. + +The host declares `webui` identity rather than borrowing `tui`/`cli`. Inspect the policy row and assembly for quarantined startup, permission/questionnaire behavior, auth leases, beta tools and optional Browser Use before modifying them. + +For host-event changes, run `pnpm --filter @mavis/webui gen:host-events`. `shared/contracts/host-event-types.generated.ts` must stay fresh; production session-fact decoding handles each catalog fact or records an explicit unhandled rationale. Generation neither changes host producers nor defines WebUI DTOs automatically. + +## Scope and dependencies + +Read `docs/known-gaps.md` and `open-questions.md` when behavior seems absent. Disabled tabs, beta tools, optional providers and temporarily failed calls differ. The retired WebUI-owned scheduler and the retired navigation generators are not current requirements; the scheduled-task surface that exists uses the v2 runtime's Cron service (`docs/adr/0027-the-webui-is-a-scheduler-host-for-scheduled-tasks.md`), and a second scheduler in this client is still refused. + +Reuse existing dependencies: React/React DOM; marked + KaTeX + selectively registered highlight.js core; xterm/fit for terminal UI; node-pty for processes; ws for loopback; archive adapters' existing libraries. Recheck manifest versions. New browser dependencies need a reviewable reason and bundle assessment; Node dependencies do not become browser-safe through reuse. diff --git a/.agents/skills/webui-development/references/source-evidence.md b/.agents/skills/webui-development/references/source-evidence.md new file mode 100644 index 000000000..c61d2c184 --- /dev/null +++ b/.agents/skills/webui-development/references/source-evidence.md @@ -0,0 +1,89 @@ +# Source evidence and refresh + +## Baseline + +Investigated on 2026-10-19 starting at HEAD `4de6a59a5df5fa8a2eeab782fdb71603e1da9197` in `/home/fectivnfy/my_data/project/my_project/minimax-code`. The working tree already contained modifications to the package manifest and browser/integration tests, and a staged test-result deletion. `test/browser/fixture.mjs` also changed during the investigation. Concurrent work advanced HEAD to `7574e4b07579570c08fd1d83af67cc92294de75d`; the intervening diff changes the package manifest and tests, with no `packages/webui/src/` changes. Final capture provenance records that later clean tree and input hashes. This skill creation did not edit or commit repository files. + +The dependency checker scanned all 435 TypeScript source files and 1,557 in-package references: zero direction violations, cycles, role violations or publication violations; one declared structural exception. The current build succeeded with 108 server inputs, 331 client inputs and 432 combined inputs. Counts describe that run only and are not acceptance thresholds. + +Human/source investigation followed all four execution boundaries, all business homes, application wiring and teardown, request-freshness and submission mechanisms, validators/registry/bindings, capability derivation, runtime resources, style/token/compiler setup and representative unit/integration/browser tests. This is comprehensive architecture-oriented coverage, not a claim that every line of every file received an exhaustive correctness review. + +Independent read-only forward-testing exercised file-preview refresh during session switching and a new appearance settings row. It confirmed useful routing and found two refinements incorporated here: inspect leaf-owner disposal rather than universalizing application fencing, and settle/verify the actual surface before screenshot capture. The helper now checks finite animation settlement, image loads, theme and settings-versus-conversation identity. The final six originals were visually inspected. Full repository test suites and real provider/account scenarios were not executed for this external skill-only change. + +## Source anchors by claim + +| Claim | Read/refresh in the repository | +| --- | --- | +| Operating rules and doc authority | root/package `AGENTS.md`, `packages/webui/docs/README.md`, `CONTEXT.md` | +| Layer and role direction | `scripts/lib/webui-dependency-rules.mjs` (`ALLOWED_EDGES`, `PROJECTION_DOMAIN`, `classifyLayers`, `classifyRole`, role/publication checks), `scripts/check-webui-dependency.mjs` | +| Browser wiring | `src/client/main.tsx`, `application/create-application.ts`, `port-slices.ts`, `bindings/application-context.tsx` | +| Canonical state and temporal identity | `application/session-store.ts`, `identity-epochs.ts`, `conversation/request-freshness.ts`, `session-scope.ts`, `stream-lease.ts` | +| Creation and submission | `application/task-session-workflows.ts`, `features/turn/contracts/submission.ts`, `application/submission-admission.ts`, `composer-submission.ts`, `turn-commands.ts`, Composer controller | +| Independent business ownership | Feature application modules listed in `architecture.md`; per-session Goal/Interactions, separate Review families, provider editor, terminal owner | +| Ingress and delivery | `client/platform/event-channel.ts`, `features/turn/application/event-coordinator.ts`, `session-fact.ts`, `conversation/stream-loop.ts`, Turn sink adapter | +| Loopback contract | `shared/operation-names.ts`, `server/operation/{operations,bind-handlers,binding-mechanism,operation-dispatch}.ts` | +| Support/readiness | `shared/capability-manifest.ts`, `server/service.ts`, `runtime/harness/adapter.ts` | +| Process close | `runtime/assembly.ts`, `runtime/lifecycle.ts`, `runtime/terminal.ts`, `server/service.ts` | +| Browser action semantics | `packages/browser-core/src/browser-core.ts`, `browser-actions.ts`, `browser-action-targets.ts`, `browser-transport.ts`; the WebUI adapters `src/runtime/browser/isolated-chromium/core-transport.ts` and `src/server/browser-bridge/core-transport.ts`; the authoritative narrative `packages/webui/docs/browser-use/10-browser-core-integration.md` | +| Visual baseline | `client/styles/{tokens,index,shell}.css`, `tailwind.config.cjs`, feature CSS, `ConversationSurface.tsx`, `WebuiClientFoundationApp.tsx`, Settings appearance source | +| Verification | `scripts/verify.mjs`, root/package scripts, `test/vitest-suites.json`, `playwright.config.mjs`, browser harness/server/fixture, token tests | + +Unless explicitly prefixed, source rows refer to `packages/webui/src/`. Use symbol search for exact current locations rather than freezing line numbers that will drift. + +### Refresh of 2026-10-26 — the Browser Core layer + +A documentation synchronization pass brought the WebUI documents and this skill in line with the +Browser Core integration (`eb6cd3e5`, `88c83655`). The facts it established, all read from the tree +at `webui` / `88c83655`: + +| Claim | Evidence | +| --- | --- | +| The browser's action semantics belong to `@mavis/browser-core`, and both surfaces execute it over a `BrowserTransport` | `packages/browser-core/src/browser-transport.ts`; `src/runtime/browser/isolated-chromium/core-transport.ts`; `src/server/browser-bridge/core-transport.ts` | +| `src/client/` and `src/shared/` may not import the core; the gate enforces it | `scripts/lib/webui-dependency-rules.mjs` — the workspace-host check for `client/` and `shared/` | +| The restricted vocabulary is stated twice, and a test pins them together | `src/shared/contracts/browser.ts`; `packages/webui/test/unit/browser-restricted-commands.test.ts` | +| The bridge's `open_tab` and `upload_files` are refused; the isolated surface refuses `return_to_previous_tab` | `src/server/browser-bridge/server.ts`; `src/runtime/browser/isolated-chromium/provider.ts`; `packages/browser-extension/src/cdp.ts` (the allowlist, 45 commands, without `Page.captureScreenshot` or `DOM.setFileInputFiles`) | +| The bridge's screenshot comes from the extension's pushed visible-tab frame | `src/server/browser-bridge/server.ts` — `#captureVisibleFrame`; `packages/browser-extension/src/images.ts` | +| The three new suites' evidence levels | `packages/webui/test/integration/browser-core-agent-actions.test.ts` (real browser), `packages/webui/test/unit/browser-bridge-core-actions.test.ts` (wiring, extension as a double), `packages/webui/test/unit/browser-restricted-commands.test.ts` (pure) | + +The line-number citations in `packages/webui/docs/browser-use/*.md` that this pass corrected were +verified against the tree by reading the cited line, not by pattern-matching the file name. **New +citations should be made the same way** — a citation that was not read at the cited line is not +evidence. + +One capability difference is recorded but **not** pinned by a test: the bridge refuses `upload_files`. + +### Refresh of 2026-10-30 — the scheduled-task surface and stale rosters + +A documentation pass brought the WebUI documents and this skill in line with the scheduled-task +surface (`c4082a66`…`16678b05`) and with rosters that had been stale for several features. Read from +`webui` at `786ad138`, working tree checked before and after: + +| Claim | Evidence | +| --- | --- | +| The WebUI is a scheduler host: the `scheduling` business lists, creates, edits, enables, deletes, runs now and reads run history of scheduled tasks | `src/client/features/scheduling/**`; the rail row in `client/features/sessions/ui/SessionRailRegion.tsx`; six operations registered at `server/operation/operations.ts:332-337` | +| The client reaches the v2 runtime's Cron service through the harness port, and its DTOs are the WebUI's own | `src/runtime/port.ts:725-730`; `src/runtime/harness/cron.ts`; `src/shared/contracts/cron.ts` | +| The `webui` row differs from `tui` in **thirteen** cells | `packages/local-runtime/test/unit/runtime-owner-policy.test.ts:317` — the assertion enumerates all thirteen | +| The roster is **99** bindings and **18** dedicated handlers covering **117** operation names | `packages/webui/test/unit/webui-operation-binding.test.ts:47-50` | +| Sixteen business ports cut **fifteen** slices (terminal folds into workspace, `turn` builds its own) | `src/client/application/port-slices.ts` (`WebuiPortSliceSource`, `createWebuiPortSlices`) | +| R2's restore write is guarded by `state` as well as both counters | `packages/local-runtime-v2/src/infra/scheduler/index.ts` — `updateRestoredJob` (`:794`), the `state` guard at `:814` | +| The scheduled-task evidence is seven test files at levels 1, 4 and 5 | `docs/verification/gates.md`, "The scheduled-task suites" | + +**One counting trap worth keeping.** `operations.ts` has 131 `registerOperation` calls, while the +roster test pins 99 + 18 = 117. The gap is handler-entry factories (browser, Mini App, Mini App agent) +registering operations the bindings-plus-dedicated list does not name. Take the numbers from the test +and describe them in its words — "N bindings and M dedicated handlers covering K operation names" — +and do not call K "operations", because the registry is larger than K. + +**Not re-verified by this pass:** every entry of `docs/known-gaps.md` (its dated record stays +2026-10-27 / `d9593276`; this pass added a note for the closed §7 and changed no entry), and the +runtime behaviour of any surface — this was a static read of source and tests. + +## Refresh workflow + +When using this skill, re-read current operating instructions and touched source. Refresh a reference only when changed source or an actual task reveals it is stale. Check callers, tests and decision rationale together; repository documents are navigation, not implementation proof. Never restore retired plans/generated navigation tables merely because a past memory mentions them. + +For a visual refresh: inspect source/CSS changes, rebuild, run the capture helper to a new external directory, inspect original PNGs in both themes, review the manifest/errors and only then deliberately replace the skill references. Keep synthetic data labels and provenance. Rendering a new screenshot does not approve a product redesign. + +The capture helper is reusable tooling. Its output should stay outside the repository; it owns an ephemeral loopback server and headless browser only. It uses existing repository Playwright and fixture code. Failure to find a build, package, browser or selector is explicit; resolve the actual mismatch rather than silently generating a fake reference image. + +Hindsight knowledge search/list were attempted during creation but returned connection failures. No Hindsight page was treated as source evidence. Historical local notes helped select lifecycle and evidence-boundary questions; current code/docs/gate output supply the actual guidance here. diff --git a/.agents/skills/webui-development/references/verification.md b/.agents/skills/webui-development/references/verification.md new file mode 100644 index 000000000..b7331188c --- /dev/null +++ b/.agents/skills/webui-development/references/verification.md @@ -0,0 +1,70 @@ +# Verification and delivery + +Read current `packages/webui/AGENTS.md`, `docs/verification/gates.md`, `docs/verification/browser-suite.md`, root `CONTRIBUTING.md`, `scripts/verify.mjs` and applicable `.agents/skills/testing-workflow/SKILL.md`. The tables below route work; current commands and required gates remain repository authority. + +## Select the narrowest relevant checks + +Reuse passing gate evidence: before starting a check, inspect results for the same commit, gate and +platform. Run each applicable check once for unchanged inputs; after a passing `pnpm verify`, do not +repeat its constituent checks for reassurance. Rerun when relevant source, configuration or generated +inputs change, a result failed or did not finish, or the required platform is uncovered. Select only +the smallest gates that cover the files and behavior changed in this task. The latest direct user +instruction overrides older repository or skill wording that asks for broader verification. Do not +run aggregate gates or full suites by default; run a full gate only when the user explicitly asks. + +| Change | Focused verification, followed by applicable delivery gates | +| --- | --- | +| Client TypeScript | `pnpm --filter @mavis/webui typecheck:client` | +| Server/runtime TypeScript | `pnpm --filter @mavis/webui typecheck:server` | +| Changed TypeScript tests | `pnpm --filter @mavis/webui typecheck:test` | +| Business behavior | Relevant declared Vitest file | +| Imports, roles, publication, file movement | `pnpm check:webui-dependency` | +| Rendered UI, CSS, theme or browser behavior | `pnpm build:webui` when the built artifact is affected, then only the relevant browser scenario; inspect actual screenshots for visual claims | +| Bundle/entries | `pnpm build:webui` then `pnpm check:webui-boundary` | +| npm package/launcher | `pnpm test:webui-package`; do not confuse local packaging with publishing | +| New/removed/renamed repo files | Review paths, `node scripts/source-inventory.mjs --write`, `pnpm check:source` | +| Changed exports | `pnpm gen:tsconfig`, `pnpm check:tsconfig` | +| Host events | `pnpm --filter @mavis/webui gen:host-events`, server freshness/type checks and decoder tests | + +For a focused Vitest run use the root config, e.g. `pnpm exec vitest run --config vitest.oss.config.mjs packages/webui/test/unit/webui-design-tokens.test.ts`. Every new Vitest file goes in the `webui` group of `test/vitest-suites.json`; otherwise it never joins the declared suite. + +`pnpm verify`, `pnpm test:webui`, and the full `pnpm test:webui-browser` suite are aggregate gates; do +not run them by default for a local change. Before a PR, follow the separate delivery workflow for +reviewed-commit and source-export requirements, but do not expand local verification beyond the +user's requested scope. Preserve unrelated dirty work. Report any requested check that could not run +and the exact coverage that remains. An external personal skill with no repository source changes +does not warrant inventing a repository code change just to run its full delivery pipeline. + +`pnpm verify --list` shows current platform gates. `pnpm lint`/TUI lint is not a WebUI ESLint gate. Build artifact checks require a current build; Vite development behavior is not the shipped esbuild artifact. + +## Test conventions + +Unit/integration/gate/browser tests live in `packages/webui/test/{unit,integration,gates,browser}`. The shared Vitest environment is Node. Use pure transitions and `renderToStaticMarkup` for deterministic state/render branches; browser interaction uses Playwright. Do not add jsdom/happy-dom/testing-library or change the shared root environment for convenience. + +Browser specs import `test` from `test/browser/harness.mjs`, with `expect` as needed from Playwright. The harness checks both health and page run IDs, not mere port reachability. Default server reuse is off; deliberate reuse must still match its run ID. Keep reports/state outside the repository. Set `WEBUI_BROWSER_OUTPUT_DIR` to a task-specific external directory instead of inheriting a system-temp default where task instructions forbid that. + +Assert outcomes users/consumers observe. Existing validators' code-only tests do not pin messages/predicates: add exact rejection assertions for touched validators. Async scenarios should deliberately control delayed answers, navigation, invalidation and disposal, not use arbitrary sleeps. A testid or helper-name string check does not establish behavior. + +## Evidence boundaries + +| Evidence | Establishes | Does not establish | +| --- | --- | --- | +| Pure unit behavior | Reducer/projection/mechanism contract | Assembly, DOM or wire | +| Assembled application with injected capabilities | Owner/command/lifecycle collaboration | Real loopback/host | +| `loopback-harness-path.test.ts` | Real service/socket/envelope/registry/event delivery with scripted leaf | Real provider/account | +| Built-browser fixture | Rendering, navigation and interactions of built client | Actual service/host/cloud behavior | +| Production host with controlled model HTTP endpoint | Real local host/turn path to that endpoint | Real provider authorization | +| `native-runtime.spec.mjs` | Production host/service and native PTY on executing platform | Other OSes, real account/provider | +| `test/integration/browser-core-agent-actions.test.ts` | The Agent's `inspect` → `click` → `type` against a **real** Chromium through Browser Core, and the stale-reference refusal. It **requires** a browser — it has no `describe.skipIf`, so on a machine with no Chrome it fails rather than skipping | The bridge path; any platform other than the one it ran on | +| `test/unit/browser-bridge-core-actions.test.ts` | The bridge's **wiring** through Browser Core, the navigation fence, and the screenshot's source | That a real browser performed any of it — the extension is a test double | +| `test/unit/browser-restricted-commands.test.ts` | That the restricted vocabulary cannot drift | That any action behaves correctly at runtime | + +Keep each claim at its actual seam. HTTP 200 or a listener is not browser readiness. A screenshot is not interaction coverage. One-page session switching is not independent-page ownership evidence. Native terminal coverage in one scenario does not turn every fixture scenario into native evidence. + +For behavior preservation, compare actual contracts: AST bodies with TypeScript tooling for moved logic; frame count/order/UTF-8 payloads and iterator/capability calls for wire; effective declarations and rendered geometry for CSS. Use a targeted fault/mutation when needed to prove a consolidation really owns behavior. These are selected techniques, not a universal obligation to mutate every change. + +## Delivery scope + +This is a reviewed public source projection: source inventory scans untracked files, so keep scratch/reports outside the repo. Review assets and licenses; do not add real sessions/accounts/credentials. Generated inventory and tsconfig paths are regenerated, not hand edited. Documentation and commit messages are English; preserve product localization and runtime prompt requirements. + +Respect explicit authorization. Default repository workflow uses purpose-prefixed feature branches and PRs, not direct default-branch pushes. Issue/PR operations target the private fork identified by the package guide; inspect remotes and pass its explicit repo argument. A task to code or create this skill is not authorization to file public issues, open PRs, send team messages or push. Read the appropriate dedicated workflow if such delivery is requested. diff --git a/.agents/skills/webui-development/references/visual-design.md b/.agents/skills/webui-development/references/visual-design.md new file mode 100644 index 000000000..7988d0038 --- /dev/null +++ b/.agents/skills/webui-development/references/visual-design.md @@ -0,0 +1,75 @@ +# Visual implementation and verification + +For adding or styling components, start with [Component design system](component-design-system.md): it defines the immediate token choices, scales, reuse recipes and interaction states. This reference covers implementation mechanics and verification. Read `packages/webui/docs/design/design-language.md` and the actual component/CSS for affected details. Existing code contains older aliases and newer semantic-token styles; do not promote incidental legacy declarations into new design rules. + +## Optional page context + +Ordinary component development does not require loading these screenshots. Consult them when page context or a layout comparison helps; use the component design system to choose styles. If consulting images, open the originals rather than inferring appearance from filenames. These built-client synthetic-fixture captures are 1440 × 900, device scale 1, Chinese locale and reduced motion; they cover only the pictured states. + +| Surface | Light | Dark | Use as reference for | +| --- | --- | --- | --- | +| New task | [Home light](visuals/home-light.png) | [Home dark](visuals/home-dark.png) | Quiet rail, welcome anchor, whitespace, composer/workspace grouping | +| Existing conversation | [Conversation light](visuals/conversation-light.png) | [Conversation dark](visuals/conversation-dark.png) | Title, scroll/composer arrangement, progress side panel and density | +| General settings | [Settings light](visuals/settings-light.png) | [Settings dark](visuals/settings-dark.png) | Navigation groups, form rows, cards, switches, theme selector and hierarchy | + +Capture provenance is in [capture-manifest.json](visuals/capture-manifest.json). A capture is a dated example, not a pixel contract or an assertion that every existing contrast choice is correct. Synthetic empty model catalogues and disabled controls are fixture states; preserve the real component's enabled behavior. Use screenshots from the user's requested baseline when supplied, and explicitly distinguish them from these fixtures. + +## Read the actual design system + +Source anchors below are under `packages/webui/`. + +| Concern | Source | Implementation guidance | +| --- | --- | --- | +| Primitive ramps, spacing/radius/type scales, semantic states | `src/client/styles/tokens.css` | Use semantic colors for meaning/state; inspect both `.light` and `.dark` values | +| Utility vocabulary and font stacks | `tailwind.config.cjs` | Token utilities coexist with stock numeric utilities; ensure the chosen class actually generates | +| Single stylesheet and global behavior | `src/client/styles/index.css` | Import business CSS here; keep one compiled stylesheet, including KaTeX | +| Frame, shared menus, geometry | `src/client/styles/shell.css`, `src/client/shell/WebuiClientFoundationApp.tsx` | Shell/layout authority is the live assembly, not the mostly transparent `ArchonShell` wrapper | +| Welcome and conversation columns | `src/client/shell/ConversationSurface.tsx` | Home and session are different layouts, not a shared fixed-height mockup | +| Composer | `src/client/features/composer/ui/composer.css` and neighboring TSX | Preserve anchored menus, card, toolbar, queue/permission states and input behavior | +| Settings rows and controls | `features/settings/ui/settings.css`, `SettingsModal.public.tsx`, `settings-atoms.tsx` | Full settings surface has its own navigation; retain selected/disabled states | +| Messages/code/diffs | `features/transcript/ui/transcript.css`, `transcript-widgets.css`, `markdown.public.tsx` | Preserve readable column, line numbers, copy/actions, file refs and math | +| Domain surfaces | `features/{sessions,models,plugins,interactions,workspace,account,goal}/ui/*.css` | Place business styles with that business; use shared primitives for shared behavior | +| Form primitives and icons | `src/client/ui/form-atoms.tsx`, `ToggleSwitch.tsx`, `ContextMenu.tsx`, `shell/icons.tsx`, `@tabler/icons-react` | Reuse the existing Tabler icon vocabulary and control primitives; do not hand-draw interface glyphs or add another icon library | + +In rows using abbreviated `features/…`, the full prefix is `src/client/`. + +For a general appearance row, the actual `GenericPage`, `Appearance`, `SettingRow`, `Section` and `Divider` are in `SettingsModal.public.tsx`. `settings-atoms.tsx` serves other settings affordances; do not assume similarly named atoms own this row. Theme preference currently uses local React state, injected storage and DOM effects in that component, with `resolveThemePreference` as a pure projection. Follow this specific persistence seam for a local appearance preference; application-owned remote settings are a different workflow. A new local preference does not automatically require a server operation or global owner. + +## Visual continuity decisions + +- **Hierarchy:** restrained neutral surfaces, a slightly darker rail, quiet borders and separators, rounded controls, sparse accent blue. Use stronger contrast for the primary action and primary text; secondary metadata should recede without disappearing. +- **Layout:** desktop is a two-column frame with collapsible/resizable rail. The observed default rail is 240px at 1440px; treat its actual state/constraints as authority rather than making an “18%” prose description a fixed rule. Home centers a welcome and approximately 743px outer content region; session layout uses its own scroll viewport and optional side panels. +- **Typography:** sans uses HarmonyOS/Segoe UI/SF/system/CJK fallbacks; mono uses Hack/system monospace. No new web-font load is implied. Named sizes include `text-body-base` (14/22px) and `text-caption-base` (12/18px); use the neighbor's established hierarchy rather than shrinking everything to fit. +- **Composer:** large rounded card, soft shadow, low-contrast edge, attachment/permission controls and model/send actions. Disabled and active send states differ; a screenshot's empty-draft button is not the universal color specification. +- **Settings and lists:** group related rows on a shared elevated/grouped surface, keep labels/descriptions aligned, align trailing controls, and maintain consistent row target sizes. Settings pages are not all modals visually merely because the component is named `SettingsModal`. +- **States:** specify focus, hover, press, selected, inactive/disabled, busy, validation/error, empty and success when the control needs them. Availability is rendered from owner/capability outcomes; do not imitate working controls with decorative placeholders. + +For a new surface, state which existing surface supplies typography, geometry and controls. If intentional differences are needed, connect each to a user requirement. Visual freedom remains available for a requested redesign; the current system is the default reference, not a ban on change. + +## CSS mechanics that matter + +Use names such as `bg-bg_default_primary`, `text-text_default_secondary`, `border-border_default`, `rounded-radius_12`, `gap-spacing_8` and `shadow-shadow_default`, or `var(--semantic_token)` inside business CSS. Keep compound/state rules in `@layer components`. Literal geometry is acceptable where the scale has no suitable value; do not invent a parallel color system. + +Tailwind scans source literals. Runtime-built classes need full-name maps or a deliberate safelist entry. Test the compiled output: a class present in TSX or CSS input may have been purged. Arbitrary-value utilities can rewrite underscores inside custom-property names; named component CSS is safer for complex token-based shadows. `test/unit/webui-design-tokens.test.ts` checks token closure and named compiled classes. + +Cascade changes require comparing effective declarations, including specificity, ordering, media/container context and utilities. A later block is not automatically the winning rule. Keep feature imports in `styles/index.css` in their intended order. + +Global thin scrollbars and scoped stable gutters live in `styles/index.css`. Do not apply gutter reservation to all elements: inline SVG's overflow can collapse its paint area. Preserve horizontal-only opt-outs. Use `min-w-0`/`min-h-0` and actual scroll ownership to handle long content, not global overflow hiding. + +Respect `prefers-reduced-motion`; shared controls use a quiet short response curve and shell animations reduce under the preference. Existing overlay positioning helpers in `projection/common/` and outside-close bindings are better starting points than another ad hoc window listener. Inspect clipping, viewport edges, stacking and keyboard/focus behavior in the browser. + +Markdown is parsed with marked tokens and rendered through React/KaTeX with registered highlight.js languages. Keep math in that pipeline; do not scan the DOM with KaTeX auto-render or import a second stylesheet. Inspect escaping/link/file-reference contracts before touching raw content handling. + +## Visual acceptance + +Build current CSS/client, then exercise the requested state and inspect screenshots. For affected surfaces compare both themes, ordinary and constrained viewport sizes, long names/content, scrolling, open overlays, focus/keyboard use, disabled/busy/error states and reduced motion as appropriate. Choose a task-relevant matrix rather than requiring every unrelated screen on every change. DOM/SSR asserts content; computed styles and screenshots establish layout/appearance. + +For reusable reference captures after a successful `pnpm build:webui`: + +```bash +node .agents/skills/webui-development/scripts/capture-visuals.mjs /path/to/minimax-code /path/outside/repository/visual-reference +``` + +The helper serves only the existing build and repository synthetic fixture on its own ephemeral loopback port, starts its own headless browser, and closes both. It never rebuilds, uses an existing live app, or talks to a real provider. Its static resource serving includes bundled assets/fonts so previews are visible; this is capture tooling, not a replacement for the official guarded browser suite. Review page errors and the manifest. Refresh skill baseline PNGs deliberately, never use snapshot replacement to hide a regression. + +New assets require provenance/license review and the source inventory. Prefer current local marks/illustrations in `src/client/assets/`; inspect originals before adapting them. Keep a source→destination filename manifest for added assets per the package guide. diff --git a/.agents/skills/webui-development/references/visuals/capture-manifest.json b/.agents/skills/webui-development/references/visuals/capture-manifest.json new file mode 100644 index 000000000..c47bf0a3c --- /dev/null +++ b/.agents/skills/webui-development/references/visuals/capture-manifest.json @@ -0,0 +1,62 @@ +{ + "capturedAt": "2026-10-19T03:08:57.024Z", + "head": "7574e4b07579570c08fd1d83af67cc92294de75d", + "trackedChanges": "", + "evidence": "Built client with repository synthetic browser fixture; no real host, account or provider", + "inputHashes": { + "dist-webui/client/client.js": "0e6375d10843466d4ee4da08f608cb353a63376cc0c1ac2e76a801fe585fe3f6", + "dist-webui/client/styles.css": "ffdcfb9081d9dc3e4a07dabe00a1b6730b7726ab7ef465ee71341017046d7e9e", + "packages/webui/test/browser/fixture.mjs": "525cabb675379adea2ce17ff7d70127cbfecc97f4f5a1f6d67674f237aa83a59" + }, + "captures": [ + { + "file": "home-light.png", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "light" + }, + { + "file": "conversation-light.png", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "light" + }, + { + "file": "settings-light.png", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "light" + }, + { + "file": "settings-dark.png", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "dark" + }, + { + "file": "conversation-dark.png", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "dark" + }, + { + "file": "home-dark.png", + "viewport": { + "width": 1440, + "height": 900 + }, + "theme": "dark" + } + ], + "pageErrors": [] +} diff --git a/.agents/skills/webui-development/references/visuals/conversation-dark.png b/.agents/skills/webui-development/references/visuals/conversation-dark.png new file mode 100644 index 000000000..84092f10d Binary files /dev/null and b/.agents/skills/webui-development/references/visuals/conversation-dark.png differ diff --git a/.agents/skills/webui-development/references/visuals/conversation-light.png b/.agents/skills/webui-development/references/visuals/conversation-light.png new file mode 100644 index 000000000..41d8be1cb Binary files /dev/null and b/.agents/skills/webui-development/references/visuals/conversation-light.png differ diff --git a/.agents/skills/webui-development/references/visuals/home-dark.png b/.agents/skills/webui-development/references/visuals/home-dark.png new file mode 100644 index 000000000..2dbdc70d5 Binary files /dev/null and b/.agents/skills/webui-development/references/visuals/home-dark.png differ diff --git a/.agents/skills/webui-development/references/visuals/home-light.png b/.agents/skills/webui-development/references/visuals/home-light.png new file mode 100644 index 000000000..8a3051bd6 Binary files /dev/null and b/.agents/skills/webui-development/references/visuals/home-light.png differ diff --git a/.agents/skills/webui-development/references/visuals/settings-dark.png b/.agents/skills/webui-development/references/visuals/settings-dark.png new file mode 100644 index 000000000..044d309bc Binary files /dev/null and b/.agents/skills/webui-development/references/visuals/settings-dark.png differ diff --git a/.agents/skills/webui-development/references/visuals/settings-light.png b/.agents/skills/webui-development/references/visuals/settings-light.png new file mode 100644 index 000000000..19e75978f Binary files /dev/null and b/.agents/skills/webui-development/references/visuals/settings-light.png differ diff --git a/.agents/skills/webui-development/scripts/capture-visuals.mjs b/.agents/skills/webui-development/scripts/capture-visuals.mjs new file mode 100644 index 000000000..212fab007 --- /dev/null +++ b/.agents/skills/webui-development/scripts/capture-visuals.mjs @@ -0,0 +1,85 @@ +import { createServer } from 'node:http'; +import { readFile, mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { createRequire } from 'node:module'; +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; + +// 仅启动本次捕获拥有的 loopback fixture,使用已有构建产物。 +const [repoArg, outputArg] = process.argv.slice(2); +if (!repoArg || !outputArg) throw new Error('Usage: node capture-visuals.mjs '); +const repo = path.resolve(repoArg); +const output = path.resolve(outputArg); +if (output === repo || output.startsWith(repo + path.sep)) throw new Error('Choose an output directory outside the repository'); +const require = createRequire(path.join(repo, 'package.json')); +const { chromium } = require('@playwright/test'); +const { installFixtureTransport } = await import(pathToFileURL(path.join(repo, 'packages/webui/test/browser/fixture.mjs'))); +const root = path.join(repo, 'dist-webui/client'); +await readFile(path.join(root, 'client.js')); +await mkdir(output, { recursive: true }); +const server = createServer(async (request, response) => { + const pathname = new URL(request.url, 'http://127.0.0.1').pathname; + const name = pathname === '/' ? 'index.html' : pathname.slice(1); + const target = path.resolve(root, name); + if (!target.startsWith(root + path.sep)) { response.writeHead(403).end(); return; } + try { + let body = await readFile(target); + if (name === 'index.html') body = Buffer.from(body.toString().replace('', ``)); + const types = { '.js': 'text/javascript', '.css': 'text/css', '.html': 'text/html', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf' }; + response.writeHead(200, { 'content-type': types[path.extname(name)] ?? 'application/octet-stream', 'cache-control': 'no-store' }).end(body); + } catch (error) { response.writeHead(500).end(String(error)); } +}); +await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); +let browser; +const captures = []; +try { + browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1, locale: 'zh-CN', timezoneId: 'Asia/Shanghai', reducedMotion: 'reduce' }); + const page = await context.newPage(); + const errors = []; + page.on('pageerror', error => errors.push(error.message)); + const base = `http://127.0.0.1:${server.address().port}`; + async function capture(name) { + await page.evaluate(() => document.fonts.ready); + await page.waitForFunction(() => [...document.images].every(img => img.complete && img.naturalWidth > 0)); + await page.waitForFunction(() => document.getAnimations().every(animation => animation.playState !== 'running' || animation.effect?.getTiming().iterations === Infinity)); + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + const settingsVisible = await page.locator('[role="dialog"][aria-label="设置"]').isVisible(); + if (settingsVisible !== name.startsWith('settings-')) throw new Error(`Wrong surface for ${name}`); + if (!(await page.locator('html').evaluate((element, theme) => element.classList.contains(theme), name.endsWith('-dark') ? 'dark' : 'light'))) throw new Error(`Wrong theme for ${name}`); + await page.screenshot({ path: path.join(output, name + '.png'), animations: 'disabled' }); + captures.push({ file: name + '.png', viewport: page.viewportSize(), theme: await page.locator('html').getAttribute('class') }); + } + await page.goto(base); + await page.getByPlaceholder('输入消息…(输入 / 唤起命令)').waitFor(); + await page.waitForFunction(() => window.__fixture.requests.some(r => r.operation === 'listSessions')); + await capture('home-light'); + await page.goto(base + '/#session=A'); + await page.reload(); + await page.getByText('History A synthetic', { exact: true }).waitFor(); + await capture('conversation-light'); + await page.getByTestId('sidebar-user-menu-trigger').click(); + await page.getByTestId('user-menu-settings').click(); + const dialog = page.locator('[role="dialog"][aria-label="设置"]'); + await dialog.waitFor(); + await capture('settings-light'); + await page.locator('.mavis-settings-theme-option').filter({ hasText: '深色模式' }).click(); + await page.waitForFunction(() => document.documentElement.classList.contains('dark')); + await capture('settings-dark'); + await page.goto(base + '/#session=A'); + await page.reload(); + await page.getByText('History A synthetic', { exact: true }).waitFor(); + await capture('conversation-dark'); + await page.goto(base); + await page.getByPlaceholder('输入消息…(输入 / 唤起命令)').waitFor(); + await capture('home-dark'); + const inputHashes = {}; + for (const relative of ['dist-webui/client/client.js', 'dist-webui/client/styles.css', 'packages/webui/test/browser/fixture.mjs']) inputHashes[relative] = createHash('sha256').update(await readFile(path.join(repo, relative))).digest('hex'); + await writeFile(path.join(output, 'capture-manifest.json'), JSON.stringify({ capturedAt: new Date().toISOString(), head: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim(), trackedChanges: execFileSync('git', ['status', '--short'], { cwd: repo, encoding: 'utf8' }).trim(), evidence: 'Built client with repository synthetic browser fixture; no real host, account or provider', inputHashes, captures, pageErrors: errors }, null, 2) + '\n'); + if (errors.length) throw new Error('Browser page errors: ' + errors.join('; ')); + console.log(`Captured ${captures.length} synthetic reference screens in ${output}`); +} finally { + await browser?.close(); + await new Promise(resolve => server.close(resolve)); +} diff --git a/.agents/skills/webui-docs-sync/SKILL.md b/.agents/skills/webui-docs-sync/SKILL.md new file mode 100644 index 000000000..81716dd4a --- /dev/null +++ b/.agents/skills/webui-docs-sync/SKILL.md @@ -0,0 +1,45 @@ +--- +name: webui-docs-sync +description: Synchronize WebUI documentation, its AGENTS.md and the webui-development skill with current source, tests and approved decisions, including development guidance and component design standards. Use for documentation or skill drift and existing plan-status updates; not for implementing product changes or choosing a new architecture direction. +--- + +# WebUI documentation sync + +Keep the WebUI documentation and development skill useful as guides to the checked-out implementation and approved development direction. Make documentation and skill-instruction changes only unless the user separately asks for product-code or tooling work. + +## Workflow + +1. **Establish the checkout and scope.** Read repository `AGENTS.md` and `CONTRIBUTING.md`, inspect `git status`, and preserve unrelated edits. Follow the publication rules for this public projection: review every added or moved path, and regenerate `release/public-source.json` for file-set changes. Content-only edits to existing documents do not require inventory regeneration. +2. **Load the WebUI map.** Read `packages/webui/AGENTS.md`, `packages/webui/docs/README.md` and [webui-development](../webui-development/SKILL.md). Follow their indexes to the relevant architecture, client, runtime, design, verification, gap, ADR, and skill references. Read `packages/webui/docs/agents/domain.md` and the relevant `CONTEXT.md` entries when domain terms or decisions are involved. +3. **Scan the full documentation set.** Enumerate `packages/webui/docs/**/*.md`, `packages/webui/AGENTS.md` and `.agents/skills/webui-development/**/*.md`, then search across them for duplicated claims, old paths, removed capabilities, stale command names, counts, dates, baselines, test evidence, and contradictory wording. Include development guidance, component recipes, design-token choices and existing approved plan status. Use the indexes to find related pages, but do not limit the scan to pages directly named by the request. +4. **Check claims against the current tree.** Trace architecture and lifecycle claims to their implementations; check contracts against their producers and consumers; check gate claims against scripts, package commands, suite declarations, and the tests that exercise them. Use documentation and prior decision memory as navigation and historical context, not as proof of current behavior. When project decision memory is available, inspect relevant entries and verify them against the checkout. +5. **Separate current behavior from decision history.** Keep dated rationale recognizable as historical. Update stale status and implementation claims when the decision remains relevant. When a decision is no longer in force, follow the WebUI documentation index's retirement rule instead of leaving a superseded decision presented as current. If the code and an ADR conflict in a way that requires a new product or architecture decision, surface the conflict rather than silently choosing one in prose. +6. **Make the smallest complete documentation update.** Edit every affected document that owns or duplicates the changed fact, including the index, operating guide and development skill references identified below. Update the skill's entrypoint routing when its references or applicability change. Prefer one authoritative explanation with links over repeated copies. Preserve source paths and verification boundaries precisely; distinguish unit, assembled, loopback, built-browser, native-host, platform, and live-service evidence. +7. **Validate the documentation change.** Run `git diff --check`; verify relative links and referenced paths in the changed pages; search again for the stale claim across both package docs and the development skill. For skill changes, follow the available skill-creator validation workflow and check entrypoint routing. Verify named design tokens against `tokens.css`, utilities against `tailwind.config.cjs`, and component recipes against their actual TSX/CSS. Do not run product tests for prose-only edits unless the repository provides a documentation-specific check or the change affects executable examples. Do not assume a documentation-only CI profile applies to `packages/webui` or `.agents/skills`; check the current classifier and verification rules. If adding, moving, or deleting files, regenerate and check the source inventory. + +## Synchronize development guidance and design standards + +Treat `.agents/skills/webui-development/` as part of the maintained guidance, not a frozen source-reading snapshot. For each consequential correction, check whether a future agent following that skill would still make the right implementation or styling decision. Update the affected references in the same synchronization pass. + +| Changed fact or decision | Development skill references to inspect | Evidence to read | +| --- | --- | --- | +| How to modify business code, place components, coordinate owners or maintain runtime resources | `references/business-maintenance.md`, relevant `SKILL.md` routing | Current owner/caller paths, operating rules, lifecycle implementations and approved decisions | +| Business homes, dependency direction, public surfaces | `references/architecture.md`, relevant `SKILL.md` guidance | Real importers, owner modules and dependency-rule implementation | +| State ownership, submission, resource identity or lifetime | `references/ownership.md` | Commands, producers/consumers, leaf-owner implementations and corresponding tests | +| Wire contracts, capability availability or runtime resources | `references/runtime.md` | Operation declarations/registration, bindings, adapters and close paths | +| Tokens, typography, spacing, radii, shared controls or interaction states | `references/component-design-system.md` | `src/client/styles/tokens.css`, `tailwind.config.cjs`, reused TSX primitives and effective component CSS | +| Theme application, layout, CSS compilation, assets or visual acceptance | `references/visual-design.md` | Theme effects, stylesheet imports, compiler configuration, affected surfaces and available rendered evidence | +| Verification commands, test conventions or delivery requirements | `references/verification.md` | Package scripts, suite declarations, gate scripts and current operating instructions | +| Source anchors, baseline limitations or reference-refresh instructions | `references/source-evidence.md` | The particular claim's current evidence; preserve dated execution records as dated records | + +Paths in the evidence column beginning with `src/` are relative to `packages/webui/`. + +Keep component standards immediately actionable: an agent adding or restyling a component should find semantic token choices, type/spacing/radius scales, reusable controls and state treatments without reconstructing them from page screenshots. Update a recipe when its underlying implementation changes; distinguish observed implementation, recommended defaults and approved future requirements. Do not generalize a legacy declaration or one component's dimensions into a universal rule. + +For development planning, update existing approved scope, sequencing, prerequisites, open decisions and implementation status where the repository records them. Keep planned, implemented and runtime-verified states distinct. Source establishes what exists; an approved plan establishes intended work. Do not mark an unmet requirement complete because guidance now describes it, or silently rewrite an approved direction to match the implementation. Propose or request a decision when reconciliation requires choosing new scope or behavior; this sync does not authorize a new plan, product implementation or restoration of retired planning documents. + +Screenshots are optional supporting evidence. Do not regenerate the visual catalog merely because token or prose guidance changed. If an affected screenshot is retained as a current reference, either refresh it through an authorized capture workflow and inspect the result, or identify its dated scope. Keep capture scripts unchanged unless tooling work is separately requested. + +## Completion report + +Summarize the documents and development-skill references reviewed and changed, the source or test evidence used for consequential corrections, and the checks actually run. State whether development guidance, component design standards or existing plan status needed updates; if none did, say what was checked. Mark unverified runtime or platform behavior explicitly. Do not describe a static source inspection as runtime verification. diff --git a/.agents/skills/webui-gate-audit/SKILL.md b/.agents/skills/webui-gate-audit/SKILL.md new file mode 100644 index 000000000..294a66cb4 --- /dev/null +++ b/.agents/skills/webui-gate-audit/SKILL.md @@ -0,0 +1,45 @@ +--- +name: webui-gate-audit +description: Audit whether scripts/ WebUI verification gates still match the current architecture, source, tests, package scripts, and CI. Use for read-only reviews of gate currency, scope, consistency, and coverage; not for product architecture implementation. +--- + +# WebUI gate audit + +Review the repository's WebUI gates against the checked-out implementation. The deliverable is a source-grounded assessment of which rules are current and useful, which are stale or misaligned, what architecture they cover, and where their guarantees stop. + +## Operating scope + +- Treat an audit request as read-only unless the user separately asks for repairs. Preserve the existing worktree state. +- Inspect a gate's implementation before running it. Run only checks that are read-only and do not require generating or rewriting artifacts; inspect generated-artifact-dependent checks without rebuilding unless asked. +- Do not run tests, builds, formatters, generators, or `pnpm verify` as part of a read-only audit unless the user explicitly requests execution. Report what was inspected separately from what was executed. +- Do not infer a gate's effect from its name, a green historical report, or documentation alone. Current code and current test registration are the evidence. + +## Audit workflow + +1. **Establish the checkout.** Confirm repository root and inspect `git status --short`. Read the root `AGENTS.md`, `CONTRIBUTING.md`, `packages/webui/AGENTS.md`, and `packages/webui/docs/README.md`. Preserve unrelated changes and keep review material outside the repository. + +2. **Load current architecture authority.** Follow the WebUI documentation index to the architecture design and relevant boundary/ownership/runtime documents and ADRs. Read the verification gate guide. When repository Hindsight is available, search and read relevant decision pages; treat them as historical context and verify every relied-on claim in the current tree. If Hindsight is unavailable, continue from current source and say so only if material. + +3. **Inventory the gate chain.** Search all of `scripts/` for WebUI-specific checkers, shared rule modules, constants, baselines, generators, and verification helpers. Trace registrations through `package.json`, `scripts/verify.mjs`, CI workflows, `test/vitest-suites.json`, and the tests that exercise each rule. Include related typecheck, build, package, browser, and source-inventory gates when they form part of the WebUI verification chain. Distinguish executable gates from retired tools mentioned only as history. + +4. **Trace claims into implementation.** For each gate, follow the CLI entry to the rule implementation and identify its actual input: source AST/import graph, compiler program, build output/metafile, package artifact, registered tests, or another input. Record ordering and generated-artifact dependencies. Check that tests reach the production rule and cover meaningful allowed and rejected cases; do not equate a test name or fixture assertion with coverage of the intended invariant. + +5. **Compare the rules with architecture.** Keep separate dimensions separate: execution layer/dependency direction, role and business ownership, publication visibility, lifecycle/state ownership, runtime/wire boundaries, and packaging/publication. Check whether path classification matches current layout; whether the evaluated rule matches the declared matrix; whether exceptions are narrow, fingerprinted or stale-checked; whether same-layer edges, aliases, type-only imports, dynamic imports, generated inputs, and indirect workspace imports are treated as intended. Inspect the evaluator rather than hand-reading a matrix when behavior depends on code. + +6. **Classify findings.** Use these labels where helpful: + - **STALE** — a path, command, exception, or claim no longer exists or describes a retired state. + - **MISATTRIBUTED** — a real guarantee is assigned to the wrong gate or evidence layer. + - **DRIFT** — the implemented rule conflicts with current architecture decisions or source layout. + - **GAP** — an architecture invariant has no effective enforcement or meaningful acceptance evidence. + - **OVERLAP** — multiple gates duplicate a rule without a clear purpose or source of truth. + - **UNVERIFIED** — the evidence needed to decide is unavailable or was not executed. + +7. **State the guarantee boundary.** Explain what the complete gate chain constrains and what remains behavioral, platform-specific, live-service, or dependent on manual review. Static dependency compliance does not by itself prove complete use-case ownership, correct resource lifetime, race handling, or product equivalence. Do not upgrade source inspection or unit evidence into browser, native, cloud, or cross-platform acceptance. + +## Evidence and reporting + +- Cite every material finding with verified `path:line` references from the current checkout. Prefer concise evidence chains such as command registration → checker → rule → rule test. +- Separate **confirmed**, **inferred**, and **unresolved** conclusions. Note if artifact-dependent checks were inspected but not executed. +- Report the scope searched, gates inspected, any read-only checks actually run and their results, prioritized findings, and a concise judgment of whether the chain provides the requested level of architecture consistency. +- Do not propose arbitrary gate additions based on file counts or directory aesthetics. Tie every recommendation to a specific architecture invariant and a demonstrable failure mode. +- Keep the audit read-only. If the user asks to fix documentation or gate code, make only that authorized scope change after presenting or recording the concrete evidence; validate the changed scope and preserve all unrelated work. diff --git a/.agents/skills/webui-gate-audit/agents/openai.yaml b/.agents/skills/webui-gate-audit/agents/openai.yaml new file mode 100644 index 000000000..3b7998904 --- /dev/null +++ b/.agents/skills/webui-gate-audit/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "WebUI Gate Audit" + short_description: "Review WebUI gates for architecture fit" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ac79b8f7..a49191601 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,16 @@ jobs: needs: changes if: needs.changes.outputs.docs_only == 'false' name: verify (${{ matrix.os }}, ${{ matrix.node }}) - timeout-minutes: 15 + # A ceiling, not a cost: a job that finishes in eleven minutes still takes + # eleven minutes. The Linux profile drives the real-browser suite, which + # launches a browser per case because each case needs a clean extension + # state, and that suite does not fit in fifteen minutes any more. The + # WebUI group now runs its files one at a time, and the browser gate + # installs the browser's system dependencies — an apt run that takes + # between a minute and a quarter of an hour depending on the runner's + # cache. The ceiling has to fit the slow end of that, or the runner + # cancels a job that was working. + timeout-minutes: 40 strategy: fail-fast: false matrix: diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index 8a0fde010..8ed30c632 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -29,7 +29,9 @@ concurrency: jobs: build: runs-on: ubuntu-latest - timeout-minutes: 25 + # The same ceiling the CI verify job has: this job runs the same + # verification pipeline, including the real-browser suite. + timeout-minutes: 40 outputs: version: ${{ steps.release.outputs.version }} matrix: ${{ steps.release.outputs.matrix }} diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 1b716b138..4208fcce8 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -12,7 +12,9 @@ jobs: compatibility: name: compatibility (${{ matrix.os }}, ${{ matrix.node }}) runs-on: ${{ matrix.os }} - timeout-minutes: 15 + # This job runs the same platform profile the CI verify job runs, so it + # needs the same ceiling. + timeout-minutes: 40 strategy: fail-fast: false matrix: diff --git a/.github/workflows/source-candidate.yml b/.github/workflows/source-candidate.yml index 878f7736c..8baab4f90 100644 --- a/.github/workflows/source-candidate.yml +++ b/.github/workflows/source-candidate.yml @@ -52,7 +52,9 @@ jobs: validate: needs: export runs-on: ${{ matrix.os }} - timeout-minutes: 20 + # This job runs the same verification pipeline the CI verify job runs, so + # it needs the same ceiling. + timeout-minutes: 40 strategy: fail-fast: false matrix: diff --git a/.github/workflows/webui-npm-preview.yml b/.github/workflows/webui-npm-preview.yml new file mode 100644 index 000000000..aa757bfc3 --- /dev/null +++ b/.github/workflows/webui-npm-preview.yml @@ -0,0 +1,119 @@ +name: WebUI npm preview + +on: + push: + branches: ["webui"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: webui-npm-preview + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + # This job runs the same verification pipeline the CI verify job runs, so + # it needs the same ceiling. + timeout-minutes: 45 + # `secrets` cannot be read inside a step's `if:` ("Secrets cannot be + # directly referenced in if: conditionals"), so a step cannot be gated on + # whether the credential exists without going through a job-level env entry + # first. Deriving a flag here keeps the token itself out of the job + # environment: it stays on the single step that needs it, below. + env: + HAS_NPM_TOKEN: ${{ secrets.NPM_TOKEN != '' }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + cache: pnpm + registry-url: "https://registry.npmjs.org" + scope: "@fectivnfy112358" + - run: pnpm install --frozen-lockfile + - name: Verify the source revision before packaging + run: pnpm verify + env: + MCODE_VERIFY_REPORT_DIR: ${{ runner.temp }}/verification + MCODE_WEBUI_VERIFY_DIR: ${{ runner.temp }}/webui-installed-check + - name: Resolve next preview version + id: release + shell: bash + run: | + # The published version is the next counter after the highest + # `-preview.` already on the registry, so a plain push never + # needs a version bump in the repository. Resolving it from the + # registry rather than from a git tag is what makes reruns safe: a + # rerun recomputes the same number only if nothing was published in + # between, and the publish step below treats an existing version as a + # no-op instead of a failure. + package="$(node -p 'require("./release/webui-npm/package.json").name')" + versions="$(npm view "$package" versions --json)" + version="$(printf '%s' "$versions" | node -e ' + let raw = ""; + process.stdin.on("data", (c) => (raw += c)).on("end", () => { + const parsed = JSON.parse(raw || "[]"); + const list = Array.isArray(parsed) ? parsed : [parsed]; + const seen = []; + for (const value of list) { + const match = /^(\d+)\.(\d+)\.(\d+)-preview\.(\d+)$/.exec(String(value)); + if (!match) continue; + seen.push({ core: [Number(match[1]), Number(match[2]), Number(match[3])], n: Number(match[4]) }); + } + if (seen.length === 0) { + console.log("0.1.0-preview.1"); + return; + } + // Numeric tuple compare, not a string compare, so 0.10.0 outranks 0.9.0. + const top = seen.reduce((a, b) => { + for (let i = 0; i < 3; i++) { + if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i] ? a : b; + } + return a.n >= b.n ? a : b; + }); + console.log(`${top.core.join(".")}-preview.${top.n + 1}`); + }); + ')" + echo "package=$package" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Publishing $package@$version" + - name: Build npm package + run: node scripts/package-webui-npm.mjs --version "${{ steps.release.outputs.version }}" --outdir "$RUNNER_TEMP/webui-package" + - name: Inspect package contents + run: npm pack --dry-run --json "$RUNNER_TEMP/webui-package" + - name: Verify installation of the exact preview package + run: node scripts/verify-webui-package.mjs --package-dir "$RUNNER_TEMP/webui-package" + env: + MCODE_WEBUI_VERIFY_DIR: ${{ runner.temp }}/webui-preview-check + - name: Publish preview to the npm registry + # This repository carries no Actions secrets, so the publish fails with + # ENEEDAUTH and takes the job red on every push to `webui`. That buries + # the part of this job worth gating: that the WebUI still packs into a + # publishable npm tarball, which the two steps above keep proving on + # every push. Skipping the one step that cannot work here, and saying so + # in the run log, keeps that signal readable. Add `NPM_TOKEN` to the + # repository secrets and the preview resumes with no change here. + if: ${{ env.HAS_NPM_TOKEN == 'true' }} + shell: bash + run: | + set -euo pipefail + package="${{ steps.release.outputs.package }}" + version="${{ steps.release.outputs.version }}" + if npm view "$package@$version" version >/dev/null 2>&1; then + echo "$package@$version is already published; nothing to do." + exit 0 + fi + npm publish "$RUNNER_TEMP/webui-package" --tag preview + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Report the skipped preview + if: ${{ env.HAS_NPM_TOKEN != 'true' }} + run: | + echo "::warning::NPM_TOKEN is not configured, so no preview was published." + echo "::warning::The package was still built and inspected above; only the registry write was skipped." diff --git a/.gitignore b/.gitignore index b7c78eabb..e6e872933 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ node_modules/ dist/ +dist-webui/ +dist-webui-npm/ .turbo/ .cache/ .pnpm-store/ @@ -8,3 +10,9 @@ dist/ .env.* !.env.example .DS_Store +.idea/ +.codegraph/ +dist-*.zip +dist-store-*/ +.temp + diff --git a/.gitleaks.toml b/.gitleaks.toml index d749e8f03..e14d42375 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -12,6 +12,20 @@ paths = ['''(^|/)packages/agent-modules/permission/src/tools/bash-checker\.ts$'' regexTarget = "match" regexes = ['''^labelKey: 'permission\.scope\.byArgvPrefix2'$'''] +[[rules.allowlists]] +description = "Published pairing transfer vector, not a credential" +condition = "AND" +paths = ['''(^|/)packages/(browser-extension/test/protocol\.test\.ts|webui/test/unit/browser-bridge-pairing\.test\.ts)$'''] +regexTarget = "secret" +regexes = ['''^00112233445566778899aabbccddeeff$'''] + +[[rules.allowlists]] +description = "WebUI shortcut storage key name, not a credential" +condition = "AND" +paths = ['''(^|/)packages/webui/src/client/projection/common/shortcuts\.ts$'''] +regexTarget = "match" +regexes = ['''^SHORTCUT_STORAGE_KEY = "webui-shortcuts\.v1"$'''] + [[rules.allowlists]] description = "Documented keyboard shortcut" condition = "AND" diff --git a/.plan/browser-use/board/.notes-numbers b/.plan/browser-use/board/.notes-numbers new file mode 100644 index 000000000..66cdf4e1b --- /dev/null +++ b/.plan/browser-use/board/.notes-numbers @@ -0,0 +1 @@ +https://github.com/mcode-webui/minimax-code/issues/173 diff --git a/.plan/browser-use/board/.spec-number b/.plan/browser-use/board/.spec-number new file mode 100644 index 000000000..53708a24d --- /dev/null +++ b/.plan/browser-use/board/.spec-number @@ -0,0 +1 @@ +https://github.com/mcode-webui/minimax-code/issues/176 diff --git a/.plan/browser-use/board/HANDOFF.md b/.plan/browser-use/board/HANDOFF.md new file mode 100644 index 000000000..e8ac08425 --- /dev/null +++ b/.plan/browser-use/board/HANDOFF.md @@ -0,0 +1,119 @@ +# Handoff: starting `/implement-spec` for WebUI Browser Use + +Prepared 2026-10-20. Nothing in this programme has been implemented yet. This file is the +operational handoff for the run that starts the implementation; it is workspace material and is not +part of the published source projection. + +## The board + +- **Tracker:** GitHub issues on `mcode-webui/minimax-code` (the `origin` remote of this checkout). + Always pass `--repo mcode-webui/minimax-code` to every `gh` call. +- **Parent specification:** [#176](https://github.com/mcode-webui/minimax-code/issues/176). Read it + first; it carries the problem, the user stories, the implementation decisions, the testing + decisions and the out-of-scope list. +- **Tickets:** #177–#194, all labelled `browser-use` and `ready-for-agent`. +- **Research notes:** #173, #174, #175 — labelled `browser-use` and `documentation` only. They are + reference material, **not** work items. Do not close them as tickets. +- **Blocking edges:** native GitHub issue dependencies. A ticket is unblocked exactly when every + blocker is closed. + +Frontier query — `gh issue list --json` does not expose the dependency summary, so the blockers are +read per issue. The parent specification is excluded explicitly, because it is a work item's +context and not work itself: + +```bash +for n in $(gh issue list --repo mcode-webui/minimax-code --state open --label "ready-for-agent" \ + --json number --jq '.[].number' | sort -n | grep -v '^176$'); do + open_blockers=$(gh api repos/mcode-webui/minimax-code/issues/$n/dependencies/blocked_by \ + --jq '[.[] | select(.state=="open") | .number] | join(",")') + [ -z "$open_blockers" ] && echo "FRONTIER: #$n" +done +``` + +Today the frontier is exactly one ticket: **#177**, the technical validation ticket, which blocks +all seventeen others. It has to land first — the capability matrix, the bridge protocol, the +credential-store feasibility, the worker-lifecycle behaviour and the reconnect contract are what +every later ticket is written against. + +## Authority chain + +ADR 0019 decides. The tickets carry the work and the acceptance criteria. The plan +(`.plan/browser-use/browser-use-development-plan.md`) carries the work packages and the acceptance +matrix, and it is **tracked**, so an implementer working in a git worktree can read it. The board +and the plan must not drift apart: an amendment belongs in the plan first, and the affected +tickets are updated to match. + +If implementation evidence contradicts ADR 0019, amend ADR 0019 first, then re-scope the ticket. + +## Branch and pull request conventions + +- Integration branch: create it from an explicit ref on the fork's `webui` branch. Do not branch + from whatever `HEAD` happens to be, and do not branch from the current dirty working tree (it + carries unrelated model-selector work in progress). +- Per-ticket branches follow the fork workflow: `feat/`, lower-case kebab-case, + no agent or tool names in the prefix. +- Pull requests target `webui` on the fork. Never push to `upstream` (`MiniMax-AI/minimax-code`) and + never file an issue there. +- Each implementer worktree's branch must be confirmed to be based on the integration branch tip + before work starts, and must merge the integration tip before reporting done. + +## Findings that will shape the implementation + +These came out of the pre-implementation exploration. They are constraints, not tasks; each one +belongs to a specific ticket. + +1. **`@mavis/browser-core` is not a WebUI dependency today.** Its only consumer is `packages/tui`. + Adding it to WebUI is a dependency-rule change, not a given. The WebUI client bundle may import + only `@mavis/shared` and `@mavis/protocol` — not `browser-core` — so the provider contract has to + reach the client as shared DTOs. Affects #179, #180, #186, #187. +2. **No OS secure credential store exists in this repository.** `oauth-core`'s credential store is + file-based; the old `os-keyring` survives only as a migration-readable literal, and the DPAPI + code present is vendored Rust that is not callable from here. ADR 0019's "no plaintext fallback" + is net-new work. A new native module may pull in the Node compatibility matrix and + `pnpm.onlyBuiltDependencies`. Affects #190, and #177's credential-store validation. +3. **`packages/shared/src/browser-manifest.ts` is a different mechanism.** It belongs to the desktop + native-messaging integration, which ADR 0019 explicitly did not select. Its extension id and its + manifest are not this programme's extension, and the manifest file and `verify-extension-id.sh` + it names do not exist in this public projection. Do not reuse that id and do not extend that + module. Affects #185. +4. **The image transport does not fit the current WebSocket framing.** Binary frames are rejected + by the frame handler, the message ceiling is 256 KB, and `sendFrame` does not await socket + back-pressure. A live image stream needs its own transport shape and its own back-pressure + policy. Affects #180, #187. +5. **The WebSocket upgrade admission is a single funnel, and development mode bypasses the token + check there.** The dedicated extension admission path must sit ahead of the generic handler and + must validate the pairing credential regardless. The generic origin policy already rejects + extension origins and must stay that way. Affects #185. +6. **`beta.browserUseTooling` is derived from provider adoption.** It is a readiness statement, not + an authorization, and must not be widened silently when no surface is authorized. Affects #178, + #179. +7. **Adding a workspace package is a four-file change.** `pnpm-workspace.yaml` is an explicit path + list, not a glob; `release/extraction.json`'s `packageRoots` is edited by hand; + `release/dependency-licenses.json` must declare the package's dependencies; and + `release/public-source.json` is regenerated. `pnpm gen:tsconfig` regenerates two files, not one. + The extension is a store artifact rather than an npm file, but its source still belongs in the + published source projection. Affects #185. +8. **A new Vitest file must be registered** in the declared suite groups or it never runs. A + Playwright spec placed outside the WebUI browser-suite directory fails the source check. Affects + every ticket that adds a test. +9. **The isolated Chromium provider must be supplied by both composition roots** — the development + entry and the packaged production entry. Validating only the development entry does not prove + the shipped path. Affects #179, #194. + +## Gates + +Each ticket names the gates that bear on it. The delivery gate is `pnpm verify`, run once for the +final inputs. `pnpm typecheck:webui` does not cover the extension package: that package carries its +own typecheck command, registered in `scripts/verify.mjs`. + +## Workspace material (tracked in the repository) + +- `.plan/browser-use/browser-use-development-plan.md` — the development plan, with §10 holding the board map. +- `.plan/browser-use/exploration/01-runtime-server.md`, `02-browser-core-client.md`, `03-repo-plumbing.md` — the + raw exploration notes behind #173, #174 and #175. +- `.plan/browser-use/board/` — the published issue bodies, one file per issue, and this handoff. + +`.plan/` is **tracked in the repository** and part of the public source projection, so a worktree +carries it and an implementer can read the plan, this handoff and the research notes without +leaving the checkout. Nothing an implementer needs may exist only in a machine-local place: the +published issues carry the same material for anyone reading the repository. diff --git a/.plan/browser-use/board/IMPLEMENT-SPEC-PROMPT.md b/.plan/browser-use/board/IMPLEMENT-SPEC-PROMPT.md new file mode 100644 index 000000000..bc8d99530 --- /dev/null +++ b/.plan/browser-use/board/IMPLEMENT-SPEC-PROMPT.md @@ -0,0 +1,151 @@ +# Prompt: run `/implement-spec` for WebUI Browser Use + +Copy everything below the line into a fresh agent session that has this repository checked out. + +--- + +Implement the **WebUI Browser Use** programme from the published ticket board. + +Call the Skill tool with `implement-spec` and follow it. The specification is GitHub issue +**#176**; the tickets are **#177–#194**; the board's blocking edges are native GitHub issue +dependencies. The programme is fully planned, fully specified and ready to execute — there is no +planning work left, and no ticket should be authored from scratch. + +**Do not start by re-planning, re-deriving the board, or re-writing tickets.** Read what exists, +then execute it. If a ticket's technical detail turns out to be wrong, that is a finding to report +against that ticket, not a licence to redesign the programme. + +## 1. Read these, in this order + +1. `gh issue view 176 --repo mcode-webui/minimax-code --comments` — the specification: problem, + solution, 40 user stories, implementation decisions, testing decisions, out of scope, and the + board table. +2. `packages/webui/docs/adr/0019-webui-browser-use-surfaces-and-external-browser-bridge.md` — the + **sole** decision record for this feature. It is in git and readable from any worktree. +3. `gh issue view 173 --repo mcode-webui/minimax-code` — research notes: runtime and server seams. + `gh issue view 174` — browser-core contracts and WebUI client conventions. + `gh issue view 175` — repository plumbing for a new package and new gates. + These three are reference material, not work items. Do not close them as tickets. +4. `gh issue view 177 --repo mcode-webui/minimax-code` — the first ticket, and the only one + currently on the frontier. +5. `packages/webui/docs/agents/issue-tracker.md`, `packages/webui/docs/agents/domain.md`, + `packages/webui/docs/agents/triage-labels.md` — this repository's tracker, glossary and label + conventions. +6. `packages/webui/AGENTS.md` and `packages/webui/docs/README.md` — the operating guide for the + package you will change, and the index of its documentation. +7. `CONTEXT.md` at the repository root — the glossary. Use its vocabulary in code, tests, commit + messages and issue comments. + +## 2. The board + +- Tickets: **#177–#194**, each labelled `browser-use` and `ready-for-agent`. +- Parent specification: **#176**. Do not modify or close it until the whole board is done. +- Blocking edges are native GitHub issue dependencies, so a ticket is unblocked exactly when every + blocker it names is closed. `#177` blocks all seventeen others; the frontier today is `#177` + alone. Two tickets become unblocked at once after `#177` and `#178` land: `#179` and `#185`. + +Find the frontier with: + +```bash +for n in $(gh issue list --repo mcode-webui/minimax-code --state open --label "ready-for-agent" \ + --json number --jq '.[].number' | sort -n | grep -v '^176$'); do + open_blockers=$(gh api repos/mcode-webui/minimax-code/issues/$n/dependencies/blocked_by \ + --jq '[.[] | select(.state=="open") | .number] | join(",")') + [ -z "$open_blockers" ] && echo "FRONTIER: #$n" +done +``` + +## 3. Repository rules that bind this run + +- **Always pass `--repo mcode-webui/minimax-code`** to every `gh` call. Without it `gh` resolves + this checkout to the public upstream `MiniMax-AI/minimax-code`, and issues and pull requests + filed there land in the published projection. Never push, comment, label or file anything on + upstream. +- **Integration branch:** create it from an explicit ref, never from whatever `HEAD` happens to be. + Base it on the fork's `webui` branch — at handoff time the local `webui` tip was `9f14141` and + `origin/webui` was `b705259`, with one unpushed commit between them. Re-check the refs yourself + and state in your first report which ref you used and why. +- **Per-ticket branches** follow the fork workflow: `feat/`, lower-case + kebab-case, no agent or tool names in the prefix. Pull requests target `webui` on the fork. +- **The main checkout is dirty with someone else's in-flight work** + (`packages/webui/src/client/features/models/**`, and possibly `src/client/ui/Dropdown.tsx`). Do + not stage it, do not commit it, do not revert it, and do not let it into a Browser Use commit. + Work in worktrees, which the skill already gives you, and leave the main checkout alone. +- **ADR 0019 decides.** If implementation evidence contradicts it, amend ADR 0019 first, then + re-scope the affected ticket. Do not create a second decision record, and do not resolve a + product question in code. +- Documentation and commit messages are **English**. +- No account data, sessions, logs, credentials or real user content in tests or commits. Use + temporary data directories and synthetic pages. +- Keep scratch material outside the repository's published source projection. + +## 4. Hard constraints found before implementation + +These are established facts about this codebase. Do not rediscover them the wrong way, and do not +design around them as if they were open questions. + +1. **`@mavis/browser-core` is not a WebUI dependency today** — its only consumer is `packages/tui`. + The WebUI client bundle may import only `@mavis/shared` and `@mavis/protocol`; `browser-core` + cannot reach the client layer, so the provider contract must arrive client-side as shared DTOs. + The WebUI must not import `packages/tui`. (Tickets #179, #180, #186, #187.) +2. **No OS secure credential store exists in this repository.** `oauth-core`'s credential store is + file-based; `os-keyring` survives only as a migration-readable literal; the DPAPI code present + is vendored Rust that is not callable from here. ADR 0019's "no plaintext fallback" is net-new + work, and a new native module may pull in the Node compatibility matrix and + `pnpm.onlyBuiltDependencies`. (#177, #190.) +3. **`packages/shared/src/browser-manifest.ts` is a different mechanism** — the desktop + native-messaging integration, which ADR 0019 explicitly did not select. Its extension id is not + this programme's extension id; the manifest file and `verify-extension-id.sh` it names do not + exist in this public projection. Do not reuse that id and do not extend that module. (#185.) +4. **The image transport does not fit the current WebSocket framing** — binary frames are rejected + by the frame handler, the message ceiling is 256 KB, and `sendFrame` does not await socket + back-pressure. A live image stream needs its own transport shape and its own back-pressure + policy. (#180, #187.) +5. **The WebSocket upgrade admission is a single funnel, and `dev: true` bypasses the token check + inside it.** The dedicated extension admission path must sit ahead of the generic handler and + must validate the pairing credential regardless of development mode. The generic origin policy + already rejects extension origins and must stay exactly that narrow. (#185.) +6. **`beta.browserUseTooling` is derived from provider adoption.** It is a readiness statement, + not an authorization, and must not be widened silently when no surface is authorized. (#178, + #179.) +7. **Adding a workspace package touches four files** — `pnpm-workspace.yaml` is an explicit path + list, not a glob; `release/extraction.json`'s `packageRoots` is edited by hand; + `release/dependency-licenses.json` must declare the package's dependencies; and + `release/public-source.json` is regenerated with `node scripts/source-inventory.mjs --write`. + `pnpm gen:tsconfig` regenerates two files, not one. The extension is a store artifact rather + than an npm file, but its source still belongs in the published source projection. (#185.) +8. **A new Vitest file must be registered** in `test/vitest-suites.json` or it never runs. A + Playwright spec placed outside the WebUI browser-suite directory fails `pnpm check:source`. +9. **Both composition roots must supply the provider** — `scripts/run-webui-server.mjs` and + `release/webui-npm/bin/mcode-webui.mjs`. Validating only the development entry does not prove + the shipped path, and the packaged path is what the acceptance matrix asks for. (#179, #194.) + +## 5. Gates + +Each ticket names the gates that bear on it. The delivery gate is `pnpm verify`, run once for the +final inputs. `pnpm typecheck:webui` covers every WebUI source and test file but **not** the new +extension package, which carries its own typecheck command registered in `scripts/verify.mjs`. +Reuse a passing gate's evidence only when its source, configuration and platform inputs are +unchanged. `packages/webui/docs/verification/gates.md` states what each gate does and does not +prove, and the evidence level a claim must name. + +## 6. Definition of done + +- All eighteen tickets closed, each with its acceptance criteria demonstrably met and its gates + recorded. +- The acceptance matrix from ADR 0019 is satisfied for the isolated Chromium, Edge and Chrome. A + surface that was not exercised on a platform is called out explicitly, never inferred from + another platform's result. Synthetic and mock evidence stays separate from real-browser + evidence. +- A pull request open against `webui` on the fork, with the required repository gates green and the + source inventory regenerated and reviewed. +- ADR 0019 still the only Browser Use decision record. Documentation describes what the source now + does, not what it was planned to do. + +## 7. Report back + +Report the integration branch, the pull request, the per-ticket outcome, the gate results with +their evidence levels, and anything you could not verify — explicitly. If you find that a ticket's +technical detail is wrong, say so against that ticket and stop the affected work rather than +quietly re-deciding it. Do not report a ticket as done on the strength of a fake provider, a mock +peer, or the development entry alone. diff --git a/.plan/browser-use/board/notes-01-runtime-server.md b/.plan/browser-use/board/notes-01-runtime-server.md new file mode 100644 index 000000000..59f59b408 --- /dev/null +++ b/.plan/browser-use/board/notes-01-runtime-server.md @@ -0,0 +1,831 @@ +**Reference material for the WebUI Browser Use programme — not a work item.** + +Captured before implementation, from the source at the current checkout. Path in the workspace: `.plan/browser-use/exploration/01-runtime-server.md` (gitignored, outside the published source projection). + +--- + +# 01 — Runtime/Server seams for WebUI Browser Use + +Investigation-only notes. No code is changed. Every claim below is anchored to a `file:line` +in the published source tree (verify against the current commit when implementing). Where +the ADR or development plan makes a claim, the claim is checked here against the actual +source and the result is stated. + +The investigation scope covers only the runtime host assembly, the two composition roots +that start it, the loopback WebSocket admission path, the per-start credential, the +operation dispatcher (with `watchTerminal` as the stream lifecycle reference), the +runtime capability port, and the existing `browser`-named seams already present. + +The runtime-side host-contract module +`packages/local-runtime-v2/src/local/host-contract.ts` is read only as far as the WebUI +forwards its options; full ownership of `browserAdapter` and `browserToolExposure` lives +in `@mavis/local-runtime-v2` and is out of scope. + +--- + +## 1. `packages/webui/src/runtime/browser-provider.ts` — the provider adoption seam + +The module declares the slot the WebUI exposes for a *host-supplied* browser provider. +The WebUI does not construct one; the host process supplies it. The whole module is +~60 lines and the contracts are small. + +### Exported types (`browser-provider.ts:13-47`) + +```ts +import type { CreateLocalRuntimeHostOptions } from "@mavis/local-runtime-v2/process-local"; + +export type WebuiBrowserAdapter = NonNullable< + CreateLocalRuntimeHostOptions["browserAdapter"] +>; + +export type WebuiBrowserToolExposure = NonNullable< + CreateLocalRuntimeHostOptions["browserToolExposure"] +>; + +export interface WebuiBrowserProvider { + readonly adapter: WebuiBrowserAdapter; + close(): void | Promise; +} + +export interface WebuiBrowserProviderBinding { + readonly browserAdapter?: WebuiBrowserAdapter; + readonly browserToolExposure?: WebuiBrowserToolExposure; + readonly browserUseTooling: boolean; + readonly closeBrowserProvider: () => void | Promise; +} +``` + +### `adoptWebuiBrowserProvider` (`browser-provider.ts:49-58`) + +```ts +export function adoptWebuiBrowserProvider( + provider: WebuiBrowserProvider | undefined, + exposure: WebuiBrowserToolExposure | undefined, +): WebuiBrowserProviderBinding { + return { + ...(provider ? { browserAdapter: provider.adapter } : {}), + ...(exposure ? { browserToolExposure: exposure } : {}), + browserUseTooling: provider !== undefined, + closeBrowserProvider: () => provider?.close(), + }; +} +``` + +### What the contracts actually mean + +- `WebuiBrowserProvider` is a two-field object the host hands the WebUI. The WebUI + never declares the adapter shape; it forwards `CreateLocalRuntimeHostOptions` + generics. In the runtime, the underlying shape is `LocalBrowserAdapter` / + `LocalBrowserToolExposure` from `@mavis/agent-tools/desktop` (see + `packages/local-runtime-v2/src/local/host-contract.ts:7,51-52,69-70`). Browser Use + implementers must look in `@mavis/agent-tools/desktop` for the actual adapter + contract; `WebuiBrowserProvider` is the *adoption slot*, not a redeclaration. +- `browserToolExposure` is the runtime's own "tool-schema exposure" object, distinct + from the adapter. The two are passed into the binding *independently* — neither is + required when the other is present, and an exposure without a provider, or a + provider without an exposure, is legal. A future browser client must accept that + the two are independent inputs. +- `browserUseTooling` is a *readiness statement*, not a request. The comment at + `browser-provider.ts:39-41` is explicit: "`browserUseTooling` is a readiness + *statement*, not a request: it is true exactly when a provider was adopted, which + is what the harness reads." The current seam therefore conflates "provider was + adopted" with "tooling is available". The development plan flags this as + insufficient — provider presence is not a user authorization (development plan + §3, ADR 0019 §"Browser Use remains a host-owned runtime capability"). There is + no separate per-browser-surface authorization slot on the binding. + +### Verdict on the plan's claim + +Plan §3: "the current runtime derives browser-tool availability from provider +adoption, while `browserToolExposure` describes tool-schema exposure. Neither is, +by itself, an independent authorization check for the three browser surfaces or the +Full CDP capability." This holds: + +- `browserUseTooling` is set to `provider !== undefined` (`browser-provider.ts:56`). + There is no independent boolean input that maps to the three default-off + authorizations or the Full CDP switch. +- The `configGetter` in `assembly.ts:316-330` reads the binding's + `browserUseTooling` into `beta.browserUseTooling`, which the runtime then + surfaces to its policy module + (`packages/local-runtime-v2/src/service/browser-use/policy.ts:8-18`, + `isBrowserUseToolingAvailable` uses `browserUseToolingEnabled === true` plus an + optional `provider` and a separate `filePanelBrowserEnabled`). The WebUI-side + seam has no notion of a per-surface switch or Full CDP. + +--- + +## 2. `packages/webui/src/runtime/assembly.ts` — adoption + lifecycle ownership + +### How the provider is adopted + +`assembly.ts:186-212` declares `CreateWebuiRuntimeHostOptions`, which includes: + +```ts +readonly browserProvider?: WebuiBrowserProvider; +readonly browserToolExposure?: WebuiBrowserToolExposure; +``` + +`createWebuiRuntimeHost` (`assembly.ts:230-477`) runs in this order: + +1. `adoptWebuiBrowserProvider(options.browserProvider, options.browserToolExposure)` + at `assembly.ts:260-263`. Result is a `WebuiBrowserProviderBinding`. +2. The binding's `closeBrowserProvider` is wired into the lifecycle at + `assembly.ts:265-269`: + +```ts +const lifecycle = createWebuiRuntimeLifecycle({ + disposeAuth: authSession.dispose, + disposeMcodeTools: () => mcodeTools?.dispose(), + closeBrowserProvider: browser.closeBrowserProvider, +}); +``` + +3. The binding's `browserAdapter`, `browserToolExposure`, and `browserUseTooling` + are plumbed into the forwarded harness options at `assembly.ts:297-337`: + +```ts +configGetter: () => ({ + ...currentConfig, + beta: { + ...currentConfig.beta, + mcodeTools: mcodeTools.ready, + browserUseTooling: browser.browserUseTooling, + }, +}), +...(browser.browserAdapter ? { browserAdapter: browser.browserAdapter } : {}), +...(browser.browserToolExposure + ? { browserToolExposure: browser.browserToolExposure } + : {}), +``` + +The `WebuiForwardedRuntimeHostOptions` (`assembly.ts:142-156`) is the runtime's own +`CreateLocalRuntimeHostOptions` with a `Pick<...>` over `browserAdapter` and +`browserToolExposure` only, so the forwarded shape cannot drift from the runtime +type. + +### Who owns closing the provider + +`packages/webui/src/runtime/lifecycle.ts` is the single close owner. Its +`WebuiRuntimeResourceOwners` interface declares `closeBrowserProvider?` as an +optional slot (`lifecycle.ts:21-22`). The close order is fixed: + +- `close()` (`lifecycle.ts:50-77`) runs, in order: `disposeAuth` → + bound host close → `disposeMcodeTools` → `closeBrowserProvider?`. + Failures are collected and raised as one error or an `AggregateError`. +- `closeAcquiredResources()` (`lifecycle.ts:78-97`) runs the same three non-host + steps in the same order when the factory threw before returning a host. + +The provider is therefore closed *after* the harness host and *after* the +mcode-tools broker. The plan's WP2 (`lifecycle cleanup for Chromium processes, +profile directories, stream pumps and pending input`) must fit inside +`provider.close()` — the host does not know what those resources are. Failures +inside `close()` are caught and folded into the aggregate. + +### Lifecycle order observed by `assembly.ts` + +- Pre-host failure path (`assembly.ts:272-296`): if `authSession.start()` or + `prepareWebuiMcodeToolsIntegration` throws, `lifecycle.closeAcquiredResources()` + is called to release the capability owners. The provider's `closeBrowserProvider` + is therefore invoked even if the host never came up. +- Factory failure path (`assembly.ts:359-380`): if `factory(forwardedOptions)` + throws after auth/mcode-tools succeeded, `lifecycle.closeAcquiredResources()` + is again called before the error is re-thrown. The comment at + `assembly.ts:362-365` is explicit: "the capability owners were already acquired + above, so release them on this path as well; otherwise a broker socket or + Browser profile survives a failed WebUI start." +- Post-host failure path (`assembly.ts:385-398`): if `mcodeTools.ensureCommandPath()` + throws, the wrapped `host.apiHost.close()` runs `lifecycle.close()`, which runs + the same full order once. + +Implementers adding a browser provider must guarantee `close()` is idempotent +because the same wrapped close can run from both the post-host failure path and +the eventual `service.close()`. + +--- + +## 3. Composition roots — where a provider would have to be supplied + +There are exactly two composition roots that build a `WebuiRuntimeHost`. Both are +read here verbatim because they are the only places a Browser Use provider can +enter the WebUI today. + +### Development root — `scripts/run-webui-server.mjs` + +```js +// run-webui-server.mjs:42-59 +const assembled = await createWebuiRuntimeHost({ + dataDir, + appVersion: "webui-dev", +}); +const port = createHarnessPortFromHost(assembled.host); +const terminal = new WebuiTerminalManager(); +const service = new WebuiService({ + port, + operations: createOperationRegistry(port, terminal), + terminal, + tcpPort, + dev: true, + clientDir, +}); +``` + +Notes: + +- `createWebuiRuntimeHost` is called with only `dataDir` and `appVersion`. No + `browserProvider`, no `browserToolExposure`. The provider adoption seam + therefore resolves to the all-undefined branch of + `adoptWebuiBrowserProvider`, which yields `browserUseTooling: false`. +- The launcher reaches the assembly through `../packages/webui/src/runtime/index.ts` + (`run-webui-server.mjs:30-32`), so any change to the runtime exports is reflected + here without copying them. +- `WebuiService` is constructed with `dev: true`. This is the development-mode + admission bypass; see §4 below. +- `tcpPort` defaults to `8787` (`run-webui-server.mjs:37`). + +### Packaged production root — `release/webui-npm/bin/mcode-webui.mjs` + +```js +// mcode-webui.mjs:28-39 +const assembled = await createWebuiRuntimeHost({ dataDir, appVersion: version }); +const port = createHarnessPortFromHost(assembled.host); +const terminal = new WebuiTerminalManager(); +const service = new WebuiService({ + port, + operations: createOperationRegistry(port, terminal), + terminal, + host: "127.0.0.1", + tcpPort, + dev: true, + clientDir: path.join(packageRoot, "client"), +}); +``` + +Notes: + +- Imports come from `../server/server.js` and `../server/runtime.js` — the bundled + server/runtime entry points, *not* the workspace source. The packaging layout + therefore has the runtime factory available as a single named import, ready + to receive `browserProvider` and `browserToolExposure`. +- The packaged entry **also** calls `createWebuiRuntimeHost({ dataDir, appVersion })` + without `browserProvider`. It passes `host: "127.0.0.1"` explicitly (the + development root leaves the default in place) and the same `dev: true`. The + comment at `mcode-webui.mjs:52-54` acknowledges the dev-mode consequence: + "dev mode keeps the per-start credential off, so the shell page and its assets + load without a query token." +- The composition root also closes in two steps (`mcode-webui.mjs:41-48`): + `await service.close(); await assembled.host.apiHost.close();`. The lifecycle + order (`service.close` → `host.apiHost.close`) is the same as the development + root; the difference is just the explicit second call. + +### What this means for the plan + +Both roots must supply a provider consistently. Validating only +`scripts/run-webui-server.mjs` does *not* prove the shipped WebUI path. Both roots +currently run with `dev: true`; both must adopt the same provider (or both must +not) for the production behavior to match the development behavior. A Browser +Use implementer must add the same option to both call sites, with the same +gating, because `WebuiService` cannot itself reach back into the assembly to +construct a provider — the assembly runs first and produces `host`. + +The plan's WP2 acceptance ("the real provider path is exercised; a fake provider +alone is insufficient for this exit") follows directly from this: the test must +go through `createWebuiRuntimeHost` with a real provider, not by stubbing the +adapter inside the harness port. + +--- + +## 4. `packages/webui/src/server/access-policy.ts` — admission rules + +### Loopback Host check (`access-policy.ts:99-106`) + +```ts +export function isLoopbackHost(host: string): boolean { + return ( + host === "127.0.0.1" || + host === "localhost" || + host === "::1" || + host === "[::1]" + ); +} +``` + +`isLoopbackBindAddress` (`access-policy.ts:108-117`) accepts loopback IPv6 zone IDs +(`%lo0`) by stripping the zone before re-checking. LAN and `0.0.0.0` are refused +at construction (`service.ts:199-202`). + +### Allowed Origins (`access-policy.ts:119-135`) + +```ts +export function isAllowedOrigin(origin: string, host: string, port?: number): boolean { + let parsed: URL; + try { + parsed = new URL(origin); + } catch { + return false; + } + const protocol = parsed.protocol.toLowerCase(); + if (protocol !== "http:" && protocol !== "https:") return false; + const hostname = parsed.hostname.toLowerCase(); + if (hostname !== "127.0.0.1" && hostname !== "localhost") return false; + if (port === undefined) return true; + const portNumber = parsed.port + ? Number(parsed.port) + : defaultPortForProtocol(protocol); + return portNumber === port && parsed.hostname === host; +} +``` + +This rejects everything except `http(s)://127.0.0.1` and `http(s)://localhost`. +A `chrome-extension://` Origin fails the protocol gate before the hostname is +even checked. The plan's directive ("do not broaden the existing origin check +to accept arbitrary extension origins") is already enforced by this code; the +generic WebUI origin policy does not need to change. + +### HTTP admission — `admitHttpRequest` (`access-policy.ts:26-45`) + +Checks the loopback `Host` header and the `Origin` header. The credential check +is intentionally left to the router (`http/router.ts:44-48`) because that +function needs the parsed request URL. Note: the HTTP credential check is +applied *after* admission but *before* route dispatch. + +### WebSocket upgrade admission — `admitWebSocketUpgrade` (`access-policy.ts:51-81`) + +```ts +export function admitWebSocketUpgrade( + request: IncomingMessage, + url: URL, + socket: Duplex, + context: WebuiAdmissionContext, +): boolean { + const urlHost = url.hostname.toLowerCase(); + const requestHost = (request.headers.host ?? "").toLowerCase(); + const requestOrigin = (request.headers.origin ?? "").toLowerCase(); + if (!requestHost || !isLoopbackHost(requestHost.split(":")[0] ?? "")) { + rejectUpgrade(socket, 403, "Forbidden Host"); + return false; + } + if (urlHost !== "127.0.0.1" && urlHost !== "localhost") { + rejectUpgrade(socket, 403, "Forbidden Host"); + return false; + } + if ( + requestOrigin && + !isAllowedOrigin(requestOrigin, context.host, context.tcpPort()) + ) { + rejectUpgrade(socket, 403, "Forbidden Origin"); + return false; + } + const presented = url.searchParams.get("token"); + if (!context.dev && !credentialMatches(context.credential, presented)) { + rejectUpgrade(socket, 401, "Unauthorized"); + return false; + } + return true; +} +``` + +Order of rejection: loopback `Host` → loopback URL host → `Origin` → token. +Note the `dev: true` bypass on the token check (`access-policy.ts:76`). Both +composition roots pass `dev: true` today, so generic WebUI token checks are +currently disabled. + +### Where the WebSocket upgrade is admitted — `websocket-server.ts:43-75` + +```ts +handleUpgrade = ( + request: IncomingMessage, + socket: Duplex, + head: Buffer, +): void => { + if (!this.context.isAccepting()) { + socket.write("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n"); + socket.destroy(); + return; + } + const url = parseWebSocketUrl(request.url); + if (!url) { + rejectUpgrade(socket, 400, "Bad Request"); + return; + } + if ( + !admitWebSocketUpgrade(request, url, socket, { + host: this.context.host, + tcpPort: this.context.boundTcpPort, + credential: this.context.credential, + dev: this.context.dev, + }) + ) { + return; + } + this.server.handleUpgrade(request, socket, head, (ws) => { + this.server.emit("connection", ws, request); + }); +}; +``` + +The hook used by the HTTP server is set in `service.ts:236-237`: + +```ts +this.httpServer.on("upgrade", this.wsServer.handleUpgrade); +``` + +A Browser Use implementer who needs a *separate* extension admission path has +two options: + +1. **Path branch inside `WebuiWebSocketServer.handleUpgrade`.** The current code + dispatches every upgrade through the single admission function. A dedicated + extension path would need to short-circuit before `admitWebSocketUpgrade` — + for example, by inspecting `url.pathname` and dispatching to an + extension-specific admission that also checks identity, pairing credential, + protocol version, message sizes, and per-browser switch state. The decision + point is `websocket-server.ts:62-71`. The plan's WP3 requires pairing + credentials to remain enforced when `dev: true`; `admitWebSocketUpgrade`'s + `dev` bypass is exactly the bypass that must not apply to extensions. +2. **Independent `WebSocketServer` instance.** The HTTP server only routes + `upgrade` events; a second `WebSocketServer({noServer: true})` bound to the + same HTTP server with its own `upgrade` listener could admit a separate + `/extension` path. This requires a URL-path branch in the upgrade hook + above and a second `WebuiWebSocketServer`-like wrapper. + +Neither option exists today. The existing infrastructure enforces the +single-origin/loopback/credential model; the extension admission is not yet +written. Implementers must decide which path to take. + +--- + +## 5. `packages/webui/src/server/credentials.ts` — per-start token + +Full file is ~40 lines. Two facts: + +- `createWebuiCredential` (`credentials.ts:19-21`) generates the token with + Node's `randomUUID()`. The token is created in `WebuiService`'s constructor + (`service.ts:207`) when no override is provided. +- `credentialMatches` (`credentials.ts:28-36`) uses `timingSafeEqual` over the + UTF-8 bytes of the two strings; the length check is explicit to keep the + timing constant. + +Implication for Browser Use: the per-start WebUI token cannot serve as a +persistent browser pairing credential. It changes on every service start +(`credentials.ts:19-21` plus the fact that the service is constructed fresh on +each `npm start`/`mcode-webui.mjs` invocation). The development plan and ADR +agree: the extension bridge requires its own credential lifecycle, persisted +in OS secure storage (WebUI side) and extension local storage (extension +side), unrelated to the per-start token. + +--- + +## 6. WebSocket operation path, streams, and cancellation + +The path goes: `ws.on("message")` → `frame-handler.ts:handleInboundFrame` → +`operation-dispatch.ts:dispatchWebuiFrame` → handler → response/event/error +frames. + +### Frame ingress (`frame-handler.ts:24-73`) + +- Binary frames are rejected with `WebuiErrorCode.invalidEnvelope` + (`frame-handler.ts:30-40`, "binary frames are not accepted"). This is the + reason an image-frame transport that wants to carry binary JPEG/PNG bytes + cannot use the *same* WebSocket as the JSON envelope without changing this + gate. Two options a Browser Use implementer would have to add: (a) lift the + binary ban and define a new envelope shape that distinguishes binary image + frames from JSON control frames, or (b) keep image frames on a *separate* + WebSocket connection (the plan's WP3 already requires a dedicated extension + admission path, which is one natural place for this). +- Frames above `maxMessageBytes` (`service.ts:46`: `WEBUI_MAX_MESSAGE_BYTES = + 256 * 1024`, configurable via `WebuiServiceOptions.maxMessageBytes`) are + rejected with `WebuiErrorCode.payloadTooLarge` + (`frame-handler.ts:42-52`). `ws.WebSocketServer({maxPayload: maxMessageBytes})` + is set in `websocket-server.ts:36-39` — note that `maxPayload` is a ws + library setting; frames larger than this are dropped before + `handleInboundFrame` runs. +- JSON parse failures are rejected with `invalidEnvelope` (`frame-handler.ts:53-66`). + +### Dispatch (`operation-dispatch.ts:30-162`) + +`dispatchWebuiFrame`: + +1. Validates the envelope (`judgeWebuiFrame`). +2. Rejects non-`request` kinds. +3. Returns `shuttingDown` when `accepting` is `false`. +4. Looks up the registry; missing operation → `unknownOperation` with the + operation name (`operation-dispatch.ts:68-78`). The terminal operations + rely on this: when no `terminal` is supplied, they are not registered and + a `watchTerminal` request becomes `unknown_operation` instead of crashing + (`operations.ts:103-108, 135-142`). +5. Calls `entry.operation.validate(parsed.body)`; failure → error frame + with the validator's code. +6. Calls `entry.handle(context, validated.body)` with + `{ requestId, signal }`. The signal is per-connection + (`websocket-server.ts:101-108, 119`): + +```ts +const connectionController = new AbortController(); +this.connectionSignals.set(ws, connectionController); +... +signalFor: () => this.connectionSignals.get(ws)?.signal, +``` + +The signal aborts when the WebSocket closes (`websocket-server.ts:104-113`) +or when `closeConnections()` runs during shutdown +(`websocket-server.ts:135`). Every handler therefore receives a per-connection +`AbortSignal` it can plumb into its source. + +7. Two result shapes: `{ body }` for unary responses, `{ stream }` for + streams. Stream results go through the pump at + `operation-dispatch.ts:96-150` (see next subsection). + +### Stream lifecycle — `watchTerminal` as the canonical reference + +The terminal handler at `operation/handlers/terminal.ts:78-89` is the +canonical "stream over an AsyncIterable with an AbortSignal" pattern: + +```ts +const watchTerminal: DedicatedHandler = ( + context, + body, +) => ({ + stream: { + ok: true, + source: terminal.watch( + String((body as Record).terminalId), + context.signal, + ) as unknown as AsyncIterable>, + }, +}); +``` + +`terminal.watch` (in `WebuiTerminalProcessPort`, `port.ts:596-603`) takes +the `AbortSignal` and yields `{ terminalId, data, exited }` records. The +implementation lives in `WebuiTerminalManager.watch` (referenced from +`runtime/index.ts:27`); not opened here, but the contract is that the source +honors `AbortSignal` so the dispatcher can drive cancellation. + +### Stream pump — `operation-dispatch.ts:108-150` + +```ts +const iterator = toAsyncIterator(result.stream.source as ...); +const signal = getSignal(); +let finalised = false; +const finalise = async (): Promise => { + if (finalised) return; + finalised = true; + await iterator.return?.(); +}; +const close = () => void finalise(); +signal?.addEventListener("abort", close, { once: true }); +if (entry.operation.acknowledgesStream) + sendFrame(ws, responseFrame(parsed.requestId, { stream: true })); +try { + while (!signal?.aborted) { + const next = await iterator.next(); + if (next.done || signal?.aborted) break; + sendFrame(ws, eventFrame(parsed.requestId, next.value)); + } +} finally { + signal?.removeEventListener("abort", close); + await finalise(); +} +``` + +Key properties an image-frame transport must reproduce: + +- **Cancellation.** Both the per-connection `signal.aborted` and the explicit + `iterator.return?.()` are wired. A disconnect on the client side aborts + the controller (`websocket-server.ts:104-113`), which in turn aborts the + handler's signal, which breaks the pump and finally calls `iterator.return`. + The `finalised` gate guarantees `return()` is invoked at most once. +- **No back-pressure.** `sendFrame` (`operation-dispatch.ts:176-182`) is + `ws.send(JSON.stringify(frame))`; it does not await the socket's internal + back-pressure, nor does the pump drop frames if the socket buffer fills. + Image frames at high frame rate will queue in the Node send buffer. A + Browser Use implementer must add back-pressure policy — either at the + handler level (skip frames if `ws.bufferedAmount` exceeds a threshold), + or by throttling at the source, or by switching to binary WebSocket + frames with their own size governance. +- **Acknowledge-frame opt-in.** `entry.operation.acknowledgesStream` controls + whether a `{ stream: true }` response is sent before pumping. The terminal + stream keeps the original wire shape (no ack); only `watchEvents` opts in + (`operation-contract.ts:33-40`). Image streams probably want the same + shape as the terminal stream — no ack — but the implementer must decide. +- **Frame size.** Each event becomes one JSON frame + (`{protocolVersion, kind:"event", requestId, body}`). At the default + `WEBUI_MAX_MESSAGE_BYTES = 256 * 1024`, a base64-encoded JPEG or PNG image + will routinely exceed the limit; this is the reason an image-frame + transport needs a separate channel or a different encoding contract. + +### Operation registry wiring (`operation/operations.ts`) + +`createOperationRegistry(port, terminal?)` (`operations.ts:69-230`) builds the +Map of registered operations. The pattern a Browser Use implementer must +follow for any new dedicated handler: + +- Terminal handlers are conditionally registered at + `operations.ts:135-142` only when the `terminal` argument is supplied. + The `unknown_operation` fallback in the dispatcher means an absent + `terminal` does not crash; it returns an error frame. The same shape + applies to a `browserExtension` adapter argument: present it conditionally, + register six `browser*` operations when present, fail closed when absent. +- The handler maps are produced by `createXxxHandlerEntries(port)` factories + (`operations.ts:74-82`). Operations are explicitly enumerated in a fixed + order at `operations.ts:88-228` so the wire order is stable. Browser Use + operations must be added in this list. + +### Per-connection lifecycle — `websocket-server.ts` + +- Connections are tracked in `this.connections: Set` + (`websocket-server.ts:30`). +- Heartbeat: 15s default (`service.ts:92`, + `WEBUI_WEBSOCKET_HEARTBEAT_INTERVAL_MS = 15_000`). The heartbeat `unref`s + its timer (`websocket-server.ts:99`) so it does not block process exit. +- `closeConnections()` (`websocket-server.ts:129-144`) terminates every + connection on shutdown before `server.close()` runs, aborting each + per-connection signal so in-flight streams finalize. + +--- + +## 7. `packages/webui/src/runtime/port.ts` — the runtime capability port + +`WebuiHarnessPort` (`port.ts:132-565`) is a flat interface with ~80 methods, +each one matching a `WebuiHarnessPort` member on the harness port that +`createHarnessPortFromHost` projects from the runtime host. Capability +groupings: + +- Session CRUD, history, turns, diff, rewind, edit + (`listSessions`, `createSession`, `forkSession`, `rewindSession`, …). +- Messages and turns (`getMessages`, `sendMessage`, `resumeSession`, + `watchEvents`). +- Permissions/questionnaire (`listPendingPermissions`, + `replyPermission`, `replyQuestionnaire`, `dismissQuestionnaire`). +- Queue, abort, models, skills. +- Workspace (`listWorkspaceFileTree`, `readWorkspaceFile`, + `getWorkspaceEnvironment`, `mutateWorkspaceGit`, workspace review, + `readCanvas`/`applyCanvas`, `readWorkspaceArchive`, + `extractWorkspaceArchive`). +- Plugin management, permission mode (`pluginManagement`, `getPermissionMode`, + `setPermissionMode`). +- Personalization (`getGlobalInstructions`, `setGlobalInstructions`, + `getAgentMemory`, `setAgentMemory`, `getUserProfile`, `setUserProfile`, + `getMemorySettings`, `setMemorySettings`). +- Provider configuration (`listUserModelProviders`, …, + `revealModelProviderApiKey`, `startCodexOAuthLogin`, `cancelCodexOAuthLogin`, + `refreshModels`, `runCommand`). +- Account/quota/billing (`getSigninPanel`, `claimSignin`, + `beginAccountLogin`, `getAccountLoginStatus`, `cancelAccountLogin`, + `signOutAccount`, `getUsageQuota`, `getCreditFallback`, + `setCreditFallback`, `listResetCards`, `useResetCard`, + `queryResetCardFlows`, `getAccountUsageSummary`, `getAccountProfile`, + `getAccountStatus`). +- `requestCompaction`, `invalidateAuth`, `readHostPrerequisites`, + `pluginManagement`, `runCommand`, `close`. + +**No browser capability exists.** A grep over `port.ts` finds the string +"browser" only inside a comment at `port.ts:128` ("the browser needs a stable +wire shape…"). There is no `listBrowserTargets`, no `selectBrowserTarget`, +no `forwardBrowserInput`, no `watchBrowserStream`, no +`getBrowserCapabilities`. Any browser capability the WebUI needs must be +added to this interface and to `createHarnessPortFromHost` so it ends up on +the port the service binds to. + +The closest analogue is `WebuiTerminalProcessPort` (`port.ts:579-606`), +which is a separate narrow port the WebUI service receives alongside the +harness port. The terminal operations are registered conditionally based on +its presence (`operations.ts:135-142`). A browser capability port can follow +the same pattern: a separate `WebuiBrowserProcessPort` (or a richer port if +the design requires more than the terminal exposes) handed into +`createOperationRegistry`, with `createBrowserHandlerEntries(port)` building +the handlers. + +The plan's WP1 will need to add browser DTOs to `shared/contracts/` (which +is the right layer — see §1 of the plan's "Architecture and ownership +constraints"). The runtime capability has to land in `port.ts` and be +projected from the host by `harness/adapter.ts`. + +--- + +## 8. Existing `browser` references in `src/runtime` and `src/server` + +Each match below is read and characterized. + +### `packages/webui/src/runtime/` + +| File | Lines | What it does | +| --- | --- | --- | +| `runtime/browser-provider.ts` | 1-58 | Full file — the adoption seam, §1. | +| `runtime/lifecycle.ts` | 8-22, 70, 91 | The browser provider is one of three close owners; close order is auth → host → mcode-tools → browser. | +| `runtime/assembly.ts` | 12, 113-128, 154-155, 186-198, 260-269, 327, 333-336 | Re-exports adoption types, plumbs `browserAdapter`/`browserToolExposure` into forwarded options and `beta.browserUseTooling` into the runtime config getter. | +| `runtime/index.ts` | 18-20 | Re-exports `WebuiBrowserAdapter`, `WebuiBrowserProvider`, `WebuiBrowserToolExposure` so the composition roots can name them. | +| `runtime/terminal.ts` | — | No browser references. | +| `runtime/auth-session.ts` | 14, 68 | Comments only ("browser session", "in the browser"). | +| `runtime/account-login.ts` | 8, 11 | Comments only ("without leaving the browser", "browser needs"). | +| `runtime/check-in.ts` | 35, 99 | Comments + a `browser_name: "mcode"` field in a telemetry payload (`check-in.ts:99`). Not related to Browser Use. | +| `runtime/usage-quota.ts` | 864 | `browser_name: "mcode"` payload field — same telemetry context. | +| `runtime/harness/host-contract.ts` | 615 | Comment about a "login completed in the browser". | +| `runtime/harness/result-conversion.ts` | 9, 86 | Comments only. | +| `runtime/profile-files.ts` | 46 | Comment: "a browser-side program has no node globals." | +| `runtime/runtime-environment.ts` | 17 | Comment: "The runtime's send path is fine; the runtime it hosts was [crashing] in browser builds." | +| `runtime/workspace-archive.ts` | 114 | Comment about "a browser … chosen by the view". | +| `runtime/workspace-archive/archive-filesystem.ts` | 31 | Comment: "The relative path arrives from the browser". | +| `runtime/workspace-archive/archive-policy.ts` | 69 | Comment: "Mirrored rather than imported because that module is browser-side and this is [Node-side]". | + +The string `browser` in `src/runtime` therefore refers to *Browser Use* only +in three files: `browser-provider.ts`, `lifecycle.ts`, and `assembly.ts`. +Every other match is incidental commentary about the Web browser client or +about Node-vs-browser globals. + +### `packages/webui/src/server/` + +| File | Lines | What it does | +| --- | --- | --- | +| `server/service.ts` | 16 | "This file holds no OAuth, broker or browser provider." — comment is true; no actual browser logic. | +| `server/service.ts` | 147 | "plain browser without scraping a per-start token" — describes the `dev` mode. | +| `server/credentials.ts` | 3, 6 | "a neighbouring browser on the same loopback interface" — refers to the Web browser client, not Browser Use. | +| `server/operation/operation-dispatch.ts` | 20 | "the runtime's stable code was discarded on its way to the browser" — Web browser client. | +| `server/operation/binding-mechanism.ts` | 308 | "the browser as 'this host cannot answer this operation'" — Web browser client. | +| `server/operation/workspace.ts` | 282 | "a browser cannot name an absolute path" — Web browser client (input validation). | +| `server/operation/common.ts` | 23 | "A browser cannot produce an absolute path" — Web browser client. | +| `server/operation/handlers/workspace.ts` | 4 | Same theme. | +| `server/operation/session.ts` | 239 | "The browser project picker cannot" — Web browser client. | +| `server/http/session-transfer.ts` | 55 | "keeps it out of the browser's heap" — Web browser client. | +| `server/http/workspace-file.ts` | 16 | "Media preview and HTML preview both need a URL the browser streams" — Web browser client. | +| `server/projections/model-provider.ts` | 12, 151 | "payload the browser believes" — Web browser client. | +| `server/projections/index.ts` | 7 | "browser builds the permission and questionnaire" — Web browser client. | + +**There is no Browser Use code in `src/server`.** Every match concerns the +Web browser client. The runtime host's browser provider surfaces in the +server only as the harness's optional `browserAdapter` and +`browserToolExposure`, which are not currently consumed by any operation +handler. + +--- + +## Plan claims verified against source + +| Plan claim | Source | Holds? | +| --- | --- | --- | +| "the current runtime derives browser-tool availability from provider adoption" | `browser-provider.ts:56` (`browserUseTooling: provider !== undefined`); `assembly.ts:316-330` reads it into `beta.browserUseTooling` | Yes. | +| "`browserToolExposure` describes tool-schema exposure" | `browser-provider.ts:25-27` (`WebuiBrowserToolExposure = NonNullable`); the two are forwarded independently | Yes. | +| "Neither is, by itself, an independent authorization check for the three browser surfaces or the Full CDP capability" | No boolean input on `WebuiBrowserProviderBinding` distinguishes the three surfaces or Full CDP | Yes. | +| "Both currently run with `dev: true`" | `run-webui-server.mjs:57` and `mcode-webui.mjs:37` both pass `dev: true` | Yes. | +| "Neither `scripts/run-webui-server.mjs` nor the packaged production entry `release/webui-npm/bin/mcode-webui.mjs` supplies a browser provider" | Both call `createWebuiRuntimeHost({ dataDir, appVersion })` without `browserProvider` | Yes. | +| "`packages/webui/src/server/access-policy.ts` admits loopback HTTP(S) origins and the WebUI's per-start token. It rejects extension origins" | `isAllowedOrigin` (lines 119-135) accepts only `http:`/`https:`; `chrome-extension://` fails the protocol gate | Yes (the protocol gate is the rejection mechanism, not the hostname gate). | +| "`src/server/credentials.ts` creates that token per service start, so it cannot meet persistent browser pairing by itself" | `createWebuiCredential` (lines 19-21) uses `randomUUID()`; constructed once in `WebuiService` (`service.ts:207`) | Yes. | +| "The WebSocket operation path supports cancellable streams" | Per-connection `AbortController` (`websocket-server.ts:101-108`), `iterator.return?.()` idempotent gate (`operation-dispatch.ts:124-129`) | Yes. | +| "`watchTerminal` and `src/server/operation/handlers/terminal.ts` provide a lifecycle pattern for a stream, but image frames may require a purpose-built transport shape and back-pressure policy" | `terminal.ts:78-89` pattern; `sendFrame` (`operation-dispatch.ts:176-182`) does not await socket back-pressure; `WEBUI_MAX_MESSAGE_BYTES = 256 * 1024` (`service.ts:46`) | Yes — and the binary frame ban at `frame-handler.ts:30-40` confirms a different transport shape is required for image data. | + +## Gaps the plan must create (not extend) + +- **Per-browser authorization boolean.** The `WebuiBrowserProviderBinding` does + not have one. The plan must add a slot for "which surfaces are authorized" + on the assembly options and on the binding. +- **Full CDP gate.** Likewise absent. The plan must add a slot on the binding + (or a sibling option) that gates full-CDP commands independently of provider + presence and per-surface authorization. +- **Extension identity + pairing credential lifecycle.** No `WebuiCredential`-like + object for the extension bridge exists. No OS-credential-store adapter exists + on the WebUI side. The implementer must create both. +- **Extension admission path.** The current `WebuiWebSocketServer.handleUpgrade` + dispatches every upgrade through the single `admitWebSocketUpgrade` function + with the `dev`-mode bypass on the token check (`access-policy.ts:76`). A + dedicated extension admission that ignores `dev` and validates pairing + credentials + browser identity + protocol version + per-browser switch state + does not exist. +- **Image transport.** No binary frame support; `WEBUI_MAX_MESSAGE_BYTES = + 256 * 1024`; `sendFrame` does not await back-pressure. A Browser Use + implementer must design a transport — either a separate extension WebSocket + carrying binary frames, or a new envelope shape on the existing channel — + with explicit back-pressure policy. +- **Capabilities port member.** `WebuiHarnessPort` has no browser methods. The + plan must add methods or a parallel port and the + `createHarnessPortFromHost` projection that materializes them. + +--- + +## Open questions / risks + +- **WebSocket message-size cap is per-frame and per-direction.** The WS + library's `maxPayload` (`websocket-server.ts:36-39`) caps inbound frames; + the WebUI does not have an explicit outbound frame cap, but `ws.send` will + queue until the OS socket buffer is full. For 30 fps JPEG at 1280×720 this + is a real bottleneck; the implementer must measure before settling on a + transport. +- **No extension storage adapter is visible in `packages/webui/src/server/` + or `src/runtime/`.** The plan assumes OS secure credential storage; the + current code has no dependency or interface for it. A WP0 task must add + this and verify availability on every supported OS. +- **`isLoopbackBindAddress` accepts `[::1]` but `admitWebSocketUpgrade` only + accepts `127.0.0.1` / `localhost` as the URL host.** If a packaged entry + binds `[::1]`, a client using `ws://localhost:...` will still connect, + but a client using `ws://[::1]:...` will fail at `access-policy.ts:64`. + Today both composition roots use `127.0.0.1`, so this is moot, but a + Browser Use implementer should not change the bind host without revisiting + this. +- **Per-connection abort signal is shared across all in-flight operations.** + A `sendMessage` stream and a `watchTerminal` stream on the same socket + both bind to the same `connectionSignals.get(ws)?.signal`. A disconnect + cancels both. For a separate extension socket, the same pattern is + desirable — one connection, one abort, all per-extension streams + finalize together. +- **The runtime `beta.browserUseTooling` is set from the binding's + `browserUseTooling` flag (`assembly.ts:327`).** A future implementation + must not silently widen this to `true` when the user has not authorized + any browser surface. The plan's WP0 must address this. diff --git a/.plan/browser-use/board/notes-02-browser-core-client.md b/.plan/browser-use/board/notes-02-browser-core-client.md new file mode 100644 index 000000000..16b6850ab --- /dev/null +++ b/.plan/browser-use/board/notes-02-browser-core-client.md @@ -0,0 +1,619 @@ +**Reference material for the WebUI Browser Use programme — not a work item.** + +Captured before implementation, from the source at the current checkout. Path in the workspace: `.plan/browser-use/exploration/02-browser-core-client.md` (gitignored, outside the published source projection). + +--- + +# Implementation context: browser-core contracts & WebUI client conventions + +Scope: implementation-context notes for "WebUI Browser Use". Read-only survey; nothing was +implemented or modified. Every claim below carries a `file:line`. Items I could not confirm from +source are marked **not verified — needs check**. + +--- + +## 0. Plan claims vs. the current code + +| Plan claim (`.plan/browser-use/browser-use-development-plan.md` §3) | Verdict | +| --- | --- | +| `packages/browser-core` owns provider-neutral browser actions and transport contracts | **Confirmed** — `packages/browser-core/src/browser-transport.ts:58`, `browser-core.ts:124` | +| WebUI must not import `packages/tui`; terminal-client implementations are references only | **Confirmed as a documented rule** — `packages/webui/docs/architecture/boundaries.md` ("Never import `packages/tui` (ADR 0003)"). See §5.4 for the caveat that it is not enforced by a literal denylist. | +| `packages/webui/src/runtime/browser-provider.ts` defines a host-supplied provider adoption seam | **Confirmed** — `runtime/browser-provider.ts:47` `adoptWebuiBrowserProvider` | +| `WorkspaceHtmlPreview.tsx` previews local HTML in an iframe and must not be repurposed | **Confirmed** — `features/workspace/ui/WorkspaceHtmlPreview.tsx:1-30` | +| browser-core is importable by WebUI | **Not true today** — see §5.3. `packages/browser-core` is in `release/extraction.json` `packageRoots`, but `packages/webui/package.json` does **not** declare it and `browser-core` has a single `"."` export that resolves types from `./src/index.ts` but runtime code to `./dist/index.js`. | + +--- + +## 1. What a provider must implement to serve browser-core + +### 1.1 The one transport contract + +`BrowserTransport` (`packages/browser-core/src/browser-transport.ts:58`) extends +`BrowserPageLifecycle` (`:36`). A provider supplies **one object per page**. + +Required members (verbatim signatures): + +```ts +// browser-transport.ts:36-49 +export interface BrowserPageLifecycle { + readonly sessionId: string; + start(signal?: AbortSignal): Promise; + /** + * Reset the provider connection/session without closing the page. + * ... Providers that do not need a reconnect may implement it as a no-op. `close()` is + * reserved for permanently destroying the page and its provider resources. + */ + resetConnection?(): Promise; + stopLoading(): Promise; + close(): Promise; +} + +// browser-transport.ts:58-100 (abridged to the members that matter) +export interface BrowserTransport extends BrowserPageLifecycle { + readonly downloadDirectory?: string; + openTab?(url: string): Promise; + send( + method: string, + params?: BrowserTransportCommandParams, + sessionId?: string, + options?: BrowserTransportCommandOptions, + ): Promise; + evaluate(expression: string, options?: BrowserEvaluateOptions): Promise; + evaluateInAllFrames?(expression: string, options?: BrowserEvaluateOptions): Promise; + listAttachedFrames?(): readonly BrowserAttachedFrame[]; + onEvent(listener: BrowserTransportEventListener): () => void; + waitForEvent(method: string, timeoutMs?: number, + options?: Pick): Promise; +} +``` + +Optional: `resetConnection` (`:46`), `openTab` (`:65`), `evaluateInAllFrames` (`:88`), +`listAttachedFrames` (`:93`), `downloadDirectory` (`:60`). +Mandatory: `sessionId`, `start`, `stopLoading`, `close`, `send`, `evaluate`, `onEvent`, `waitForEvent`. + +`CdpTransport` (`:103`) is a backward-compatible alias of `BrowserTransport`. + +### 1.2 Narrower view for helpers + +`BrowserPageTransport` (`browser-page-transport.ts:17`) is a `Pick` over +`send | evaluate | evaluateInAllFrames | listAttachedFrames | onEvent | stopLoading | close`. Its +header comment states it exists "to prevent a second transport contract from drifting while +preserving the narrow dependency used by Electron". **A new provider must implement +`BrowserTransport`, not `BrowserPageTransport`.** + +### 1.3 Addressing a page / target + +- **Page** = one `BrowserTransport` instance. Identity is `sessionId` (`browser-transport.ts:37`). +- **Child targets / OOPIFs** are addressed by an explicit `sessionId` argument on `send(...)` + (`:77`) and by `BrowserAttachedFrame` (`:26-34`), which carries `sessionId`, `frameId`, + optional `parentSessionId`, `url`. +- `BrowserCoreSession` tracks the child session in `state.commandSessionId` + (`browser-state.ts:121`); `BrowserCoreResolvedTarget` (`browser-core.ts:61-67`) is the + snapshot-scoped identity an adapter gets back for an opaque ref: + `{ element, snapshotId, generation, frameId, frameRef }`. + +### 1.4 Command, image, input and cancellation flow + +- **Commands**: `send(method, params, sessionId, options)`; params are + `BrowserTransportCommandParams = Record` (`browser-transport-types.ts:2`). + Browser Core's own shorthand is `cdp(session, method, params)` (`browser-state.ts:325`) and + `evaluate(session, expression)` (`browser-state.ts:353`). +- **Mandatory provider error codes** (`browser-transport.ts:66-73`): providers must reject caller + cancellation with `code: "ABORTED"` and a deadline with `code: "BROWSER_OPERATION_TIMEOUT"`, so + Browser Core can reset a connection whose late command may still reach Chromium. The code set is + `BrowserOperationInterruptionCode = 'ABORTED' | 'BROWSER_OPERATION_TIMEOUT' | + 'BROWSER_TRANSPORT_CLOSED'` (`operation-timeout.ts`), built by + `browserTransportInterruptionError()` and classified by `isBrowserOperationInterruption()`. + `browserOperationFailure()` renders the public `{ success: false, code, error, label?, timeoutMs? }`. +- **Cancellation** flows: `BrowserCoreSession.execute(action, input, signal)` + (`browser-core.ts:258`) creates one `AbortController`, forwards the caller's abort, sets a timer, + and stores the signal as `state.commandSignal`. On interruption it calls + `transport.stopLoading()`, runs `cdpHelper.recoverTransportAfterInterruption(...)`, then + `invalidate(...)` and sets `poisoned` when recovery/settlement fails (`:336-353`). A poisoned + session reports `requiresRecreation` (`:205`) and every later action throws + `BROWSER_SESSION_POISONED`. +- **Screenshots**: `Page.captureScreenshot` in `browser-action-artifacts.ts:65,75`; providers may + override via the optional seam `BrowserScreenshotCapture` + (`browser-state.ts:97-100`, wired at `browser-core.ts:57` / `:150`). +- **Input**: `Input.dispatchMouseEvent` (`cdp-helper.ts:1302,1309,1335,1401`), + `Input.dispatchKeyEvent`, `Input.insertText`, `Input.dispatchDragEvent` + (`cdp-helper.ts:1362-1377`), `Input.setInterceptDrags` (`:1330`). +- **Deadlines** (`operation-timeout.ts`): `cdpCommand: 30_000`, `cdpScan: 30_000`, + `tabOperation: 60_000`, `longTabOperation: 125_000`, `ipcHandler: 275_000`, nested + "inner settles before outer". + +### 1.5 Connection state and reconnect + +- The **only** reconnect seam is the optional `resetConnection()` (`browser-transport.ts:39-46`). + Browser Core calls it through `CDPHelper` (`cdp-helper.ts:217`, `:269`); `cdp-helper.ts:224` + comments that a provider "must implement resetConnection so timeout recovery is page-safe". +- Reference implementation: `packages/tui/src/runtime/browser/headless-chrome-transport.ts:604` + (`resetConnection`), forwarded by `headless-chrome-provider.ts:648`. +- Session poison/reset: `browser-core.ts:205` `requiresRecreation`, `:264-268` + `BROWSER_SESSION_POISONED`, `:300-353` recovery + settlement. +- `synchronizeGeneration(n)` (`browser-core.ts:251`) lets a host invalidate refs after navigation it + observed outside Browser Core. + +--- + +## 2. The CDP commands browser-core issues today (the restricted set) + +Distinct method names, by domain, with the issuing file. This is the baseline a Full CDP gate +extends. + +**Page** — `Page.navigate` (`browser-actions.ts:203`), `Page.navigateToHistoryEntry` (`:245`), +`Page.reload` (`:276`), `Page.getFrameTree` (`:348`), `Page.getNavigationHistory` (`:359`), +`Page.getLayoutMetrics` (`cdp-page-scanner.ts:453`), `Page.captureScreenshot` +(`browser-action-artifacts.ts:65,75`), `Page.enable` (`cdp-page-scanner.ts:188`, +`cdp-helper.ts:441`), `Page.setInterceptFileChooserDialog`, `Page.stopLoading` +(`browser-snapshot.ts:650`), `Page.fileChooserOpened` (event, `browser-action-artifacts.ts:135`), +`Page.downloadWillBegin` / `Page.downloadProgress` (events, `browser-action-artifacts.ts:135,163`). + +**DOM** — `DOM.getDocument` (`cdp-page-scanner.ts:101,215`; `browser-action-targets.ts:173`), +`DOM.querySelector` (`:179`), `DOM.querySelectorAll` (`cdp-page-scanner.ts:118`), `DOM.describeNode` +(`:127`; `browser-action-targets.ts:186`), `DOM.resolveNode` (`browser-actions.ts:1043`), +`DOM.getBoxModel` (`cdp-page-scanner.ts:465`; `browser-action-targets.ts:276`), +`DOM.getContentQuads` (`cdp-page-scanner.ts:603`), `DOM.getFrameOwner` (`:451`), +`DOM.getAttributes`, `DOM.getOuterHTML`, `DOM.pushNodesByBackendIdsToFrontend`, `DOM.enable` +(`:167`; `cdp-helper.ts:490`), `DOM.scrollIntoViewIfNeeded` (`browser-action-targets.ts:73`), +`DOM.focus` (`browser-actions.ts:826,1117`; `browser-extended-actions.ts:333`), +`DOM.setFileInputFiles` (`browser-extended-actions.ts:314`), `DOM.setAttributeValue`; +DOM mutation events consumed at `cdp-helper.ts:528-532` (`DOM.childNodeInserted`, +`DOM.childNodeRemoved`, `DOM.attributeModified`, `DOM.attributeRemoved`, +`DOM.characterDataModified`, `DOM.documentUpdated`). + +**Runtime** — `Runtime.evaluate` (`cdp-page-scanner.ts:638`; `browser-snapshot.ts:594`), +`Runtime.callFunctionOn` (`browser-actions.ts:1052`; `browser-extended-actions.ts:382`), +`Runtime.getProperties` (`cdp-page-scanner.ts:756`), `Runtime.releaseObject` +(`browser-actions.ts:1091`; `browser-extended-actions.ts:394`; `cdp-helper.ts:1009`), +`Runtime.enable` (`cdp-helper.ts:380,513`); events `Runtime.executionContextCreated/Destroyed/ +Cleared`, `Runtime.exceptionThrown`, `Runtime.exceptionRevoked`, `Runtime.consoleAPICalled`. + +**Input** — `Input.dispatchMouseEvent`, `Input.dispatchKeyEvent`, `Input.insertText`, +`Input.dispatchDragEvent`, `Input.setInterceptDrags`, and the `Input.dragIntercepted` event. + +**Accessibility** — `Accessibility.enable` (`cdp-page-scanner.ts:181`), +`Accessibility.getFullAXTree` (`:227`). + +**DOMSnapshot** — `DOMSnapshot.enable` (`cdp-page-scanner.ts:173`), `DOMSnapshot.captureSnapshot` +(`cdp-page-scanner.ts:206`). + +**Network** — `Network.enable` (`cdp-helper.ts:380,499`); events `Network.requestWillBeSent`, +`Network.responseReceived`, `Network.loadingFinished`, `Network.loadingFailed` (used by the console +collector, `cdp-helper.ts:499-513`). **No Network domain command is issued for interception — +`Fetch.*` is never used.** Verified: `grep -rn "Fetch\." packages/browser-core/src` → **no hits**. + +**Target** — browser-core *consumes* `Target.detachedFromTarget` (`browser-core.ts:190`) but issues +no `Target.*` command; target attach/flattening is the provider's job. Verified: `grep -rn +"attachToTarget\|setAutoAttach" packages/browser-core/src` → **no hits**. + +**Browser** — `Browser.downloadWillBegin` / `Browser.downloadProgress` as **events** only +(`browser-action-artifacts.ts:163`). + +Summary: browser-core's ordinary Browser Use path needs Page, DOM, Runtime, Input, Accessibility, +DOMSnapshot, plus Network/Target **events**. It does **not** issue `Fetch.*`, `Network.setRequest* +`, `Storage.*`, `Emulation.*`, `CSS.*`, `Security.*`, `Performance.*`, `ServiceWorker.*` or any +`Target.*` command. That list is the restricted set a Full CDP gate must widen. + +--- + +## 3. Element references, staleness, and "target generation" + +### 3.1 Refs are opaque, snapshot-scoped, and generation-fenced + +`ElementRecord` (`browser-state.ts:37-58`) carries `ref`, `nodeId`, `backendNodeId`, `tag`, `role`, +`type`, `selector?`, `text`, `rect`, `coordinateSpace`, `isInteractable?`, `frameId?`, +`commandSessionId?`, `frameOffset?`, `parentRegionPath?`, `virtualEditor?`, `attributes`. + +`Snapshot` (`browser-state.ts:60-87`) carries `id`, **`generation`**, `createdAt`, +`lastAccessedAt?`, `continuationKind?`, `elements`, `semanticSource?`, `semanticTree`, `page`, +`durationMs?`, `nextOffset`, `continuationInvalidated?`, `frameRefs?`, `editableQuery?`. + +**A "target generation" concept already exists**, but it is **page/session-scoped, not tab-scoped**: + +- `BrowserSessionState.generation: number` (`browser-state.ts:106`), initialised to `0` + (`browser-core.ts:145`), exposed by `get generation()` (`browser-core.ts:200`). +- Invalidation is one method, `BrowserActionTargetSupport.invalidate` + (`browser-action-targets.ts:332-338`): + ```ts + protected invalidate(session: SessionState): void { + session.generation += 1; + session.frameEpochs?.clear(); + session.elementMap.clearOpaqueRefs(); + clearBrowserSnapshots(session); + session.keyboardContinuationTarget = undefined; + } + ``` +- **What makes a ref stale**: `getBrowserSnapshot` returns `undefined` when + `snapshot.generation !== session.generation` **or** `continuationInvalidated === true` + (`browser-state.ts:176-181`); `resolveBrowserSnapshotElement` skips snapshots whose generation + differs (`browser-state.ts:193`); pruning uses the same test plus a 5-minute TTL + (`browser-state.ts:145`, `:306`), retaining at most `MAX_RETAINED_BROWSER_SNAPSHOTS = 3` + (`browser-state.ts:144`). +- Ref authority: `BrowserCoreSession.resolveOpaqueRef` (`browser-core.ts:210-219`) consults the + snapshot store first; the `ElementMapManager` opaque-ref map is a **fixture-only fallback**, + disabled once `snapshotAuthorityEstablished === true`. + +### 3.2 What triggers invalidation today + +`BrowserCoreSession`'s event listener (`browser-core.ts:162-193`): + +| Event | Scope | Effect | +| --- | --- | --- | +| `Page.frameNavigated` | child frame | `invalidateBrowserFrame` | +| `Page.frameNavigated` | main | full `invalidate` | +| `Page.navigatedWithinDocument` | child / main | same split | +| `Page.frameDetached` | main frame id | full `invalidate`; else frame-scoped | +| `Target.detachedFromTarget` | by `params.sessionId` | frame-scoped | + +Plus post-interruption `invalidate` (`browser-core.ts:352`) and host-driven +`synchronizeGeneration()` (`:251`). + +### 3.3 Frame-scoped staleness (the OOPIF case) + +`BrowserFrameIdentity = { frameId?, commandSessionId? }` (`browser-state.ts:139-142`). +`invalidateBrowserFrame(session, identity)` (`browser-state.ts:224-254`) bumps per-frame epochs in +`session.frameEpochs`, removes matching elements from every retained snapshot, sets +`continuationInvalidated = true`, deletes the frame from `frameRefs`, filters `editableQuery`, and +clears a matching `keyboardContinuationTarget`. + +**Gap for the feature**: there is no notion of *tab*/browser-target generation. `generation` is a +per-`BrowserCoreSession` page counter, and a session is per page. Selecting a different tab, closing +a tab, or reconnecting a provider produces a *different session object*, not a bumped generation on +one. A WebUI-owned target-generation layer (WP1) is therefore **must be created**, and it must map +its own target generation onto `BrowserCoreSession` instances. + +### 3.4 The legacy ElementMap cache (not the production ref path) + +`ElementMapManager` (`element-map-manager.ts:113`) is an LRU cache keyed by `host + pathname` +(`getUrlPath`, `:164`) with `maxCacheSize` default 5 (`:142`), optionally persisted to +`element-map--.json` (`:178-186`). It is **not** how refs are validated in production — +§3.1 applies. `update()` (`:194`) writes the entry; `getElementById`/`getElementBySelector` +(`:322`, `:330`) look up by index/selector. Do not treat this cache as the staleness mechanism. + +--- + +## 4. browser-core package entry point + +- Package: `@mavis/browser-core`, private, ESM. **Single subpath `"."`** only + (`packages/browser-core/package.json`), `types: "./src/index.ts"`, runtime `./dist/index.js`. + There is no `./contracts` or `./transport` subpath; an implementer imports the root. +- `packages/browser-core/src/index.ts` re-exports the provider-neutral surface: + `BrowserTransport`, `BrowserAttachedFrame`, `BrowserPageLifecycle`, `CdpTransport`, + `CoreBrowserEvaluateOptions`, `CoreBrowserTransportEvent(EventListener)` (`index.ts:72-81`); + `BrowserPageTransport` + non-prefixed `BrowserEvaluateOptions` (`:1-6`); `BrowserTransportCommandParams` + (`:71`); contracts `BrowserDOMNode/BrowserAXNode/BrowserSnapshotNode/BrowserInteractiveElement/ + BrowserWaitInput/BrowserWaitState` (`:20-31`); `BrowserCore`, `BrowserCoreSession`, + `BrowserCoreResolvedTarget` via `export * from './browser-core.js'` (`:36`); `ElementMapManager` + (`:47`); `cdp`, `evaluate`, `ElementRecord`, `BrowserSessionState`, `PageSummary`, `Snapshot` + (`:49-70`); `BROWSER_TIMEOUTS`, interruption helpers (`:48`). +- Note the duplicated aliases: `BrowserEvaluateOptions` and `BrowserTransportEvent` are exported + **both** prefixed and unprefixed (`index.ts:1-6` vs `:72-81`). Import the unprefixed + `CoreBrowser*`-free pair or the prefixed pair consistently — **not verified which the WebUI gate + will require; needs check**. +- `BrowserCoreSessionOptions` (`browser-core.ts:43-58`) is what a provider passes to + `createSession(transport, options)` (`:108-113`): `clipboardProvider?`, `cdpHelper?`, + `actionTimeoutMs?`, `commandTimeoutMs?`, `inputCoordinateScaleProvider?`, + `beforePointerAction?`, `clickNavigationDetectionMs?`, `captureScreenshot?`. + The action allow-list is per-provider: `BrowserCoreOptions.actions` asserted by `assertAction` + (`:92-96`). + +--- + +## 5. WebUI client conventions for a new business + +### 5.1 The five roles and what each may do + +From `docs/client/ownership.md` ("Businesses own resources; roles own kinds of code") and +`docs/architecture/module-map.md`: + +| Role | Owns | Must not | +| --- | --- | --- | +| `contracts/` | published surface: capability port slice, view models, commands | import implementation | +| `application/` | workflows, state owners, query coordination | touch browser globals, DOM, storage, concrete transport | +| `projection/` | pure derivations wire/owned-state → render shape | import React, perform IO | +| `bindings/` | React subscriptions, context, navigation, DOM effects | acquire write authority, construct owners | +| `ui/` | presentation | call IO ports, instantiate owners, reach a generic store writer | + +Not every business populates every role; empty directories are not created +(`module-map.md`, "Client — businesses"). A business with no screen has no `ui/`. + +**`projection/` classification** (`boundaries.md`): a pure derivation is `domain` if its basename is +in `PROJECTION_DOMAIN` in `scripts/lib/webui-dependency-rules.mjs` (a business rule), otherwise +`view` (reads/shapes only). **Adding a projection that carries a rule means adding its basename to +that list.** + +### 5.2 Worked example — the `goal` business (small; populates all five roles) + +`packages/webui/src/client/features/goal/` — exact directory list: + +``` +goal/contracts/goal-port.ts +goal/projection/goal-state.ts +goal/application/goal-coordinator.ts +goal/application/goal-session-owners.ts +goal/application/goal-submission.ts +goal/bindings/goal-owner.public.ts +goal/ui/GoalBanner.public.tsx +goal/ui/goal.css +``` + +Naming conventions observed: + +- **contracts** — `-port.ts`. One `WebuiPort` with every method **optional** + (`goal-port.ts:16-28`; the header says "`undefined` means 'the operation is not wired'") plus a + narrow `WebuiCommands` slice of the methods presentation may call + (`goal-port.ts:11-15`). Requests/responses come from `shared/contracts/.js`. +- **application** — `-coordinator.ts` holds *policy*; `-session-owners.ts` holds + *whose* policy it is and when it exists. `goal-coordinator.ts:22` defines + `WebuiGoalPortSlice = Pick`; `WebuiGoalSink` (`:34`) is the single injected store + write. Both files state "Framework-free: no React, no DOM." +- **projection** — `-state.ts`, pure, no React. +- **bindings** — hooks, `.public.ts`. +- **ui** — `Webui.public.tsx` + `.css`. + +### 5.3 Publishing a contract to other modules (the `.public.ts` marker) + +Rule (`boundaries.md`): "A feature may use another feature's public `contracts/`, or a module that +feature published as its surface: a `.public.ts` for a derivation or interface, a `.public.tsx` for a +component. Everything else in another feature is private, and there is no exception list." + +A published module is constrained: "every explicit export is public, no export-star, no foreign +re-export, and no foreign namespace, default or dynamic access" (`boundaries.md`). Enforced by +`classifyRole` / `ROLE_FORBIDDEN_TARGETS` in `scripts/lib/webui-dependency-rules.mjs` via +`pnpm check:webui-dependency`, with baseline `scripts/lib/webui-dependency-baseline.json`. + +Concrete example — `goal/bindings/goal-owner.public.ts` publishes +`webuiGoalRecordKey(sessionId)`, `useWebuiGoalOwner(sessionId): WebuiGoalCoordinator` and +`useWebuiGoalReaction(sessionId, listener)`. It reads the owner from the application +(`useWebuiApplicationOptional()` / `application?.goals`) and falls back to a per-mount registry so a +composer mounted without an application still works. A settings or workspace surface that needs the +goal owner imports **only** this `.public.ts` module. + +Other examples: `review/application/workspace-review-owner.public.ts`, +`models/bindings/use-provider-editor.public.ts`, `terminal/projection/terminal-size.public.ts`, +`workspace/projection/workspace-name.public.ts`, `settings/ui/SettingsModal.public.tsx`. + +### 5.4 Composition root and lifetime (ADR 0016/0017/0018 rules, per `docs/client/ownership.md`) + +- The composition root is `client/application/create-application.ts`. It constructs **dormant** + owners, then start installs routes, hydrates persisted state and starts ingress. It is the only + place that knows which capability is wired. +- `dispose()` is idempotent; a disposed instance cannot restart; a fresh lifecycle gets fresh + identity epochs. +- Exactly **one** process-event ingress per application instance: the single long-lived + `watchEvents` subscription (`client/platform/event-channel.ts`). +- Temporal ownership rules: interest leases; generation fences (one live stream lease per session); + incarnation + freshness (`client/application/identity-epochs.ts`, + `client/conversation/request-freshness.ts`); keyed resources vs selected slots. +- `WebuiTransport` (`client/contracts/transport.ts:31-45`) extends the thirteen business ports; it + "declares no method of its own" and every method is optional. +- `client/bindings/` (`application-context.tsx`, `use-query-state.tsx`, `browser-capabilities.tsx`, + `browser-effects.ts`, `navigation.ts`, `port-slices-context.tsx`, …) is the **only** React edge + to application owners. `use-query-state.tsx:1-10` states components "read its snapshot and submit + query commands, and they never receive a transport method or a store writer". + +### 5.5 The tui ban — nuance + +`boundaries.md` says "Never import `packages/tui` (ADR 0003). Read it as a specification, compose the +equivalent here." It is not implemented as a literal `packages/tui` denylist entry in +`webui-dependency-rules.mjs`; the load-bearing facts are structural: +`packages/webui/package.json` declares **no** dependency on a tui package, and +`scripts/lib/webui-dependency-rules.mjs` has no allow-list of tui subpaths, so an import would +simply be an unresolvable workspace dependency. `packages/browser-core` is the sanctioned read-only +reference (it is a separate workspace package, in `release/extraction.json` `packageRoots`). +**Not verified — needs check:** whether `check:webui-dependency` would emit a specific message for a +tui import or merely fail to resolve it. + +--- + +## 6. Settings controls + +### 6.1 Where they live + +- Settings business: `src/client/features/settings/` — `contracts/settings-port.ts`, + `contracts/memory-handoff.ts`, `projection/{settings-view,usage-report,usage-plan}.ts`, + `application/settings-workflows.ts`, `ui/*` (17 files), plus `ui/SettingsModal.public.tsx`. +- Skeleton primitives are shared, business-free: `client/ui/settings-layout.tsx` exports + `SettingsPage` (`:35`), `SettingsSection` (`:47`), `SettingsCard` (`:86`), `SettingsRow` (`:101`), + `SettingsDivider` (`:133`), `InfoHint` (`:153`). `client/ui/ToggleSwitch.tsx:14` exports the shared + switch. +- Tabs and their enabled/disabled state come from **one** derivation: + `features/settings/projection/settings-view.ts` — `SettingsTabKey` (`:11`), + `DESKTOP_SETTINGS_TABS` (`:15`), `resolveThemePreference` (`:20`), `filterSettingsTabs` (`:22`), + `SETTINGS_GROUPS` (`:24`). `ownership.md` notes `voice`, `shortcuts` and `connection` are declared + but disabled. + +### 6.2 How a control is written + +`ToggleSwitch` props (`client/ui/ToggleSwitch.tsx:3-12`): `Omit, +"aria-checked" | "aria-label" | "onChange" | "role" | "type"> & { checked, label, +onChange?: (checked: boolean) => void, testId?, … }`. It renders a `