Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 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
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
38 changes: 36 additions & 2 deletions docs/webui.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ The fourth registered provider is the **`exec` transport** (M4-2) — the one-sh
| sessionCrud | partial — missing `createSession`, `listSessions`, `getSession`, `updateSession`, `renameSession`, `archiveSession`, `deleteSession`, `forkSession`, `getSessionForkOptions`, `loadSession`, `activateSession`. Only `--session` / `--continue` exist, and they re-enter a session rather than choose one |
| streamingSend | full (`--input -` plus the `stream-json` event stream) |
| interrupt | none — interface-absent: nothing to call. The SIGINT/SIGTERM/SIGHUP in `packages/tui/src/cli/run-exec-command.ts` are signals webui sends to the child **it** spawned, i.e. webui's own kill cascade, not a capability the transport offers |
| toolSkillInvocation | partial — missing `listSkills`, `listRuntimeSkills`, `listPendingPermissions`, `replyPermission`, `setMode`. The transport PRODUCES `tool_call` items and webui does not read them; and `--permission` is fixed at spawn time (the CLI says `ask` requires TUI/ACP), so there is no permission request/reply pair |
| toolSkillInvocation | partial — missing `listSkills`, `listRuntimeSkills`, `listPendingPermissions`, `replyPermission`, `setMode`. The transport PRODUCES `tool_call` items and webui consumes but does not render them; and `--permission` is fixed at spawn time (the CLI says `ask` requires TUI/ACP), so there is no permission request/reply pair |
| turnDiff | none — interface-absent, **servedBy `local-runtime-v2`** |
| turnRewindRedo | none — interface-absent (`mcode exec review` reviews local git changes and carries no turn coordinate, so it is not a rewind surface) |
| plugins | none — interface-absent, **servedBy `local-runtime-v2`** |
Expand All @@ -257,7 +257,20 @@ The fourth registered provider is the **`exec` transport** (M4-2) — the one-sh

Three cells are **weaker** than acp, and for structural reasons rather than unfinished engine work: `interrupt` (acp has a cancel notification, exec has nothing to declare one on), `subagents` (acp can parse sub-agent activity off its stream, exec's event union has no such kind) and `authCredentials` (acp has two RPC methods, exec has no channel). The reverse exception is the same two keys for the same reason, which is a finding rather than a copy: `/api/turn-diff*` and `/api/plugins*` project the in-process v2 host and gate on **no transport**, so every transport inherits it.

**A known mismatch this audit surfaced, recorded rather than hidden.** The three event names `collectExecResult` branches on — `delta`, `message`, `exec.result` — are the *supervisor's internal* stream-event names. `--output-format stream-json` writes only what `ExecEventProjector` produces (`packages/tui/src/headless/output.ts` refuses the format with no projector, and `packages/tui/src/headless/runner.ts` always supplies one), so the wire carries the ten `ExecEvent` types and the two name families do not intersect. That is a real gap in the exec data plane. M4-2 does not fix it — the batch registers a declaration and changes no routing — but it is pinned in `EXEC_INTERFACE.consumedEvents` and asserted by a test that fails if the intersection ever becomes non-empty in either direction.
**The mismatch this audit surfaced, and what it cost.** The three event names `collectExecResult` branched on — `delta`, `message`, `exec.result` — are the *supervisor's internal* stream-event names. `--output-format stream-json` writes only what `ExecEventProjector` produces (`packages/tui/src/headless/output.ts` refuses the format with no projector, `packages/tui/src/headless/runner.ts` always supplies one, and the encoder's `result()` leg goes through `projector.complete()` rather than writing the `ExecResult` itself), so the wire carries the ten `ExecEvent` types and the two name families did not intersect.

That was not a missing feature but a **dead data plane**, and it is worth spelling out what a user saw on `MCODE_USE_ACP=0` — and on every non-`Full access` permission mode, which silently re-routes to exec: reasoning and answer text streamed in and never landed, the turn ended with no answer line at all, the context counters never moved, and the follow-up turn started a brand-new engine session because the session id was never read back. D1 rewrote the consumer against the wire:

| Wire event | Consumed as |
| --- | --- |
| `sessionId` on every line | the engine session this run entered, written back to `cs.mcodeSessionId` so the next turn continues it |
| `item.started` / `item.updated` | `item.contentDelta` appended to the answer / reasoning accumulator and streamed as a `●` / `▲` line |
| `item.completed` | `item.content`, adopted only for an item that never streamed a delta |
| `turn.completed` | per-turn `usage` and `durationMs` |
| `turn.failed` | `status` and `error` |
| `exec.completed` | the terminal `ExecResult`, and the call to `finalize()` |

`EXEC_INTERFACE.consumedEvents` now names exactly those six, `EXEC_INTERFACE.baseOnlyEvents` names the four that carry nothing beyond `ExecEventBase`, and a test asserts the two partition the union, that the parser's `switch` arms are the same set, and that every consumed name is one the wire can emit. A `tool_call` item is consumed but not rendered — it carries a `toolCall` payload rather than text — which is why `toolSkillInvocation` stays `partial`.

**`servedBy` is the plan's one reverse exception, and it is load-bearing.** `turnDiff` and `plugins` are honestly `none` on the protocol, and the three `/api/turn-diff` and ten `/api/plugins` endpoints still work on the default acp transport, because they project the in-process local-runtime-v2 host through `getEngineCatalogueHost()` and are gated on no provider declaration. Reading the level alone would eventually 501 two working features the moment a frontend consulted the transport's provider instead of the default one. `summarizeCapabilityHosting(capabilities)` and `resolveCapabilityHostProvider(providerId, key)` expose the routing fact; the hosted keys deliberately stay in `summarizeUnavailableCapabilities`, because the provider really has none and that `{none, partial}` shape is already on the wire. The `exec` provider carries the same two fields for the same structural reason, which is what makes `servedBy` a per-key declaration field rather than an acp special case.

Expand Down Expand Up @@ -1648,6 +1661,27 @@ implementation accident:
The `mermaid` dependency (11.12.1, MIT) is recorded in
`release/dependency-licenses.json`.

### Raw HTML in Markdown is text, never markup (P17)

HTML written into a Markdown source — an assistant message, an activity
group, a `.md` file preview — is **shown as the source text**. It is never
parsed into DOM elements. That is the whole contract; the rest is how it is
kept, and how you would notice a regression.

| Aspect | Contract | Backed by |
| --- | --- | --- |
| Author HTML | Inline and block HTML are escaped, so a pasted `<svg><path …/></svg>`, a `<div onclick=…>`, or a `<script>` block appears verbatim as text. The prose around it is unaffected — one snippet does not blank the message | `webapp/lib/markdown.ts` (the `renderer.html` override) |
| Why the parser and not the sanitiser | `marked` has no "no raw HTML" option: without the override the snippet reached the tag allowlist, which admits `svg`/`path` for KaTeX geometry, and `components/markdown-html.tsx` then called `createElement("path")` — an unknown host element, and one `The tag <path> is unrecognized in this browser` console error per occurrence (nine in a single UAT round; `doc/uat/2026-10-03-16-master-sub-agent-comm-redline1.md`, anomaly #2) | `webapp/lib/markdown.ts`, `components/markdown-html.tsx` (`htmlToReact`) |
| Generated HTML is exempt | Markup this app *generates* never passes through that override: KaTeX arrives from the `webuiMath` inline extension and every fenced language from `registerLanguageRenderer`, both of which return their HTML directly. Formula geometry — real `<svg>`/`<path>` — therefore still renders | `webapp/lib/math-renderer.ts`, `webapp/lib/markdown.ts` (`safeLanguageRenderer`) |
| Line structure | A block snippet keeps its original line breaks; escaping never collapses a multi-line paste onto one line | `webapp/lib/markdown.ts`, `webapp/test/markdown-raw-html.test.ts` |
| How to tell it works | The regression suite asserts the React tree, not just the string: no `svg`/`g`/`path` element is ever created for author HTML, and a formula still creates `svg` + `path`. A change that lets a tag through turns the suite red, and so does one that over-tightens and kills formula geometry | `webapp/test/markdown-raw-html.test.ts` |

Rejected alternatives: **rendering model-authored SVG** (an XSS surface — an
`<svg>` can carry `<foreignObject>`, animation and event handlers, and the
product intent is a transcript, not a renderer); **adding DOMPurify** (a
multi-megabyte dependency to defend markup the app never needs, when the
parser can refuse it outright).

### Code block wrapping and scrollbars (ticket 52)

Every markdown codeblock — chat messages, activity groups and markdown
Expand Down
67 changes: 60 additions & 7 deletions docs/webui.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ host 对象(子进程没有对象可反射),而是线路表 `MCODE_ACP_CAP
| sessionCrud | partial——缺 `createSession`、`listSessions`、`getSession`、`updateSession`、`renameSession`、`archiveSession`、`deleteSession`、`forkSession`、`getSessionForkOptions`、`loadSession`、`activateSession`。只有 `--session` / `--continue`,而它们是「重新进入」一个会话而非「挑选」一个 |
| streamingSend | full(`--input -` 加上 `stream-json` 事件流) |
| interrupt | none——接口无:没有可调的东西。`packages/tui/src/cli/run-exec-command.ts` 里的 SIGINT/SIGTERM/SIGHUP 是 webui 发给**自己 spawn 的**子进程的信号,即 webui 自己的 kill 级联,不是传输提供的能力 |
| toolSkillInvocation | partial——缺 `listSkills`、`listRuntimeSkills`、`listPendingPermissions`、`replyPermission`、`setMode`。传输**产出** `tool_call` 项而 webui 不读它们;且 `--permission` 在 spawn 时就定死(CLI 明说 `ask` 需要 TUI/ACP),所以没有权限请求/应答对 |
| toolSkillInvocation | partial——缺 `listSkills`、`listRuntimeSkills`、`listPendingPermissions`、`replyPermission`、`setMode`。传输**产出** `tool_call` 项而 webui 消费了它们却不渲染;且 `--permission` 在 spawn 时就定死(CLI 明说 `ask` 需要 TUI/ACP),所以没有权限请求/应答对 |
| turnDiff | none——接口无,**servedBy `local-runtime-v2`** |
| turnRewindRedo | none——接口无(`mcode exec review` 审阅本地 git 变更、不带回合坐标,所以不是 rewind 面) |
| plugins | none——接口无,**servedBy `local-runtime-v2`** |
Expand All @@ -274,14 +274,34 @@ RPC 方法,exec 没有通道)。反向例外是同样两个键、同样一
复制:`/api/turn-diff*` 与 `/api/plugins*` 投影的是进程内 v2 host,且**完全不按
传输门控**,所以每条传输都继承它。

**这条审计查出的一处错配,选择记录而非隐藏。** `collectExecResult` 匹配的三个
**这条审计查出的一处错配,以及它的代价。** `collectExecResult` 匹配的三个
事件名——`delta`、`message`、`exec.result`——是 **supervisor 内部**的流事件名。
而 `--output-format stream-json` 只写 `ExecEventProjector` 的产出(`packages/tui/src/headless/output.ts`
在没有 projector 时直接拒绝该格式,`packages/tui/src/headless/runner.ts` 总会提供一个),所以线路上跑的是
那十个 `ExecEvent` 类型,两个名字族并不相交。这是 exec 数据面上的真实缺口。
M4-2 不修它——本批只注册声明、不改路由——但它被钉在
`EXEC_INTERFACE.consumedEvents`,并由一条「相交集一旦在任一方向变为非空就转红」
的测试守住。
在没有 projector 时直接拒绝该格式,`packages/tui/src/headless/runner.ts` 总会提供一个,
且编码器的 `result()` 那条腿走 `projector.complete()` 而非直接写出 `ExecResult` 本身),
所以线路上跑的是
那十个 `ExecEvent` 类型,两个名字族并不相交。

这不只是「少一个功能」,而是**一整条死掉的数据面**。值得把用户在
`MCODE_USE_ACP=0` 下会看到什么写清楚——以及在每一种非 `Full access` 的权限模式下,
因为那会静默改道到 exec:思考与回答的文本流进来却从不落地,回合结束时连一行回答
都没有,上下文计数从不走动,而下一轮会另起一个全新的引擎会话,因为会话 id 从未被读回。
D1 把消费面改回线路本身:

| 线路事件 | 消费为 |
| --- | --- |
| 每一行都带的 `sessionId` | 本次运行进入的引擎会话,写回 `cs.mcodeSessionId`,使下一轮能续接 |
| `item.started` / `item.updated` | `item.contentDelta` 追加进回答/思考累加器,并以 `●` / `▲` 行流式呈现 |
| `item.completed` | `item.content`,仅对从未流出增量的 item 采纳 |
| `turn.completed` | 每轮 `usage` 与 `durationMs` |
| `turn.failed` | `status` 与 `error` |
| `exec.completed` | 终态 `ExecResult`,以及 `finalize()` 的调用 |

`EXEC_INTERFACE.consumedEvents` 现在正好点名这六个,`EXEC_INTERFACE.baseOnlyEvents`
点名除 `ExecEventBase` 外无内容的另外四个,并由一条测试断言二者恰好划分整个联合、
解析器的 `switch` 分支与之是同一集合、且每个被消费的名字都是线路真能发出的名字。
`tool_call` item 会被消费但不会被渲染——它携带的是 `toolCall` 负载而非文本——
这正是 `toolSkillInvocation` 停在 `partial` 的原因。

**`servedBy` 是计划里唯一的反向例外,且有承重意义。** `turnDiff` 与 `plugins`
在协议上如实 `none`,而那三个 `/api/turn-diff` 与十个 `/api/plugins` 端点在缺省
Expand Down Expand Up @@ -1220,6 +1240,39 @@ flowchart LR

依赖:`mermaid` 11.12.1(MIT),已登记于 `release/dependency-licenses.json`。

### Markdown 里的裸 HTML:按代码文本显示(P17)

Markdown 源里写的 HTML——助手回复、活动组、`.md` 文件预览——一律**当作
源码文本显示**,绝不会被解析成页面元素。这就是全部契约;下面是它靠什么
维持、以及怎么发现它坏了。

**你会看到什么**

- 模型在推理或正文里贴 `<svg><path …/></svg>`,屏幕上是这段源码本身,
不是一张图,浏览器控制台也不会因为 `<path>` 报警。
- `<div onclick=…>`、`<script>` 之类的片段同理:原样显示成文字。周围的
正文不受影响,一段 HTML 不会让整条消息消失。

**为什么不是"把 SVG 画出来"**

`<svg>` 能携带 `<foreignObject>`、动画与事件属性,渲染模型输出的 SVG
等于开一个 XSS 面;而这个产品的定位是会话记录,不是渲染器。所以选择
"安全显示为文本",也没有为此引入 DOMPurify 这类重型依赖——问题出在解析
层允许裸 HTML 透传,在解析层拒绝即可(`webapp/lib/markdown.ts`)。

**为什么不影响公式**

本应用**自己生成**的 HTML 不走这条规则:KaTeX 由行内扩展产出、各代码围栏
由语言渲染器注册表产出,两者都直接返回自己的 HTML。因此公式的几何图形
(真正的 `<svg>`/`<path>`)照常渲染(`webapp/lib/math-renderer.ts`)。

**怎么发现它坏了**

回归用例断言的是 React 树而不只是字符串:作者写的 HTML 永远不会生成
`svg`/`g`/`path` 元素,公式仍然生成 `svg` + `path`。往松了改(放行标签)
或往紧了改(连公式几何一起转义)都会让用例变红
(`webapp/test/markdown-raw-html.test.ts`)。

### 代码块的换行与滚动条(工单 52)

所有 markdown 代码块——聊天消息、活动组、markdown 文件预览——都经同一
Expand Down
Loading
Loading