Repository navigation
Conversation
ARCHITECTURE.md:492 counted five files under server/engine/ while the directory has shipped six since #143. The missing one is providers/local-runtime-v2.capabilities.js, the declaration-only module whose sole import is ../capabilities.js — the split that keeps the v2 host's ~4.7 s TypeScript dependency tree off the boot path. The local-runtime-v2.js row credited itself with the declaration it only re-exports, so that credit moves to the file that actually defines it. The zh-CN mirror takes the same edit in the same commit (equal weight); check-docs-alignment.mjs resolves the new bare-path citation against the merged tree.
…ine-abstraction M2) Baseline: feat/engine-capabilities (M1, PR #143), NOT main — the server/lib/engine -> server/engine path fix has not landed yet. - test/lib/engine/capability-snapshot.test.js boots ONE real catalogue host on an isolated tmp data dir (MINIMAX_DATA_DIR + every MCODE_WEBUI_* path pinned before the provider import) and audits every full/partial key of both providers against the reflected surfaces: full requires every tracked method (REQUIRED_METHODS, derived from the live prototype chains — 91 adapter / 94 CliService methods — not copied from the design matrix); partial requires the present half to exist, method-named missing items to be genuinely absent (under-declaration goes red), and kebab-case sub-capability names to have no covering method; none is not method-checked. - Mutation tests pin the checker itself: flipped level / deleted method / grown sub-capability each go red (also verified by hand: three file mutations red at exit 1, restored byte-identical). - Registry-driven static guard: every registered provider declares exactly ENGINE_CAPABILITY_KEYS — typo keys cannot pass silently, and M4 providers are swept without editing the test. - Docs: ARCHITECTURE.md/.zh-CN.md M2 section, webui.md/.zh-CN.md migration-state entry; tmp prefix registered in the leak gate.
… path The M1 path move took server/lib/engine to server/engine. This file was written against the old one and rebase carried the code forward without carrying the import, so the suite failed on MODULE_NOT_FOUND and said nothing about the capabilities it was meant to check.
…ther Three leaks, all from state that lived outside the component that should have owned it. page.tsx read localStorage during render, so the pre-rendered HTML and the first client frame could not agree — a skeleton screen was hiding it, which is exactly the kind of cover that disappears the moment someone edits the shell. The first frame now uses defaults and one mount effect restores; the three write-back mirrors are gated so a default never overwrites a stored value. Scroll position is read by the same sessionKey effect Chat already had, which reads the same key. The draft store was a module-level bucket, so a draft, a failure banner and a model chip all followed you across sessions — type into one conversation, switch, and your words are in the other. The store is keyed by session now. Isolation is not discarding: switching back finds the draft still there. Clearing was the alternative and it destroys an unread banner every time you return to a conversation. #126 left the accepted-but-unconfirmed banner without anything to consume it when a turn ended. unconfirmedPatchOnTurnEnd clears it on the falling edge of running, and only there — the three-value decision about when to show it is untouched.
…the engine facade
Migration step M3, batch B0 (engine-abstraction). 13 endpoints across
routes/plugins.js and routes/turn-diff.js reached the catalogue host by
importing lib/acp-client.js#getCatalogueHost directly. They now call
getEngineCatalogueHost() from the facade.
- engine/host.js: the lazy bridge. Its only import is a dynamic
`await import("../lib/acp-client.js")` inside the function body, so
engine/index.js gains a function and not a module load. That boundary
is the whole point: app.js reaches engine/index.js through
routes/engine-capabilities.js, and a static import of acp-client there
would put the ACP client tree on every server start — the regression
M1 paid for once (209ms -> 2700ms; facade load 4685ms -> 5ms once
declaration and construction were split). The value is forwarded
verbatim, `null` included, so "host did not boot" stays
RUNTIME_UNAVAILABLE and never a second host.
- engine/index.js re-exports the getter; the two routes import it from
there and no longer name acp-client.js.
- No endpoint behaviour changes: same wire shapes, statuses, codes, same
`deps.getCliService` / `deps.getDiffApplication` seams, same one
process-wide host. Measured on the module graph: routes/plugins.js
drops from 13 product files + @mavis/shared to 8 files and zero bare
packages; engine/index.js's whole closure is 6 files and 0 bare
specifiers. Server start and the facade's own load are unchanged
(facade ~1.2ms -> ~3ms, i.e. one more 45-line zero-import file; boot
stays in the same 200-300ms band) because lib/state-bus.js already
pulls acp-client into app.js's boot graph — closing that edge belongs
to the catalogue read/write batches (M3-B1+), not here.
Tests: test/lib/engine/host-facade.test.js pins the contract against the
real module graph rather than against source text — a resolve hook
(module.registerHooks) in a fresh process reports, per parent, which
specifiers each entry resolved. It asserts neither route has a direct
edge to acp-client/runtime-host/acp.mjs, that loading engine/index.js
pulls no host module and no @mavis/* or @minimax/* package, that
engine/host.js is in that closure, and the source-shape tripwires
(dynamic import only, facade re-export). Mutation-checked: making the
facade import statically turns 4 tests red, making plugins.js import
directly turns 4 more red. The existing plugins/turn-diff suites pass
unchanged under both transports (158 tests x acp and x runtime).
Docs: ARCHITECTURE.md + .zh-CN.md — the engine/ file table gains
engine/host.js on top of the six files #143 + the doc batch settled, the
"one host" rule now names the facade, and the boot-path discipline is
stated where the file list lives. docs/webui.md + .zh-CN.md are
untouched: no user-visible change. Source inventory regenerated for the
two new files (rebase conflict in it was resolved by taking the upstream
copy and regenerating, never by hand).
…ites immune to the gate's isolation env The webui gate runs with MCODE_WEBUI_DATA_DIR, MCODE_WEBUI_SETTINGS_PATH and MINIMAX_DATA_DIR exported at a scratch directory. Two suites read paths those exports take away from them: - config.js#resolveDataDir reads MINIMAX_DATA_DIR ?? MAVIS_DATA_DIR, so the gate's MINIMAX_DATA_DIR outranked mavis-usage.check.mjs's own MAVIS_DATA_DIR fixture and every DB-backed case resolved null against a scratch dir that holds no runtime-state.sqlite. The suite now exports the name that wins. - config.js resolves SESSIONS_DB as MCODE_WEBUI_SESSIONS_DB || join(WEBUI_DATA_DIR, "sessions.json"). A caller that exports MCODE_WEBUI_SESSIONS_DB redirects the store, while the suite's beforeEach still cleared join(DATA_DIR, "sessions.json") — so each run read the previous run's records and the mid-run switch resolved an id whose workspace belonged to a since-removed tmp dir. Both chat-route suites now pin MCODE_WEBUI_SESSIONS_DB to the same path their cleanup clears. Test-only: no server/ code, no helper under test/helpers/_setup.js, and no assertion weakened or skipped. Verified with the three variables set, with MCODE_WEBUI_SESSIONS_DB additionally set, and bare.
…the engine facade
Migration step M3, batch B0 (engine-abstraction). 13 endpoints across
routes/plugins.js and routes/turn-diff.js reached the catalogue host by
importing lib/acp-client.js#getCatalogueHost directly. They now call
getEngineCatalogueHost() from the facade.
- engine/host.js: the lazy bridge. Its only import is a dynamic
`await import("../lib/acp-client.js")` inside the function body, so
engine/index.js gains a function and not a module load. That boundary
is the whole point: app.js reaches engine/index.js through
routes/engine-capabilities.js, and a static import of acp-client there
would put the ACP client tree on every server start — the regression
M1 paid for once (209ms -> 2700ms; facade load 4685ms -> 5ms once
declaration and construction were split). The value is forwarded
verbatim, `null` included, so "host did not boot" stays
RUNTIME_UNAVAILABLE and never a second host.
- engine/index.js re-exports the getter; the two routes import it from
there and no longer name acp-client.js.
- No endpoint behaviour changes: same wire shapes, statuses, codes, same
`deps.getCliService` / `deps.getDiffApplication` seams, same one
process-wide host. Measured on the module graph: routes/plugins.js
drops from 13 product files + @mavis/shared to 8 files and zero bare
packages; engine/index.js's whole closure is 6 files and 0 bare
specifiers. Server start and the facade's own load are unchanged
(facade ~1.2ms -> ~3ms, i.e. one more 45-line zero-import file; boot
stays in the same 200-300ms band) because lib/state-bus.js already
pulls acp-client into app.js's boot graph — closing that edge belongs
to the catalogue read/write batches (M3-B1+), not here.
Tests: test/lib/engine/host-facade.test.js pins the contract against the
real module graph rather than against source text — a resolve hook
(module.registerHooks) in a fresh process reports, per parent, which
specifiers each entry resolved. It asserts neither route has a direct
edge to acp-client/runtime-host/acp.mjs, that loading engine/index.js
pulls no host module and no @mavis/* or @minimax/* package, that
engine/host.js is in that closure, and the source-shape tripwires
(dynamic import only, facade re-export). Mutation-checked: making the
facade import statically turns 4 tests red, making plugins.js import
directly turns 4 more red. The existing plugins/turn-diff suites pass
unchanged under both transports (158 tests x acp and x runtime).
Docs: ARCHITECTURE.md + .zh-CN.md — the engine/ file table gains
engine/host.js on top of the six files #143 + the doc batch settled, the
"one host" rule now names the facade, and the boot-path discipline is
stated where the file list lives. docs/webui.md + .zh-CN.md are
untouched: no user-visible change. Source inventory regenerated for the
two new files (rebase conflict in it was resolved by taking the upstream
copy and regenerating, never by hand).
…ites immune to the gate's isolation env The webui gate runs with MCODE_WEBUI_DATA_DIR, MCODE_WEBUI_SETTINGS_PATH and MINIMAX_DATA_DIR exported at a scratch directory. Two suites read paths those exports take away from them: - config.js#resolveDataDir reads MINIMAX_DATA_DIR ?? MAVIS_DATA_DIR, so the gate's MINIMAX_DATA_DIR outranked mavis-usage.check.mjs's own MAVIS_DATA_DIR fixture and every DB-backed case resolved null against a scratch dir that holds no runtime-state.sqlite. The suite now exports the name that wins. - config.js resolves SESSIONS_DB as MCODE_WEBUI_SESSIONS_DB || join(WEBUI_DATA_DIR, "sessions.json"). A caller that exports MCODE_WEBUI_SESSIONS_DB redirects the store, while the suite's beforeEach still cleared join(DATA_DIR, "sessions.json") — so each run read the previous run's records and the mid-run switch resolved an id whose workspace belonged to a since-removed tmp dir. Both chat-route suites now pin MCODE_WEBUI_SESSIONS_DB to the same path their cleanup clears. Test-only: no server/ code, no helper under test/helpers/_setup.js, and no assertion weakened or skipped. Verified with the three variables set, with MCODE_WEBUI_SESSIONS_DB additionally set, and bare.
…ransport (M3-B1) The directory-read family — #9 acp-sessions, #10 acp-session-title, #72 protocol/list-sessions, #74 state, #75 health — reached the engine through whatever MCODE_WEBUI_TRANSPORT happened to be, so "does the engine support this" had no answer anywhere except the absence of a crash. server/engine/ session-reads.js gives it one: each endpoint declares the capability and the provider method it needs, the facade checks the registered provider's declaration first, and a provider that does not offer the read answers 501 through invokeHandler instead of an empty list. Nothing on the wire moves. The facade forwards to the same acp-client exports the routes already called, so the 30s cache, the cwd normalisation, the 30s-stale sidebar push semantics and the catalogue-sessions projection are the same code; handleHealth becomes async because the version now resolves through the facade, which is why app-hono's legacy-parity helper learned to await it. /api/state's snapshot field list is untouched — snapshotViewFields and mcodeSessionsSnapshotFields are the first-frame render contract and this batch adds and removes nothing. Each read also reports where its bytes came from — catalogue, acp, or acp-fallback when the runtime transport asked for a host that never booted. That is metadata, not wire, and it is the difference between a sidebar that degraded and one that pretends. Two things this batch found rather than assumed: the catalogue host exposes no version accessor, so /api/health keeps answering from the ACP initialize mirror and says so rather than inventing a method; and protocol.js#72's old test drove a mock key nothing read, so "the cwd filter works" had never actually been proven.
…ade (M3-B2) Routes #8 GET /api/session-tree and #11 GET /api/sessions/:id/export through the engine facade instead of the transport, keeping every response shape, status code and reason string unchanged. The two families are separate files because their gate policies are opposite. The tree is entirely engine data, so a provider that cannot list sessions genuinely has no tree: assertSessionTreeCapability throws and invokeHandler answers 501. Export's primary source is sessions.json and the engine only contributes a best-effort transcript enrichment the endpoint has always promised never to block on, so checkSessionExportCapability reports and never throws — gating it hard would delete working functionality in response to a declaration about a capability the endpoint does not depend on. The tree route re-throws the capability error, matched with the class's own instanceof helper rather than a `.name` compare: `name` is a writable instance property, so a stray `err.name = "…"` would silently turn that 501 back into the 200 soft-fail the gate exists to prevent. A test pins both halves — the real class propagates, an impostor carrying the right `.name` does not. Verified by exporting the real tree (303 rows, 32 projects, 299 nodes) before and after and diffing every node's id/title/parent/depth: 3289 field comparisons, zero differences. A synthetic fixture covers what the live data does not contain (orphans, cycles, four-level nesting, exotic titles): 165 comparisons, zero differences. Two pre-existing shapes are pinned because a "cleanup" would silently break them — child nodes carry no `children` key (all 66 of them), and the response has no parent_session_id key at all.
…ed figures get one home (M3-B3)
…and record the whole-namespace mock trap
…acade (M3-B4) #20 /api/account, #57 /api/models and #73 /api/protocol/capabilities now reach the engine through three new engine/ modules instead of naming lib/mcode-rpc.js, lib/models.js, lib/providers-config.js, lib/engine-catalogue.js and lib/acp-client.js themselves. Three modules because the three gate policies are all different: the account read gates HARD on authCredentials.getAccountStatus (the same provider method B3's #15/#16 read, so a provider that drops it takes both down together), the model catalogue gates SOFT (its primary sources are files webui owns, so a hard gate would delete a working picker), and #73 declares nothing at all because it IS the declaration endpoint. The model projection moved whole — three sources, the per-provider dedupe, both builtin-tree annotations and the three derived figures are now named pure functions pinned on their inputs, and #57 is verified by a full snapshot whose oracle was captured from the pre-refactor implementation. Its read stays synchronous so handleGetModels keeps its contract, which is also why engine/model-reads.js is not re-exported from engine/index.js: its four sources reach @mavis/shared and js-yaml, and the boot-path guard is right to refuse that under the shared facade. #73 is the one response body in the migration that changes: it gains an `engine` key carrying the engine-capabilities view, with `providerFor` saying whether the declaration came from the active transport's provider or from the default one standing in. Every pre-existing key keeps its name, position and value, and the ACP wire table is not replaced by the 14 matrix keys.
…ilities view (user-approved contract change)
`GET /api/protocol/capabilities` used to answer from two hand-maintained
places: `MCODE_ACP_CAPABILITIES`, a flat `{method: boolean}` table of the
ACP JSON-RPC surface, and the `initialize` agentInfo mirror. The engine's
DECLARED capability surface already existed — the 14-key per-provider
object that `GET /api/engine-capabilities` serves — so webui was carrying
two parallel answers to "what can this engine do", able to disagree, with
no test able to notice. This makes `capabilities` the declared object and
drops the wire table from the endpoint.
This is a reviewed, user-authorised endpoint contract change, not a refactor
side effect, and it is stated as such in the module header, in
`docs/API.md` and in both ARCHITECTURE twins. The twelve old accessors are
asserted GONE, so a consumer reading `capabilities.set_mode` gets
undefined and fails loudly rather than receiving a truthy object field.
No runtime consumer exists: nothing in `webapp/` reads this endpoint, and
`engine/capability-reads.js` no longer imports `lib/mcode-rpc.js` at all
(pinned by a static tripwire, because an unused import is behaviourally
inert and no behavioural test could see it).
The `engine` key the previous commit added is REMOVED rather than kept:
with `capabilities` already the declaration, an `engine` block would carry
the same 14 keys a second time in one response. What survives from that
shape is the provenance — `capabilitiesProvider` / `capabilitiesProviderFor`,
the honest bit that says whether the declaration came from the active
transport's provider or from the default one standing in — plus
`capabilitiesUnavailable` for the derived degradation roll-up. A test
counts the declaration's occurrences in the serialised body and requires
exactly one, so a second carrier is a red bar.
`MCODE_ACP_CAPABILITIES` is kept and stays pinned by
`test/lib/mcode-rpc.check.mjs`: it is still a true statement about the
ENGINE's ACP surface and `docs/CAPABILITIES.md` cites it as one. It has no
webui consumer left, recorded as debt in the module header rather than
deleted as a side effect.
`docs/webui.md` and `docs/webui.zh-CN.md` gain a diff here for the first
time in this migration: they carried the old response shape in their
endpoint tables, and an authorised contract change has to be documented
where the contract is written.
…ger has Found by the dual-axis code review (Standards + Spec) before B4 merges. No logic changes; both were documentation lying about the code next to it. `engine/index.js` still described #73 as an additive change — "the response body gains a key (`engine`, the engine-capabilities view) … additive rather than a replacement". The second B4 commit made it a REPLACEMENT and deleted the `engine` key, so the facade's own export table was the one place still telling a reader the opposite of what the endpoint does. It now states the replacement, why the `engine` key was removed rather than kept, and what survived from it (the provenance keys and the derived roll-up). `docs/ARCHITECTURE.md` and its zh-CN twin called `engine/account-reads.js`'s read **synchronous**. It is `async` — `readEngineAccount` awaits a `Promise.all` of dynamic imports — and the boot-path note the row pointed at describes model-reads, not this module. The same two documents already listed account-reads correctly under the `await import()` rule a few paragraphs down, so the file contradicted itself in two languages at once. Both rows now say asynchronous and point at the ordinary rule. Also drops a dead `assertEngineCapability` import from `engine/model-reads.js`: the soft gate inspects the declaration inline, so the throwing helper was never called and its presence read as if the soft path could still throw. Replaced by a comment saying why it is absent, so the next reader does not "fix" it back in. And the one comment with Chinese embedded mid-sentence (仓库 review 要求注释用英文) is now English; the header parentheticals naming each family (账户读 / 模型目录读 / 能力声明读) stay, as do the quoted product strings — `本地用户` is the real zh-CN value of `userMenu.localUser` and `shell.tsx` cites it the same way.
M3 batch B5: #7 DELETE /api/sessions/:id, #4 POST /api/sessions/rename and #6 POST /api/sessions/cleanup-orphans stop driving the store, the caches and the engine's own local_runtime_* tables from the route. They ask engine/session-writes.js instead, so the load -> resolve -> authorize -> intent-audit -> mutate ordering — and the resurrection guard inside it — becomes named, testable code rather than a two-line helper a route could call out of order. #7 and #6 gate HARD on sessionCrud.deleteSession, because the rows they destroy are the engine's own; #4 declares no capability at all, because a rename writes webui's own store and touches no engine surface. The policy is decided by who owns the rows the write destroys, which is a different question from the read families' and does not have the same answer twice in a row here. The facade exposes a plan/commit pair rather than one deleteSession(), so the write-ahead audit still lands between "know what the user asked to delete" and "delete it". Response bodies are built in the facade once, which is what lets the #6 and #7 dryRun shapes be pinned byte-for-byte by unit tests. No status code, response body or error code changes. The 32-table delete SQL stays in lib/mcode-session-delete.js and is reached by dynamic import; acp-client.js and four test files bind to that specifier, so collecting it is a later batch's job. Recorded as KNOWN DEBT, along with rename writing a webui-side label only, and delete not detecting an in-flight session.
…rfaces The authorize-button fix (08599472) surfaced five more primary surfaces pairing text-text_default_inverted_static with bg_interaction_primary_default; dark mode inverts that background to pure white while the token stays near-white, so the label composites to white-on-white. Swap all of them to text-text_label_primary_default and add a source-scan guardrail that keeps the pairing out of primary surfaces while pinning the sanctioned status-badge exception (toolbar).
M3-B6: #3 POST /api/sessions/switch now asks the engine facade instead of reaching into lib/acp-client.js, lib/transcript.js, lib/mavis-usage.js, lib/models.js and lib/config.js from the route. The new engine/session-switch.js owns the four load-bearing facts the ~290-line handler had accumulated: the mvs-sid-first resolution order (single base-session identity), the backfill decision and its read, the workspace containment gate (which runs before any cs mutation, so a refused switch leaves the client untouched), and the response body. The gate is SOFT — it reports and never throws — because the switch's primary data is webui's own record and both engine touches have a defined degradation. Gating hard would remove a working endpoint over a title and a transcript, and would do it on the default acp transport first. The route keeps what is its own: the "id required" 400, the status mapping, the fail-closed audit and the SSE push — the audit has to land after the switch has already mutated cs, and the push must not fire when it fails. Behaviour is unchanged and pinned: the four red lines (transcript backfill, cumulative detection, workspace containment, single base session identity) each get named tests with their negative half, and the success body's key ORDER is compared as a string. Six mutations of the facade were run to prove the tests are load-bearing. KNOWN DEBT 1 in the new module records what this batch did NOT retire: the 3-candidate transcript probe. The default acp transport has no engine surface to replace it with, getMessages paginates where the probe caps lines, their orderings differ, and export's enrichment is still byte-pinned to the same candidates. What IS retired is the coupling — the route no longer names lib/transcript.js, and the probe list is an implementation detail behind one seam.
gitleaks' generic-api-key rule flags the deliberate sk-secret-should- never-leak fixture that model-reads.test.js uses as a leak-prevention tripwire (asserting the facade never serializes provider keys). The value is fake and the assertion exists to catch real leaks; allowlist the exact pairing instead of weakening the fixture.
The full-history scan flags two synthetic-credential fixtures: the model-reads leak tripwire (fake provider keys asserting the facade never serializes them) and the fs-credential-guard canaries (fake id_rsa/pem bodies asserting the 403 guard). Pin their fingerprints in .gitleaksignore; the .gitleaks.toml path allowlist for the same files stays as a coarse first line.
The match-targeted entry missed the byok fixture key (the generic rule's match string differs from the tripwire value the entry was written for). Scope both entries to the two fixture files themselves — every finding in them is synthetic by construction — and keep .gitleaksignore as the precise fingerprint layer.
The batch plan transcribed the abort bound as 5s; the migrated file ran 2000ms. The product call (2026-10-03) takes the plan's value: the longer grace gives a stubborn child more time to finalize at the cost of 'already stopped' staying a lie for three extra seconds. The pinning test moves with it and KNOWN DEBT 1 records the resolution.
main's #158 squash carries M4-1 (acp provider registration) and P18 (code block overflow); resolving toward dev-lhl advances the merge base past the squash (SPEC rule 8).
main's #159 squash carries M4-2 (exec provider registry), M4-3a (deleteSession facade), P17 (svg escape), D1 (exec stream events) and PB-1 (session context actions); resolving toward dev-lhl advances the merge base past the squash (SPEC rule 8).
…dout The General page's 上下文窗口用量显示 switch wrote `webui-context-window-usage` and nothing read it, so flipping it changed nothing on screen. The key now has a live channel in lib/settings-local.ts — `subscribeContextWindowUsage`, the same subscribe*/unsubscribe shape lib/theme.ts uses for the appearance picker — and components/context-meter.tsx reads the flag at mount, follows the channel, and draws its ring only while the switch is on. Toggling it takes effect in the already-open page; the composer mount point stays unconditional so there is one gate, not two that can disagree. The stored default stays "false", the desktop reference's default, and the bare "true"/"false" format stays: the key did not move onto the webui:ui:v1 envelope, which would have broken the reference-shared contract. What does change is that an untouched profile no longer sees the meter — it used to draw unconditionally while the switch did nothing. Also moves the panel's `expanded` hook above the component's early returns; it was declared after `if (!context || !context.limit) return null;`, which made a hook conditional on whether a snapshot had arrived. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The item was a placeholder claiming a browser cannot reach the OS file
manager. That was never true — `POST /api/fs/reveal` has been
implemented and registered all along (`server/routes/fs.js#handleFsReveal`,
`server/app.js`), and a webui install is normally the same machine that
holds the workspace. Only the wire was missing.
`webapp/lib/project-reveal.ts` carries the two claims the batch makes, as
injected-transport functions so both are driveable in `node:test` without
a render harness: `projectRevealTarget` resolves the project's path
(first repo root, else first directory — the same rule the 切换目录 row
already used, now sharing one helper), and `runProjectReveal` fires the
request and reports whatever did not work.
The pre-check is the interesting half. `revealInFileManager` RESOLVES
with `{ok: false}` on an HTTP error rather than rejecting, so a
try/catch-only implementation treats every containment refusal and every
missing opener as success and the user watches a menu that did nothing.
The `!result.ok` branch is asserted for that reason.
The row is disabled for exactly one reason now — the project is bound to
no local directory — and the tooltip says so in those words instead of
shrugging with `common.notLocal`. Success is silent (the file-manager
window is the feedback); failure goes to the same banner as the menu's
other writes, labelled with the menu's own localized name.
The SESSION-level reveal stays a placeholder: the desktop reference
disables it too, so there is no parity to chase, and unlocking it would
be a product decision this build has not made. Both menus live in one
file, so the session one is pinned as still-honest.
Tests: `webapp/test/project-reveal.test.ts` (19 cases across behaviour,
bilingual coverage, and menu wiring); the expired reveal half of
`shell-elements-parity.test.ts` is corrected. Six mutations verified red
(dropped path argument, swallowed refusal, re-hard-disabled row, pathless
click, retired tooltip, bypassed pre-check).
The settings page printed ten desktop shortcut rows disabled behind a 「浏览器环境不适用」 notice while app/page.tsx dispatched Ctrl+N and Ctrl+,. Both statements could not be true, and neither the page nor the handler owned the truth. webapp/lib/shortcuts.ts is now the single registry: per row it records the combination, whether the browser hands that combination to a page at all, and — when it does not — which of three reasons applies (the browser owns the combination, the WebUI has no surface, the action's semantics are undecided). app/page.tsx matches keydowns through it and the settings page renders it, so the two cannot drift apart. Unlocked: Ctrl+K (search surface) and Ctrl+Alt+O (new task) join the two bindings that already worked; the three live rows are rebindable, the overrides persist under webui-shortcut-bindings and are re-validated against the registry on read, and a combination another dispatched row already owns is refused with the conflicting action named. Ctrl+N stays dispatched but is labelled platform-limited rather than offered as rebindable, because the browser takes it on Windows and Linux. The notice now says what each state means, and every blocked row prints its own reason instead of sharing one blanket denial.
SB-7 — the A1 revision for the Token Plan view. Ticket 53's A1 ruling
("no source, so placeholder") was applied to the whole plan card, but two
sources exist: the plan quota over `POST /api/usage` (already live) and the
plan tier over `GET /api/account`. The card is now split by source instead
of by card.
- `PlanCard` takes the plan name and renders it verbatim; `planNameOf` is a
pure resolver (a failed account surface, no plan, and a blank tier all
collapse to null) and there is deliberately no default tier.
- The container fetches `/api/account` on mount, as the user menu's account
card does, and passes the resolved name down.
- Credits, expiry and invoicing stay placeholders, but the reason is now the
accurate one — the cloud account domain, which this self-hosted session
has no credentials for — instead of "not applicable to the local
edition", which was already false of the plan name above it.
The 「用量与模型」 tab's source switcher, its 「使用中」 badge and the
MiniMax API key row were three `useState` / `disabled` controls under a
comment claiming this repo has no `setMiniMaxModelSource` backend. The
four engine methods behind them have existed the whole time
(local-runtime-v2 `cli-service.ts`: getMiniMaxModelSource,
setMiniMaxModelSource, upsertMiniMaxApiKey, testUserModel) and had no
HTTP window. This adds the window and makes the tab's three claims
true.
Four new endpoints, all over `host.cliService` through the existing
`getEngineCatalogueHost()` facade:
GET /api/model-source the active source + masked key status
PUT /api/model-source switch the source
PUT /api/model-source/api-key upsert the key (absent = keep)
POST /api/model-source/test connectivity probe, stored key
The gate is on the LIVE member, not on a declaration: the four methods
hang off the v2 cli-service's own `modelProviders` requirement, which is
not one of the 14 declared capability keys, and adding a 15th for one
batch would restate every provider declaration and the snapshot audit
(PB-1 met the same situation for `pinSession` and resolved it the same
way). No host is 503, a host without the method is 501, and an engine
refusal keeps its own `LocalModelProviderError` status and code.
Four decisions worth stating:
- The keep-key sentinel. The GET can only return a mask and the engine
rejects a mask submitted as a key, so an absent or empty `apiKey`
keeps the stored one, calls no engine write, and answers
`{changed:false}` with the current status. Same convention and same
empty-string spelling as `PUT /api/providers`.
- Every write answers from a READ BACK, never from the request, so a
response cannot report a source or a key status the engine does not
hold.
- A key-status read that is missing or throws degrades to
`available:false` rather than to `hasKey:false`, which would tell a
user with a stored key that they have none.
- An error with no engine status becomes a fixed 500 whose body carries
no engine text: an exception string from an unrecognised thrower is
the one place a credential could still be echoed.
On the tab: the pill stays the VIEW and the badge is fed only by a
read-back, so a refused switch (the engine's `NO_API_KEY`) leaves the
key field the user needs on screen while the badge keeps showing what is
really in use. The probe reads the STORED key and says so
(`tested:"stored_key"`); the button is disabled while the field holds
an unsaved value, because v2's `testUserModel` takes no key override.
Not in this change: the add-model dialog's 「自动获取」 still resolves
against the built-in preset directory (v2 has no per-provider catalogue
query for an arbitrary key), and the Token Plan cards stay on decision
A1. Both are recorded in the module's KNOWN DEBT.
Verified on an isolated instance (own port, own engine data dir) against
the real v2 runtime: a synthetic key is stored and masked, `saveAndUse`
switches the source in one call, the switch survives a process restart,
and the probe returns a completed 200 with a real 401 status for that
key. Nine mutations were injected and each is killed by the suite.
…g fix) Squash of eight local commits (38dfe2cc..2a8b2285 on merge/dev-lhl); same final tree, single remote commit.
…ey card contradicting itself (P20, UAT4-1/4-2)
main's #160 squash carries the settings placeholder waves (SB-1..7), the P19 delete-hang fix and the P20 plan-name fix; resolving toward dev-lhl advances the merge base past the squash (SPEC rule 8).
…nthetical fix Squash of three local commits; same final tree as one delta from the 6e89ccf6-content remote tip.
fengzhi09
enabled auto-merge (squash)
October 3, 2026 16:17
fengzhi09
added a commit
that referenced
this pull request
Oct 3, 2026
main's #161 squash carries SB-10 (DOM harness), PB-3 (worktree page) and the docs fix; resolving toward dev-lhl advances the merge base past the squash (SPEC rule 8).
fengzhi09
added a commit
that referenced
this pull request
Oct 3, 2026
main's #161 squash carries SB-10 (DOM harness), PB-3 (worktree page) and the docs fix; resolving toward dev-lhl advances the merge base past the squash (SPEC rule 8).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
内容(#160 合入后 dev-lhl 的新增)
5a1ab444):webapp DOM 测试 harness——happy-dom 选型(4 传递依赖 vs jsdom 22)、renderToStaticMarkup的洞关闭(事件派发/effect/重渲可测)、快捷键重绑路径纳入+SB-2 变异 M9 补杀实证(新套件 4 红/旧 56 绿=当年存活的数字形态)+canUseDOM 静默陷阱入档。含 1 个 devDependency(lockfile 全 importer 变宽,消费方需pnpm install --frozen-lockfile——文档正文已写明)96b0d52c):工作树设置页——列表+三档时间筛选(边界含上界+缺失值三档全显)+一键移除(真删/not_found/dirty_worktree逐条拒绝形态)+PB-8 三态门控第一个消费者+无新建按钮=桌面形态诚实边界;顺带揪出 P21(locale 耦合判据,另批修复中);2 新端点bf56d466):过期「no DOM harness」括注修正门禁
各批独立 verify 18/18;双传输 acp 全量 0 fail;SB-10 合并后 webapp 2307 全绿(新依赖 install 实测);UAT 23 点轮(回归复核+console 自动断言四类全零)通过。