Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
ab59020
docs(webui): the engine layer has six files, not five (webui-parity 107)
fengzhi09 Oct 1, 2026
1dc559a
test(webui): M2 capability-declaration snapshot vs the real host (eng…
fengzhi09 Oct 1, 2026
8c085fa
test(webui): point the capability snapshot at the engine layer's real…
fengzhi09 Oct 1, 2026
aa5ab47
fix(webui): stop the shell from carrying one session's state into ano…
fengzhi09 Oct 1, 2026
af01e0e
refactor(webui): the plugins and turn-diff routes take the host from …
fengzhi09 Oct 1, 2026
f1842ba
test(webui): make the run-mirror, first-turn-guard and mavis-usage su…
fengzhi09 Oct 1, 2026
726d286
refactor(webui): the plugins and turn-diff routes take the host from …
fengzhi09 Oct 1, 2026
a4fad96
test(webui): make the run-mirror, first-turn-guard and mavis-usage su…
fengzhi09 Oct 1, 2026
e7c0ce9
feat(webui): the five read endpoints ask the engine facade, not the t…
fengzhi09 Oct 1, 2026
e053ae7
feat(webui): the session-tree and export endpoints ask the engine fac…
fengzhi09 Oct 1, 2026
4fb8267
feat(webui): the usage endpoints ask the engine facade, and the deriv…
fengzhi09 Oct 1, 2026
88b9a48
fix(webui): rebase M3-B3 onto M3-B2, register B2's two tmp prefixes, …
fengzhi09 Oct 1, 2026
6bc24bd
feat(webui): the account, model and capability reads ask the engine f…
fengzhi09 Oct 2, 2026
eb2a429
feat(webui): #73 swaps the ACP wire table for the 14-key engine-capab…
fengzhi09 Oct 2, 2026
2baf051
fix(webui): stop two B4 comments describing behaviour the code no lon…
fengzhi09 Oct 2, 2026
edf2b1e
feat(webui): move the session write family behind the engine facade
fengzhi09 Oct 2, 2026
1506cc2
fix(webui): drop whitespace text nodes in markdown tables and dedupe …
fengzhi09 Oct 2, 2026
8cca235
fix(webui): sweep the non-flipping inverted text token off primary su…
fengzhi09 Oct 2, 2026
eecd8c0
feat(webui): move session switch behind the engine facade
fengzhi09 Oct 2, 2026
0cfd51f
Merge main into dev-lhl
fengzhi09 Oct 2, 2026
e4cf052
chore: allowlist the leak-tripwire fixture in model-reads tests
fengzhi09 Oct 2, 2026
3f5b8d2
test(webui): pin session-writes cleanup-orphans test to isolated paths
fengzhi09 Oct 2, 2026
e7df93d
chore: ignore gitleaks fingerprints of deliberate test fixtures
fengzhi09 Oct 2, 2026
62814ff
chore: make the gitleaks fixture allowlists path-only
fengzhi09 Oct 2, 2026
3074010
feat(webui): move interrupt and load endpoints behind the engine facade
fengzhi09 Oct 3, 2026
063a43a
fix(webui): take the plan's 5s abort force-kill bound by product call
fengzhi09 Oct 3, 2026
90cf85e
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
a9af820
docs(webui): add session-switch, interrupt and session-load to the ar…
fengzhi09 Oct 3, 2026
4d904c3
docs(webui): add the missing zh-CN section for the B5 write family
fengzhi09 Oct 3, 2026
fdc3ff2
fix(webui): make webui-only session delete return promptly instead of…
fengzhi09 Oct 3, 2026
dab453d
fix(webui): retire lossy streaming mirrors when the engine transcript…
fengzhi09 Oct 3, 2026
7138b5b
feat(webui): add the streaming-send capability gate and pure stream b…
fengzhi09 Oct 3, 2026
a2223f4
feat(webui): run send on the runtime transport behind the engine facade
fengzhi09 Oct 3, 2026
8b51fdd
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
964c0cf
feat(webui): answer set-mode and set-config-option with structured 50…
fengzhi09 Oct 3, 2026
bdde1eb
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
a112e45
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
245a101
docs(webui): add the streaming-send architecture section, bilingual
fengzhi09 Oct 3, 2026
d9e181d
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
679d0fe
docs(webui): add the streaming-send architecture section, bilingual
fengzhi09 Oct 3, 2026
a078ee6
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
591ccff
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
b529543
fix(local-runtime): make an abandoned migration lease recoverable at …
fengzhi09 Oct 3, 2026
a8e56dc
feat(webui): move model and permission writes behind the engine facade
fengzhi09 Oct 3, 2026
48199c5
Reset dev-lhl to the full local integration line (B9+B10+docs+P13+P14…
fengzhi09 Oct 3, 2026
5661bb9
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
4b5e8d2
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
9fdd8d1
fix(webui): surface truncated acp stderr in failure alerts
fengzhi09 Oct 3, 2026
5427f23
feat(webui): move the provider family behind the engine facade with s…
fengzhi09 Oct 3, 2026
afa995e
fix(webui): acknowledge in-flight messages explicitly instead of echo…
fengzhi09 Oct 3, 2026
6c30484
fix(webui): normalise the expected side of the provider cwd path asse…
fengzhi09 Oct 3, 2026
e68a8df
feat(webui): bridge thinkingEffort as the third config id and gate th…
fengzhi09 Oct 3, 2026
1473183
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
15a3f42
feat(webui): register the acp transport as the first engine capabilit…
fengzhi09 Oct 3, 2026
313286a
fix(webui): keep over-tall code blocks inside their scroll container
fengzhi09 Oct 3, 2026
506c0a9
feat(webui): replace the flat provider form with the desktop-style di…
fengzhi09 Oct 3, 2026
25da27a
docs(webui): document the provider dialog interaction, bilingual
fengzhi09 Oct 3, 2026
38befac
feat(webui): route session deletion through the engine deleteSession …
fengzhi09 Oct 3, 2026
06a0e8e
feat(webui): open the host services window for capability exposure ba…
fengzhi09 Oct 3, 2026
60e5361
feat(webui): register the exec transport in the engine capability reg…
fengzhi09 Oct 3, 2026
64914d7
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
382c10c
fix(webui): escape raw svg tags in markdown output instead of mountin…
fengzhi09 Oct 3, 2026
bc49315
fix(webui): consume the real stream-json events on the exec transport
fengzhi09 Oct 3, 2026
35f1e0c
feat(webui): unlock the session context menu actions backed by the en…
fengzhi09 Oct 3, 2026
2b3a9e4
test(webui): register the PB-1 real-host tmp prefix
fengzhi09 Oct 3, 2026
f9829b3
chore(release-tools): register the mcode-exec-stream- tmp prefix
fengzhi09 Oct 3, 2026
b5bad19
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
33629d7
feat(webui): wire the context-window usage switch to the composer rea…
fengzhi09 Oct 3, 2026
c10736d
feat(webui): unlock the project menu's reveal-in-folder (SB-6)
fengzhi09 Oct 3, 2026
92abe74
feat(webui): make the Shortcuts page state what the browser can do
fengzhi09 Oct 3, 2026
595c06d
feat(webui): plan card reads the account tier, honest cloud placeholders
fengzhi09 Oct 3, 2026
d087f28
feat(webui): wire the usage-and-models model source to the engine (SB-1)
fengzhi09 Oct 3, 2026
43d0b2a
dev-lhl: SB-3/6/2/7/1 + SB-5 + P19 + SB-4 (settings waves, delete-han…
fengzhi09 Oct 3, 2026
c96e7a2
fix(webui): re-read the account after a source switch, and stop the k…
fengzhi09 Oct 3, 2026
c290440
Merge main into dev-lhl
fengzhi09 Oct 3, 2026
2250209
dev-lhl: SB-10 (DOM harness) + PB-3 (worktree page) + docs stale-pare…
fengzhi09 Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 120 additions & 3 deletions docs/webui.md
Original file line number Diff line number Diff line change
Expand Up @@ -1196,7 +1196,8 @@ settings (`refs/ui/03-settings-usage-models.jpg`, `04-settings-general.jpg`).
Ten tabs in four groups. Every tab carries the reference's 18×18 stroke
glyph; the state column says what a user actually gets, and a control that
renders but cannot act is called **placeholder** — a designed outcome, not a
missing feature. Only Worktree is genuinely not implemented.
missing feature. None of the ten is a bare placeholder any more: the last
one, Worktree, reads the engine since PB-3.

| Group | Tab | State |
| --- | --- | --- |
Expand All @@ -1208,9 +1209,66 @@ missing feature. Only Worktree is genuinely not implemented.
| Management | Connection | implemented |
| Management | Account | implemented as a read — the section reads `GET /api/account` on mount and renders the account name, the current plan name, the quota overview (plan-quota state plus the 5-hour and weekly remaining figures) and the account status; sign-out stays disabled (no engine method acts on it) |
| Coding | Code review | implemented — 自定义审查准则 persists to `localStorage`; 审查方式 is a disabled single-option dropdown showing 子会话 |
| Coding | Worktree | **not implemented** — the tab is a one-line panel reading 「本地版暂不支持工作树管理」 |
| Coding | Worktree | implemented as a CLEANUP page since PB-3 — see **Worktree — what the page can and cannot do** |
| Archived | Archived tasks | the tab renders its empty state 「暂无已归档任务」; the list and its actions need an archived-session contract that does not exist |

**Worktree — what the page can and cannot do.** The Worktree tab is a
**cleanup** page, not a workspace manager. It lists the worktrees Git already
knows about for the current repository and removes the ones you select; the
desktop reference (`design-ref/screenshots/ref-23.jpg`) has no 「新建工作树」
button, and `ManagedWorktreeServicePort` declares no create either, so adding
one would be inventing a capability on both sides at once. Two endpoints:

| Endpoint | Engine call | Notes |
| --- | --- | --- |
| `GET /api/worktrees?workspace=<repo>` | `services.managedWorktrees.list` | `workspace` is optional; without it the request's own conversation workspace is used, and with neither source the endpoint is 400 rather than guessing the server's cwd |
| `POST /api/worktrees/remove` | `services.managedWorktrees.removeBatch` | body `{items: [{workspace, worktreeDir}], activeWorktreeDir?}` |

Both are gated by PB-8's three-state presence gate rather than by a capability
key, because no key covers `services.managedWorktrees`: no host → **503**
`engine_host_unavailable`, a host with no owner graph → **501**
`engine_services_unavailable`, an owner graph without the service → **501**
`worktree_service_unavailable`. None of them answers 200 with an empty list — a
page that says "nothing to clean up" for a runtime that failed to boot would
tell the user their project is clean when it was never read.

A folder that is not a Git repository is a **report**, not a failure: 200 with
`ok: false` and the engine's own `code` (`not_git_repository`,
`workspace_unavailable`, `worktree_list_failed`). The page names the code, so
the three mean three different operator actions instead of one 「读取失败」.

The three toolbar tabs (**近 3 天 / 3-7 天前 / 7 天以上**) filter on
`lastModifiedMs` and the boundaries are inclusive at the top of each band:
exactly 3 days old is still 近 3 天, exactly 7 days old is still 3-7 天前. They
filter rather than sort, because each tab is an age band in the desktop's mental
model, and sorting would make the older bands unreachable without scrolling.
A row whose timestamp the engine could not read (`lastModifiedMs` is genuinely
optional — the engine falls back from the directory mtime to the last reflog
entry) appears in **every** tab labelled 时间未知. Filing it at 0 would put a
worktree modified seconds ago under 7 天以上; hiding it would make a real
worktree invisible.

`removeBatch` verdicts pass through per item and are never collapsed: a batch
where every item was refused is `ok: true` with a full `failedItems` list, each
carrying its `WorktreeRemovalReason` (`main_worktree` / `active_worktree` /
`not_found` / `locked_worktree` / `dirty_worktree` / `unknown`). The page maps
each through one table and lists them under the toolbar. The main worktree, a
locked worktree and the worktree an active session runs in render their
checkbox **disabled with the matching reason beside it**, because a checkbox
that ticks and then fails on submit teaches the user the button lies. The
runtime-safety check the engine runs before any removal
(`listRunningWorktreeDirs`) is inside the service, so the UI cannot bypass it.

The `workspace` a browser names is containment-gated on both endpoints
(`assertWorkspacePath`, the same boundary as `/api/fs/*`); a batch is refused
as a whole when one of its repositories is out of root, because an out-of-root
repository is a forged request rather than a worktree that happened to fail.
`worktreeDir` is deliberately **not** gated separately: the engine only removes
a path that `git worktree list` reports as a linked worktree of that repository,
which is strictly stronger than a root check, and adding the weaker gate in
front would make legitimate out-of-root worktrees (a sibling checkout next to
the repo) unremovable.

**There is no Browser tab in Settings.** The browser surface is a workspace
column tab (`workspaceTabs.tab.browser`) that mounts `BrowserPanel` over the
workspace tabs, not a settings section; the `settings.tab.browser` dictionary
Expand Down Expand Up @@ -2529,7 +2587,8 @@ Invariants worth keeping when touching either branch:
snapshot, which is the cold-load path.
- Rendering tests for both components and the reduced-motion tripwire live in
`webapp/test/loading-skeleton.test.ts` (SSR through
`renderToStaticMarkup`; the suite has no DOM harness).
`renderToStaticMarkup`; interaction-level coverage of this component can
now use the DOM harness — see the tests section).
- **Streaming-label phrase rotation (webui-parity 61, restoring the desktop
shape)**: the desktop does not park one static label on screen for the
length of a turn. The schedule and the draw are transcribed from the
Expand Down Expand Up @@ -3213,11 +3272,69 @@ name: `mcode-trajectory-studio`, version `0.1.1`. Protocols supported:
```bash
pnpm --filter @mavis/webui test # full node:test suite (unit + mocked + integration + matrix + trajectory)
pnpm test:webui # same, from the repository root (CI gate)
pnpm test:webapp # webapp (browser) suite: render + interaction tests
node packages/webui/scripts/check-docs-alignment.mjs
```

The package has three runtime dependencies (`hono` + `@hono/node-server` for the HTTP layer, `@mavis/shared` for the workspace path contract) and requires Node 22.19+ (the trajectory studio additionally needs `node:sqlite`, floor 22.13).

### Two ways to test a webapp component

`pnpm test:webapp` runs `webapp/test/**/*.test.ts` on `node --test`. It has two rendering tools, and they answer different questions.

| Tool | Answers | Cannot answer |
| --- | --- | --- |
| `renderToStaticMarkup` (`react-dom/server`) | What does this page print? | Anything requiring an event, an effect or a re-render |
| the DOM harness (`webapp/test/helpers/dom.ts`) | What happens when the user presses a key? | Nothing about a tree that is not mounted — it is not a snapshot tool |

A defect that only appears once a keydown reaches a handler is invisible to static markup: a string has no listeners. That is not hypothetical. The Shortcuts page's capture → verdict → conflict-report path had no test at all, and the mutation that swallowed the conflict report stayed green. Keep static markup for "what does it print" (`settings-extra-pages.test.ts`) and reach for the harness when the answer is "what happens when".

```ts
// webapp/test/helpers/dom.ts — mount a component, drive it, unmount it.
import { withDom, mount, resetStorage } from "./helpers/dom";

test("the conflict is reported on the row that was edited", async () => {
await withDom(createElement(ShortcutsSection, { t }), async (view) => {
await view.pressKey("settings-shortcuts-binding-global-search", {
key: "O", ctrlKey: true, altKey: true,
});
assert.match(view.text("settings-shortcuts-conflict-global-search") ?? "", /新建无项目任务/);
});
});
```

The handle `mount` / `withDom` returns:

| Member | Purpose |
| --- | --- |
| `find(id)` / `query(id)` / `findAll(id)` / `text(id)` / `has(id)` | Look up by `data-testid`; `find` throws and lists the testids that *were* rendered |
| `pressKey(target, {key, ctrlKey, altKey, shiftKey, metaKey})` | Dispatch a bubbling, cancelable keydown and flush React |
| `keyEvent(press)` | Build that keydown without dispatching, to assert on `defaultPrevented` afterwards |
| `click(target)` / `fire(target, type, init)` / `type(target, value)` | The other events, all flushed |
| `run(fn)` | Run an arbitrary block inside `act`, for a raw `dispatchEvent` |
| `rerender(node)` / `flush()` / `html()` | Re-render, drain timers, serialize |
| `unmount()` | Detach the root; `withDom` does it on the throw path too |
| `window` | The happy-dom window, for its `localStorage` |

Four rules, each one a way the harness would otherwise lie:

1. **Import `helpers/dom` before any component import.** `react-dom` captures `canUseDOM` when it is first evaluated; with no window at that moment it falls back to a host config with no event system. The harness publishes the window in its own module body and pulls `react-dom/client` in dynamically, so the order is safe as long as the harness import comes first. Getting it wrong is loud rather than silent: the tree renders empty and the first assertion fails.
2. **Targets are `data-testid` strings**, matching the convention the components already use. A missing one throws with the testids that *are* on screen.
3. **`resetStorage()` between tests.** One window, one storage origin, exactly like a browser tab — state you did not clear leaks into the next test.
4. **`webapp/test/helpers/dom-shim.ts` is still the right tool for the markdown walker.** It serves a `DOMParser` over `parse5` and needs no window at all.

### Why `happy-dom`

The webapp suite runs on `node:test`, not Vitest's DOM environment, so `@testing-library` would bring a `beforeEach` / auto-cleanup protocol this runner does not have — and the three APIs it would add are the three the handle above already exposes.

| Candidate | Transitive deps | Cost | Why not |
| --- | --- | --- | --- |
| `jsdom` 30 | 22 | `undici` + `css-tree` + `whatwg-url`, ~20 MB | Reference-complete. The suite asserts on attributes, text and event delivery — none of which is where the two implementations diverge in practice. |
| `happy-dom` 20 | 4 (`entities`, `whatwg-mimetype`, `buffer-image-size`, `ws`) | 8 MB unpacked | **Chosen.** |
| neither | 0 | — | The `parse5` shim in `dom-shim.ts` shows the middle path works for a parser, but a `DOMParser` cannot dispatch an event. |

`happy-dom` is a `@mavis/webui` devDependency, so it never reaches the product bundle. It does widen `pnpm-lock.yaml`: `vitest` declares it as an optional peer, so the vitest resolution key changes in every workspace importer. Any checkout that consumes the lockfile needs `pnpm install --frozen-lockfile` afterwards. Measured on this machine: 103 ms to import and 3 ms to construct a window, paid once per test file, and only by files that import the harness.

## Origin

The package migrates the community mcode-webui plugin (v1.0.0 → v2.0.0, MiniMax-Code-Plugins PRs #16/#23/#31/#55) and the mcode-trajectory-studio plugin (PR #56) into the product. The full people and history record is [co-builders.md](../co-builders.md).
Loading
Loading