源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s

This commit is contained in:
agent-radar
2026-09-01 19:05:14 +08:00
commit 7be535059e
3358 changed files with 1306357 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Documentation archive
Files here are retained for historical context. They are not current contracts, roadmaps, or implementation instructions.
## Frontend
- `design-system-v0.2-wave-10.md` — the former design-system document; retired because stable rules, implementation snapshots, and future PR plans had become inseparable.
- `full-product-test-plan-2026-05.md` — a completed one-month delivery plan.
- `ui-quality-plan-2026-05.md` — a time-sensitive rollout and coverage snapshot.
- `design-refinement-roadmap-2026-07.md` — completed design decisions and an obsolete documentation backlog.
The root `DESIGN.md` owns shared product design intent. `docs/frontend-css-governance.md`, local frontend READMEs, source, and contract tests own implementation rules and executable behavior.
## Audits
- `maka-capability-audit-v1-2026-05.md` — a point-in-time capability and release-gate audit whose frontend authority references are no longer current.
## Validation snapshots
- `desktop-smoke-plan-legacy.md` — completed PR acceptance paths and deferred UI designs removed from the current executable smoke runbook.
## Former repository notes
Tracked `notes/` files were retired as an undocumented parallel authority. Point-in-time audits, migration plans, reference reverse-engineering, and design research remain here only for provenance. Current repository rules live in `ARCHITECTURE.md`, `DESIGN.md`, active cross-cutting contracts, local READMEs, source, and tests; workspace-specific agent instructions are supplied outside the tracked documentation set.
- `frontend-simplification-map-2026-07-13.md` — baseline knip/size measurement for the frontend simplification rounds landed in #871–#887.
- `composer-mentions-spec-2026-07-14.md` — the shipped v1 `@`/`/` composer mention-popup spec (#979); current behavior lives in `packages/ui` source and the composer-mention contract test.
## Security contract snapshots
- `memory-threat-model-pr-memory-1.md` — the contract-only PR-MEMORY-1 boundary, superseded as a description of the current product.
- `search-service-threat-model-pr-search-0.md` — the PR-SEARCH-0/1.5 boundary, superseded as a description of current search.
- `voice-threat-model-pr-voice-0.md` — the docs/core-only PR-VOICE-0 boundary, superseded as a description of current voice.
## Implementation plans
The dated runtime files are completed execution plans. Each file records the pull request that landed its implementation.
`heavy-task-mainline-system-design.md` preserves the original PR, trace, and rollout chronicle. The Headless heavy-task mechanism it describes was retired by #2598 and has no current implementation authority.
`deepseek-reasonix-cost-runtime-design.md` preserves the original implementation slices and open decisions. Backend architecture chapters and runtime source own current request-shape, replay, compaction, and cost mechanisms.
`runtime-kernel.md`, `runtime-v2-architecture-evolution.md`, and `runtime-mainline-teaching-manual.md` preserve the extraction rationale and overlapping teaching narrative that led to the current runtime. Current authority starts at `ARCHITECTURE.md`.
`runtime-v2-implementation-notes.md` records the initial Phase 1–4 runtime skeleton and is superseded by the backend architecture chapters and current source.
## Computer use follow-up records
The computer-use follow-up files are per-PR follow-up records, incident investigations, and run logs from the #857–#985 delivery chain. Current computer-use authority lives in the contracts listed in `../README.md` and in source and contract tests.
- `computer-use-delivery-state.md` — `outcome_unknown` delivery-state follow-up; postcondition semantics are owned by the foundation contract and runtime tests.
- `computer-use-evidence-hardening.md` — fail-closed report hardening; the current boundary is owned by the provider evidence contract and evidence classes.
- `computer-use-executor-hardening.md` — post-merge review record of PR #893; durable decisions are folded into the foundation contract and the model-loop foundation, remaining work belongs in issues.
- `computer-use-runtime-hardening.md` — lifecycle fix record of PR #892; current fencing is owned by runtime source and tests.
- `computer-use-physical-input-guard.md` — physical-input interference incident and the guard policy it produced.
- `computer-use-process-restart-e2e.md` — process-restart soak goal, investigation, and run logs; the PID-reuse non-claim is restated in the host events contract.
- `computer-use-real-ax-model-e2e.md` — real-model AppKit AX harness record and provider run logs; current evidence classes and operator commands live in `../computer-use-evidence-classes.md`.
## Reverse-engineering records
- `expert-team-plan.md` and `expert-team-implementation.md` — historical WorkBuddy and QoderWork reverse-engineering notes for the retired expert-teams feature (#971).
## Benchmark logs
- `economic-mechanisms-benchmark.md` — the first synthesis-cache benchmark entry for economic-mechanisms tracking issue #481 and slice #578. Current progress belongs in those issues.
@@ -0,0 +1,106 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Composer mention popups (`@` file / `/` skill) — v1 spec
Date: 2026-07-14
Branch: `feat/composer-mentions`
## What shipped
Typing `@` or `/` in the chat composer (at a word boundary) opens a popup:
- `@` → workspace file reference. Filtered live against `workspace:searchFiles`
(git `ls-files` in a repo, bounded readdir walk otherwise). Selecting a file
inserts the plain-text token `@<relativePath> ` (with the `@` and a trailing
space).
- `/` → skill reference. Filtered client-side against the enabled skills list.
Selecting a skill replaces the `/query` token with `使用 <skillName> 技能:` —
the exact house convention from `useSkillInChat` (app-shell.tsx). This is
human-in-the-loop: it fills the composer, it NEVER auto-sends.
## Competitor model vs. our v1 model
**Competitors (QoderWork / WorkBuddy, from decompiled bundles):** the composer
is a `contenteditable` surface. A mention becomes an *atomic chip* — a
non-editable inline node carrying a typed wire token (`@[file:/abs/path]`,
`@[skill:id]`) that the runtime parses out of the message. Backspace deletes the
whole chip; the rendered label and the wire token are decoupled.
**Our v1:** the composer is an *uncontrolled native `<textarea>`*
(`packages/ui/src/composer.tsx`; text lives in the DOM, not React state). Rather
than migrate to `contenteditable` + a chip model, v1 inserts **plain text
tokens** directly into the textarea value:
- File → `@<relativePath> `
- Skill → `使用 <skillName> 技能:`
Our agent runtime already reads files by path via its tools, so a bare path
token is sufficient context — no `@[file:…]` wire format, no chip nodes, and no
runtime changes are needed. The popup is pure composer-local UI.
## Trigger detection (pure, unit-pinned)
`detectMentionTrigger(value, caret)` in `packages/ui/src/chat-input-behavior.ts`
(tests in `packages/ui/src/__tests__/chat-input-behavior.test.ts`):
- The char before a trigger must be start-of-input or whitespace, and this
boundary rule defines what *counts* as a trigger: a `/` inside a path
(`@src/app`) or an `@` inside an email (`user@host`) is NOT a trigger, just
query text. Detection scans left from the caret for the **nearest
boundary-anchored** `@`/`/` (this is the "consider the nearer one" rule
applied to real triggers).
- `@` query: invalid on `\n` or a double space `" "` (single spaces allowed —
filenames have spaces).
- `/` query: invalid on ANY whitespace (single-token).
- Returns `{ trigger, query, start }` where **`start` is the index of the
trigger char itself**.
Matcher `mentionQueryMatches(query, text)`: case-insensitive AND-of-substring
over whitespace-split tokens.
## Deviations from the synthesized spec
1. **Insertion splice range.** The spec text said "replace `[start-1, caret)`",
which assumes `start` points *just past* the trigger. Our unit-pinned
`detectMentionTrigger` returns `start` = the trigger-char index, so Composer
replaces `[start, caret)` (trigger char inclusive). Same net behavior (the
trigger char is replaced); the field just has a cleaner meaning.
2. **Boundary-anchored nearest trigger (not raw `lastIndexOf`).** The spec's
literal "last `@` and last `/`, nearer wins" makes a path-internal `/` (e.g.
`@src/app`) the nearer trigger; since it fails the boundary check the whole
popup would close the instant you type a slash — breaking the primary use of
`@` (file paths). Because the spec explicitly wants `@` queries to hold
slashes and spaces, the boundary rule is applied *while choosing* the
trigger: only boundary-anchored `@`/`/` are candidates, and the nearest such
candidate wins. A non-boundary `@`/`/` is treated as query text. Pinned by
the `chat-input-behavior` tests (`@src/app` → `@` with query `src/app`).
## v2 upgrade path
If/when richer references are needed (display label ≠ inserted value, atomic
delete, structured wire tokens, `@`-mentioning symbols/URLs):
1. Migrate the composer to `contenteditable` (or a controlled rich editor).
2. Insert mentions as atomic inline chip nodes carrying a typed token
(`@[file:…]`, `@[skill:…]`).
3. Serialize chips → wire tokens on send; teach the runtime to parse them.
4. Caret-rect anchoring for the popup (v1 is bottom-anchored above the textarea;
caret-rect measurement was explicitly out of scope).
@@ -0,0 +1,45 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Computer Use Delivery State
This follow-up preserves delivery uncertainty after a native or Electron action
has reached the executor.
## Problems
- AX and CDP text writes became `capture_failed` when readback did not confirm
the value, even though the write had already been delivered.
- A successful semantic action became `capture_failed` or
`sensitivity_blocked` when its required fresh screenshot failed.
- A failed screenshot observation was stored before normalization and could
evict an earlier usable observation.
- The model-facing description still claimed Electron text was always refused.
## Fix
- Delivered but unverifiable writes and semantic actions return
`outcome_unknown`.
- Fresh-capture errors after dispatch retain the action's delivered state.
- Observations enter the bounded FIFO only after screenshot normalization
succeeds.
- The tool description documents the unique CDP click and text path.
These changes do not weaken pre-dispatch freshness, identity, occlusion, or
physical-input checks.
@@ -0,0 +1,59 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Computer Use Evidence Hardening
This follow-up makes real-model reports fail closed as evidence.
## Unified Report Boundary
Every direct or Desktop report passes through one schema-driven sanitizer.
Only validated attribution, terminal state, action summaries, synthetic fixture
state, assertion results, and allowlisted dispatch evidence survive.
Prompts, screenshots, coordinates, typed text, raw UI content, arbitrary trace
strings, provider bodies, and outer exception messages are excluded.
## Qualification
A real provider matrix cell requires:
- schema version 1 and `real-runtime` evidence;
- matching provider, model, scenario, producer, and live transport;
- launcher status `pass` and terminal `complete/end_turn`;
- successful allowed actions bound to the owned fixture;
- minimum and maximum per-action counts plus the total action budget;
- canonical expected-state and forbidden-effect assertions;
- AX or semantic dispatch evidence for mutation scenarios.
Missing evidence is invalid or inconclusive, never a pass.
## Runtime Guard
Real-model mode requires an explicit policy. It enforces exact fixture app
selectors, observation ownership for later semantic actions, per-action counts,
and the total count before dispatch.
Scenarios with dedicated runners or unavailable execution capabilities fail
closed in the ordinary launcher.
## Fixture Safety
Partial fixture construction destroys already-created windows. Replacement
scenarios enumerate surviving windows rather than stale scenario declarations.
@@ -0,0 +1,75 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Computer Use Executor Hardening
This note records the post-merge review of PR #893 against the current
`cua-driver` executor and the local Codex Computer Use reverse-engineering
evidence.
## Fixed In This Follow-Up
- Semantic refetch now requires one unique role/label/value candidate and then
verifies that its frame, depth, and value still match the observed control.
A same-label replacement or ambiguous candidate set fails closed.
- Native content fingerprints include label and value, so a control changing
meaning in the same structural slot invalidates coordinate actions.
- Window screenshots use the same compression threshold and 8 MiB cap as
desktop screenshots.
- Unconsumed observations are bounded to 16 per session and evicted FIFO.
- Keyboard ownership is invalidated when the bound PID or window no longer
matches the click-established target.
- Delivered but unverifiable Electron pointer actions and delivered text writes
preserve `outcome_unknown` instead of becoming retryable `capture_failed`
results.
- `select_text` and `secondary_action` fail closed because the pinned driver
registry does not expose their claimed tools.
## Deliberately Not Changed
- Coordinate click, scroll, drag, and key dispatch remain disabled by default.
Re-enabling the compatibility CGEvent path would restore the physical-input
interference found during real-machine testing.
- The physical-input callback remains optional at this executor layer because
semantic AX/CDP operations are also used by non-Desktop hosts. Desktop wiring
must supply the guard before advertising concurrent-user safety.
- The process-wide operation queue remains global. Maka currently owns one
action-child stdio connection, and the fresh-snapshot/action pair must remain
atomic across that shared connection. The Codex native service supports
concurrency through separate connections while serializing one connection
and one application instance. Removing the queue without introducing
separate service connections broke the existing ordering contract.
## Remaining Work
- If cross-session concurrency becomes necessary, create isolated driver
connections or per-target service instances and preserve snapshot/action
transactions explicitly.
- Initial `observeApp` failures cannot return the full typed capture result
through the current observation-only interface. The backend still enforces
the screenshot cap, but the Runtime interface needs a result-bearing
observation contract to preserve `sensitivity_blocked` end to end.
- Continue real-provider model-loop testing with coordinate actions fail closed
until an isolated native event executor exists.
## Verification
The focused `@maka/computer-use` suite passes 111 tests, including semantic
replacement, registry mismatch, observation eviction, window compression,
shared-client ordering, lifecycle error, and keyboard-target regression cases.
@@ -0,0 +1,116 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Computer Use Physical-Input Guard
## Reported Symptom
During the no-focus process-restart soak, the user's pointer did not move and
the synthetic fixture never became frontmost. The user still observed
occasional abnormal physical clicking while continuing to use the Mac.
The symptom cannot be dismissed as general system load. The Computer Use pixel
path posts mouse down/up events to a target PID. Those events do not warp the
real cursor, but they can still interleave with physical mouse events in the
system and application event queues.
## Root Cause
Runtime already models physical intervention:
```text
physical input
-> user_intervened
-> invalidate the observation
-> require a new observation
```
Desktop did not provide a real macOS physical-input producer or pre-dispatch
guard. The concurrent E2E allowed user input while the backend continued to
dispatch target-PID mouse events.
## Final Fix
Desktop supplies a host-owned guard using:
```ts
powerMonitor.getSystemIdleTime() < 1
```
Electron reports whole idle seconds, so this is a conservative approximately
one-second quiet window rather than a precise millisecond deadline.
The backend checks the guard at the last practical boundary before every
mouse, keyboard, semantic, scroll, drag, or text dispatch. If input is active,
or if the guard cannot establish the idle state, it returns:
```text
user_intervened
```
No cua-driver input tool is called. Runtime invalidates the consumed
observation and requires an explicit new observation before another action.
Live testing then proved the quiet window alone was not a sufficient safety
claim: PID-bound CGEvent mouse delivery can still interfere with physical
button state when the user and agent overlap.
The production policy therefore also disables the compatibility event backend
by default:
- coordinate click, right/middle/double/triple click: blocked;
- scroll and drag: blocked;
- `press_key`: blocked;
- semantic action fallback to pixel: blocked.
The retained paths are:
- observation, screenshots, wait, zoom, and virtual cursor presentation;
- native AX element actions and `set_value`;
- uniquely targeted Electron CDP semantic actions and `insertText`.
Compatibility dispatch can only be re-enabled through an explicit backend
option used by controlled tests. Desktop does not set that option.
## Verification
Unit and integration coverage proves:
- coordinate and semantic input return `user_intervened`;
- no `click` call reaches cua-driver;
- no dispatch trace is emitted for a guarded action;
- selector and Desktop host wiring preserve the guard;
- the Desktop production source uses the Electron idle-time signal;
- existing Computer Use behavior remains green.
The real restart soak no longer emits compatibility mouse or keyboard events.
It proves stale-process rejection and fresh-process recovery with native AX
`set_value` plus exact state readback. Physical-input fencing remains covered
by unit/integration tests without synthesizing global HID events.
The soak still launches and orders a real visible AppKit fixture window.
`open -g` avoids explicit activation, but launch can remain noticeable and may
perturb WindowServer responsiveness. It is not a zero-disturbance
background-run proof.
## Remaining Boundary
Re-enabling coordinate input requires a native executor that can prove its
event backend does not affect global physical button state. A host-side quiet
window alone is not sufficient evidence.
@@ -0,0 +1,195 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Computer Use Process-Restart E2E
## Goal
Prove that a Computer Use observation from one live application process cannot
authorize an action after the same canonical application path is relaunched as
a new process.
The expected lifetime follows the recovered Codex boundary:
```text
canonical application path
+ current live process instance
```
Conversation or transport continuity must not extend an observation across a
real target-process restart.
## Incident And Investigation
The first real-machine harness attempts exposed several independent test
assumptions before reaching the process boundary:
1. A fresh worktree had no workspace `node_modules`, so downstream packages
could not resolve `@maka/core`. Running `npm ci` established the same
dependency layout used by CI.
2. The ignored cua-driver artifact was absent in the new worktree. The launcher
now runs `prepare:cua-driver` and then verifies the pinned artifact before
starting the fixture.
3. WKWebView AX nodes can appear late and can be mirrored with duplicate labels
and different frames. The restart gate therefore uses the native
`CUA Lab Coordinate Target`; OOP behavior remains covered by the dedicated
guarded E2E.
4. Activating the fixture made the test pass but stole the user's focus.
`open -g` avoided activation but kept the app hidden from the driver's
on-screen window set. The final fixture mode uses LaunchServices background
launch plus `unhideWithoutActivation`, `moveToActiveSpace`, and
`orderFrontRegardless` so the window is on-screen without becoming the
frontmost application.
5. Runtime tool failures are projected through model-visible text rather than a
top-level `error` property. The gate verifies both the backend typed outcome
and the model-visible `target_missing` result.
These were harness defects or environment prerequisites. None dispatched an
action before the target-process identity gate was reached.
## Real Sequence
The launcher and harness keep one cua-driver backend and one Runtime tool
instance alive across five target restarts:
```text
repeat 5 times with one backend/Runtime instance:
-> observe current PID/window
-> bind coordinate action
-> terminate current app
-> launch same canonical app path
-> require globally new host PID and WebContent PID
-> attempt old observation
-> require target_missing, no dispatch, mutation 0 -> 0
-> clear session
-> observe new PID/window
-> execute fresh native AX set_value action
-> if visible, require exact readback on the new process
-> if covered by the user's window, require target_occluded and zero mutation
-> require cua-driver generations stable and restartAttempts == 0
```
The launcher owns sleep prevention, fixture cleanup, bounded child
termination, and private temporary handshake/report files. The synthetic app
is launched with `CUA_LAB_BACKGROUND=1`. In that mode the fixture orders its
window visible without activating the application or making it the user's
frontmost app. A continuous Swift sentinel allows the user to switch among
their own applications, type, and move the pointer, but fails immediately if
the synthetic fixture ever becomes frontmost or the screen locks. Cleanup
preserves the user's current application; restoration is only an emergency
path if the fixture itself stole focus.
The fixture is still a real visible AppKit window. Background launch avoids
explicit activation, but window creation and `orderFrontRegardless` can remain
noticeable and can perturb WindowServer responsiveness. This is an attended
release test, not a zero-disturbance background-run proof.
Physical user pointer movement is allowed and reported as observation data.
When the user's window occludes the target, the stronger non-interference
proof is that the backend emits no dispatch and both target and decoy mutation
remain zero.
## Historical Pixel Result
Before compatibility input was disabled, the July 14, 2026 five-round
no-focus pixel soak proved:
```text
restart rounds: 5
distinct host PIDs: 6
distinct WebContent PIDs: 6
old observation per round:
outcome: target_missing
native dispatch: none
target mutation: 0 -> 0
fresh observation:
background px success: 4 rounds
fail_closed_occluded: 1 round
decoy mutation: always 0 -> 0
cua-driver service:
action generation: 1 throughout
capture generation: 0 throughout
restartAttempts: 0 throughout
desktop concurrency:
fixture became frontmost: never
user pointer moved: 151.2 logical points
user app switching: allowed
```
The occluded round proved:
```text
outcome: target_occluded
native dispatch: none
target mutation: 0 -> 0
decoy mutation: 0 -> 0
```
The test command is:
```bash
npm run computer-use -- real-ax --scenario restart-recovery
```
## Current AX-Only Result
The replacement soak uses AX `set_value`, not pixel input. The corrected
five-round run proved:
```text
restart rounds: 5
old observation:
target_missing: 5/5
native dispatch: 0
fresh observation:
AX set_value + readback: 5/5
cua-driver service:
action generation: 1 throughout
capture generation: 0 throughout
restartAttempts: 0 throughout
desktop concurrency:
fixture became frontmost: never
user pointer moved: 217.1 logical points
user mouse/keyboard: reported normal
```
The first AX-only attempt had already confirmed stale-process rejection and a
successful fresh AX mutation, but used the wrong external oracle: direct
AXValue mutation does not invoke the fixture's Cocoa callback that writes
`state.json`. The final run instead verified the fresh AX observation returned
by Runtime.
## Remaining Boundary
This proves ordinary process restart isolation. It does not force the operating
system to reuse the old numeric PID. PID-reuse safety still requires either:
- a native atomic process-instance identity exposed by the executor; or
- a deterministic driver/host fixture that can substitute a new process under
the same PID-shaped identity.
The current fail-closed old-window lookup is sufficient for the observed real
restart, but it is not evidence for deliberate PID reuse.
@@ -0,0 +1,155 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Real Model To AppKit AX E2E
## Scope
This harness connects a live provider model to the production Maka model and
tool path, the pinned `cua-driver`, and the owned `Codex CUA Lab.app`.
```text
live provider
-> getAIModel
-> AiSdkBackend / streamText
-> ToolRuntime
-> maka_computer
-> cua-driver
-> AppKit Accessibility element action
```
The fixture launches in the background and never uses explicit activation.
The launcher owns `caffeinate`, lock/frontmost monitoring, temporary files,
fixture shutdown, and child termination.
The action policy allows only scenario-specific subsets of:
```text
list_apps
observe
click_element
set_value
wait
```
Coordinate click, scroll, drag, type, key input, and compatibility pixel
fallback are not available. A read-only HID age probe fences native semantic
dispatch while the user is actively providing physical input.
## Real Results
### OpenAI through Azure bridge
`gpt-5.6-sol` completed:
- observe-only: two tools, zero dispatch;
- AX set value: one verified AX dispatch, zero pixel dispatch;
- physical intervention recovery: first mutation returned
`reobserve_required`, then the model observed and succeeded;
- process restart recovery: the old semantic action returned
`target_missing`, then the model rediscovered the new process and succeeded;
- AX click: one `click_element` dispatch and the external AppKit button oracle
changed exactly once.
- safe multi-step task: `set_value` followed by `click_element`, each using the
fresh observation from the prior step; one dispatch per semantic action and
zero pixel dispatch.
### Anthropic through coproxy
`claude-sonnet-4-6` completed:
- AX set value through the full `AiSdkBackend` product path;
- physical intervention recovery with the same re-observe sequence;
- one verified AX dispatch and zero pixel dispatch in both mutation runs.
- the same safe multi-step `set_value` then `click_element` task through the
full product loop.
### Kimi and MiniMax protocol paths
No live Kimi or MiniMax credential is configured on this machine. Their
product paths were therefore validated as `hermetic-protocol`, not
`real-runtime`.
Both `kimi-coding-plan` and `minimax-coding-plan` completed:
```text
getAIModel
-> Anthropic-compatible streaming protocol
-> AiSdkBackend
-> ToolRuntime
-> maka_computer
-> list_apps -> observe -> set_value -> final response
```
The server fixtures verify each provider's exact URL prefix, API-key header,
model ID, streaming `tool_use`, tool-result reinjection, and final semantic
state. They also verify that a failed semantic action is reinjected as
`tool_result.is_error:true`; Runtime records the failed result once, while the
AI SDK adapter exposes it to the provider as a recoverable tool execution
error.
## Dynamic Structure Finding
The model observed one stale target. An independent AX setup action inserted a
second target with the same semantic identity before the model action.
The real executor returned `target_changed`, with zero model dispatch and zero
target mutation. The content fingerprint gate currently runs before strict
element identity refetch. This is safe, but stricter than the recovered Codex
behavior where semantic element actions may continue to a unique/missing/
ambiguous refetch decision after an AX structure change.
The finding is reported on executor hardening PR #910. The model-runner treats
either strict refetch rejection or conservative `target_changed` as a
fail-closed result and records the exact result code.
## Runtime Finding
Real model runs exposed that returned Computer Use results with a top-level
`error` were being recorded as successful tool invocations. A model could also
retry the same ambiguity-rejected semantic target after a new observation
because the generic loop signature included the changing observation ID.
The independent Runtime fix:
- classifies returned top-level errors consistently for persisted results,
events, telemetry, and loop control;
- gives explicit ambiguity failures a stable semantic signature that ignores
observation ID;
- blocks the next identical ambiguity retry before native execution;
- does not cache recoverable `user_intervened` or `target_missing` outcomes.
The real evidence sanitizer also previously hard-coded an OpenAI producer.
Reports now preserve an explicit producer and provider while retaining the
same privacy projection.
## Evidence Finding
The Desktop runner previously inferred fixture windows from action output, so
an incorrect action window could add itself to the ownership allowlist. The
launcher now discovers the exact fixture PID/window set independently before
the model runs, binds actions by tool-call dispatch traces or observation
lineage, and waits for trace flush before qualification.
## Evidence Privacy
Reports are launcher-owned temporary files with mode `0600`. The sanitized
projection retains provider/model, action types, result codes, aggregate
state, and allowlisted trace fields. It drops coordinates, typed values, AX/UI
text, credentials, URLs beyond origin, and raw provider payloads.
@@ -0,0 +1,78 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Computer Use Runtime Hardening
This follow-up addresses lifecycle gaps found during review of PR #892.
## Problems
- `clearSession()` did not create a stop tombstone when no session-state record
existed yet, so a first queued invocation could activate after cleanup.
- Read-only host actions did not acquire a session lease and could continue
after `user_stopped`.
- Later lifecycle events could overwrite `blocked_url` or `user_stopped`.
- A process-wide executor queue blocked unrelated windows, while a first
per-session replacement allowed two sessions to interleave
snapshot/validation/dispatch against the same window.
- The ambiguity gate included ephemeral element IDs even when a stable element
identity was available.
- `cursor_position` formatting existed without a production backend result.
- Service release reasons did not distinguish a session-only notification from
a real child-generation release, so `clearSession(A)` could discard retained
observations and keyboard ownership for unrelated session B.
## Root Cause
The Runtime treated observation and mutation leases as the only operations that
needed lifecycle fencing. Cleanup also mutated only an already-created state
record, while terminal transitions shared the same unrestricted transition
helper as recoverable states.
The executor queue was scoped to the caller rather than the resource being
validated. Ambiguity signatures also mixed stable identity with snapshot-local
IDs, and the cursor result test used only a fake backend.
The backend also inferred generation loss from the release reason. The
no-in-flight `clearSession()` path emits `session_cleared` without stopping the
child, so the reason alone cannot establish that shared executor state changed.
## Fix
- Create the same-turn stop tombstone unconditionally during `clearSession()`.
- Require an observation lease for every host-reading or waiting action.
- Make `blocked_url` and `user_stopped` absorb later lifecycle events.
- Serialize mutations by bound PID/window, with one conservative queue for
unbound mutations; unrelated bound windows may still progress independently.
- Ignore ephemeral element IDs when a stable element identity is present.
- Read `get_cursor_position` through the pinned cua-driver and return the
resolved point without moving the pointer.
- Carry `generationReleased` as an explicit service release fact. Session-only
releases invalidate only their listed sessions; actual child exits sweep all
retained session observations and keyboard targets.
A new turn still creates a fresh Computer Use session state, preserving the
existing explicit recovery boundary.
## Verification
- `npm --workspace @maka/runtime run typecheck`
- `npm --workspace @maka/runtime test`
- `npm --workspace @maka/computer-use test`
- `git diff --check`
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,237 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Maka 质感提升路线图(2026-07)
> Archived on 2026-07-13. Completed decisions remain visible for provenance; unfinished queues must be tracked in GitHub issues rather than treated as a current documentation backlog.
> 来源:四个设计 skill 资源的系统学习(taste-skill / superdesign /
> ui-skills.com 生态 / vercel-labs agent-skills),对照 maka 现状
> (2026-06-24 skills 轮已 shipped 项 + 2026-07-03 三层 code review)
> 提炼的真增量,加上产品 owner 拍板的四个路线决策。
>
> 阅读前提:`notes/ui-skills-deep-read-2026-06-24.md`(已 shipped 项,
> 避免重做);`docs/frontend-css-governance.md`(token 治理规则)。
## 0. 已拍板的路线决策(2026-07-03,jackwener)
| # | 决策 | 含义 |
|---|------|------|
| D1 | **转向 4pt spacing 系统** | 所有 padding/gap 收敛到 4/8/12/16/24/32;允许偏离 QoderWork 逐像素值 1-2px。「高级感来自一致性而非对标精度」 |
| D2 | **Dark mode 一等公民** | dark 需要独立 elevation 体系(阴影塌缩为 border ring)、语义色 desaturate;UI PR 双主题截图验收 |
| D3 | **动效人格:快而准** | 高频操作(切会话/发送/palette)零动画;其余全部 <300ms 且取区间下限;AI 流式输出的 typing 节奏除外(那是语义不是装饰) |
| D4 | **暂不做签名元素** | 先把 QoderWork 镜像的基本功(layering/圆角/阴影/状态)做精,视觉身份创新延后 |
## 1. 四源精华 × maka 适用清单
只列「真增量」——已 shipped 项见 ui-skills-deep-read 笔记。
### 1.1 表面层级体系(interface-design "subtle layering",最高杠杆)
- Surface 亮度逐级只差几个百分点:shell(base) → 白卡(+1) → popover/dropdown(+2)。
dropdown 必须比它的父表面高一级。
- **Input 比周围略暗**(inset 隐喻「往里输入」):composer、搜索框、
palette 输入行。当前 maka 输入面与卡面同色 → 缺一级。
- Border 四档递进:standard / softer / emphasis / focus-ring,全部
低透明度 rgba/oklch alpha 而非实色 hex。
- Depth 策略四选一并从一而终:maka 的选择 = **白卡 layered shadow +
卡内 hairline border**,写死,不混用。
### 1.2 阴影(taste-skill + interface-design 共识配方)
- Light mode 三层:`0 0 0 1px α.06 / 0 1px 2px -1px α.06 / 0 2px 4px α.04`,
全部 oklch(from var(--foreground)),opacity 永不超 0.05-0.06。
- Hover 提升:`0 2px 8px α.04`,200ms。
- **Dark mode 阴影塌缩为单 ring**:`0 0 0 1px oklch(1 0 0 / 0.08)`(D2)。
### 1.3 圆角(同心圆角公式)
- **嵌套时内 radius = 外 radius − padding**。审计对象:灰壳→白卡、
气泡→内嵌代码块、卡→内部输入框、modal→内部卡。
- 现有 4 档 token(control 6 / surface 8 / modal 12 / pill 999)保留,
公式约束的是嵌套关系而非档位本身。
### 1.4 间距(D1:4pt 网格)
- 新代码强制 4 的倍数;存量逐步迁移(每次触碰某文件时顺手收敛该文件)。
- 疏密节奏:相关项 8–12px 紧组,组间 24–32px(桌面工具档,非 landing
的 48-96px)。
- 治理方式:契约测试对新增 CSS 声明检查(存量 allowlist 冻结)。
### 1.5 动效(D3:快而准,Emil duration 表)
| 交互 | 时长 | 备注 |
|------|------|------|
| 高频操作(切会话/发送/palette 开合) | **0ms** | 每天百次的操作不配动画 |
| 按压反馈 | 100–160ms | `:active scale(0.97)` 已 shipped |
| tooltip / 小 popover | 125–200ms | origin-aware |
| dropdown / select | 150–250ms | |
| modal / drawer | 200–300ms | 取下限 |
| AI 流式 / typing 指示 | 语义节奏 | 不受 300ms 上限约束 |
- 永不 ease-in;永不从 scale(0) 入场(0.95 起);出场比入场快。
- 可中断 UI 用 transition 不用 keyframes。
- hover 动画包 `@media (hover: hover) and (pointer: fine)`。
### 1.6 文字层级(收敛,而非增加)
- 三档制:primary(`--foreground`,100% ink)/ secondary
(`--foreground-secondary`,80% ink)/ muted
(`--muted-foreground`,50% ink)。`--foreground-N` 十几档是
层级糊的来源,UI 新代码只从三档语义别名取值。40% 和 50% 肉眼
barely 可分,桌面工具场景不需要在"不重要"内部再分一档,所以
从原四档制进一步收敛到三档。
- CJK 注意:Windows 中文字体缺中间字重 → color/opacity 杠杆权重高于
weight;**禁收紧 CJK letter-spacing**(拉丁负 tracking 规则不适用)。
- 数字全面 tabular-nums(token 数、时间戳、计数)——部分已 shipped,
按面补齐。
### 1.7 对比度硬线
- 正文与 **placeholder 均 ≥4.5:1**(「浅灰显优雅」是中文小字的头号
可读性杀手);大字 ≥3:1。进 visual audit 流程。
### 1.8 工程质量清单(vercel web-interface-guidelines 摘录)
- flex/grid 文本容器 `min-w-0` 才能截断(已有多起此类 bug)。
- modal/drawer 内 `overscroll-behavior: contain` 防滚动穿透。
- 破坏性操作永不立即执行(confirm 或 undo 窗口)。
- 提交中按钮转 spinner 但不提前 disable;错误 inline + focus 第一个错误。
- 拖拽时禁文本选择;`h-dvh` 不用 `h-screen`。
### 1.9 明确不采纳(避免后人重提)
- Display 字体池 / serif 纪律 / hero 字号 —— 中文 + system stack 无意义
- GSAP scrolltelling / marquee / 磁性按钮 / bento —— marketing 专属
- em-dash 全域禁令 —— 中文破折号「——」是合法标点
- 「禁版本号」—— 桌面工具 About 页显示版本正当
- superdesign 的 AI 消息入场 600ms —— 与 D3 冲突,取 Emil 值
## 2. 落地队列(按杠杆排序)
1. **P-SHADOW** ✅ (#457):三层阴影配方升级 + dark 塌缩 ring
2. **P-INSET** ✅:palette/搜索输入行 3% 前景 wash step-down;通用
Input/Textarea 2% wash。composer 例外保持 elevated —— 它是主命令台
不是表单字段(PR-UI-LAYOUT-8 的有意设计)。
3. **P-RADIUS** ✅(首轮):palette/搜索 modal 输入行按同心公式修正
(12−8=4px,写法 `calc(var(--radius-modal)-8px)` —— 契约的 calc
allowlist 要求无空格)。其余嵌套对(settings modal 内卡、tool card
内 pre)审计通过或留待触碰时修正。
4. **P-MOTION** ✅(已达标,无需动作):duration token
120/150/180/280ms 完全符合 Emil 表且取下限;装饰性入场动画已被
#406 gap 3 清除;剩余 keyframes 全部是功能性流式动画(D3 豁免)
5. **P-TEXT** ✅(首轮):孤儿档 -20/-30 清除(8 处调用点:文字并入
-40 兼修对比度、装饰内联 color-mix);文字主力收敛为
40/50/60/70/80。全量四档语义别名迁移留待逐面触碰。
6. **P-4PT** ✅✅(#448 完全体):#459 的渐进 ratchet 服役半天后被
#448(#430 PR3)取代 —— 它修复了 --spacing 0.25rem=3.75px 的真 bug
(Tailwind 与手写 px 两把尺),全量 687 CSS + 125 TSX 收敛到 14 档
--space-* scale,并用 spacing-converge-contract 硬 ban bare px。
ratchet 契约已删除(一个机制,留强的)
7. **P-DARK** ✅:elevation 独立化由 P-SHADOW dark-collapse 完成;
语义色审计通过(dark 的 chroma 微调是感知补偿而非过饱和,
accent 0.135→0.15 配 L+0.04 在暗底维持等感知强度)
8. **P-STATE** ✅(审计轮):技能/回顾/聊天 empty 构图完备;发送失败
= toast + turn 级失败徽章双通道;会话列表本地快路径合理缺席
skeleton(300ms 延迟原则,本地 SQLite <50ms);无通用 Skeleton
组件正符合「形状对齐」原则。缺口留逐面触碰时补。
每项独立 PR,双主题截图验收(D2),契约测试锁行为。
## 3. 队列完成记录(2026-07-03 首轮全清)
- #457 P-SHADOW(roadmap + 阴影配方 + dark collapse)
- #458 P-INSET + P-RADIUS 首轮 + P-MOTION 审计
- #459 P-4PT ratchet 契约 + P-TEXT 孤儿档清理
- 前置质感修复:#454(玻璃色板复活/token 卫生)、#452(blocked
语义/时间戳/i18n)、#451(copy-feedback 契约同步)
- P-FOREGROUND-TIER-CONVERGE(issue #430 PR4):5 档文字梯
(40/50/60/70/80)收敛到 3 档语义别名 — `--foreground`
(100% ink)、`--foreground-secondary`(80% ink)、
`--muted-foreground`(50% ink)。-90/-95 零调用直接删。
444 处调用点全量替换,契约测试锁住文字用法不回退到裸
mix stops。
下一轮候选(按杠杆):P-STATE 边缘面补齐、storybook Design
System 页同步新阴影/层级规则。
(原「文字档 60/80 的逐面归并」已由 P-FOREGROUND-TIER-CONVERGE
解决,落地为 3 档制而非 4 档 — 50% 和 40% 肉眼 barely 可分,
桌面工具场景不需要在"不重要"内部再分一档。)
(原「module-pages 76 个 off-grid 值」已由 #448 全量收敛解决。)
## 4. 第二轮全量精读增补(2026-07-03,四源全覆盖)
> 覆盖:taste-skill 全部 26 文件(含 v2 遗漏章节、v1 §9、research/laziness)、
> ui-skills registry 全部 126 条目(~110 份独立文档)、vercel-labs 88 文件
> (react-best-practices 70 rules 全读)、superdesign 全段亲读。
> 原文缓存:session scratchpad `skills2/` 与 `agent-skills/`。
### 4.1 新增可落地规则(相对 §1 的真增量)
**Motion 增补**
- **300ms loading 阈值**:加载 <300ms 什么都不显示,杜绝闪烁 skeleton(ui-ux-pro-max)
- **exit = enter × 60–75%**;动画必须可中断(快速触发用 transition/spring 可重定向,禁 keyframes 重播);context menu 只做退场动画(Emil review-animations / transitions.dev)
- **80ms 瞬时感知阈值**(micro-interaction 目标);「慢即真」:AI 生成类复杂操作的可见工作过程传达真实感(pbakaus animate — 属 D3 的流式语义豁免区)
- **transitions.dev 参数表**:dropdown open 250/close 150ms、pre-scale 0.97;modal 250/150、scale 0.96;text swap 200ms+8px+blur 2px;error shake 6px+4px overshoot、3s 自动回退;replay 前 `void el.offsetWidth`
- **换字元素宽度锁定**:状态标签/token 计数/「已复制」按最宽态锁 min-width —— chat 工具最高频 layout-shift 根治(compact-landing)
- spring 决策表:手势/可中断→spring(500/30);系统状态→easing;时间表征→linear;高频→零动画
**Color/表面增补**
- **灰字禁上彩底**:彩底文字用同色相深一档或前景加 alpha(pbakaus 三处反复强调)
- **OKLCH 三定律**:修对比只调 L 锁 C/H;palette 各阶 hue 漂移 >10° = bug;跨 hue 等鲜艳度用 max-chroma 相同百分比(oklch-skill)
- dark 深度 = 三档 surface 亮度(15/20/25% L 同色相),不靠阴影 —— 与 P-SHADOW dark collapse 互证
- alpha 滥用是 palette 不完整的信号:除 focus ring 外定义显式 overlay 色(对 @layer 陷阱也更友好)
- Anti-Nested-Box:卡中卡中卡硬 ban;密集区用 border-t/divide-y/负空间替代套卡
**排版/细节增补**
- `…` 不用 `...`(loading 文案以 … 结尾);快捷键 `⌘ K` 用 nbsp;ALL-CAPS 短标签 +5–12% tracking
- 暗底亮字三轴补偿:line-height +0.05–0.1、letter-spacing +0.01–0.02em、字重调档
- italic 含降部字符(y g j p q)最低 leading-[1.1]
- kbd 键帽 recipe:1px 边 + 4px radius + 浅底 + mono(桌面快捷键提示刚需)
- 负 inset 伪元素扩热区(44px 达标不改布局);`dialog::backdrop` + blur 4px 替代手写遮罩
**治理增补**
- **icon 四条**:单 family、stroke 统一 1.5/2px、filled-outline 不同层级混用禁、尺寸 token(16/20/24)
- z-index 显式 scale 立法(maka 已有语义 z token ✓ 复核覆盖率即可);modal scrim 40–60% black
- toast 纪律:3–5s 自动消失、aria-live polite、永不作关键信息唯一渠道、低风险删除用 Undo toast
- 文案:按钮写确切动作、同意图全应用一个措辞(发送/确认/提交不混用)、error = 原因+修复路径、禁「轻松/简单/只需」、AI 腔清单(段首总结过渡/datasheet 腔/碎句连发)
**React 性能(vercel 70 rules 全读 → maka grounding)**
现状:全仓 0 处 useTransition/useDeferredValue;chat-view 仅 MessageBody 有 memo。
- ✅ content-visibility 已落地(#468)
- 待做(按序):TurnView memo 化 + props 收窄(流式不重渲染旧 turn)→ 流式更新包 startTransition / 搜索过滤用 useDeferredValue → markdown parse 结果按 text 缓存 → 面板 hover 预热 `void import()` → 频繁 toggle 面板用 <Activity> → 滚动三件套(passive listener / 贴底判定走 ref / 读写分离)→ barrel import 审计(rollup-plugin-visualizer)
- composition-patterns:composer.tsx 792 行按 compound components 重构候选;react19-no-forwardRef 全仓清理候选
- 原生 `document.startViewTransition` 做 settings/panel 切换(Electron 固定 Chromium,零依赖)
### 4.2 明确不采纳增补
- Lucide→Phosphor 全量换 icon(80+ 调用点,见 §4.3 问题 1,待拍板)
- Motion/GSAP 库编排、bento/hero/marquee 全家(landing 专属)
- 声音反馈两篇(超 UI-only scope,规则已存档:默认音量 0.3、打字禁声)
- react@canary 的 <ViewTransition> 组件(生产依赖不划算,用原生 API 替代)
- brutalist/gpt-tasteskill/brandkit/imagegen 系(风格化/资产生成,与 QoderWork 镜像路线冲突)
### 4.3 已拍板决策(2026-07-03 第二轮,jackwener)
| # | 决策 | 含义 |
|---|------|------|
| D5 | **保留 Lucide + 治理** | 不迁 Phosphor;用治理抵消「默认感」:stroke 统一 1.5px、尺寸 16/20/24 token 化、禁 filled/outline 同层级混用 |
| D6 | **AI 等待感全谱引入** | taste v1 §9 五套微动效全部评估落地:shimmer processing、typewriter 轮播、呼吸指示+overshoot 通知、layoutId 列表重排、横向流。流式语义豁免区,不违反 D3 |
| ~~D7~~ **D7'** | **中性冷 zinc(hue 286)** | 2026-07-03 晚 owner 看到实际暖米观感后推翻 D7:暖褐与玻璃通透语言相抵触(「追求的是玻璃质感」)。全局 ink/壳/dark/darwin 玻璃板换 zinc 族;sage accent 与 sand 可选主题不动。参考基准:冷净白侧栏截图(zinc Canvas White + Charcoal Ink) |
| D8 | **copy lint 做可机械化的那半** | check-copy.mjs 上线(pressure-word / ascii-ellipsis);同意图同措辞、AI 腔清单留人工 review |
**产品级附赠**(超 UI,存档):research/laziness 的反截断体系 —— skill description 具体度决定 68%→90% 触发率、`[PAUSED - X of Y]` 续写协议、EmotionPrompt 量化数据,可用于 maka 自身的 agent prompt 设计。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,121 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Economic mechanisms benchmark — synthesis cache
Tracking issue: [#481](https://github.com/maka-agent/maka-agent/issues/481) ·
Slice: [#578](https://github.com/maka-agent/maka-agent/issues/578)
This is the first entry in the economic-mechanism benchmark log. It measures the
**synthesis cache** now that it is wired into the headless Harbor cell.
## What the mechanism does
When the context budget prunes and **archives** an old tool result, a later
replay that needs it back would normally **re-hydrate the full archived
payload**. The synthesis cache instead persists a compact *synthesis block*
(summary + coverage + source refs) the first time an archive is retrieved, and
on subsequent replays injects that block **in place of** the raw payload. The
saving is `hydrated_archive_tokens − synthesis_block_tokens`, paid back on every
replay that reuses the block.
The runtime has always implemented the mechanism (`AiSdkBackend` consumes
`loadSynthesisCache` / `writeSynthesisCache` + the `contextBudget.synthesisCache`
policy). Desktop wired it; headless did not. This slice adds the headless wiring
and the measurement below.
## Result — deterministic A/B
Because the effect is a deterministic function of the request projection, it is
measured against a **mock model** (no network, reproducible in CI) rather than a
live LLM. The same replay is projected twice; the prompt the model actually
receives is ground truth.
| Arm | Replay prompt (tokens) | Δ vs baseline |
| --- | ---: | ---: |
| **baseline** — re-hydrate archived tool result | 2818 | — |
| **arm** — inject synthesis block | 764 | **−2054 (−72.9%)** |
Scenario: a ~2.4 KB archived `Read` tool result is pruned, then recovered on the
next turn. Baseline hydrates the full body; the arm swaps in its synthesis block.
`charsPerToken = 1`, so prompt chars are the token estimate directly.
Reproduce:
```bash
npm run build --workspace @maka/runtime
node --test --test-name-pattern="synthesis cache cuts replay tokens" \
packages/runtime/dist/__tests__/ai-sdk-backend.test.js
# → [synthesis-cache A/B] baseline=2818 arm=764 saved=2054 (72.9%)
```
The exact percentage scales with archived-payload size vs block size; the test
asserts a floor (`>40%`) so it stays a meaningful regression guard, not a brittle
golden number.
## Live activation evidence (Harbor)
A live autonomous run on `terminal-bench-sample` / `sqlite-with-gcov`
(`deepseek-v4-pro`, 2 attempts + prior-attempt context replay) confirms the
wiring is **active end-to-end** on the real path. From `task-run.json →
budget.contextBudget`:
```json
{
"synthesisCacheEnabled": true,
"synthesisCacheMode": "fallback_archive_retrieval",
"archiveRetrievalMode": "eager",
"synthesisCacheWriteSkipped": 1,
"synthesisCacheWriteSkippedReasonCounts": { "source_missing": 1 },
"retrievedArchiveToolResults": 0,
"keptTurns": 1
}
```
The policy is live and the **write gate is reached and evaluated every step** —
it only records `source_missing` because no archived tool result was retrieved to
synthesize from.
## Why the live cost / pass-rate A/B is deferred
The write path needs archive retrieval to **retrieve a previously archived tool
result** (`retrievedArchiveToolResults > 0`). Archiving happens at the **turn**
level: stale-prune archives *older turns'* tool results. A Terminal-Bench task
run — even autonomous with replay — collapses into effectively **one turn**
(`keptTurns: 1`, thousands of events in a single turn), so there are no older
turns to archive, nothing to retrieve, and no synthesis source. This is a
structural mismatch between the mechanism's trigger (multi-turn history, as in
the interactive desktop app) and the benchmark's single-long-turn shape — not a
wiring defect.
A live cost/pass-rate/steps entry will be added once a multi-turn Harbor workload
that archives-then-retrieves across turns is available. The arm environment for
that run:
```bash
MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE=off \
MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE=on \
MAKA_CONTEXT_MAX_HISTORY_ESTIMATED_TOKENS=6000 \
MAKA_CONTEXT_ARCHIVE_RETRIEVAL=on MAKA_CONTEXT_ARCHIVE_RETRIEVAL_MODE=eager \
MAKA_CONTEXT_SYNTHESIS_CACHE=on MAKA_CONTEXT_SYNTHESIS_CACHE_MODE=read_write
```
These keys are on the `runtime-policy-ab-run` arm allowlist (derived from
`HARBOR_CELL_CONTEXT_ENV_KEYS`), so the arm is A/B-toggleable once the workload
exists.
+331
View File
@@ -0,0 +1,331 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# 专家团实现详情 — WorkBuddy & QoderWork 逆向(实现级)
> 本文是"专家团/数字同事"两款竞品的**完整实现拆解**,用于指导 maka-agent 的实现。
> 基于对本机安装的 WorkBuddy(腾讯 CodeBuddy 换皮)与 QoderWork(阿里 Qoder)的解包逆向。
> 上层设计与 maka 落点见 [expert-team-plan.md](expert-team-plan.md)。
> 逐层原始 spec(含 byte offset / 逐字字符串)在 scratchpad:
> `IMPL-workbuddy-runtime.md`、`IMPL-workbuddy-mainprocess.md`、`IMPL-workbuddy-renderer.md`、
> `IMPL-workbuddy-colleague.md`、`IMPL-qoderwork-runtime.md`、`IMPL-qoderwork-plugins.md`。
> 标注:**[C]** = 代码/文件中确证,**[I]** = 推断。
---
# 总览:两家的架构范式
两家惊人地同构,都是 **"Electron 壳(manager)+ 一个 Claude-Code 派生的 CLI agent 引擎 + 一个远程 marketplace"**:
| | WorkBuddy | QoderWork |
|---|---|---|
| 壳 | Electron 主进程(DI/CellJS 组件) | Electron 主进程 `main.js` |
| Agent 引擎 | `agent-cli`(`codebuddy.js` 21.6MB,品牌 "CodeBuddy Code"),**每会话一个 `agent-cli --serve` 子进程** | `qodercli`(`@qoder-ai/qoder-agent-sdk`),**host(94KB) + worker(35MB obf) 两进程** |
| 引擎↔壳 通信 | HTTP (`/api/v1/*`) + ACP endpoint | host↔worker transport + SDK API |
| "专家"单元 | plugin bundle(含 `agents/*.md` 人格)从 COS 拉取 | Plugin 文件夹(skills+commands+connectors)本地/云 |
| 多智能体 | **两种原语**:sub-agents + Agent Teams(`TeamCreate`) | sub-agents(`Agent` 工具,无 team 原语) |
| 人格来源 | 专家 plugin 的 agent md(身份覆盖 system prompt) | awareness `SOUL.md` 分层(system-reminder 注入) |
| 数字同事 | 有(cloud-agent + Claw IM + BackgroundAgent 云端) | 无(只到 plugin/awareness 层) |
**关键洞察**:两家的"专家"本质都是 **"带人格的 agent 定义(md + frontmatter)+ 一个能热加载它的 CLI 引擎"**。真正的差异在**多智能体编排**(WorkBuddy 有完整的 Agent Teams 原语,Qoder 靠 sub-agent + 工具作用域)和**产品化外壳**(WorkBuddy 有数字同事 + IM + 云端)。
---
# 第一部分:WorkBuddy
## A0. 进程/目录拓扑
- **引擎**:`agent-cli`(`wb-app/cli/dist/codebuddy.js`),`--serve` 模式暴露本地 HTTP + ACP。**每个会话一个子进程**(`SidecarServer` / `sidecar-entry.js`,macOS/Linux 用 `@lydell/node-pty` PTY,Windows 用 `spawn`)。[C]
- **状态目录**:引擎侧 `~/.codebuddy/`;壳侧 `~/.workbuddy/`(部分镜像)。
- **专家 marketplace**:腾讯 COS `https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/expert-marketplace`。
- **引擎自带人类可读文档**(逆向金矿):`wb-app/cli/dist/web-ui/docs/cn/cli/{sub-agents,agent-teams,plugins-reference,skills,sdk-hooks,acp,permissions}.md`。
## A1. 数据模型(marketplace)
Manifest = `expert_center.json`(331 专家 / 13 分类),外加并行拉取 `internalExpert.json`/`externalExpert.json` 合并(企业内外部隔离)。每个专家条目关键字段:
```
id, categoryId, displayName{en,zh}, profession, description,
promptFile, # COS 相对路径,指向人格 md(如 /plugins/content-creator/agents/content-creator.md)
avatar, defaultInitPrompt{en,zh}, quickPrompts[], tags[],
expertType: "agent"(285) | "team"(46), # team 就是"专家团"
agentName, plugin, # 指向 bundle 内的 agent id
members[] # 仅 team:[{id, displayName, profession, avatar, promptFile, role:"lead"|"member"}]
```
人格 md = YAML frontmatter(`name, description, color, emoji, vibe`;team lead 加 `maxTurns:200`)+ body(🚨身份覆盖指令 + Role/Capabilities/内置Skill触发 + 🔒身份提醒)。
## A2. 主进程专家生命周期(`IMPL-workbuddy-mainprocess.md`)
DI 组件(CellJS `@Component`):`ExpertManifestProvider`(+`ExpertCacheManager`+`ExpertCenterResourceLoader`)、`ExpertService`/`ExpertCloudService`/`ExpertDesktopService`、`ExpertPluginService`、`ExpertHistoryService`。
### A2.1 Manifest pipeline — 缓存优先 + 前台增量 + 后台重校验
```
CURRENT_SOURCE_SIGNATURE = JSON.stringify({baseUrl, manifestPath})
cacheDir = <runtimeUserDataDir>/cache/experts → manifest.json / version.txt / metadata.json
metadata = {version, cachedAt, sourceSignature, manifestHash: sha256(JSON), cacheFormatVersion:2}
doInit():
loadFromCache() // sourceSignature 不符则 clear
有缓存 → syncManifestBeforeReady()(前台 fetch,version==&&hash== 则保留,否则替换)
无缓存 → fetchAndCacheManifest()(冷启动远程拉)
backgroundManifestUpdate() // setImmediate 后台重校验,变了才替换
fetchManifest() = Promise.all([base expert_center.json, ...scoped]) → mergeExpertManifests(id-keyed Map,scoped 覆盖 base)
```
**无 ETag**,靠 sha256+version+sourceSignature。企业/visibility 过滤在**渲染层**做。
### A2.2 激活 `ExpertPluginService.activateExpert()`
```
bundleName = pluginName || manifest.experts[].plugin || kebab(expertId)
localPath = 下载 COS /bundles/<bundle>.tar.gz(zip 仅限可信运营平台 URL,host allowlist + zip-slip guard)→ 解压
manifest = 读 .codebuddy-plugin/plugin.json
agentName = resolveExpertAgentName(5 级:manifest.agentName → team leadAgent → agents/<kebab(id)>.md 的 name → 唯一 .md → kebab)
→ 注册 marketplace(marketplace.json 增量)→ 删旧 agent-override settings.json → activeExperts.add
return {expertType, agentName, localPath, pluginRegisteredName}
```
`resolveExpertLocation` 优先级:hint → 官方 `experts` → 自建 `my-experts`,缺失则懒下载。下载有 in-flight dedup + `.downloaded_at` 时间戳判新旧。
### A2.3 会话绑定 + CLI 注入(核心机制)
`sessions` 表列:`expert_id / expert_locale / expert_runtime_identity / expert_marketplace`(`upsertSession` 用 COALESCE 合并)。
> **纠正**:`expert_runtime_identity` 是**不透明激活 key 字符串**(默认 `expert:<id>`,实际是 `name@marketplace` 的 sourcePluginId),**不是** JSON 人格快照。用作幂等重激活的 dedup key。
**人格如何进入 CLI —— 不是 spawn 参数,而是 before-prompt hook + HTTP:**
```
createWorkbuddyAppServerBeforePromptExpertActivation({sessionId, desiredConfig}):
resolved = resolveExpertLocation(expertId, marketplace) // 懒下载
headers = {"X-Expert-Id": expertId, ...(team ? {"X-Expert-Team-Task":"true"} : {})}
switchExpertPluginForSession(sid, disable=ø, enable=manifest.name, agentName,
sourcePluginId=`${manifest.name}@${marketplace}`, headers)
→ POST `${httpBase}/api/v1/plugins/switch`
body {persist:false, enable:`name@mp`, agentName, sourcePluginId, internalModelRequestHeaders}
```
`persist:false` → 只在当前 CLI 进程内生效。这一个 HTTP 调用让通用 `agent-cli` 把该 plugin 的 agent md 当作 system prompt 加载。team 专家额外带 `X-Expert-Team-Task` header,触发 agent-teams 环境。自建专家(`@my-experts`)走两次 POST 的 override-refresh 让编辑后的人格重载。失败则"降级到当前 prompt"(会话仍跑,只是没人格)。
### A2.4 IPC(`expert:*` / `expert-history:*`)
21 个 `expert:*` channel(`init/getCategories/getExperts/getExpert/getFeaturedScenes/getRanking/activatePlugin/switchPluginForSession/exportZip/importFromUrl/preCheckShare/queryShareSecurityScan/scanCustomExperts/addUserExperts/...`)。渲染层实际召唤走**一个 facade RPC `expert:summonExpert`**(不是细粒度 channel),主进程内部编排 detail→download+activate→history。
### A2.5 分享 & 安全
- 导出 `exportExpertZip`:adm-zip,顶层单目录=expertId,manifest 里盖章分享者。
- 导入 `installExpertZipFromPath`:强制单顶层目录 + `plugin.json` + 路径逃逸 guard → 落 `my-experts`。
- 安全扫描:腾讯 **XTI** API(`SkillSecurityClient`,`SkillAnalysisUpload`/`SkillAnalysisInfo`,md5 键,threatLevel→low/medium/high)。`runSkillPreCheck` **fail-open on scanner error, fail-closed on risk verdict**(扫描器出错放行,返回风险则 `security_check_blocked`)。
## A3. Agent 运行时(`IMPL-workbuddy-runtime.md`)—— 最有价值的部分
### A3.1 两种协作原语(不要混)
引擎自带文档明确区分:
| | **Sub-agents** | **Agent Teams** |
|---|---|---|
| 派生 | `Agent` 工具(**无** `name`) | `TeamCreate` 后 `Agent`(**带** `name`) |
| 上下文 | 独立 window;**结果 summary 回传给 parent** | 独立 window;**完全独立** |
| 通信 | 只能把 final result 上报 parent | 成员间 `SendMessage` mailbox 互发 |
| 协调 | parent 全权编排 | 共享 task list,成员自领任务 |
| 拓扑 | 星型(仅 parent) | mesh(成员↔成员 + `@all` 广播) |
marketplace 的 `team` 专家跑在 **Agent Teams** 上;lead 人格 md 里写的编排指令**驱动**真实的 `TeamCreate`→`Agent`→`SendMessage` 工具(**没有服务端 router**)。
### A3.2 `Agent` 工具(逐字 schema,offset ~11,263,540)
```js
name = "Agent"
parameters = z.object({
description: z.string(), // 3-5 词任务描述
prompt: z.string(), // 交给 agent 的任务
subagent_type: z.string().optional(),
model: z.enum(["default","lite","reasoning"]).optional(),
resume: z.string().optional(), // 传上次 agentId 续跑(带完整历史 transcript)
run_in_background: z.boolean().optional(), // 返回 task id,用 TaskOutput 取
name: z.string().optional(), // 有 name → teammate(可被 SendMessage 寻址);"team-lead" 保留
team_name: z.string().optional(),
mode: z.enum(["acceptEdits","bypassPermissions","default","plan"]).optional(),
max_turns: z.number().int().positive().optional()
})
```
`execute()` 路由顺序:**fork 特化**(`<fork-boilerplate>` prompt 禁止嵌套 fork)→ **teammate**(有 `name` + team 启用 → `spawnTeammate`)→ **background**(`run_in_background` → 返回 task id)→ **同步 sub-agent**(默认;`isolation:"worktree"` 则建 git worktree)。
### A3.3 执行模型
- sub-agent/teammate 是**进程内 `AgentTask`**(`detached:true, captureOutput:true`),不是独立 OS 进程;可选 `--swarm`/`CODEBUDDY_INPROCESS_TEAMMATES`。
- **独立 context window,全新状态**:成员不继承 lead 历史,spawn 时重载 `CODEBUDDY.md`/MCP/skills。
- **返回=summary 非 transcript**:完成时先跑 `SubagentStop` hooks,再用 `result.finalOutput ?? "Task completed"` resolve parent 的 await callback。大输出外置到 `tool-results/{callId}.txt` 用指针引用。
- **禁止嵌套**:sub-agent 不能再 spawn sub-agent;team 不能建 sub-team(只有 lead 管 team)。
- **并行 = 一个 assistant turn 里多个 `Agent` tool_use block**。
- **resume**:每次跑有 `agentId`,transcript 存 `~/.codebuddy/projects/{proj}/{parentSid}/subagents/agent-{agentId}.jsonl`。
- **内置 agent 类型**:`general-purpose`(全工具)/`explore`(只读,gemini-3.0-flash)/`plan`/`fork`/`compact` + 保留的 `team-lead`。
### A3.4 人格→system prompt(身份覆盖机制)
agent md body → agent `systemPrompt`。`buildSystemPromptAgentOverride(name, instructions, appendInstructions)`:**`instructions` 完全替换** base persona,`appendInstructions` 追加。marketplace 人格用 `instructions`(完全替换)装入"🚨你是X且只是X,绝不暴露底层模型"的身份覆盖模板。`defaultInitPrompt` **不在** system prompt 里 —— 它作为**首条 user message** 注入。
### A3.5 `TeamCreate` / `SendMessage` / `TaskStop`(逐字 schema)
```js
// TeamCreate(lead-only,一会话一 team;offset ~11,586,300)
"TeamCreate" params: {team_name, description?, agent_type?}
execute: 若已在 team → 拒绝;createTeam → ctx.meta.isTeamLead=true → 启动 inbox 轮询 →
返回 "Spawn teammates with the Agent tool using `name` and `team_name`"
// SendMessage(mailbox;offset ~11,450,000)
"SendMessage" params: {
type: z.enum(["message","broadcast","shutdown_request","shutdown_response","plan_approval_response"]),
recipient?, content?, summary?, request_id?, approve? }
// TaskStop:取消 running/pending 的 AgentTask 或 background shell
```
**Delegate Mode**(Shift+Tab):把 lead 限制为协调工具 `Agent, TaskStop, SendMessage, AskUserQuestion, StructuredOutput`(无 Read/Edit/Bash)。**Plan-before-implement**:`Agent{mode:"plan"}` → 成员交计划 → lead `SendMessage{plan_approval_response, approve}`。
**Team 磁盘状态**:`~/.codebuddy/teams/{team}/config.json`、`inboxes/{member}.json`、`~/.codebuddy/tasks/{team}/`(任务 pending/in_progress/completed,支持依赖,完成自动解锁下游,成员自领)。清理 team 必须 lead 做。
### A3.6 Skills / Plugins / Hooks / ACP
- **`skill` 工具**:params `{skill | command, args}`,`/` 开头走 slash command,否则执行 Skill。人格 frontmatter `skills:` 在 agent start 自动加载。
- **Plugins** 打包 `agents/ skills/ commands/ connectors/`(`.codebuddy-plugin/plugin.json`),marketplace 注册进 `PluginManager`;专家=从 COS 懒拉的 plugin。
- **Hooks**:`PreToolUse/PostToolUse/SessionStart/Stop/SubagentStart/SubagentStop`。`SubagentStop` 在 parent callback resolve 前触发。
- **权限优先级**:`Agent{mode}` → `--subagent-permission-mode` → env → settings → 继承(Delegate Mode 强制 `default`)。
- **ACP team bridge**:`session_info_update._meta["codebuddy.ai/teamUpdate"]` 推 member 状态;成员流用 `_meta["codebuddy.ai/memberEvent"]` 打标路由到各自 timeline;`AcpTeamBridge` 刷新时重放历史。
### A3.7 一个 team 专家端到端
1. 召唤 team 专家 → 下载/解压 bundle → 写 session 行 → CLI 以 **lead** md 为身份覆盖 system prompt 启动 → 首条 msg=`defaultInitPrompt`。
2. lead 人格指示它调 `TeamCreate{team_name, agent_type}` → 写 `~/.codebuddy/teams/…` → 启动 inbox 轮询。
3. lead 发 1+ 个 `Agent{name:"role", subagent_type:"memberId", team_name, prompt, mode?}` → 各成为进程内 detached `AgentTask`。
4. 成员并行、从共享 task list 自领任务、用 `SendMessage` 协调。
5. 每成员完成触发 `SubagentStop`,`finalOutput`(仅 summary)回 lead;lead 综合 → `SendMessage{shutdown_request}` → 清理 team。
`agent` 型专家(86%)折叠 2-4 步:单个人格注入的对话 agent,可选用 `Agent` 工具做星型 sub-agent。
## A4. 渲染层(`IMPL-workbuddy-renderer.md`)
- **召唤流程**:卡片"立即召唤" → `handleSummon`(team 警告 + 金融风险 gate)→ `useExpertSummon().summonExpert` → **一个 facade RPC `expert:summonExpert`**(detail→download+activate→history)→ `session.prepare({selectedExpert})` 设 pending expert → `goHome()`(新会话)或 `navigate('/task/:id')`。
- **`defaultInitPrompt` 在专家中心路径下只 pre-fill composer**(`applyExpertDefaultPrompt` = `setInputValue`),**不自动发送**;只有 colleague chat page 的 `autoSendPrompt` 会真发。`quickPrompts` = 带 `promptOverride` 重召唤的按钮。
- **状态**:无 redux,真值源是 `WorkbuddyAgentAdapterNext.selectedExpertBySession`(Map,含 `PENDING_EXPERT_SESSION_KEY`)+ RxJS Subject 总线(`pendingExpertModeActivation$` 等)。会话内切专家 → `expert:switchPluginForSession`(原子 disable-old+enable-new,一次 `rebuildAgents`)。深链导入 buffer 在 `globalThis.__GENIE_PENDING_EXPERT_INSTALL_INTENT__`。
- **主题**:`useExpertTheme()` 读 `document.body.classList`(`vscode-dark`/`vscode-high-contrast`→dark)+ `MutationObserver` 监听 body class;`useThemeClassName()`→`expert-center-dark|light` 盖在 `.expert-center-page`,切换 `--ec-*` CSS 变量族。per-expert accent 来自人格 frontmatter `color`/`emoji`,与明暗正交;头像 fallback 用 8 色 `--ec-avatar-fallback-bg-N`(`name.charCodeAt(0)%8+1`)。
## A5. 数字同事 / 云助理(`IMPL-workbuddy-colleague.md`)
UI 词"助理/云助理/数字同事"映射**三个内部子系统**:**cloud-agent**(定义+实例+会话,后端 CRUD)、**cloud-assistant**(entitlement 门)、**Claw**(IM 渠道 + 后台 agent)+ **BackgroundAgent**(云后端,webhook + Redis ChannelRegistry)。
### A5.1 双存储面
- **云面(权威)**:colleague 是**云对象**,非本地行。`POST /v2/user/cloudagent/agents` 创建,带**版本化 `manifest`**。REST:`orchestrator/agents/validate-field/quota/clone/versions/enterprise-published` + `/v2/as/conversations/*`。IPC `cloudAgent:*` / `cloudAssistant:*` 镜像。
- **本地面(调度/会话)**:`automations` 表(RRULE、`schedule_type`、`next_run_at`、**`expert_id`/`expert_marketplace`**=雇佣的专家、`connector_ids_json`、`skills_json`、**`push_to_wechat`**)→ `automation_runs`/`automation_runtime_state`。IM 绑定存 `settings.json → claw.channels` + `ioa-im-override.json` + `AgentImBindingStore`。
### A5.2 创建对话字段
基本信息(avatar ≤5MB / 昵称 validateField 查重 / 职能预设)+ 能力配置(**hireExpert** 从 `{expert|team|企业智能体}` 选 1 / **codeRepo** github·cnb·工蜂 OAuth / **knowledgeBase** / **model** listAvailableModels)。雇佣的专家存为 `expert_id`+`expert_marketplace`,会话启动时按普通召唤解析人格;额外由 **MemoryCollector** 注入 `ClawMemory_1/2/3` + `<cwd>/.workbuddy/memory/MEMORY.md`(cap 8000 字)。
### A5.3 IM 绑定(最新颖)—— 无第三方 IM SDK 打包,两种模式
- **`webhook`(云中继,默认)**:`ClawService.registerChannelWithBackend` → `POST /v2/backgroundagent/localProxy/register` 写后端 ChannelRegistry(Redis)返回 `webhookUrl`。IM 平台 → webhook → 后端 → cloud-agent 会话 → 回复。**桌面可离线**。
- **`websocket`/`scan`(本地)**:沙箱化的 IM **CLI plugin 子进程**(`lark-channel`/`wecom` 等)持 WebSocket 到平台,通过 unix-socket JSON-RPC 流给 daemon。
- 各平台认证走 CLI(`lark-cli config init --new`+`auth login`、`wecom-cli init --noninteractive`,抓 stdout 的 auth-URL/QR/device-code)+ 微信机器人 QR + QQ QR。
- 绑定键 `users[uid].targets[targetType][targetId].backends[backendMode][channelType]`,`targetType∈{assistant,colleague}`,`backendMode∈{local,cloud}`。@提及 → `triggerSource="user_mention"`。**IOA gate**(`ioa-im-override.json`)在腾讯内网机器上拦非腾讯 IM。
### A5.4 云/daemon/调度/配额
- `daemon-app-server` 是 Electron main 的独立 stdio-only Node 子进程,跑同一 `module.app-server` 容器 + 常驻后台服务(Claw 渠道 plugin + automation 调度器)——即"本地云"。云模式用后端沙箱 + 微信小程序做"手机远程"。
- 调度:`automations`(RRULE)→ `automation_runs`。
- 配额:服务端 `GET /v2/user/cloudagent/quota`,渲染层映射 个人体验版/专业版/SaaS企业版;`cloudAssistant:getEntitlement` 门控。
---
# 第二部分:QoderWork
## B0. 架构(`IMPL-qoderwork-runtime.md` §0)
Electron 是 **manager**;真实 agent loop 在打包的 **`qodercli`(`@qoder-ai/qoder-agent-sdk`)**:
```
app.asar.unpacked/node_modules/@qoder-ai/qoder-agent-sdk/
package.json # name "@ali/qoder-agent-sdk-next" v1.0.13, qoderCliVersion 1.0.41
dist/index.js # 94KB host(query/listSubagents/hooks/session)
dist/_worker/qoder-worker-runtime.obf.mjs # 35MB 真 agent loop(obfuscated,base64+XOR key "hABjM4Eb0CeC")
```
worker 由 `install` 从 `download.qoder.com/qodercli/releases/1.0.41/...tgz` 下发。`defaultTransport:"worker"`。**Qoder 没有 team 原语**,多智能体只靠 sub-agent + 工具作用域。
## B1. Plugin(专家套件)打包/安装/热加载(`IMPL-qoderwork-plugins.md`)
### 目录 & schema
```
~/.qoderwork/plugins(市场装) / plugins-custom(自建/fork) ; Resources/plugins-example(15 个内置"本地市场")
配置目录候选 [".qoder-plugin", ".claude-plugin"](兼容 Claude-Code 导入)
plugin.json: {name, displayName, version, description{En,Zh}, category, customizedFrom?, tags,
skills:["./skills/xxx"], commands:["commands/x.md"], qoderMarket?:{pluginId}}
guards: isValidPluginFolderName(无 ../\/前导.); BLOCKED{员工管理,招聘管理,薪酬绩效}; MAX_ZIP 50MB
```
### 安装 → 迁移 → 热加载
- tRPC `plugin.*`(`installMarket/forkPlugin/updateMarket/uninstall/listInstalled/...`);本地源从 `plugins-example` 拷贝,远程源按 `pluginId` 下载。
- **`plugin-cli-migration-service`**:把 plugin 所有权从 Electron 迁给 qodercli(feature-gate `qoderworkPluginCliMigrationV1Succeeded`,幂等一次性)。`PluginCliManagementService.syncInstalled` → SDK `installPlugin(path)` 注册目录 → `enablePlugin/disablePlugin` 开关。
- **热加载**:`reloadMainSessionPlugins(reason)` → `runtime.reloadPlugins()` → `query.reloadPlugins()` 返回 `{plugins[], commands[], agents[], mcpServers[]}`,**live session 内热注入无需重启**。一个 plugin 一次贡献 skills+commands+sub-agents+MCP。
## B2. Skills / Commands
- **Skills**:`SKILL.md`(YAML frontmatter `name/version/description/description_zh/disabled` + body)。发现优先级 builtin→user→plugin→market;首次运行把 default-enabled 内置 skill **拷进** user 目录供编辑。**注入=懒加载 + description 触发**:模型只见 catalog(name+description,故意堆同义触发词),body 在 `@`/`/`/`Skill` 工具激活时加载,`references/` 按需(progressive loading,SKILL.md <500 行)。`user-invocable:false` = 隐藏知识库 skill,只被别的 skill 引用。启停=改 `disabled:` frontmatter。
- **Commands**:`commands/*.md`(frontmatter + body=prompt macro),`/name` 调用。
- **`external-commands`**:`registry.json` 声明的**下载的原生 CLI 可执行文件**(per-platform 签名 zip,sha256 校验,`entry`/`defaultArgs`/`env`)——如 `/wiki` 跑 `wiki-cli`。区别于 prompt-macro 的 `commands/`。
## B3. Connectors(MCP)三层
1. **native/market**:在 `qoderwork.settings.connector.market`,产品级,**不写** `.mcp.json`,`CONNECTORS.md` 文档化。
2. **custom**:用户本地加的,不可移植。
3. **guided-setup**:需 per-user 凭证的第三方(钉钉/飞书),写 `.mcp.json`(`type:"guided-setup"`,`_setup.url` 只是公开配置页),装后详情页渲染指引让用户贴自己的 config。
`CONNECTORS.md` 用 `~~类别` 占位符 —— plugin **工具无关**,每个 skill 必须能独立工作,connector 只在 SKILL.md 末尾加"If Connectors Available"增强段。`reloadMcp()` → mcp-adaptor 进程(`~/.qoderwork/mcp-adaptor.config`),CLI 收到 MCP 工具名 `setMainSessionMcpToolSnapshot`。
## B4. Sub-agent 运行时(`IMPL-qoderwork-runtime.md`)
### `Agent` 工具(解码后逐字 schema `uji`)
```js
{ description*, prompt*, subagent_type(default "general-purpose"), cwd,
run_in_background, name, team_name, isolation:enum["default","worktree"] }
// subagent_type 非必填;isolation:"worktree" 跑隔离 git worktree(无改动则自动清理)
```
类 `TG`:`isReadOnly=true`(工具本身只读,子的工具才带权限);`checkPermissions` 按 tool+agentType 规则 allow/ask/deny;`getDefinition` miss 抛 `Unknown agent type: '<x>'. Available types: …`;definition 有 `kind`(`"local"` md-file vs `"workflow"`)。
### Roster 注入(路由靠 description)
`gjn(registry, disabled)` 动态建工具描述:base "Launch a new agent…" + 每 agent 一 bullet `- <name>: <description> (Tools: <All tools|list|All tools except X>)` + `subagent_type` enum = **sorted agent names 闭集**。另有 `**Available custom agents configured:**` 列表 + `**Connected MCP servers:**`。模型**纯靠 description 匹配路由,无独立 team manifest**。静态指引逐字含:"send a single message with multiple tool uses so they run concurrently"、"result … is not visible to the user … send a text summary"、"Trust but verify"。
### 执行 & hooks
- **进程内嵌套 sub-chat**:子 md body=子 system prompt,`tools` frontmatter=scoped allow-list,独立 transcript,parent 的 `Agent` 调用 **await** 子完成,子 final message=tool_result。
- **并行=一条 assistant message 多个 `Agent` tool_use block**(harness await all)——唯一并发原语。
- `run_in_background` 脱离,返回 `{executionId, agentId, outputPath, transcriptPath}`。
- 完成发 `SUBAGENT_INVOKED`(`status = terminate_reason==="GOAL" ? success : error`);进度事件 `{isSubagentProgress, agentName, state, ...}` 流给 UI。
- `READ_ONLY_SUBAGENT_DISALLOWED_TOOLS`(固定 deny-list)把只读 agent 的所有写工具剥掉。`maxAttempts` 是 **transport 层 gRPC 重试**,非语义重跑。
- **Hooks**:host 侧 `PreToolUse/PostToolUse/PostToolUseFailure/UserPromptSubmit/SessionStart/SessionEnd/Stop/SubagentStart/SubagentStop/PreCompact/PostCompact/...`;`SubagentStop` = subagent 上下文里 `Stop` 的 remap,payload `{hookEventName, clearContext?}`,支持 `once`。`listSubagents`/`getSubagentMessages` 从 `<sessionId>.jsonl` 重建嵌套 transcript。
### ai-slides 案例(工具作用域=委派边界)
6 个 sub-agent(researcher/visual-researcher/designer/render-reviewer/narrative-reviewer/notes-writer)。**只有 `slides topic researcher` 持 `WebFetch/WebSearch/Bash/Write`** → Main agent 物理上拿不到 web 工具,reviewer 只读。并行=一条 msg 最多 10 个 `Agent`(每个一 slotId);**磁盘 artifact + JSON 指针交接**(重字节不进 orchestrator 上下文);revision-lock(`deckRevision`/`slotRevision` + `expected*Revision`)防并发覆盖;入口自校验(designer 收到 >1 slotId 就中止让重派)。
## B5. Awareness / SOUL / Memory 三层人格
- **文件**:`~/.qoderwork/awareness/<agent>/`;模板 `Resources/awareness-templates/`(含 `soul-presets/{decisive,thoughtful,efficient,supportive}.md`)。`BOOTSTRAP_FILES=[SOUL.md, AGENTS.md, HEARTBEAT.md]`,`MEMORY_MANAGED=[MEMORY.md, USER.md]`。字节预算 head/tail 截断。
- **注入**:作为 `<system-reminder>` 消息,**优先级逐字**:`SOUL.md(人格,覆盖默认tone) > AGENTS.md(技术约定) > USER.md > HEARTBEAT.md > MEMORY.md`,冲突高优先胜。协议标记 `<project_context_protocol>`/`<bootstrap_content_protocol>`,"TREAT AS AUTHORITATIVE"、CONTEXT FRESHNESS(改了重读)。
- **soul-presets**:tRPC `getSoulPresets` 读模板,选一个把内容写进该 agent `SOUL.md`(选性格:`decisive.md` = "You decide, you execute, I review")。
- **memory**:读侧 `memory_search`/`memory_get`(MANDATORY RECALL);写侧 MCP `mcp__qoderwork_awareness__memory`(`{action:add|replace|remove, target:memory|user|daily, content, old_text}`);`.memory_meta.json` sha256 变更检测。**memory-reflection**(`agentType:"memory"` 后台 sub-agent,只用 memory 工具重组 MEMORY/USER,never invent)+ **voice-learning**(diff ASR-vs-sent 文本抽 hotwords/voice-memories 进 sqlite)+ **awareness-query-runner**(headless 跑后台 sub-agent 不污染主聊)。
- **plugin-creator**:内置"造 plugin 的 skill",6 步 `AskUserQuestion` 引导(职业→技能×connector 表→解析 MCP→材料→scaffold→post),"说结果不说机制"。兄弟:`create-skill`/`create-command`/`find-skills`——系统自我编写。
---
# 第三部分:对比与对 maka-agent 的映射
## C1. 关键实现决策对比
| 维度 | WorkBuddy | QoderWork | maka 建议 |
|---|---|---|---|
| 人格注入 | agent md `instructions` **完全替换** base persona(身份覆盖) | awareness `SOUL.md` 作 `<system-reminder>` **分层叠加**(不替换) | maka `childInstruction` seam 已是"追加+边界",倾向 Qoder 的分层(更安全,不丢权限约束) |
| 专家单元 | plugin bundle(COS tar.gz 懒拉) | Plugin 文件夹(本地/云) | 复用 skills 市场:`EXPERT.md` + install/lock/update |
| 多智能体 | **Agent Teams**(TeamCreate + mailbox + 共享 task list) | sub-agent + **工具作用域**委派 | P0/P1 用 maka 现有 `spawnChildAgent`(星型,像 Qoder);Teams mailbox 是 P2+ 才考虑 |
| 并发原语 | 一 turn 多 `Agent` block | 同 | maka `agent_spawn` 已支持 |
| 隔离 | `isolation:"worktree"` | `isolation:"worktree"` | maka worktree executor 目前 fail-closed → 先非隔离 |
| 人格→CLI | before-prompt hook + `POST /plugins/switch`(persist:false) | `reloadPlugins()` 热注入 | maka 同进程,直接 `mode:` 标签 gate fragment(更简单) |
| 结果回传 | finalOutput summary + `tool-results/*.txt` 指针 | final message + 磁盘 artifact + JSON 指针 | **抄"回指针不回 payload"** |
| 目录缓存 | sha256+version+sourceSignature,无 ETag,后台重校验 | plugin-market-data.json + 远程 | maka `managed-skill-sources` 的 `contentSha256` 已同款 |
| 安全 | 腾讯 XTI 扫描(fail-open error / fail-close verdict) | 无(本地 plugin) | 远程专家期再考虑 |
## C2. 两个最值得直接抄的实现细节
1. **工具作用域即委派边界(Qoder)**:不靠 prompt 纪律,靠 member agent 的 `tools` allow-list + `READ_ONLY_SUBAGENT_DISALLOWED_TOOLS` **物理**限制。maka 的 `AgentDefinition.tools` + `permissionMode` 天生支持——专家团 member 必须各自窄作用域。
2. **磁盘 artifact + JSON 指针 fan-in(两家都用)**:member 把重产物落盘,只回小 JSON 指针,orchestrator 上下文保持极小。maka 实现专家团时必须这样,否则 lead 上下文爆炸。
## C3. maka 落地要点(详见 [expert-team-plan.md](expert-team-plan.md))
- **单专家** = `mode:expert:<id>` 标签 + 人格 fragment(照抄 Deep Research 的 `deep-research.ts` 路径),人格经 `childInstruction` 边界注入(继承权限/隐私/工作区)。`defaultInitPrompt` 学 WorkBuddy 只 pre-fill composer(P0 简单)。
- **专家团** = lead 用 `mode:expert:<teamId>` + orchestrator 人格,member 注册进 `agent-catalog.ts` 的 `BUILTIN_AGENT_DEFINITIONS`(各带窄 `tools`),lead 用现有 `agent_spawn`/`spawnChildAgent` 派生(星型,非隔离,单轮 fan-out + await fan-in,回指针)。**先不做 Teams mailbox**。
- **专家中心** = 克隆 `skills-panel.tsx`(市场/内置/已安装)+ `managed-skill-sources.ts`(加 `sourceType:'remote'` 拉 COS 式 manifest,`contentSha256` 已就绪)。
- **数字同事** = P2 持久化外壳(雇佣专家 + 昵称/头像/仓库/资料库/模型),IM 绑定/云端是最重的 P3+,需评估是否值得(WorkBuddy 靠云后端 + IM CLI 子进程,成本高)。
+191
View File
@@ -0,0 +1,191 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# 专家团 (Expert Team) — 调研与实现方案
> 目标:在 maka-agent 中实现"专家团 / 数字同事"功能。
> 本文基于对 **WorkBuddy**(腾讯 CodeBuddy 换皮)与 **QoderWork**(阿里 Qoder)两款已安装应用的逆向,
> 加上对 maka-agent 自身架构的映射,给出可落地的设计。
> 逆向原始 spec:`scratchpad/RE-workbuddy-experts.md`、`scratchpad/RE-qoderwork-agents.md`、`scratchpad/maka-architecture-map.md`。
---
## 1. 调研结论:两家怎么做的
### 1.1 WorkBuddy —— 完整的"专家中心 + 数字同事"(最值得抄)
WorkBuddy 有**两个不同层次**,不要混为一谈:
1. **专家 / 专家中心 (Expert Center)** —— 一个**市场目录**,331 个专家人格、13 个分类,托管在腾讯 COS。
- Manifest:`https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/expert-marketplace/expert_center.json`,
并行拉取 `internalExpert.json` / `externalExpert.json` 合并(企业内/外部专家隔离),
缓存到 `~/.workbuddy/app/cache/experts/`(sha256 + `cacheFormatVersion:2`)。
- 每个专家 = 一个 **plugin bundle**(zip 懒加载解压),人格来自一个 **agent markdown**(`promptFile`)+ 头像 PNG。
- `expertType` 两种:**`agent`(285 个,单人格)** 与 **`team`(46 个,即"专家团")**。
2. **数字同事 / 云助理 (Digital Colleague)** —— 用户自建的**持久化助理**,它"雇佣(hire)"一个专家/专家团作为能力来源,
拥有自己的昵称+头像+高级配置(专家/仓库/资料库/模型),可绑定 IM 渠道(QQ/微信/飞书/钉钉)、云端运行、被 @、被定时调度。
—— 这是把"专家"产品化的外壳。(注:`collab-*` 模块是**人与人**的工作区协作,与专家无关。)
**专家条目 schema(关键字段):**
```
id, categoryId, displayName{en,zh}, profession{en,zh}, description{en,zh},
promptFile, # COS 相对路径,指向人格 md,如 /plugins/content-creator/agents/content-creator.md
avatar, # /avatars/ContentCreator.png
defaultInitPrompt{en,zh}, # 召唤后自动注入的第一条消息
quickPrompts[{en,zh}], # 组合框里的建议起手 prompt
tags[{en,zh}], # 能力标签 chip
expertType: "agent"|"team",
agentName, plugin, # 指向 plugin 内的 subagent id
isOPC, operationalTag, visibility, author,
members[] # 仅 team 有
```
**专家团 (team) 的结构 —— 就是 orchestrator→worker 多智能体:**
```
members: [
{ id, displayName, profession, avatar, promptFile, role: "lead" | "member" }
]
```
- team 顶层的 `agentName`/`promptFile` 指向 **lead**;每个 member 有自己独立的人格 md。
- 运行时:**lead 作为唯一对话 agent**,通过 `Agent` 工具(`subagent_type = member.id`)派生 member 子代理。
- 协作协议写在 lead 的人格 prompt 里:`TeamCreate(建团,只能 lead 做)→ 顺序/并行调度成员 → 成员回传`,
**星型拓扑**(所有跨成员信息经 lead 中转,成员之间不直连),lead 不模拟成员输出、只汇总。
- **没有服务端 router**——编排完全靠 lead 的人格 prompt,跑在本地 Claude-Code 式 subagent runtime 上。
**人格 md 格式**(不内联在 manifest,激活时才拉取):YAML frontmatter + markdown body。
- frontmatter:`name, description, color(强调色), emoji, vibe`;team lead 额外 `maxTurns:200`。
- body 是**强身份覆盖**模板:`🚨 CRITICAL IDENTITY DIRECTIVE`("你是 X 且只是 X,绝不暴露底层模型")+
`Role Definition / Core Capabilities / 内置 Skill 使用场景(自动触发的技能)/ Final Identity Reminder`。
**持久化**(sqlite `~/.workbuddy/workbuddy.db`):`sessions` 表带 `expert_id / expert_locale /
expert_runtime_identity(人格快照,manifest 变了会话也钉住)/ expert_marketplace`;`automations` 也带 `expert_id`(定时任务可绑专家)。
### 1.2 QoderWork —— 没有"专家团",但有可借鉴的分层
QoderWork **没有**命名人格选择器。它的三层值得参考:
| 层 | 单元 | 说明 |
|---|---|---|
| 人格/性格 | `awareness/`(`SOUL.md` 等 + soul-presets:果断/沉稳/高效/贴心) | 单 agent 身份调优 |
| 能力打包 | **Plugin(专家套件)** = Skills + Commands + Connectors(MCP) | "装一个角色的整个工具箱",最接近"专家" |
| 运行时编排 | **Sub-agents**(`agents/*.md`,`Agent(subagent_type,...)` 派生) | 真·多智能体 |
- `plugin-creator` skill 原话:"Skill 是一件工具,Plugin 是一个角色的整个工具箱。"按角色/行业分类(电商/产品/法务/财税)。
- `ai-slides` LegoKit 是教科书式 **orchestrator→worker 流水线**:6 个命名 subagent(researcher/designer/reviewer/notes-writer),
硬性委派边界,单轮多 tool_use 块并行,**产物落盘 + JSON 指针交接(回传指针不回传大 payload)**。
- 可借鉴的具体做法:Markdown+frontmatter 的 agent 按 `description` 自动路由;单轮 fan-out + 等待 fan-in(**不要后台 detach**);
每个 worker 入口自校验;性格 preset;用 `AskUserQuestion` 引导"自建专家"。
---
## 2. maka-agent 现有可复用底座(三块几乎现成)
后端主轴(ARCHITECTURE.md):`Desktop/TUI/Headless → SessionManager → AgentRun → Model + Tool Runtime → 事件日志 → 投影`。
存储是**文件/JSON**(无 sqlite)。
| 关注点 | 复用/扩展 | 位置 |
|---|---|---|
| 专家**人格注入** | `mode:` 标签 + prompt fragment(照抄 Deep Research) | `apps/desktop/src/main/system-prompt-main.ts`(已有 `childInstruction` seam);`packages/core/src/deep-research.ts`(`mode:deep_research` 先例) |
| 专家=**带工具作用域的专业 agent** | 扩展 `BUILTIN_AGENT_DEFINITIONS`(每条已带 `systemPrompt`) | `packages/runtime/src/agent-catalog.ts`、`subagent-tools.ts`(`agent_spawn`/`SessionManager.spawnChildAgent`) |
| 专家**目录 + 按需安装** | 克隆 managed/bundled 技能目录 | `apps/desktop/src/main/managed-skill-sources.ts`、`bundled-skill-catalog.generated.ts`、`skills.ts`(install→`skill.lock.json`→baseline→update diff,PR #842) |
| 专家**定义文件格式** | 复用 SKILL.md frontmatter 解析 | `packages/runtime/src/skills.ts`(`parseSkillFrontMatter`) |
| 专家**选择器 + 中心 UI** | 克隆"市场/内置/已安装"三 tab | `packages/ui/src/skills-panel.tsx`、`nav-selection.ts`、`session-sidebar-nav.tsx`、`module-pages.tsx` |
| 专家**聊天界面** | 普通会话 + expert 标签 | `packages/ui/src/chat-view.tsx`、`composer.tsx` |
| **非 UI 一致性**(CLI/headless) | 共享 runtime prompt builder,自动继承 | `packages/cli/src/cli-system-prompt.ts`、`packages/headless/src/tools.ts` |
**关键判断:**
1. maka 的 `agent-catalog.ts`(每个 `AgentDefinition` 带 `id/name/description/systemPrompt/tools/permissionMode`)
与 WorkBuddy 的"专家 = 带人格的 subagent"**结构同构**。→ 单专家直接落在这里。
2. maka 的 subagent runtime(`spawnChildAgent` + `agent_spawn` 工具,父子 AgentRun)
与 WorkBuddy team 的"lead 通过 Agent 工具派生 member"**结构同构**。→ 专家团直接落在这里。
3. maka 的技能市场(远程/内置目录 + `contentSha256` + install/lock/update)
是"专家中心目录"**现成模板**,只差一个 `sourceType:'remote'` HTTP 拉取。
4. ⚠️ 约束:worktree 隔离的子代理目前 **fail closed**(没有 worktree executor)。专家团 member 先用**非隔离**子代理跑。
---
## 3. 推荐方案
### 3.1 数据模型:专家 = 一份 `EXPERT.md`(SKILL.md 同族)
一个专家一个目录,`EXPERT.md` = frontmatter + 人格 body:
```yaml
---
id: content-creator
displayName: { zh: 文博凯, en: Kai }
profession: { zh: 内容创作专家, en: Content Creator }
description: { zh: ..., en: ... }
category: 06-ContentCreative
avatar: avatar.png
color: teal # 强调色(借 WorkBuddy)
expertType: agent # agent | team
allowed-tools: [...] # 声明式,不授予;PermissionEngine 仍是权威
quickPrompts: [{ zh, en }]
defaultInitPrompt: { zh, en }
# team 专用:
members:
- { id, displayName, profession, promptFile, role: lead|member }
---
<身份指令 + Role / Capabilities / 内置技能触发场景 + 身份提醒>
```
- 复用 `parseSkillFrontMatter`(扩字段),复用技能的 install/lock/baseline/update 生命周期。
- 目录:内置 `bundled-expert-catalog.generated.ts`(复用 memory 里已逆向的 13 skills 那套种子流程);
managed `~/.maka/experts/<id>/EXPERT.md`;后续加 `sourceType:'remote'` 拉远程 manifest(COS 那套)。
### 3.2 运行时:单专家 vs 专家团
- **单专家(expertType=agent)**:给会话打 `mode:expert:<id>` 标签 → `system-prompt-main.ts` 按标签 gate 出人格 fragment,
经 `childInstruction` 边界注入(继承权限/隐私/工作区约束)。**照抄 Deep Research 的 `mode:deep_research` 路径。**
召唤时把 `defaultInitPrompt` 作为首条消息注入,`quickPrompts` 渲染成起手按钮。
- **专家团(expertType=team)**:lead 作为对话 agent(`mode:expert:<teamId>` + lead 人格),
lead 人格 prompt 里写 orchestrator 协议(TeamCreate→派发→回传,星型),
member 作为 `AgentDefinition` 注册进 catalog,lead 用现有 `agent_spawn` 派生(`subagent_type=member.id`)。
**不后台 detach**:单轮 fan-out + 等待 fan-in(借 Qoder 教训);member 回传落盘+指针。
### 3.3 UI
- 导航:`nav-selection.ts` 加 `{section:'experts'}`;`session-sidebar-nav.tsx` 加"专家团/同事"入口;`module-pages.tsx` 加 `ExpertsPage`。
- 专家中心:克隆 `skills-panel.tsx` 的 市场/内置/已安装 三 tab + 分类过滤 + 搜索 + 卡片"召唤"。
- 专家聊天:就是普通 `chat-view.tsx` 会话,带 expert 标签 + 头像 + `color` 强调色(借 `use-expert-theme` 思路,light/dark 感知)。
### 3.4 分期落地
| 阶段 | 交付 | 复用 |
|---|---|---|
| **P0 单专家 MVP** | `EXPERT.md` 格式 + 内置目录 + `mode:expert:` 注入 + 专家中心 UI + 召唤即聊 | Deep Research 注入路径、技能目录/UI、agent-catalog |
| **P1 专家团** | team schema + lead orchestrator 人格 + member 子代理派发(非隔离)+ 团队卡片 | `spawnChildAgent`/`agent_spawn`、subagent runtime |
| **P2 数字同事** | 持久化助理外壳(自建昵称/头像/雇佣专家/配置仓库·资料库·模型)+ 定时调度 | 会话标签、scheduled-task-store |
| **P3 远程专家中心 + 分享** | `sourceType:'remote'` 拉 COS 式 manifest + 缓存校验 + 自建专家导出/导入(安全扫描) | managed-skill-sources 远程化、lock/sha256 |
| **P4(可选)IM 绑定 / 云端 / @ 协作** | 绑 IM 渠道、云端运行、@ 数字同事 | 需新连接层,评估 |
### 3.5 待你拍板的分叉点
1. **专家人格的落点**:(a)纯 overlay = 标签会话 + 人格 fragment(最轻,先做)vs(b)数据驱动 `AgentDefinition` = 带独立工具作用域/权限(重,给"真·工具受限同事")。建议 P0 走 (a),P1 team member 走 (b)。
2. **专家团 member 隔离**:先接受非隔离(共享 cwd)还是先补 worktree executor?建议先非隔离,P1 后再评估隔离。
3. **目录来源**:先只做内置 + 本地 managed(P0),远程 COS 式 manifest 放 P3;还是一开始就上远程?
---
## 4. 一句话总结
WorkBuddy 的"专家 = 带身份覆盖人格的 subagent,专家团 = 一个 lead 星型编排多个 member subagent,专家中心 = COS 上的
plugin 市场,数字同事 = 雇佣专家的持久化助理外壳"。这套模型和 maka-agent 的
**agent-catalog(人格 subagent)+ spawnChildAgent(多智能体)+ 技能市场(目录/安装/锁)+ Deep Research(mode 注入)**
四块现有底座几乎一一对应,可分 4~5 期低风险落地。
@@ -0,0 +1,181 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Frontend Simplification Map (2026-07-13)
Maintainer goal: the project has run long; prune redundant/messy frontend code at
architecture level. Method: measure first (knip + wc + grep), stage rounds, gate each
on the full suite + typecheck + dead-css + alignment auditor.
Baseline (this tip, 1301404d): 45.5K lines TS/TSX (non-test) + 22.0K lines CSS.
Hotspots: app-shell.tsx 1733 · chat-turn 970 · bot-chat 961 · OnboardingHero 955 ·
provider-oauth 870 · composer 866 · scheduled-task 865 · connection-detail 861.
CSS: maka-tokens 1509 · onboarding 916 · scheduled-tasks 864 · skills 833.
Knip verification notes: storybook stories + ui .test.tsx flagged "unused" are FALSE
positives (apps/desktop/.storybook globs both story roots; ui test runner executes
dist/**/*.test.js). Real finds verified by hand before acting.
## Rounds
- [x] **A — SHIPPED (b2a1afd6): knip governance + dead code + dep hygiene.** 6 symbols deleted, 1 demoted, dev-hmr.mjs removed; @base-ui/react→deps, streamdown→devDeps (test-only; deviation on correctness), overlayscrollbars NOT declared (overlay-scrollbars contract forbids it — knip ignoreDependencies instead); scroll-area.tsx retained (a contract reads its content — follow-up: remove file+test together); knip.json (entries per workspace, ignoreExportsUsedInFile, -knipignore tag) + 2 CI steps in the typecheck job; ignore reasons documented below. Gates: desktop 2397/2397, ui 125/125, typecheck, 4 static checks, auditor — all identical before/after.
- Delete packages/ui/src/primitives/scroll-area.tsx (orphaned by overlay-scroll-area)
- ~40 unused exports/types across desktop main+renderer and packages/ui — for each:
grep __tests__ first (contract tests read source text; some pins assert the export
form) — demote `export` or delete the symbol per-site, never break a pin silently
- Declare real deps: apps/desktop needs streamdown, @base-ui/react, overlayscrollbars
(currently resolving via hoisting — fragile)
- Add knip.json (workspaces, storybook entry globs, test entry globs) + CI step so
dead code cannot re-accumulate
- [~] **B — app-shell.tsx decomposition (flagship) — 4/5 blades SHIPPED (branch
refactor/app-shell-decomposition), 1733 → 1666 lines, zero behavior change.**
Gates identical each commit: desktop 2397/2397, ui 125/125, typecheck, dead-css,
knip (desktop+ui); final alignment auditor (AUDIT_PORT_BASE=19300) clean on all 9
fixtures + branch-vs-baseline CDP pixel captures identical (turn-narrative,
module-skills, settings-general).
1. use-pending-action-registry — SHIPPED first (only blade that removes dup). ONE
generic useKeyedPendingRegistry replaces the four hand-rolled keyed sets
(turnActions state+timers, sessionRow, permissionMode, sessionModel). keysRef
stays a stable Set so factories + unmount cleanup are byte-identical. Re-pinned
sticky-model / status-presentation / row-actions-fail-soft contracts.
3. use-project-context — SHIPPED. appInfo, branchList/pending, recentProjectPaths,
projectPicker pending+refs + createAppShellProjectActions wiring.
4. use-module-data — SHIPPED. skills/managedSkillSources/bundledSkillCatalog/
scheduledTasks + both skill/plan action factories; surface-active predicates
injected.
5. use-shell-connections — SHIPPED (partial). connections/defaultConnection +
connectionsEqual + refreshConnections + handleConnectionEvent. theme/userLabel/
defaultPermissionMode stayed: default-permission-mode contract pins the
defaultPermissionMode useState + refreshShellSettings + closeSettings mirror to
app-shell.tsx specifically, and the theme setters have multiple app-shell writers
(visual-smoke, settings-overlay onChange) — moving them needs setter injection
with no net simplification.
2. use-shell-navigation — SKIPPED (disproportionately risky, per the "ship what's
done" rule). Its two anchors — openSessionInChat and closeSettings — are pinned
to app-shell.tsx by source-slice contracts, so they cannot move. Its highest-value
movable code (the stable bridge callbacks searchModalOnNavigate /
paletteOnSelectSession / paletteOnOpenSearchModal / useSkillInChat) carries
PR-FE-BUG-HUNT-0 runtime identity-stability semantics that have ZERO source-pin or
e2e coverage — a subtle extraction error would silently regress search-during-
stream with no gate to catch it. A state-only move nets ≈0 app-shell lines because
every setter (setNavSelection ×15 sites, setSearchScrollTarget via the pinned
openSessionInChat/createSession, setSettingsOpen via the pinned closeSettings) must
be threaded straight back out. Left in place with this note.
Note: the map's < 900 target is not reachable through the five state-cluster blades
alone — app-shell's bulk is the derived-value block (~210 lines of model/thinking/
alert memos) and the JSX return (~340 lines), neither of which the blade definitions
cover; reaching < 900 would require extracting those + splitting the JSX into
sub-components (out of scope, separate risk).
- [x] **C — SHIPPED (refactor/css-token-consolidation): CSS strata consolidation.**
Census (machine-generated, var() consumers across all CSS+TSX): reference-shell.css
23 defined → 16 dead (removed), 3 live color-aliases moved to maka-tokens
(--color-bg-container/--color-border-tertiary/--color-text-quaternary), 4 live
layout-locals kept in-file (--agents-layout-bg/--agents-content-area-bg/-gap,
--sidebar-width). theme-glass.css 2 token overrides (darwin --color-bg-container light+dark,
--color-text-quaternary) relocated to maka-tokens; glass-material + text/label RULES stay.
maka-tokens.css: added token-authority README (authoritative --background family vs
compatibility --color-* aliases, alias policy) + condensed the superseded hue-80 archaeology
(it named deleted tokens). Both themes migrated (light :root + .dark + darwin light/dark).
NO computed value changed — pure reorg; the shipped `--color-bg-container: var(--background)`
exception kept as-is. maka-tokens internal 0-consumer tokens are all governance-contract
scale members (z-index/control-height/spacing/radius/406) — re-pinned, NOT deleted.
Gates: desktop 2397/2397, ui 125/125, typecheck 0, check-dead-css clean, knip desktop+ui 0,
auditor exit 0. CDP light+dark captures (turn-narrative/module-skills/settings-general/first-run)
before/after — no visual diff.
- [x] **D-1 — SHIPPED: shared useMountedRef + last formatBytes fork.** Case-insensitive
re-census found the mounted-guard boilerplate at ×38 (not ×6 — the first grep missed
`fooMountedRef` casings). Shipped: `useMountedRef` lives in @maka/ui (exported from
index) so both workspaces reach it; converted the 6 canonical `mountedRef/
onboardingMountedRef` sites (OnboardingHero ×2, password-input, daily-review/
general/permission-center settings pages) and re-pinned 5 contracts to the
shared-hook form; voice formatVoiceBytes (last formatBytes fork) deleted in favor
of the @maka/ui helper. Deliberately NOT converted: use-workspace-instructions-
controller (lifecycle-counter variant, not boilerplate) and app-shell.tsx
rendererMountedRef (Round B owns that file).
- [x] **A follow-up — orphan scroll-area.tsx removed.** primitives/scroll-area.tsx
(34 lines, zero consumers, never exported from the ui index) deleted together with
its knip ignore; the overlay-scrollbars contract's per-file assertion upgraded to a
repo-wide ban on @base-ui react scroll-area imports (stronger invariant, no
coverage lost).
- [x] **D-2 — SHIPPED: mounted-guard long tail.** Converted 34 of the 35 census
`*MountedRef` sites to `useMountedRef` — 20 renderer settings sites, 4 other
renderer sites (OnboardingHero readyHero, FirstRunChecklist, artifact-pane,
browser-panel), 10 packages/ui panels (chat-turn, chat-model-switcher, search-modal,
scheduled-task-panel, clipboard-feedback, skills-panel, composer, permission-dialog,
session-history-list, daily-review-panel). Kept each site's companion-ref cleanup
effect and its ref name; deleted the whole effect only where it did nothing but the
mounted flag (browser-panel, permission-dialog). packages/ui sites import the hook
from `./use-mounted-ref.js` per house style. Re-pinned ~30 contract assertions across
~20 test files to the shared-hook shape (definition lines + effect blocks). The
useRef(false) variants all read the flag only inside async handlers — no pre-effect
reads — so flipping to true-initial semantics is behavior-preserving. Deliberately
NOT converted: use-memory-settings-controller (lifecycle-counter variant — cleanup
reset is guarded by a lifecycle counter and reads combine mounted with lifecycle
equality, same shape as use-workspace-instructions-controller) and app-shell.tsx
rendererMountedRef (Round B owns that file).
- [~] **E — app-shell derived-value extraction (Round B follow-on) — blade 1 SHIPPED,
blade 2 SKIPPED. branch refactor/app-shell-view-split. app-shell.tsx 1680 → 1562.**
1. **Derived-value extraction — SHIPPED (2 commits).** The whole ~210-line derived
block moved into two pure-derivation hooks following the use-shell-connections /
use-project-context house style, zero behavior change, every memo keeping its exact
dep array + referential stability:
- `use-shell-chat-model.ts` (195 lines): model/thinking selection (chatModelChoices,
active/new-chat model+label, thinking-variant lists, sticky-pick validation, both
pending new-chat states) + the two chat-header alert memos. openSettingsSection is
injected so chatConnectionAlert keeps its identical exhaustive-deps-excluded memo.
- `use-shell-live-turn.ts` (112 lines): live-turn projection (activeShellRunUpdates,
streaming/thinking slices, streamingSessionIds pulse set, liveTools/
hasInFlightLiveTools) + the #646 turn-wait cues. `activeLiveTurn` stays pinned in
app-shell.tsx (streaming-timeline source-slice contract) and is passed in.
Re-pinned: added both files to renderer-shell-source-helpers combined allowlist;
added use-shell-chat-model.ts to the composer-new-chat model-picker contract's two
subset reads (pendingNewChatModel / validPendingNewChatModel / newChatThinkingLevel
declarations moved into the hook). All other model/live-turn contracts read combined
source and auto-followed via the allowlist.
2. **JSX return split — SKIPPED (disproportionately risky + net-negative, per the
ship-what's-done rule; Round B skip note is the model).** The ~340-line return's
content area is irreducibly coupled to ~110 AppShell locals, and every meaningful
chunk is pinned to app-shell.tsx by a DIRECT-read (non-combined) contract: ChatView
`liveTurn={activeLiveTurn}` (streaming-timeline), Composer `onPickAttachments`/
`onAttachFilePaths` (attachment-frontend) + `onPickNewChatModel` (composer-new-chat),
SessionListPanel `statusGroups={sessionListGroups}` (session-project-view), the
DailyReviewPage `onCopyMarkdown` (daily-review-copy-feedback), and the onboarding
`onSkip` handler (onboarding-one-time-regression). Extracting any of them into a
layout sub-component (a) forces ~50–110 props of straight-through drilling that ADDS
net interface/destructure boilerplate rather than pruning it (counter to the round's
goal), (b) requires structurally re-pinning 5+ behavioral contracts that specifically
assert "the shell orchestrator wires handler X into element Y", and (c) still lands
app-shell.tsx ≈ 1300 — the ≤1100 target is not safely reachable without over-
extraction under the zero-behavior mandate. Left in place with this note; the
derived-value extraction (blade 1) captures the genuine simplification.
Gates (each commit): desktop 2399/2399, ui 125/125, full typecheck 0, check-dead-css
clean, knip desktop+ui 0. Final: alignment auditor (AUDIT_PORT_BASE=19900) exit 0, all
9 fixtures clean. CDP branch-vs-baseline captures (real Electron, light+dark 1280) of
turn-narrative / module-skills / settings-general / first-run: module-skills + first-run
byte-identical; settings-general independently non-deterministic (baseline itself yields
two hashes across repeated passes — pre-existing capture flake, unrelated); turn-narrative
differs ONLY in a 311×24px region at the very bottom of the frame — the composer footer
git-branch chip, which renders the worktree's real HEAD (the baseline had to be captured
in DETACHED-HEAD state since the `main` branch ref is held by the concurrent worktree, so
the chip shows `—` vs the feature-branch name). Pixel-diff bbox confirmed x:[1106-1417]
y:[1570-1594]; every other pixel identical, and the chip-less fixtures are byte-identical
— i.e. the derivation extraction is a proven render no-op.
Update checkboxes as rounds ship. Every round: suite + typecheck + dead-css +
alignment auditor + CDP spot captures, exit-code gated.
@@ -0,0 +1,565 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Maka full product completion and test plan
> Archived on 2026-07-13. This one-month delivery plan is retained as historical context; current work and progress belong in GitHub issues and pull requests.
> Owner: @xuan
> Date: 2026-05-22
> Scope: one-month completion plan for Maka desktop product capability, quality gates, and release testing.
This document is a delivery contract. A feature is not done because UI exists or a PR merged. It is done only when the user flow, data contract, tests, fixtures, smoke path, and security/privacy gates are all present.
## 0. Non-negotiable Rules
Every PR must answer these five questions in its description:
1. **Contract**: What data structure, IPC channel, runtime event, persisted state, or component contract changed?
2. **User Flow**: What exact path can the user complete after this PR?
3. **Tests**: Which unit, storage, runtime, IPC/main, renderer-helper, fixture, and smoke tests cover it?
4. **Security**: What trust boundary, secret handling, path guard, sandbox, redaction, or permission rule applies?
5. **Not Included**: What adjacent work is explicitly out of scope and tracked separately?
No release if any of these are true:
- A configured ready default model exists but old sessions can still block sending.
- A provider key, raw provider error, filesystem absolute path, chatId, or secret-shaped value can leak to stdout, UI, telemetry, export, diagnostics, or artifact metadata.
- Renderer can read or open arbitrary absolute paths.
- HTML/Markdown content can navigate the Electron renderer, open a Node-enabled window, or escape its sandbox.
- A new UI surface has no empty, loading, error, and focus state.
- A new stateful feature has no deterministic fixture scenario and no smoke path.
- A new logic branch exists only in React code without a pure helper or automated test when a pure helper is practical.
- A manual-only gate is used for a case that can be deterministically tested with node:test.
## 1. One-Month Delivery Plan
### Week 1: restore trust and complete Artifact Workbench
Goal: core chat send path and generated work output must be reliable.
Required deliverables:
- P0 stale-session send/rebind fully closed.
- Artifact pane becomes a real workbench surface, not a transcript ornament.
- Real save/export behavior for artifacts.
- Artifact runtime hooks cover common file-producing tools.
- Deterministic fixture and smoke coverage for normal, error, deleted, too-large, unsupported MIME, and reload states.
Done means:
- Old `fake`, legacy backend, deleted connection, stale model, and valid Z.ai default cases are all tested.
- Artifact records are file-backed, renderer never sees absolute paths, deleted tombstones block reads, symlink escapes fail.
- HTML previews are view-only with sandbox and blocked navigation.
- Binary previews use sniffed MIME allowlist.
- Smoke paths cover light, dark, narrow, reload, and failure states.
### Week 2: model catalog, workstation shell, session status, turn controls
Goal: Maka stops behaving like a generic chat list and starts behaving like a workbench with explicit state.
Required deliverables:
- `ModelCatalogEntry` with normalized capabilities, source, stale/unsupported reason, context, and pricing fields.
- Chat default cannot be image-only, embedding-only, unsupported, disabled, missing, or stale without a visible reason.
- Session status model: active, running, waiting, blocked, review, done, archived, stale, errored.
- Sidebar/header expose workspace, model, status, blocked reason, and old-session migration state.
- Turn controls: retry, regenerate, branch-from-turn, cancel, checkpoint-before-tools.
Done means:
- Status transitions have node:test coverage.
- Turn controls cannot overwrite old output.
- Cancel persists an explicit aborted state.
- Unsupported model cases are visible in ModelTable and fail closed in send readiness.
- Fixture scenarios seed each status and model capability combination.
### Week 3: Health Center, First-run, Quick Chat, Settings completion
Goal: setup, debugging, and entry points are first-class.
Required deliverables:
- Health Center for provider, credential, bot, proxy, search, voice, open-gateway, storage, artifact, and workspace health.
- Redacted diagnostics copy.
- First-run stepper: provider preset, paste key, test/fetch models, choose default, send smoke prompt.
- Quick Chat MVP: global shortcut and panel window; no accessibility-tree capture in MVP unless separately approved and gated.
- Settings panels for font/sidebar, chat tuning, editable keybindings, and advanced flags.
Done means:
- First-run does not allow fallback-as-success.
- Health Center uses generalized reasons and never raw provider errors.
- Quick Chat opens fast, focuses composer, reuses readiness guard, and fails closed when no ready model exists.
- Keybindings detect collisions and have reset defaults.
### Week 4: Open Gateway, Memory, Voice, Search, MCP, Sources/Skills/Automations
Goal: complete the promised ecosystem and automation surfaces without widening permissions invisibly.
Required deliverables:
- OpenAI-compatible local gateway with auth, SSE, model mapping, usage telemetry, and shutdown.
- Memory MVP with explicit inspect/delete controls and no hidden permission widening.
- Voice input MVP with permission state and transcript correction.
- Search/web citation surface with source chips and export behavior.
- MCP servers panel with status, scopes, tool list, and disable controls.
- Sources, Skills, Automations view: auth/scope, allowed tools, last run, last error, and disable.
Done means:
- Each external integration has auth, missing, timeout, network, rate limit, and revoked states.
- Each automation is visible, disable-able, and auditable.
- Skill installation never implies permission widening.
- Diagnostics and telemetry are redacted and reason-coded.
## 2. Testing Layers
### 2.1 Core unit tests
Use for:
- data contracts and enum validation,
- permission categorization,
- redaction and generalized error messages,
- model capability/readiness rules,
- session/turn status transitions.
Gate:
```bash
npm --workspace @maka/core test
```
### 2.2 Storage tests
Use for:
- JSONL header migration,
- artifact metadata and file-backed payloads,
- credential/connection persistence,
- telemetry aggregation,
- symlink and traversal path guards,
- tombstones and purge behavior.
Gate:
```bash
npm --workspace @maka/storage test
```
### 2.3 Runtime tests
Use for:
- SessionManager lifecycle,
- backend rebuild on config changes,
- streaming events,
- tool artifact derivation,
- cancellation,
- permission parking,
- provider model fetch and connection testing.
Gate:
```bash
npm --workspace @maka/runtime test
```
### 2.4 Desktop main / IPC tests
Use for:
- chat readiness and auto-rebind,
- external link guard,
- window state,
- open path guard,
- visual smoke fixture mode,
- connection status,
- settings IPC helpers,
- artifact IPC failure reasons,
- sandbox bridge sanity.
Gate:
```bash
npm --workspace @maka/desktop test
```
### 2.5 Renderer pure-helper tests
Use for:
- state derivation,
- keyboard transition helpers,
- display copy matrices,
- status priority,
- turn materialization,
- command palette filtering,
- sidebar stale/session status projection.
Rule: if a React branch decides behavior from data, extract it to a pure helper unless the branch is trivial.
### 2.6 Fixture scenarios
Each new surface gets a deterministic fixture. Fixtures must:
- run only in dev/test,
- use isolated `workspaces/visual-smoke-*`,
- seed from scratch on launch,
- not depend on real keys or network,
- expose transient state only through `visualSmoke.getState()`,
- return `null` when fixture mode is off.
Required scenarios:
| Scenario | Purpose |
|---|---|
| `first-run` | empty workspace, no connection |
| `provider-workspace` | fetched models, default, verified |
| `provider-fallback` | fallback source and refresh error |
| `provider-empty` | fetched-empty state |
| `connection-error` | needs_reauth/error header |
| `turn-narrative` | user, tools, assistant, token summary, thinking |
| `streaming-sidebar` | streaming preview and unread priority |
| `permission-destructive` | destructive PermissionDialog |
| `artifact-pane` | html, diff, markdown/file artifacts |
| `artifact-errors` | deleted, too_large, unsupported_mime, missing |
| `stale-sessions` | fake/stale/deleted session rows and header badges |
| `workstation-statuses` | active/running/waiting/blocked/review/done/archive |
| `turn-controls` | retry/regenerate/branch/cancel/checkpoint |
| `model-catalog` | chat/image/embedding/unsupported/stale models |
| `health-center` | all healthy + all errors |
| `first-run-stepper` | happy path + test/fetch failure |
| `quick-chat` | panel open, no-ready default, ready default |
| `sources-skills-automations` | source auth/scope, skill tools, automation last run |
### 2.7 Smoke paths
`apps/desktop/tests/smoke.md` is the release checklist. Every fixture scenario above needs a smoke path or an explicit reason it is covered by an existing path.
Each smoke path must include:
- launch command,
- fixture scenario,
- exact user steps,
- expected UI state,
- failure state,
- light/dark/narrow screenshot requirement,
- reload persistence expectation,
- no-go regressions.
### 2.8 Visual regression
Required screenshots for each new surface:
- light desktop,
- dark desktop,
- narrow width,
- loading,
- empty,
- error/failure,
- active/focus state.
Current automation:
- `npm --workspace @maka/desktop run screenshots` captures all fixture
scenarios across light/dark, 1280/990 width, normal/reduced motion.
- `npm --workspace @maka/desktop run screenshots:diff:stable` is the
blocking sanity gate for the stable subset (`artifact-pane`,
`first-run`, `artifact-errors`).
- The stable gate fails only for capture/pipeline/viewport failures:
missing PNGs, corrupt PNGs, too-small/truncated PNGs, and wrong
dimensions.
- Byte-size drift is a warning, not a blocker.
Explicit limitation: the current PR-IR-02 gate is **not** a pixel-level
visual regression test. It does not prove UI layout, color, typography,
spacing, or focus rendering stayed correct. Reviewers must still inspect
the screenshots for visual quality and use the smoke paths for behavior.
Future automation target:
- pilot pixel-level diff only on the stable subset first,
- use calibrated tolerance instead of byte/SHA equality,
- support ignored dynamic regions for timestamps/streaming/transient UI,
- save diff artifacts for review,
- expand beyond the stable subset only after the gate is quiet on main.
### 2.9 Security and privacy gates
Every feature must declare:
- path boundary,
- network boundary,
- secret boundary,
- renderer/main trust boundary,
- export/clipboard boundary,
- telemetry/log boundary,
- permission boundary.
Required checks:
- `scripts/check-console.mjs` passes.
- new `console.*` is dev-only or allow-listed with reason.
- user/provider text in export/diagnostics is redacted.
- raw provider errors go through `generalizedErrorMessage`.
- renderer never receives decrypted secrets.
- absolute paths are not shown unless the surface is explicitly a local-path management surface.
- file operations use realpath containment, not string prefix checks.
- Electron navigation/window-open remains blocked for untrusted content.
## 3. Feature Done Definitions
### 3.1 Chat send and session readiness
Cases:
- no default connection,
- default points to `fake`,
- missing connection,
- disabled connection,
- missing API key,
- missing model,
- empty model list,
- model not enabled,
- stale fake session with ready default,
- stale missing connection with ready default,
- stale session without ready default,
- active backend cache after rebind.
Done means:
- ready default + stale old session sends successfully after auto-rebind,
- no ready default fails with original machine-readable reason,
- renderer keeps unsent input when send fails,
- header/sidebar explain stale state before send,
- tests cover all cases.
### 3.2 Artifact Workbench
Cases:
- list/get/read text/read binary/delete,
- live artifact creation from tool output,
- deleted tombstone blocks read,
- symlink escape,
- traversal path,
- too large text,
- unsupported MIME,
- HTML with external links,
- reload persistence,
- reveal in Finder,
- real Save As.
Done means:
- artifact is a first-class object,
- transcript references are compact,
- pane previews are reliable,
- export/save works without exposing renderer to absolute paths.
### 3.3 Model Catalog
Cases:
- fetched source,
- fallback source,
- fetched-empty,
- stale cache,
- unsupported image-only,
- unsupported embedding-only,
- missing tool-use in execute mode,
- custom OpenAI-compatible,
- pricing override.
Done means:
- UI displays facts from backend-normalized catalog,
- chat readiness rejects unsupported defaults,
- model table explains disabled rows.
### 3.4 Workstation Shell
Cases:
- active,
- running,
- waiting permission,
- blocked by config/auth,
- review,
- done,
- archived,
- stale/rebound,
- error.
Done means:
- sidebar, header, and chat body agree on status,
- status changes are persisted,
- status transitions are tested,
- no status is inferred only by styling.
### 3.5 Turn Controls
Cases:
- retry failed turn,
- regenerate assistant answer,
- branch from a prior turn,
- cancel running turn,
- checkpoint before tools,
- old output preserved.
Done means:
- persisted turn state prevents overwrite,
- branch copies the right message boundary,
- cancel writes aborted,
- UI buttons are disabled when invalid.
### 3.6 Health Center
Cases:
- provider OK/error/reauth,
- credential missing/revoked,
- bot disabled/error/connected,
- proxy disabled/error/ok,
- storage path unavailable,
- artifact root unavailable,
- open gateway stopped/running/error,
- search/voice/MCP unavailable.
Done means:
- user has one place to inspect system health,
- copy diagnostics is redacted,
- every subsystem uses reason-coded status.
### 3.7 First-run
Cases:
- no connections,
- invalid key format,
- provider test 401,
- model fetch error,
- fetched-empty,
- choose default,
- send smoke prompt.
Done means:
- user can get from empty workspace to first real message in four steps,
- failures are inline,
- no fallback-as-success.
### 3.8 Quick Chat
Cases:
- global shortcut registered,
- hotkey conflict,
- no ready model,
- ready model,
- existing active session context,
- send/stop,
- close/reopen preserves draft policy.
Done means:
- Quick Chat is an entry point to the same readiness/runtime contract,
- it does not introduce a second send path.
### 3.9 Integrations
Cases:
- Open Gateway auth/SSE/errors,
- Memory inspect/delete,
- Voice permission/transcription errors,
- Search citations/export,
- MCP server install/connect/tool list,
- Sources/Skills/Automations scope and disable.
Done means:
- each integration is visible, scoped, disable-able, and testable,
- no integration silently widens tool permissions.
## 4. PR Checklist
Copy this into PR descriptions:
```md
## Contract
- [ ] Data/API/event/state changes described
- [ ] docs/design-system.md or docs/full-product-test-plan.md updated if contract changed
## User Flow
- [ ] Main happy path described
- [ ] Failure path described
- [ ] Reload/persistence behavior described
## Tests
- [ ] core/storage/runtime/desktop tests added or marked N/A with reason
- [ ] renderer pure helper test added where practical
- [ ] fixture scenario added/updated
- [ ] smoke.md path added/updated
- [ ] light/dark/narrow screenshots captured or visual gate marked N/A with reason
## Security
- [ ] secrets redacted
- [ ] raw provider errors generalized
- [ ] path boundary uses realpath containment
- [ ] renderer does not receive arbitrary absolute paths
- [ ] Electron navigation/window-open/sandbox boundary unchanged or tightened
- [ ] console/log behavior checked
## Not Included
- [ ] Follow-up work listed explicitly
```
## 5. Command Gate
Before merging any non-doc-only PR:
```bash
npm run build
npm run typecheck
npm test --workspaces --if-present
```
For UI surfaces, also run the relevant fixture smoke path from `apps/desktop/tests/smoke.md`.
## 6. Current Immediate Next Work
Priority order after the P0 stale-session fix:
1. Artifact Workbench completion: real Save As, artifact error fixture, deleted/too-large/unsupported smoke.
2. ModelCatalogEntry and unsupported default guard.
3. Workstation shell/session status.
4. Turn controls.
5. Health Center.
6. First-run stepper.
7. Quick Chat.
8. Open Gateway / Memory / Voice / Search / MCP / Sources-Skills-Automations.
The next implementation PR should target item 1 and must not expand scope into unrelated UI polish.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,291 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Maka Code Bug / Flow Audit - 2026-05-22
> Archived on 2026-07-13. This is a point-in-time audit, not current implementation guidance.
Owner: @xuan
Thread: #my-ai:02c56cb6
Scope: current Maka code, runtime/main/renderer/core/storage/settings surfaces. Layout work is tracked separately by @yuejing.
## Fixed on main
### 1. Shell safe-prefix commands bypassed Ask prompt
Commit: `778077d Fix runtime failure states and shell permission gaps`
Evidence:
- `packages/core/src/permission.ts` categorized safe-prefix commands before checking generic shell control operators.
- Commands like `echo hello > out.txt`, `cat package.json | wc -l`, `pwd && npm test`, backticks, and `$()` could be treated as `shell_safe`.
Impact:
- Ask/Explore mode could auto-allow command forms that mutate files, chain commands, or expand secret-bearing command substitutions.
Fix:
- Added `SHELL_CONTROL_PATTERNS` and classify those commands as `shell_unsafe` before safe-prefix matching.
- Regression tests added in `packages/core/src/__tests__/permission.test.ts`.
### 2. Backend build/send failure after user append left false session state
Commit: `778077d Fix runtime failure states and shell permission gaps`
Evidence:
- `packages/runtime/src/session-manager.ts` appended the user message and turn running state before backend creation, but backend build failures happened before the active stream bookkeeping/finally state transition.
Impact:
- A persisted user message could be left with a running/active session instead of a failed/blocked turn.
Fix:
- Wrapped connection lock, backend build, stream bookkeeping, and backend send in a single try/finally.
- Catch appends failed turn state and final header becomes `blocked/unknown`.
- Regression test added in `packages/runtime/src/__tests__/session-manager.test.ts`.
### 3. Renderer stale message refresh could overwrite current chat
Commit: `778077d Fix runtime failure states and shell permission gaps`
Evidence:
- `apps/desktop/src/renderer/main.tsx` `refreshMessages(sessionId)` always called `setMessages(await readMessages(sessionId))`.
Impact:
- If the user switched sessions while an async read was in flight, the old session could overwrite the active chat view.
Fix:
- Guard `setMessages` with `activeIdRef.current === sessionId`.
### 4. Streaming / permission UI could remain stuck after error/abort/complete
Commit: `778077d Fix runtime failure states and shell permission gaps`
Evidence:
- Error and abort events refreshed sessions/messages but did not consistently clear streaming text or pending permission dialog state.
Impact:
- Composer/session UI could remain in a streaming/waiting state after the turn had already failed or aborted.
Fix:
- Added `clearStreaming(sessionId)` and clear pending permission state on error/abort; non-permission complete clears streaming.
### 5. Packaged app could create FakeBackend sessions from renderer input
Commit: `778077d Fix runtime failure states and shell permission gaps`
Evidence:
- `apps/desktop/src/main/main.ts` accepted renderer `sessions:create({ backend: 'fake' })` directly.
Impact:
- Packaged app had a fake-session creation path that bypassed the real readiness contract.
Fix:
- Added `canCreateFakeSessionFromRenderer()` and only allow fake sessions in unpackaged dev/visual-smoke contexts.
### 6. Skills were listed but not injected into runtime prompts
Commit: `a4a9b7e Wire skills into prompts and stop parked permissions`
Evidence:
- `skills:list` scanned `{workspaceRoot}/skills/*/SKILL.md`, but `buildSystemPrompt()` only included personalization.
Impact:
- UI made skills look installed, but the model never received their instructions.
Fix:
- Extracted skill scanning/prompt assembly into `apps/desktop/src/main/skills.ts`.
- `buildSystemPrompt()` now includes bounded local skill instructions.
- Prompt fragment explicitly says skills are lower priority, cannot grant tools, cannot bypass permission, and cannot override higher-priority instructions.
- Tests added in `apps/desktop/src/main/__tests__/skills.test.ts`.
### 7. Stop did not reject parked tool permission requests
Commit: `a4a9b7e Wire skills into prompts and stop parked permissions`
Evidence:
- `AiSdkBackend.stop()` aborted the model stream but did not end the current permission-engine turn.
Impact:
- If a tool was waiting on a permission dialog, Stop could leave the tool wrapper parked until a later user decision.
Fix:
- `stop()` now calls `permissionEngine.endTurn(currentTurnId, 'aborted')`.
- Regression test added in `packages/runtime/src/__tests__/ai-sdk-backend.test.ts`.
### 8. Auto-approved read tools could escape session cwd
Commit: `21522a9 Constrain read tools to session cwd`
Evidence:
- `Read`, `Glob`, and `Grep` were `permissionRequired: false` but accepted absolute paths, `../`, or symlink escapes.
Impact:
- Explore/Ask mode could read files outside the session workspace without a prompt.
Fix:
- `Read` rejects absolute paths and uses lexical + realpath containment under session cwd.
- `Glob` rejects absolute/parent-traversal patterns and constrains optional cwd.
- `Grep` constrains optional path under session cwd.
- Tests added in `packages/runtime/src/__tests__/builtin-tools.test.ts`.
### 9. Main-process stream catch emitted random turn ids
Commit: `1e41f64 Preserve turn ids for stream errors`
Evidence:
- `streamEvents()` catch synthesized renderer error events with `turnId: randomUUID()`.
Impact:
- Even after runtime persisted the correct failed turn, the renderer error event could be detached from the actual turn lineage.
Fix:
- Send/retry/regenerate/quick-chat now generate explicit turn ids and pass them as `fallbackTurnId` into `streamEvents()`.
- Catch uses that id and emits both session and turn status change events.
### 10. Prompt assembly lacked workspace instruction context
Commit: `0afcf2e Inject workspace instructions into prompts`
Evidence:
- `buildSystemPrompt()` included personalization and installed skills, but did not read project-local engineering instructions from the active session cwd.
Impact:
- Coding sessions could ignore repository-local rules such as `AGENTS.md` unless the user repeated them manually.
Fix:
- Runtime system-prompt callbacks now receive `{ sessionId, cwd, workspaceRoot }`.
- Desktop prompt assembly reads bounded allowlisted files from the session cwd: `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`.
- The loader realpath-checks containment, skips symlink escapes, strips control chars, caps per-file and total prompt size, and marks workspace files as lower-priority/untrusted context that cannot grant tools or weaken permissions.
### 11. Health Center had validation signals but no LLM runtime probe signal
Commit: current runtime health-probe change set
Evidence:
- `health:getSnapshot` emitted `healthSignalFromConnection()` only, so LLM rows represented connection-test validation (`lastTestStatus`) but not the real send/stream/abort path.
Impact:
- The UI correctly said "verified != operational", but had no first runtime signal to show whether a connection had actually sent successfully, failed, or been aborted.
Fix:
- LLM telemetry records now include `connectionSlug`.
- `TelemetryRepo` can return the latest runtime probe per connection/model.
- Health snapshots now emit a separate `runtime_probe` signal per configured enabled connection: unknown before first send, ok after last success, info after user abort, warning after last runtime error.
### 12. Write/Edit mutation tools could escape session cwd
Commit: current write-tool containment change set
Evidence:
- `Read`, `Glob`, and `Grep` were constrained to session cwd, but `Write` and `Edit` still resolved paths with `resolve(cwd, path)` only.
- Absolute paths and `../` traversal could write outside the session cwd when the permission mode allowed the tool, and symlink parents could route a write outside the workspace.
Impact:
- Even with permission prompts, the tool contract said session-cwd work, but the implementation allowed broader filesystem mutation than the user could reasonably infer.
- In Execute mode, `Write`/`Edit` are allowed by policy and therefore needed a hard path boundary too.
Fix:
- `Write` now rejects absolute paths, parent traversal, and symlink-parent escapes before writing.
- `Edit` now reuses the existing realpath containment helper and rejects absolute paths, parent traversal, and symlink file escapes.
- Regression tests added for both mutation tools.
### 13. Session store accepted path-shaped session ids
Commit: current session-id containment change set
Evidence:
- `FileSessionStore.sessionDir(sessionId)` joined renderer/runtime supplied session ids directly under `workspaceRoot/sessions`.
- Operations such as `remove(sessionId)` could receive a traversal-shaped id before path validation existed.
Impact:
- Most normal calls use generated UUIDs, but IPC/runtime boundaries should still reject path-shaped ids before any filesystem operation.
Fix:
- Session store now accepts only bounded ids matching `[A-Za-z0-9_-]{1,128}`.
- `list()` skips malformed folders.
- All filesystem paths and write queues assert the same id contract before reading, writing, or removing.
- Regression test verifies traversal ids reject and do not delete an outside victim directory.
## Remaining Product / Architecture Findings
### A. Coming Soon settings surfaces still need a product decision
Evidence:
- `apps/desktop/src/renderer/settings/SettingsModal.tsx` exposes `daily-review`, `voice-models`, `open-gateway`, and `search` as enabled nav items with Coming Soon copy.
- `packages/core/src/settings.ts` has section ids, but there is no real settings contract for these features yet.
Risk:
- This is honest enough as product-stance copy, but still takes Settings real estate and can feel like shipped capability.
Recommendation:
- @WAWQAQ should choose per page: implement now, hide from nav, or keep as disabled roadmap copy.
- If kept visible, each page needs a real snapshot/status source before it becomes actionable.
### B. Health / Capability center is still only partially probe-complete
Evidence:
- `apps/desktop/src/main/capability-snapshot.ts` marks Computer Use, Activity, Voice, Open Gateway, and Memory Write as `not_available` with scaffold reasons.
- Bot readiness is now safer.
- LLM connection health now has a first real runtime signal from usage telemetry.
- Most non-LLM runtime probes are still placeholders.
Risk:
- UI is no longer lying about operational state, but the actual parity capabilities are not implemented.
Recommendation:
- Next engineering PRs should implement real probes before flipping any non-LLM row to enabled/operational.
- Computer Use requires real helper process + AX/screenshot probe; Voice requires mic/TTS chunk probe; Open Gateway requires heartbeat.
### C. Main / renderer / settings / UI component files are still too large
Current sizes:
- `apps/desktop/src/main/main.ts`: 1300 lines
- `apps/desktop/src/renderer/main.tsx`: 1428 lines
- `apps/desktop/src/renderer/settings/SettingsModal.tsx`: 1643 lines
- `packages/ui/src/components.tsx`: 2654 lines
- `packages/runtime/src/ai-sdk-backend.ts`: 912 lines
Risk:
- Fixes are harder to isolate; merge conflicts increase; subtle event/state coupling is easy to miss.
Recommendation:
- Split by ownership, not by arbitrary helpers:
- main IPC: sessions/connections/settings/artifacts/capabilities/skills modules
- renderer: session event reducer / settings shell / chat shell
- ui components: chat, sessions, permissions, composer, tool activity modules
- runtime backend: stream pump, tool wrapper, telemetry/artifact hooks
### D. Open Gateway / search / voice / daily review are still no-op roadmap features
Evidence:
- They have Coming Soon UI copy and capability snapshot placeholders but no backend implementations.
Risk:
- Users may interpret the nav as feature presence.
Recommendation:
- Do not wire buttons/toggles until each has snapshot/degraded/revoke/audit/probe contracts.
- Keep all four behind disabled copy or hide them until implementation starts.
## Verification Log
After the latest fixes:
- `git diff --check` passed.
- `npm run typecheck` passed.
- Full `npm test --workspaces --if-present` passed after `0afcf2e`: core 122 / storage 38 / runtime 99 / desktop 327 = 586.
- Full `npm test --workspaces --if-present` passed after runtime health probe work: core 124 / storage 39 / runtime 99 / desktop 327 = 589.
- Pushed commits so far: `778077d`, `a4a9b7e`, `21522a9`, `1e41f64`, `5c324c4`, `0afcf2e`.
@@ -0,0 +1,755 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Maka 能力成熟度审计 v1
> Archived on 2026-07-13. This is a point-in-time audit against a May 2026 code baseline; its progress and frontend-document references are not current guidance.
> 编写:@xuan
> 日期:2026-05-22
> 基线:`main` at `832bbac`
> 范围:能力 / runtime / storage / IPC / release gate。UI 视觉规则以 `docs/design-system.md` 为准。
本文档回应 task #12 中 "@xuan 详细挖掘现在项目存在的问题,和需要继续做的能力"。
目标不是再列一组 wish list,而是把 Maka 从 "能聊天的桌面 app" 推向
reference implementation / reference implementation 那类成熟工作台时必须补齐的系统 contract。
审计参考:
- Maka 当前代码:`packages/core/src/session.ts`,
`packages/core/src/events.ts`, `packages/core/src/llm-connections.ts`,
`packages/runtime/src/session-manager.ts`, `packages/runtime/src/builtin-tools.ts`,
`apps/desktop/src/main/main.ts`, `apps/desktop/tests/smoke.md`
- reference implementation 本地资料:local research notes,
architecture gap notes,
`docs/11-providers.md`, `docs/17-workspaces-git.md`, `docs/39-streaming-deep.md`,
`readable/preload.js`
- reference implementation 本地资料:`~/.reference implementation-agent/docs/reference implementation-cli.md`,
`~/.reference implementation-agent/release-notes/*.md`
## 0. 当前基线判断
Maka 目前已经不是早期 chat demo:
- provider model discovery 已走 live fetch + source/fetchedAt contract;
- Settings / ModelTable / turn grouping / typed tool renderer / visual smoke fixture 已有基本 gate;
- Electron 安全边界、console audit、readiness reason、window state 等桌面基础已补了一轮;
- `docs/design-system.md` 已把 visual release gate 写成契约。
但能力层仍然薄:核心数据模型仍围绕 "session + JSONL messages",没有 artifact、
workspace status、turn control、source/automation、health、memory 这些成熟工作台必须有的持久化对象。
接下来最重要的不是继续堆 Settings polish,而是补数据结构和状态机。
## 1. 第一优先级:ArtifactRecord + right pane
### 现状问题
Maka 当前的 tool result 都落在 transcript 里:
- `ToolResultContent` 只有 `file_diff` / `file_write` / `terminal` / `image` / `summary` 等结果形状;
- UI 通过 typed renderer 把 diff/terminal 渲染好,但仍然是 chat turn 的一部分;
- 大 HTML、图片、PDF、长 diff、生成文件没有成为可选择、可复看、可导出的工作成果。
用户路径上的不成熟感:
- 一次生成网页 / 图片 / 文档后,用户只能在长聊天里找结果;
- terminal/diff 输出占据主叙事,缺少 "成果面板";
- 后续 snapshot / rollback / preview 没有对象可挂。
### reference implementation / reference implementation 对照
reference implementation preload 已有 `snapshot.*` 一组能力(create / file / list / get / diff / rollback / cleanup),并把 workspace diff/snapshot 从 transcript 中分离出来。
reference implementation 的 release notes 也反复强调 diff / preview / data table / terminal preview 的独立呈现。
可复制的是:artifact 作为第一等对象;不可照搬的是 reference implementation wide-open local API。
### Contract / API
新增 core contract:
```ts
export type ArtifactKind =
| 'file'
| 'html'
| 'image'
| 'pdf'
| 'diff'
| 'terminal'
| 'json'
| 'markdown';
export interface ArtifactRecord {
id: string;
sessionId: string;
turnId?: string;
toolUseId?: string;
kind: ArtifactKind;
title: string;
createdAt: number;
updatedAt: number;
source: 'tool_result' | 'user_upload' | 'export' | 'snapshot';
ref: StorageRef;
mimeType?: string;
bytes?: number;
summary?: string;
status: 'ready' | 'streaming' | 'errored' | 'deleted';
errorReason?: string;
}
```
Storage:
- `artifacts.jsonl` per session or `artifacts/metadata.jsonl` per workspace;
- large payload 必须 file-backed,不进入 `session.jsonl`;
- HTML preview 使用 sandboxed iframe,禁止 inline untrusted script 执行;
- deletion 先 soft-delete,避免 transcript 中引用断裂。
IPC:
- `artifacts:list(sessionId?)`
- `artifacts:get(id)`
- `artifacts:open(id)`
- `artifacts:delete(id)`
- `artifacts:export(id, format?)`
Renderer:
- right pane 作为 `--z-panel` 消费者;
- Chat turn 内只显示 compact artifact card;
- pane 负责 full preview / copy / open / export。
### Failure state
需要固定 reason:
- `artifact_missing`
- `artifact_deleted`
- `artifact_too_large`
- `artifact_preview_unsupported`
- `artifact_load_failed`
- `artifact_sandbox_blocked`
### Gate
- node:test:ArtifactStore append/list/get/delete;HTML artifact 必须 file-backed;
- fixture:新增 `artifact-pane` scenario,包含 html + image + diff + errored artifact;
- smoke.md:新增 "Artifact pane open / preview / export / error" path;
- screenshot:light/dark/narrow + missing artifact failure state。
## 2. Tool output streaming delta 协议
### 现状问题
Maka 已有 `ToolProgressEvent`,但当前 runtime tools 多数只在 tool 完成后返回完整 result。
`Bash` 使用 `exec`,只有完成后一次性拿 stdout/stderr。UI 的 terminal renderer 也只能显示 settled output。
用户路径上的不成熟感:
- 长测试 / build / install 没有实时 stdout;
- 用户以为 app 卡住;
- Stop 后看不到 partial output。
### reference implementation / reference implementation 对照
reference implementation streaming docs 强调 tool output delta;reference implementation release notes 也多次修复 partial output / streaming error recovery。
可复制的是 "delta 先到 UI,final result 再落盘"。
### Contract / API
扩展事件:
```ts
export interface ToolProgressEvent extends BaseEvent {
type: 'tool_progress';
toolUseId: string;
sequence: number;
stream: 'stdout' | 'stderr' | 'progress' | 'text';
text: string;
truncated?: boolean;
}
```
Runtime:
- Bash 从 `exec` 改成 `spawn`;
- stdout/stderr chunk 经 redaction 后发 `tool_progress`;
- final `tool_result` 保留 capped output + artifact ref(完整日志入 artifact);
- abort 时落 `tool_result` with `isError=true` + `reason='aborted'` 或独立 aborted event。
### Failure state
- `tool_aborted`
- `tool_timeout`
- `tool_output_truncated`
- `tool_spawn_failed`
- `tool_permission_denied`
### Gate
- node:test:fake streaming tool emits ordered progress → result;
- fixture:`turn-with-streaming-tool`;
- smoke:长 running command 能实时显示 stdout/stderr,Stop 后保留 partial output。
## 3. ModelCatalogEntry:模型目录不再只是 ModelInfo
### 现状问题
当前 `ModelInfo` 只有 `id/contextWindow/maxOutputTokens/capabilities`,capabilities 也只有 vision/reasoning/functionCalling。
不足:
- 没有 `mode` 区分 chat/image/audio/embedding/rerank;
- 没有 pricing/source/stale/unsupported;
- default model 校验只看 model id 是否在 enabled list;
- 无法阻止 image-only / embedding-only model 被设为 chat default;
- Health / first-run 无法解释 "模型存在但不能用于当前 chat"。
### reference implementation / reference implementation 对照
reference implementation provider 层会根据 model id 补 capability;reference implementation 在 release notes 中也反复修 model resolution / Bedrock region / unsupported model 过滤。
可复制的是 backend-normalized catalog,而不是 UI 猜。
### Contract / API
新增:
```ts
export type ModelCapability =
| 'chat'
| 'tool_use'
| 'vision'
| 'reasoning'
| 'image_generation'
| 'audio_input'
| 'audio_output'
| 'embedding';
export interface ModelCatalogEntry {
id: string;
displayName?: string;
providerType: ProviderType;
capabilities: ModelCapability[];
contextWindow?: number;
maxOutputTokens?: number;
pricing?: {
inputPerMtok?: number;
outputPerMtok?: number;
currency: 'USD' | 'CNY';
source: 'builtin' | 'provider' | 'user';
};
source: 'fetched' | 'fallback' | 'user_custom';
fetchedAt?: number;
stale?: boolean;
unsupportedReason?: 'not_chat_model' | 'missing_tool_use' | 'provider_deprecated' | 'unknown';
}
```
Readiness guard:
- chat default requires `capabilities.includes('chat')`;
- tool mode requires `tool_use`;
- image/audio model 不能设为 chat default。
### Failure state
- `model_unsupported`
- `model_stale`
- `model_not_chat_capable`
- `model_requires_refresh`
### Gate
- node:test:unsupported/image-only cannot become default;
- fixture:provider has mixed chat/image/embedding models;
- smoke:ModelTable shows unsupported row disabled + reason。
## 4. Session status / workstation shell
### 现状问题
`SessionSummary` 当前没有 status。运行态由 renderer 的 `streamingBySession` map 临时推断。
这对单窗口短聊天够用,但对工作台不够:
- session 正在 running / waiting permission / blocked / errored / review / done 无持久状态;
- app 重启后无法恢复 "上次卡在 permission" / "上次工具失败";
- sidebar 只能显示 unread / streaming dot,不知道工作流阶段;
- Health / automations / artifacts 都缺 workspace shell 可挂载的位置。
### reference implementation / reference implementation 对照
reference implementation 有 thread/workspace/snapshot 体系;reference implementation 有 workspace、labels、sources、automations,release notes 里也提到 branch/session recovery。
可复制的是 session 状态机和 workspace metadata。
### Contract / API
扩展 `SessionHeader`:
```ts
export type SessionStatus =
| 'active'
| 'running'
| 'waiting_permission'
| 'blocked'
| 'review'
| 'done'
| 'errored'
| 'archived';
export interface SessionHeader {
status: SessionStatus;
statusReason?: string;
workspace?: {
root: string;
branch?: string;
pr?: string;
dirty?: boolean;
};
}
```
Runtime:
- send start → `running`
- permission_request → `waiting_permission`
- permission deny → `blocked`
- complete → `done` or `review`(由 turn control 决定)
- error → `errored` with reason
- archive → `archived`
### Failure state
- `session_connection_missing`
- `session_permission_blocked`
- `session_tool_failed`
- `session_aborted`
- `session_recovery_needed`
### Gate
- node:test:status transitions;
- fixture:running / waiting_permission / errored / done sessions;
- smoke:sidebar row + header status match。
## 5. Turn control:retry / branch / regenerate / checkpoint
### 现状问题
Maka 有 turn grouping,但没有 turn-level controls:
- retry/regenerate 没有 contract;
- branch-from-turn 没有 lineage;
- cancel/abort 只是一条 system note,不是 turn status;
- tool 执行前没有 checkpoint/snapshot,因此 destructive 修改后无法 rollback;
- 旧输出不可覆盖这个原则没有被模型化。
### reference implementation / reference implementation 对照
reference implementation 有 snapshots / branch / compact;reference implementation release notes 明确 session branching 和 branch cutoff 是长期复杂点。
这说明分支/重试必须从第一天就有 sidecar anchor 和 lineage,不要先做 UI 按钮。
### Contract / API
新增 `TurnRecord`:
```ts
export interface TurnRecord {
id: string;
sessionId: string;
parentTurnId?: string;
branchId?: string;
status: 'running' | 'completed' | 'aborted' | 'errored' | 'superseded';
startedAt: number;
completedAt?: number;
modelId?: string;
snapshotIdBeforeTools?: string;
errorReason?: string;
}
```
IPC:
- `turns:retry(sessionId, turnId)`
- `turns:branch(sessionId, turnId)`
- `turns:cancel(sessionId, turnId)`
- `turns:createCheckpoint(sessionId, turnId)`
### Failure state
- `turn_not_found`
- `turn_already_running`
- `branch_cutoff_invalid`
- `checkpoint_failed`
- `retry_requires_model`
### Gate
- node:test:branch copies messages up to turn boundary only;
- fixture:branched session + aborted turn;
- smoke:branch from prior turn creates new sidebar session, original unchanged。
## 6. Health Center:分散状态收拢
### 现状问题
目前健康状态分散:
- provider credential 在 Account / Models;
- proxy 在 Network;
- bot 在 Bot Chat;
- model fetch source 在 ModelTable;
- storage / visual smoke / console gate 不在 UI;
- missing connection 只在 chat header。
用户路径上的不成熟感:
- 出错时不知道是 key、网络、proxy、provider、bot、model list、workspace 权限哪一个;
- bug report 不能一键 copy redacted diagnostics;
- release smoke 也没有一个综合状态页。
### reference implementation / reference implementation 对照
reference implementation 有大量 `/api/*/status` 和 settings tree;reference implementation CLI 的 `source test/validate`、automation validate/lint 都是 health surface。
### Contract / API
新增:
```ts
export type HealthComponent =
| 'provider'
| 'credential'
| 'model_catalog'
| 'proxy'
| 'bot'
| 'storage'
| 'skills'
| 'search'
| 'voice'
| 'open_gateway';
export interface HealthCheckResult {
component: HealthComponent;
id: string;
label: string;
status: 'ok' | 'warning' | 'error' | 'disabled' | 'unknown';
reason?: string;
message: string;
lastCheckedAt?: number;
details?: Record<string, unknown>; // redacted only
}
```
IPC:
- `health:list()`
- `health:run(component?, id?)`
- `health:copyDiagnostics()`
Diagnostics must include:
- app version / OS / workspace schema;
- provider statuses without secrets;
- latest generalized errors;
- proxy enabled/type without credentials;
- storage file presence / JSON parse status;
- no raw paths unless user explicitly copies workspace path。
### Failure state
Use same reason vocabulary as readiness / connection / proxy:
- `auth`
- `timeout`
- `network`
- `provider_unavailable`
- `connection_missing`
- `storage_corrupt`
- `unsupported`
### Gate
- node:test:diagnostics redaction;
- fixture:mixed ok/warning/error health;
- smoke:Health Center copy output contains no key/path secret。
## 7. Sources / Skills / Automations visible system
### 现状问题
Maka only lists installed skills from `workspaceRoot/skills`. There is no first-class source, no source-scoped permissions, no automations.
Current gaps:
- skill visibility does not mean invocability / permissions;
- no source auth/scope/status;
- no automation last-run/history;
- user cannot audit which capability can touch what。
### reference implementation 对照
reference implementation CLI has first-class `source`, `skill`, `automation`, `permission` entities with validate/test/history commands.
This is a strong model for Maka because it keeps external capability visible and revocable.
### Contract / API
Minimal first step:
```ts
export interface SourceRecord {
slug: string;
name: string;
type: 'mcp' | 'api' | 'local';
enabled: boolean;
authType: 'oauth' | 'bearer' | 'none';
scopeSummary: string[];
status: 'ready' | 'needs_auth' | 'error' | 'disabled';
lastTestAt?: number;
lastErrorReason?: string;
}
export interface AutomationRecord {
id: string;
name: string;
enabled: boolean;
trigger: 'manual' | 'schedule' | 'event';
permissionMode: 'explore' | 'ask' | 'execute';
lastRunAt?: number;
lastRunStatus?: 'ok' | 'error' | 'skipped';
}
```
### Failure state
- `source_needs_auth`
- `source_scope_denied`
- `automation_disabled`
- `automation_last_run_failed`
- `skill_missing_source`
### Gate
- node:test:skill cannot widen permission mode;
- fixture:source needs auth + disabled automation;
- smoke:Settings surface shows source/skill/automation enabled/disabled/test states。
## 8. First-run to value
### 现状问题
OnboardingHero exists and provider save auto-fetch works, but flow is still fragmented:
1. Provider preset;
2. paste key;
3. save;
4. auto-fetch;
5. choose default;
6. go back to chat;
7. send prompt。
There is no explicit "you are ready" smoke prompt, no inline model fetch failure lane, and no first-run state machine.
### reference implementation / reference implementation 对照
reference implementation `validate-server` auto-bootstraps temp workspace/connection for validation;reference implementation onboarding has permission timing and settings tree.
可复制的是 first-run guided state, not cinematic onboarding.
### Contract / API
```ts
export type FirstRunStep =
| 'choose_provider'
| 'enter_secret'
| 'fetch_models'
| 'choose_default'
| 'send_smoke_prompt'
| 'done';
export interface FirstRunState {
current: FirstRunStep;
providerSlug?: string;
errorReason?: string;
completedAt?: number;
}
```
### Failure state
- `secret_invalid`
- `model_fetch_failed`
- `no_chat_models`
- `default_model_missing`
- `smoke_prompt_failed`
### Gate
- fixture:`first-run`, `first-run-auth-error`, `first-run-fetched-empty`;
- smoke:4 screens max from provider pick to first successful assistant answer;
- node:test:step transition reducer。
## 9. Memory MVP
### 现状问题
Maka has personalization prompt, but no memory object:
- no persistent facts;
- no retrieval;
- no memory source visibility;
- no user review/delete;
- no sleep/consolidation。
Do not jump directly to reference implementation's sqlite-vec + ONNX. The first missing piece is user-visible memory CRUD and retrieval contract.
### reference implementation 对照
reference implementation has memory store, embeddings, sleep cycle, Recall tool, memory archive. Useful structure:
- memory rows have durability;
- retrieval is a tool / context source;
- sleep is previewable and cancelable。
### Contract / API
MVP:
```ts
export interface MemoryRecord {
id: string;
content: string;
source: 'user_pinned' | 'assistant_suggested' | 'imported';
durability: 'temporary' | 'permanent';
createdAt: number;
updatedAt: number;
archivedAt?: number;
}
```
No embeddings in first PR. Use keyword search + explicit user pin/delete.
### Failure state
- `memory_search_failed`
- `memory_disabled`
- `memory_import_invalid`
### Gate
- node:test:CRUD/search/archive;
- fixture:3 memories + archived item;
- smoke:Settings/Memory page can add/delete/search and chat can cite retrieved memory source。
## 10. Open Gateway / Voice / Search Coming Soon
### 现状问题
Settings has Coming Soon panels for search, voice, open gateway. The copy is better now, but capability is absent.
This creates expectation debt.
### Recommended order
1. Open Gateway SSE bridge: easiest to contract and test locally.
2. Search service: can start with provider abstraction + disabled health state.
3. Voice input: needs macOS permissions / audio device UX, do later unless user prioritizes.
### Open Gateway contract
Expose a local OpenAI-compatible endpoint only when explicitly enabled:
- bind `127.0.0.1` by default;
- per-workspace token generated locally;
- no plaintext secret in logs;
- `/v1/models` returns chat-capable `ModelCatalogEntry` only;
- `/v1/chat/completions` routes to selected connection;
- health reason if default connection unavailable。
Failure reasons:
- `gateway_disabled`
- `gateway_token_invalid`
- `gateway_model_unsupported`
- `gateway_connection_unavailable`
Gate:
- node:test for token auth + disabled path;
- fixture for gateway enabled/unavailable;
- smoke with curl local endpoint。
## 11. Recommended PR order
### Wave A: Workbench foundation
1. `ArtifactRecord` core/storage contract + fixture seed (no UI pane yet).
2. Right artifact pane renderer + smoke path.
3. Tool streaming delta for Bash + terminal artifact ref.
Why first: this changes Maka from "chat transcript" to "work product workspace".
### Wave B: Decision quality
4. `ModelCatalogEntry` + chat default gate.
5. Health Center core result type + provider/proxy/bot/storage checks.
6. First-run state reducer + first-run fixture expansion.
Why second: user setup and provider/model trust become understandable.
### Wave C: Workflow control
7. Session status state machine.
8. TurnRecord + cancel/abort persistence cleanup.
9. Branch/regenerate/checkpoint API.
Why third: these are more invasive and should build on artifact/snapshot shape.
### Wave D: Ecosystem
10. Sources/Skills/Automations records.
11. Memory MVP.
12. Open Gateway.
13. Search / Voice.
## 12. Do-not-copy list
From reference implementation/reference implementation, do not copy these patterns into Maka:
- wide-open local HTTP API without explicit enable/token;
- plaintext secret returns through IPC / API;
- auto-approval or subagent `--approve-all` trust shortcuts;
- activity/screen recorder defaults;
- cinematic onboarding that delays first value;
- hidden skill permission widening;
- raw console logs for chat IDs, API responses, provider bodies。
Maka should copy structure and state models, not unsafe shortcuts.
## 13. Immediate next action
My recommendation for the next implementation PR:
**PR-A1: `ArtifactRecord` core/storage/fixture contract only.**
Deliverables:
- `packages/core/src/artifacts.ts`
- `packages/storage/src/artifact-store.ts`
- `apps/desktop/src/main/visual-smoke-fixture.ts` adds `artifact-pane`
- node:test for append/list/get/soft-delete/file-backed invariant
- docs update: `docs/design-system.md` §9.1 fleshed out
- no renderer pane yet
This keeps the first step small, testable, and directly unlocks the right pane.
+322
View File
@@ -0,0 +1,322 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Maka 核心技术解读
> Archived on 2026-07-13. Current backend guidance lives in `ARCHITECTURE.md`, its architecture chapters, source, and tests.
> 一份面向工程师的 Maka 技术栈通读笔记。覆盖 runtime 内核、权限系统、持久化与恢复、Electron 集成。
> 生成日期:2026-06-25
## 目录
- [整体定位](#整体定位)
- [仓库分层](#仓库分层)
- [Runtime 内核架构](#runtime-内核架构)
- [SessionManager:公共 runtime API](#sessionmanager公共-runtime-api)
- [AgentRun:单 turn 的 durable 编排](#agentrun单-turn-的-durable-编排)
- [AiSdkBackend:流式 + 多步工具循环](#aisdkbackend流式--多步工具循环)
- [ToolRuntime:权限门控 seam](#toolruntime权限门控-seam)
- [ModelAdapter:provider 归一化](#modeladapterprovider-归一化)
- [RunTrace:best-effort 诊断](#runtracebest-effort-诊断)
- [权限系统](#权限系统)
- [四档 PermissionMode](#四档-permissionmode)
- [12 类 ToolCategory](#12-类-toolcategory)
- [mode × category 策略矩阵](#mode--category-策略矩阵)
- [纯函数原则](#纯函数原则)
- [parked Promise 机制](#parked-promise-机制)
- [持久化与恢复](#持久化与恢复)
- [文件布局](#文件布局)
- [AgentRun ledger](#agentrun-ledger)
- [启动恢复](#启动恢复)
- [Electron 集成层](#electron-集成层)
- [进程边界](#进程边界)
- [密钥安全边界](#密钥安全边界)
- [多入口](#多入口)
- [数据流:一次对话的完整路径](#数据流一次对话的完整路径)
- [技术亮点小结](#技术亮点小结)
---
## 整体定位
Maka 是一个**本地优先(local-first)的 Electron 桌面 AI agent 工作台**。一句话概括它的技术追求:让用户在自己的电脑上跑一个**可观察、可控、可恢复**的 agent——所有数据本地落盘,敏感值按各自边界保存在本地,工具调用走权限策略,单次对话崩溃后能从 ledger 恢复。
## 仓库分层
monorepo 用 npm workspaces,分层干净:
- `packages/core`:纯契约层,零运行时依赖,只定义类型和纯函数。
- `packages/storage`:文件持久化层。
- `packages/runtime`:内核实现,最核心的部分。
- `packages/ui`:共享渲染组件。
- `apps/desktop`:Electron 壳子,把 runtime 装进窗口。
**核心设计原则**:契约与实现分离,runtime 内核再拆成可独立理解的边界,公共 API 保持稳定的同时内部可演化。
---
## Runtime 内核架构
这是整个项目最值钱的部分。分层关系:
```
SessionManager ← 对外公共 API(桌面 / bot / gateway 都走它)
-> AgentRun ← 单次 turn 的生命周期 + 启动恢复
-> AiSdkBackend ← 流式 + 工具循环引擎
-> ModelAdapter ← provider stream / usage / error 归一化
-> ToolRuntime ← 工具输入校验 / 权限 / watchdog / abort / telemetry
-> RunTrace ← best-effort 诊断 trace,写失败不影响对话
-> AgentRunStore ← durable run ledger
```
关键代码入口:
| 模块 | 文件 |
| --- | --- |
| `SessionManager` 类 | `packages/runtime/src/session-manager.ts:232` |
| `AgentRun.execute()` | `packages/runtime/src/agent-run.ts` |
| `send()` 流式泵 | `packages/runtime/src/ai-sdk-backend.ts:435` |
| `wrapToolExecute` 权限门控 | `packages/runtime/src/tool-runtime.ts:146` |
| provider 适配 | `packages/runtime/src/model-adapter.ts` |
### SessionManager:公共 runtime API
对外(桌面 IPC、bot adapter、open-gateway 三类入口)暴露的唯一 runtime 门面。职责:
- session CRUD
- backend registry 编排
- active run 查找
- 恢复入口
真正干活的 turn 生命周期被委派给 `AgentRun`。
### AgentRun:单 turn 的 durable 编排
理解恢复机制的关键。一次 `sendMessage` 会创建一个 `AgentRun`,它负责:
1. 生成 runId,写 `run_created` 事件到 ledger。
2. 追加用户消息、写初始 turn 状态。
3. 锁定连接快照(`connectionLocked`),防止 turn 跑到一半连接被改。
4. 构建 prior runtime context(从历史 run 的 `RuntimeEvent` 投影成模型历史)。
5. 驱动 backend 流事件,投影 session 状态。
6. 写 turn 完成 / 失败 / abort / permission-wait 状态。
7. `finalize()` 收尾:注销 active run、更新 header、写 `run_completed / run_failed / run_cancelled`。
`execute()` 是 async generator,核心结构简洁:
```ts
async *execute(): AsyncIterable<SessionEvent> {
try {
const begin = await this.begin();
for await (const ev of begin.backend.send(begin.backendInput)) {
await this.recordSessionEvent(ev);
yield ev;
}
} catch (error) {
await this.recordFailure(error);
throw error;
} finally {
await this.finalize();
}
}
```
每个 backend 事件都会被 `recordSessionEvent` 投影成 session 状态变更(running / blocked / aborted),并记录到 run ledger。即使进程崩了,重启时也能从 ledger 重建。
### AiSdkBackend:流式 + 多步工具循环
agent loop 的引擎。用 Vercel AI SDK 的 `streamText` 配合 `stopWhen: stepCountIs(N)` 驱动多步工具调用循环——循环本身交给 AI SDK,但所有"自己的机器"(权限、持久化、materializer、watchdog)都保留在 Maka 这边。
`send()` 结构(`ai-sdk-backend.ts:435`):
1. 建 `AsyncEventQueue<SessionEvent>`,作为后台泵和前台 yield 之间的缓冲。
2. `ModelAdapter.resolveModel()` 解析 LanguageModel,失败立即推 error + complete。
3. 构建 provider tools dict,**每个工具的 `execute` 被权限层包了一层**。
4. 从 RuntimeEvent 历史构建模型消息。
5. 后台泵:`streamText({...})` → `for await (chunk of result.fullStream)` → `ModelAdapter.handleStreamChunk` 归一化 → 推 queue。
6. 前台 `yield* drain(queue)`。
精巧点:
- **StreamWatchdog**:流式连接有 connect timeout 和 idle timeout,超时会 abort controller 并推 error 事件,防止挂死。
- **step cap grace**:当 `finishReason === 'tool-calls'`(撞了 step 上限)且没有 assistant 文本时,注入一条确定性提示,告诉用户已达本轮工具上限、可发"继续"。
- **prepareStep 动态工具加载**:同 turn 内可以逐步激活更多工具(deferred load),active 工具集按 step 重算。
### ToolRuntime:权限门控 seam
整个安全模型的执行点。工具的 `execute` 回调被 `wrapToolExecute` 包裹后交给 AI SDK,每次模型调用工具都经过这条链:
1. **loop-gate**:同一个 tool + 同一组 args 连续失败 N 次,直接 block,防止 agent 死循环。block 本身不记 outcome,streak 停在阈值,后续相同调用继续被拦。
2. **PermissionEngine.evaluate()** 返回三态:
- `allow` → 跑真实 `impl`,写 `ToolResult`。
- `block` → 合成 `isError:true` 的工具结果返回给模型,不执行真实逻辑。
- `prompt` → 推 `PermissionRequestEvent` 给 UI,await 一个 parked Promise,等用户决定。allow 则跑,deny 则合成"用户拒绝"。
3. 工具执行期间**暂停 stream watchdog**(避免长任务被判超时),`finally` 里恢复。
4. abort signal 透传进工具,stop 按钮能中断。
5. 记录 telemetry、artifact candidate、错误分类。
### ModelAdapter:provider 归一化
把 provider / AI SDK 的细节挡在外面:stream chunk 类型、provider setup、usage 归一化、provider error mapping。
- `startStream` 统一调 `streamText`。
- `handleStreamChunk` 把各种 chunk 翻译成 Maka 的 `SessionEvent`(text delta、thinking delta、complete 等)。
这样未来加 provider 不需要重复权限 / 工具 / run / session 逻辑。
### RunTrace:best-effort 诊断
记录 turn 的里程碑事件:turn started、model resolved / stream started / completed / failed、tool started / completed / failed、permission requested / decided、usage recorded、abort requested。
**关键约束:trace 写失败绝不影响用户对话**。它和 session 消息 JSONL 是分开的两套持久化。
---
## 权限系统
这是 Maka 区别于普通 chat demo 的核心安全设计,定义在 `packages/core/src/permission.ts`。
### 四档 PermissionMode
`explore` / `ask` / `execute` / `bypass`,从只读到全自动。
### 12 类 ToolCategory
`read`、`web_read`、`file_write`、`fs_destructive`、`shell_safe`、`shell_unsafe`、`git_destructive`、`network_send`、`privileged`、`browser`、`custom_tool`、`subagent`。
### mode × category 策略矩阵
`PERMISSION_POLICY` 是纯函数决策表,决定每个组合是 `allow` / `prompt` / `block`。设计很克制:
| 模式 | 读 | 写 | 危险操作 | 网络 | 浏览器 |
| --- | --- | --- | --- | --- | --- |
| `explore` | allow | block | block | web_read=prompt | block |
| `ask` | allow | prompt | prompt | prompt | prompt |
| `execute` | allow | allow | **永远 prompt** | allow | **永远 prompt** |
| `bypass` | allow | allow | allow | allow | allow |
核心规则:**不可逆操作(`fs_destructive`、`git_destructive`、`privileged`、`browser`)在 `execute` 模式下仍强制 prompt**——浏览器操作可能发帖 / 下单,视为不可逆。`bypass` 全 allow,仅用户显式选择时进入。
`BUILTIN_TOOL_CATEGORY` 把 Claude SDK 风格的工具名映射到类别,`Bash` 默认 `shell_unsafe`,再由 `categorizeBash()` 根据命令前缀动态降级或升级(`ls`/`pwd` → `shell_safe`,`rm` → `fs_destructive`,`git push --force` → `git_destructive`)。
### 纯函数原则
`preToolUse()` 给定输入必然返回相同结果,不生成 UUID(requestId 由 runtime 层的 `PermissionEngine` 生成),便于测试。
### parked Promise 机制
`PermissionEngine` 为每个 outstanding 权限请求维护一个 parked Promise,UI 的决定通过 `respondToPermission()` resolve 回等待中的 adapter。turn 结束时未回答的请求会被 reject 成 `user_stop`。
---
## 持久化与恢复
### 文件布局
工作数据放 Electron `userData` 下的 workspace 目录:
```
<Electron userData>/workspaces/default/
llm-connections.json
credentials.json
settings.json
sessions/<sessionId>/
messages.jsonl
runs/<runId>/
run.json ← run header(原子写)
events.jsonl ← append-only run 事件
```
### AgentRun ledger
`FileAgentRunStore`(`packages/storage/src/agent-run-store.ts`)实现:
- `run.json` 原子写(`writeAtomic` 先写临时文件再 rename)。
- `events.jsonl` append-only。
- **同 run 写串行化**(`writeQueues` per sessionId+runId 的 Promise 链),避免并发写竞争。
- ID 校验 `SAFE_ID_PATTERN`,防路径穿越。
- 读事件时容忍损坏行(`event_corrupt`)和未终止尾部。
这套 ledger 和用户可见的 session 消息 JSONL 分开,诊断 / 恢复状态不污染对话历史。
### 启动恢复
`recoverInterruptedSessions()` 优先用 run ledger:扫描持久化的 run header 和事件,分类非终态 run(`created` / `running` / `waiting_permission`),修复后收敛 session / turn 投影。
覆盖的恢复场景:
- stale 的 `created` / `running` run
- 停在 `run_started` / `model_stream_started` 的 run
- `tool_started` 后的残留
- `permission_requested` 后的等待
- 缺终态事件的 `model_stream_completed`
- 损坏事件行
没有 run ledger 的老 session 走旧的 message + turn-state 恢复路径。
---
## Electron 集成层
### 进程边界
| 进程 | 文件 | 职责 |
| --- | --- | --- |
| main | `apps/desktop/src/main/main.ts`(4593 行) | 装配 `SessionManager`、注册 IPC、管理窗口、OAuth、bot、gateway |
| preload | `apps/desktop/src/preload/preload.ts` | `contextBridge` 暴露 `window.maka.*` 白名单 API |
| renderer | `apps/desktop/src/renderer/` | React UI + Settings |
`main.ts:1262` 装配 `runtime = new SessionManager({...})`,`main.ts:564` 起 `OpenGatewayService`。IPC 用 `ipcMain.handle` 注册大量通道(memory、artifacts、settings、connection、session 等)。
### 密钥安全边界
- Provider/API key、bot token、proxy password、gateway token、Tavily key 写入本机凭据文件,依赖 OS 账号边界和文件权限;subscription OAuth token 使用独立的系统安全存储。
- Renderer 永远拿不到明文密钥,Settings 只显示 masked 状态和测试结果。
### 多入口
除了桌面 UI,runtime 还服务两类外部入口:
- **Bot adapter**(`packages/runtime/src/bots/`):Telegram、飞书、企业微信、微信 iLink、Discord、钉钉、QQ,都继承 `base-adapter.ts`,统一对接 SessionManager。
- **Open Gateway**(`open-gateway.ts`,1489 行):本地 HTTP / SSE API,用 token 保护,让外部读取会话状态 / 事件 / 能力 / 健康摘要。
---
## 数据流:一次对话的完整路径
1. 用户在 renderer 输入消息 → `window.maka.session.send` → IPC → `SessionManager.sendMessage`。
2. 创建 `AgentRun`,写 `run_created` 到 ledger,append 用户消息到 `messages.jsonl`。
3. 从历史 run 的 RuntimeEvent 投影模型上下文。
4. `AiSdkBackend.send()`:构建权限包裹的工具 → `streamText(stepCountIs(N))` → 后台泵 fullStream。
5. 每个流 chunk 经 `ModelAdapter.handleStreamChunk` 归一化成 `SessionEvent` → 推 queue → 前台 yield → IPC 流到 renderer。
6. 模型调用工具 → `wrapToolExecute` → loop-gate → `PermissionEngine.evaluate` → allow / block / prompt → 执行或合成错误。
7. assistant 文本落 `messages.jsonl`,usage 记 telemetry,trace 写 run ledger。
8. `finalize()` 收尾,写 `run_completed`,更新 session header。
---
## 技术亮点小结
- **ledger + projection 模式**:run 事件是 source of truth,session 状态是投影,崩溃可重建——整个恢复能力的根基。
- **纯函数策略 + runtime 包装**:权限决策表是纯函数好测试,requestId / parking 状态由 runtime 管,职责分得清楚。
- **权限门控作为工具 execute 的 seam**:不动 AI SDK 的循环,在 execute 回调里插入 allow / block / prompt,最小侵入。
- **best-effort trace 不影响主路径**:诊断信息尽力写,失败静默,对话绝不受拖累。
- **同 run 写串行化 + 原子写**:文件持久化的并发安全靠 Promise 链和 rename 保证。
- **安全分层**:模式 × 类别矩阵 + 命令分类器 + 凭据文件权限 / subscription token 安全存储 + preload 白名单,多层防御。
@@ -0,0 +1,273 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Maka memory — white-box contract anchor (2026-05-29)
> Archived on 2026-07-13. This snapshot no longer owns the current memory contract.
Owner: yuejing. Anchor for the team's evolving local-memory feature
once xuan's transparent-file MVP landed at `c06e13f`. Aligned to:
- `packages/core/src/local-memory.ts` (xuan)
- `apps/desktop/src/main/local-memory-service.ts` (xuan)
- Settings → 记忆 UI (xuan)
- external reference deep-dive note `notes/external reference-deep-dive-2026-05-29.md` §3.5
- PilotDeck deep-dive note `notes/pilotdeck-deep-dive-2026-05-29.md` §2.2
- kenji `19b0996f` boundary: local transparent file MUST NOT become
implicit durable memory; agent-read default-off.
This note is a contract anchor, not a PR. It freezes the field names
+ status semantics so V0.2 / V0.3 PRs can extend predictably without
breaking V0.1 disk format. If reality changes, edit here first.
## V0.1 — what xuan shipped (`c06e13f`)
### Settings shape (live)
```ts
interface LocalMemorySettings {
enabled: boolean; // file feature on/off (default ON)
agentReadEnabled: boolean; // agent may consume in system prompt
// (default OFF)
}
```
Two switches were the right scope:
- `enabled` ON + `agentReadEnabled` OFF — the file lives on disk,
user can edit, agent gets NOTHING.
- `enabled` ON + `agentReadEnabled` ON — file appears in system
prompt as a dedicated section. Incognito context fails closed.
- `enabled` OFF — file untouched on disk; reads return disabled
state.
### File location + permissions
- Directory mode `0700`, file mode `0600`.
- Lives under the workspace directory.
- `LOCAL_MEMORY_MAX_BYTES = 128 KB` enforced at parse time.
### Disk format (live)
Each entry is a Markdown H2 section followed by an HTML-comment
metadata line:
```markdown
# Maka Memory
## <title or slug>
<!-- maka-memory: id=<id> origin=manual createdAt=<unix-ms> -->
<content lines>
## <next title>
<!-- maka-memory: id=<id2> origin=manual createdAt=<unix-ms> -->
...
```
Parser is fail-open: an entry with a malformed comment still
renders its content. The HTML-comment metadata is intentionally
hidden from human readers in most Markdown viewers.
### Parse output (live)
```ts
interface LocalMemoryEntryPreview {
id: string;
origin: 'manual' | 'unknown';
title: string;
content: string;
createdAt?: number;
}
interface LocalMemoryState {
path: string;
enabled: boolean;
agentReadEnabled: boolean;
status: 'ok' | 'disabled' | 'safe_mode' | 'incognito_blocked' | 'error';
content: string;
entryCount: number;
latestEntry?: LocalMemoryEntryPreview;
reason?: string;
}
```
Status enum invariants:
- `disabled` — `settings.enabled === false`. UI shows the toggle.
- `incognito_blocked` — workspace privacy context active. Even when
enabled, no read happens.
- `safe_mode` — file is over `LOCAL_MEMORY_MAX_BYTES`. Returns
empty entry list with a banner; user can still open + edit.
- `error` — IO error. Generalized copy in `reason`.
- `ok` — normal read.
## V0.2 — extensions on top of the V0.1 disk format
V0.2 extends the metadata comment with optional fields. **Parser
MUST stay fail-open for unknown fields** so V0.1 readers never
crash on a V0.2-written file.
```html
<!-- maka-memory: id=<id> origin=<origin> createdAt=<ms> updatedAt=<ms> status=<status> tags=<csv> decayTtlMs=<ms> -->
```
### Origin (V0.2)
```ts
type MemoryOrigin = 'manual' | 'extracted' | 'imported' | 'unknown';
```
- `manual` — user typed it. V0.1 only writes this.
- `extracted` — written by a future `extract_memory` agent tool.
That tool MUST be `permissionRequired: true` with its own user
approval prompt, gated on `agentReadEnabled` (you cannot
extract what you said the agent cannot read), gated on
`!incognitoActive`.
- `imported` — bulk-imported (Honcho / mem0 / supermemory export
/ OpenAI ChatGPT memory JSON / etc.).
### Status (V0.2)
```ts
type MemoryEntryStatus = 'active' | 'archived';
```
- `active` — surfaces in agent prompt + UI list.
- `archived` — stays in the file (user can re-activate) but does
not enter the prompt. UI lists archived entries in a separate
pane.
`decayTtlMs` is the auto-archive timer: when `now > createdAt +
decayTtlMs`, the next parse promotes the entry to `archived`. The
file is rewritten with `status=archived` in-line — a write the
user can see, not a hidden background mutation.
### Tags (V0.2)
`tags=foo,bar` — string CSV. Used for filtering in the UI and
optional system-prompt section grouping.
### Stable id (V0.2)
Currently V0.1 uses `manual-<unix-ms>`. V0.2 switches new entries
to `sha256(content + createdAt).slice(0, 16)` so id is stable
across edits of the title. Old `manual-*` ids remain valid; the
parser accepts both.
### Settings additions (V0.2)
```ts
interface LocalMemorySettings {
enabled: boolean;
agentReadEnabled: boolean;
// V0.2:
defaultDecayDays?: number; // entries created without an
// explicit decay inherit this
extractToolEnabled?: boolean; // gates the extract_memory tool
// registration; default false
}
```
`extractToolEnabled` is the third switch that makes the
`extract_memory` agent tool surface. It is OFF by default and
disabled when `agentReadEnabled` is OFF (you cannot extract
memory the agent cannot use).
## V0.3 — open questions (NOT contract; revisit when relevant)
These are explicitly OPEN questions. Recording them here so the
next person who picks up the lane knows the field is not
settled.
### Provider abstraction
external reference ships `plugins/memory/{honcho,mem0,supermemory}`. PilotDeck
has `EdgeClawMemoryProvider`. reference implementation is built-in `MemoryService` +
sqlite-vec. **Three-way convergence is NOT present** — kenji's
finding `7749c411` #3. Defer abstraction until a real external
backend lands (a paying user wants Honcho or a vector store).
If we adopt:
- `MemoryProvider` interface in `@maka/core/memory`
- Built-in `LocalMarkdownProvider` (today's file)
- Plugin path for `HonchoProvider` etc.
### Vector search
Embedding generation is a separate provider lane (reference implementation's
`__local__` / OpenAI / aihubmix / openrouter / Google selector).
Independent of the memory storage backend. Defer.
### Cross-session recall
V0.1 / V0.2 always returns the whole memory file. V0.3 may
implement retrieval — emit only top-K relevant entries based on
a query. Requires a retrieval algorithm choice; vector vs FTS5
vs LLM-judge. Defer.
### Dream Mode (PilotDeck term)
Idle-window memory consolidation: when the app sits idle for X
minutes, summarize chat sessions into new memory entries. Has
serious privacy implications. Even with `extractToolEnabled` ON
this would be an additional opt-in. Defer.
### Multi-workspace memory
PilotDeck's pitch is per-workspace memory + cross-workspace
aggregation. Maka has workspaces. V0.3 could expose a
`memory.scope: 'workspace' | 'global'` field per entry. Defer.
## Contract test surface (suggested, not shipped)
When V0.2 lands, the following invariants should be enforced by
contract tests:
1. `parseLocalMemoryMarkdown` is fail-open on unknown comment
fields — never throws, never drops a known entry.
2. `agentReadEnabled === false` ⇒ system prompt NEVER contains
the memory section, regardless of `enabled`.
3. Workspace privacy context `incognitoActive === true` ⇒
`LocalMemoryState.status === 'incognito_blocked'` even when
both switches are ON.
4. `extract_memory` agent tool registration is gated on
`extractToolEnabled === true && agentReadEnabled === true`.
5. The token-boundary contract is preserved: memory content
crosses the renderer↔main IPC; raw tokens / API keys never
leak into a memory entry via redactSecrets on the write path.
## Decisions log
| Date | Decision | Source |
|---|---|---|
| 2026-05-29 | Two-switch model (file enabled / agent-read enabled) | xuan `c06e13f` |
| 2026-05-29 | `agentReadEnabled` defaults OFF | kenji `19b0996f` boundary |
| 2026-05-29 | Defer provider plugin abstraction (no 3-way) | kenji `7749c411` #3 |
| 2026-05-29 | `extract_memory` agent tool: future, permission-gated | this note |
| 2026-05-29 | Dream Mode / vector / cross-session: V0.3+ open | this note |
## Pointers
- xuan implementation: `packages/core/src/local-memory.ts` +
`apps/desktop/src/main/local-memory-service.ts`.
- Settings UI: `apps/desktop/src/renderer/settings/` (find the
memory section).
- Test: `packages/core/src/__tests__/local-memory.test.ts`.
- external reference inspiration: `notes/external reference-deep-dive-2026-05-29.md` §3.5
(warns against plugin abstraction without external demand).
- PilotDeck inspiration: `notes/pilotdeck-deep-dive-2026-05-29.md`
§2.2 (white-box memory) and §3.2 (extension proposal).
@@ -0,0 +1,140 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Memory Subsystem Threat Model
> Archived on 2026-07-13. This document records the contract-only PR-MEMORY-1 boundary, not the current product implementation. Source and focused tests own the active contract.
PR-MEMORY-1 is a **contract-only** package. It MUST NOT add IPC handlers, storage repositories, embedding providers, Recall tool runtime, renderer UI, or settings flags. Implementation packets (PR-MEMORY-2+) sit downstream of this contract — none may bypass `validateMemoryWriteRequest`.
Anchors:
- `notes/reference implementation-memory-reverse-2026-05-25.md` (kenji msg `66fd3eab`) — reference implementation facts + Maka quasi-memory inventory.
- `notes/pr-memory-0-audit-plan.md` — pre-audit plan skeleton.
- `#my-ai:2f91befb` thread, msgs `22209a1b` (xuan gate review + scope adjust) + `fb95a158` (kenji invariant ask) + `68a1bcb5` (xuan review priorities).
## Source separation (gate #8 + type system)
Memory is not one surface. The contract types separate them so a quasi-memory observation cannot become a durable memory entry by typing alone.
- **`MemorySource`** (durable-capable): `user_authored`, `chat_extracted`. These can produce `'active'` entries IF a `confirmedAt` timestamp is supplied AND the request did not originate from the renderer.
- **`MemoryCandidateSource`** (non-durable): `voice_transcript`, `activity_observation`, `cu_observation`, `search_recall`, `daily_review`. These can only produce `'draft'` or `'review_required'` entries. The normalizer rejects any candidate-source request with `persistenceState: 'active'` (`MemoryBlockReason='candidate_source_no_active'`).
`MemorySource` and `MemoryCandidateSource` are disjoint enums. The `DurableMemoryEntry` shape's `source` field is typed `MemorySource`, and `DraftMemoryEntry`'s `source` field is typed `MemoryCandidateSource`. A Voice transcript cannot type-check into a `DurableMemoryEntry` even before reaching the validator.
## Assets
- Memory entry `content` (NFC-normalized, control/zero-width-stripped).
- Memory entry `source` and the user-visible provenance derived from it.
- Memory entry `scope` (`'workspace'` / `'session'`).
- Memory entry `confirmedAt` (a CLAIM about user action — gate #9 forbids renderer from setting this).
- Pending/review queue contents.
- Future embedding vectors (out of scope for v1 contract; `embeddingProvider: 'disabled'` is hard-coded).
- Citation index / prompt-injection references when memory is consumed (gate #4).
- Export bundles (gate #6 reversibility).
## Boundaries
1. **Renderer → main**: renderer may PROPOSE memory writes but MUST NOT supply `confirmedAt`. Confirmation event recording is a main-side responsibility.
2. **Main → store**: every write passes `validateMemoryWriteRequest`; the store boundary refuses any request that did not.
3. **Store → embedding/provider**: NOT IMPLEMENTED in v1. `embeddingProvider: 'disabled'` literal in `MemoryCapabilitySnapshot` prevents accidental wiring of a provider.
4. **Store → prompt-build**: only `'active'` durable entries are eligible for injection, and only with a visible citation surface (`MemoryUsePolicy='cited_only'`). The contract does NOT include `'silent'`.
5. **Quasi-memory surfaces → memory**: forbidden as a direct write path; the only way into memory from quasi-memory is via a draft entry that the user explicitly promotes.
## Nine privacy gates (locked by @xuan `22209a1b`)
| # | Gate | Enforcement in contract |
|---|---|---|
| 1 | default-off | `MEMORY_MODES` includes `'off'`; fresh-install snapshot MUST be `'off'`. Step #1 of validator rejects with `MemoryBlockReason='mode_off'`. |
| 2 | manual confirm before durable write | Durable `'active'` path requires `confirmedAt`. Validator rejects with `MemoryBlockReason='manual_confirm_required'` when missing/invalid. |
| 3 | reversible delete/export | Contract docs require reversible operations exist BEFORE any auto-write capability is added. v1 contract has no auto-write; downstream packets MUST add `delete` + `export` shapes before adding any write driver. |
| 4 | incognito read+write disable | `MemoryWriteRequestContext.incognitoActive` short-circuits validator at step #2 with `MemoryBlockReason='incognito_active'`. (Read path is a future contract — the same flag MUST gate it.) |
| 5 | no auto sleep consolidation | NOT in `MemorySource` enum; NOT in `MemoryCandidateSource`. No type exists for an automated consolidation source — adding one is a contract change requiring explicit review. |
| 6 | visible citation | `MemoryUsePolicy` only allows `'never'` or `'cited_only'`. No `'silent'` policy. Adding `'silent'` requires expanding the enum (i.e. requires explicit review). |
| 7 | no hidden activity promotion | `activity_observation` / `cu_observation` are `MemoryCandidateSource` only. Validator rejects `persistenceState='active'` for these via gate `candidate_source_no_active`. |
| 8 | provider+embedding leakage boundary | `MemoryCapabilitySnapshot.embeddingProvider` is the literal `'disabled'`. v1 has no provider field on entries. Downstream wiring of any provider requires extending this snapshot type. |
| 9 | renderer cannot forge provenance/readiness | `MemoryWriteRequestContext.originatedFromRenderer=true` blocks any durable `'active'` write — even with valid `confirmedAt` — returning `MemoryBlockReason='renderer_provenance_forged'`. |
## What NOT to copy from reference implementation (negative reference list)
Per `notes/reference implementation-memory-reverse-2026-05-25.md`:
1. **Sleep cycle / 4-layer consolidation** (`22-embeddings.md`) — Maka has no sleep stage. No code path automatically aggregates, summarizes, or merges memory entries.
2. **Auto-extract from chat** — every `chat_extracted` write requires explicit confirmation. Even after assistant suggests, user must click confirm; no silent insertion.
3. **Auto-retrieve / Recall tool runtime** — out of scope for v1 contract. Adding it requires a separate retrieve-side contract with its own threat model.
4. **LLM-mediated forget/delete** — only user-initiated deletes allowed. No "the model decided this is no longer relevant".
5. **Activity-derived memory** — Activity recorder feeds Maka memory only via `activity_observation` candidate source, never as a durable source. Activity audit (separate lane) MUST run before any activity→memory pipeline.
6. **Unauthenticated local route** (reference implementation's `05-express-api.md` exposes memory over local HTTP) — Maka memory MUST NOT have a local HTTP endpoint; if Local Gateway lane lands, it MUST go through capability scope allowlist + audit log, never default-on.
7. **Cloud embedding fallback** — `embeddingProvider: 'disabled'` lock. Adding a provider requires an explicit contract extension; no silent OpenAI/Cohere/local hybrid.
8. **Soul tree / `~/.config/reference implementation` long-term file** — out of v1 scope. If durable persistence lands (PR-MEMORY-2+), it sits inside the existing workspace store, not a separate global tree.
## Quasi-memory exclusion list (gate #7 + #8)
These existing Maka surfaces contain durable user-correlated data BUT MUST NOT be treated as `MemorySource` and MUST NOT auto-promote:
- `settings.json` — personalization fields, onboarding milestones.
- `skills/` — user-installed skill content.
- `usage_log` (`telemetryRepo`) — historical queries / latency / errors.
- `sessions/*/session.jsonl` — session history.
- `workspace/` instruction files — prompt-time injection only.
- Capability snapshot + runtime probe history (Health Center).
- Visual-smoke fixtures.
- Daily Review candidates (post-Daily Review lane).
- Search index hits (`MemorySource='search_recall'` is the candidate-source bridge, not durable).
- Voice transcripts (`voice_transcript` candidate source).
- CU / Activity observations (candidate sources).
**What the contract catches** (gate, per @xuan `0c9c68f9`): a request whose `source` value is the literal name of a quasi-memory surface — `source: 'usage_log'`, `source: 'health_probe'`, `source: 'session_summary'`, etc. — is rejected by `validateMemoryWriteRequest` with `MemoryBlockReason='unknown_source'`, regardless of other fields. The string is not in `MEMORY_SOURCES` or `MEMORY_CANDIDATE_SOURCES`.
**What the contract CANNOT catch** (source-laundering — downstream concern): if a downstream IPC handler or store boundary reads a usage-log entry, copies its body into a fresh `{ source: 'chat_extracted', confirmedAt: ..., content: ... }` payload, and submits THAT to the validator — the validator accepts it. The validator sees a well-formed durable `chat_extracted` write; it has no way to know the body originated from a quasi-memory surface. Source-laundering defense is a per-IPC-handler / per-store-boundary responsibility (provenance lock at the input layer), NOT a contract-layer responsibility. Any future implementation packet that introduces a "promote draft to active" or "summarize quasi-surface as chat_extracted" path MUST add a separate provenance gate at that boundary.
## Minimum test matrix (locks the gates above)
The companion test file `packages/core/src/__tests__/memory.test.ts` MUST contain at least these assertions (each labeled G#N for the gate locked):
- G1: `mode='off'` → `mode_off` block (any source, any state).
- G2: durable `user_authored` + `persistenceState='active'` without `confirmedAt` → `manual_confirm_required`.
- G2b: durable `chat_extracted` + `persistenceState='active'` without `confirmedAt` → `manual_confirm_required`.
- G3: contract has no `delete`/`export`-bypass shape (must be added by downstream packet only).
- G4: `incognitoActive=true` + any valid write → `incognito_active`.
- G5: no automated-consolidation source exists in `MEMORY_SOURCES` or `MEMORY_CANDIDATE_SOURCES`.
- G6: `MEMORY_USE_POLICIES` includes only `'never'` and `'cited_only'`.
- G7: candidate-source + `persistenceState='active'` → `candidate_source_no_active` (one assertion per candidate kind).
- G8: `MemoryCapabilitySnapshot.embeddingProvider` is the literal `'disabled'`.
- G9: durable + active + `originatedFromRenderer=true` → `renderer_provenance_forged`.
Plus normalizer matrix:
- `normalizeMemoryContent`: typeof reject, NFC, control/zero-width strip, trim, empty reject, cap reject.
- `normalizeMemorySource`: known durable, known candidate, unknown rejected.
- `normalizeMemoryMode` / `normalizeMemoryPersistenceState` / `normalizeMemoryScope`: closed-enum reject for non-members.
- `validateMemoryWriteRequest` returns canonical `DurableMemoryEntry` or `DraftMemoryEntry` (not raw input) on success.
- `validateMemoryWriteRequest` rejects `mode_only` + candidate (`mode_disallows_candidate`).
## Forbidden surfaces in PR-MEMORY-1 source diff
Reviewer source-grep MUST NOT find these in the PR:
- `ipcMain.handle` — no IPC handler is wired.
- `BrowserWindow` / renderer imports — no renderer code path touches memory.
- `fetch(` / `XMLHttpRequest` — no provider/network call.
- `embedding` / `vector` — no embedding logic (the contract field is the literal `'disabled'`).
- `Recall` / `recall_tool` — no Recall tool runtime.
- Storage repo names (`telemetryRepo`, `connectionStore`, `sessionStore`, etc.) — no persistence path.
- Settings field additions for memory — contract is independent of settings shape until PR-MEMORY-2 adds them.
@@ -0,0 +1,55 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# PR-OAUTH-SUBSCRIPTION-0 Gate
> Archived on 2026-07-13. The gate predates the current subscription send path; source, tests, and `SECURITY.md` own current behavior.
Scope: Claude subscription authentication, account state, and quota display only.
The chat send path remains blocked until a later runtime-smoke PR.
## Product Decisions
- Claude subscription only. Codex, Gemini, and Copilot are separate PRs.
- Settings placement is `账号`; model settings only consume readiness.
- Cloaked Claude Code headers are default-off and must stay behind `MAKA_CLAUDE_SUBSCRIPTION_CLOAK=1`.
- Quota v1 displays only values returned by the provider endpoint: window utilization and fetch time.
## Blocking Gates
- Tokens stay in the main process. Renderer, preload, and shared UI must not expose `access_token`, `refresh_token`, `id_token`, or camelCase variants.
- Token persistence requires Electron `safeStorage` encryption and file mode `0o600`. If encryption is unavailable, login fails closed instead of writing plaintext.
- PKCE verifier and OAuth state are main-process only. Paste-code completion must validate TTL, strict shape, and state, then consume pending auth before the token exchange network call.
- The token exchange request must send the pasted OAuth state, not the PKCE verifier, when those values are distinct.
- Refresh failure sets `refresh_failed` and does not delete the token file automatically.
- `oauth_token` providers must not pass `isConnectionReady` / `requireReadyConnection` until the subscription send path is implemented and runtime-smoked.
- The cloak module must be isolated from the default request path. Default code must not statically import cloaked request helpers.
- Account UI must not claim operational readiness. Authenticated means account/quota visible only.
## Logout Revoke Gate
PR-OAUTH-SUBSCRIPTION-0 requires local clear:
- Delete the local token file. Missing file (`ENOENT`) counts as already cleared.
- Clear in-memory tokens, profile, quota, pending authorizations, and diagnostic state.
- If local deletion fails, return `storage_failed`; UI must not show logout success.
Remote OAuth revocation is not required for this PR because Anthropic does not expose a public RFC 7009 revocation endpoint as of 2026-05-28. reference implementation's `ld.logout` path also local-clears only (local reference excerpt). Do not call non-public or guessed revocation endpoints.
If Anthropic publishes a revocation endpoint, add `PR-OAUTH-SUBSCRIPTION-LOGOUT-REVOKE-0` with remote revoke, local clear fallback semantics, and negative tests.
@@ -0,0 +1,312 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# QoderWork MCP 专项逆向与 Maka 演进方案
> 日期:2026-07-18
> 目标:从本机安装包还原 MCP runtime、market、OAuth、Plugin 与 Agent 接入链路,并映射到 Maka 的可执行路线。
> 边界:只分析程序代码、公开 catalog、数据库 schema 与非敏感配置结构;没有读取或记录 credential、token、cookie、authorization header 或用户数据行。
## 1. 样本与方法
| 项目 | 值 |
|---|---|
| App | `/Applications/QoderWork.app` |
| Version | `0.9.11` |
| Bundle ID | `com.qoder.work` |
| `app.asar` SHA-256 | `0f90145b0efbc18f947b5ec4a8ea00c67486df33315f8456487bd6f491b24292` |
| Electron | `33.4.5` |
| 解包样本 | `/tmp/qoderwork-mcp-re-0.9.11.K8JlS0` |
| 格式化 main bundle | `/tmp/qoderwork-mcp-pretty-0.9.11.yvsnld/main.js` |
方法包括:`asar` 静态解包、bundle formatting、symbol/string 交叉定位、调用链回溯、只读 SQLite schema 检查、公开 market catalog 校验。临时路径只用于复现实验,不是 Maka runtime 依赖。
## 2. 结论摘要
QoderWork 的关键不是“支持三种 transport”,而是把所有来源汇总为一个有生命周期、有权限边界的 MCP control plane:
1. `McpClientPool` 统一管理 builtin、market、custom、Plugin 与 external connector。
2. 独立 `QoderWorkMcpAdaptor` 把多个 upstream MCP server 汇总成一个本机 authenticated proxy。
3. Agent runtime 只连接一个稳定的 `qw-builtin` endpoint,不直接感知上游数量和 transport 差异。
4. market 安装、setup、OAuth、enable 是分离状态;“进入配置”不等于“已经可用”。
5. tool snapshot 以 chat/subChat 为单位写入,active turn 中的 Plugin reload 延迟到 turn end。
6. OAuth token 按 user 与 server 隔离,使用 Electron `safeStorage`,logout 会 suspend pool 并断开 upstream。
7. reconnect、OAuth refresh、HTTP session failure、SSE failure 分流处理,而不是统一粗暴 restart-all。
Maka V1 已具备 transport、tool projection、permission boundary、config reconciliation 与 desktop management;最大差距是 OAuth、authenticated loopback proxy、market setup schema 和 crash recovery。
## 3. Process topology
```mermaid
flowchart LR
UI["Renderer: market / custom / Plugin"] --> SVC["Main: McpService"]
SVC --> CFG["Config aggregation"]
CFG --> POOL["McpClientPool"]
POOL --> A["stdio upstream"]
POOL --> B["Streamable HTTP upstream"]
POOL --> C["SSE upstream"]
POOL --> ADAPTOR["localhost MCP adaptor"]
ADAPTOR --> SNAP["per-session tool snapshot"]
SNAP --> AGENT["Agent runtime: qw-builtin"]
```
### 3.1 Pool
Pool entry 持有 normalized config、client、transport、status、connect/reconnect lock、OAuth state 与 tool metadata。所有 source 最终进入相同 pool,因此状态、disconnect、tool refresh 和 error reporting 不需要为 market/Plugin 重复实现。
### 3.2 Adaptor
Adaptor 在 `127.0.0.1` 暴露 MCP HTTP endpoint;macOS/Windows 另提供 Unix Socket/Named Pipe。每次实例使用随机 `x-api-key`。Agent 只看到 adaptor 汇总后的 tool surface。
这层解决了三个问题:
- 多个 upstream 只占 Agent 一个 MCP slot。
- upstream reload 不要求重启 Agent process。
- 可以集中做 header filtering、tool snapshot 与 session isolation。
### 3.3 Header boundary
Adaptor 明确拒绝转发:
- `Authorization`
- `cookie`
- proxy headers
- internal headers
上游 credential 只能由对应 connector/OAuth provider 注入,不能借 Agent 请求穿透本机 proxy。
## 4. Config source 与优先级
聚合顺序为:
1. enabled builtin connector
2. market-installed/custom/enterprise `mcp.json`
3. enabled Plugin `.mcp.json`
同名 server 不覆盖已有项:要么跳过,要么进入 conflict resolution。Plugin server 使用 namespace `plugin:<folder>:<server>`,并由 `pluginConnectorEnabledList` 控制启用状态。
Plugin enable、disable、install、uninstall 都会触发 `reloadMcpAdaptor()`。如果当前 turn 仍在运行,reload 延迟到 turn end,防止半轮中 tool set 改变。
## 5. Transport 与 lifecycle
支持:
- stdio
- Streamable HTTP
- legacy SSE
连接管理包含:
- per-server connect/reconnect lock
- exponential backoff:1s 到 60s
- 20% jitter
- HTTP session failure、SSE close、network error、dynamic token failure、OAuth refresh failure 的独立恢复路径
- pool/adaptor startup degraded mode:单个 server 失败不阻塞整个桌面启动
这比 stop-all/start-all 更适合桌面 Agent:失败域限制在单 server,已经可用的 tools 不被无关 connector 拖下线。
## 6. Market、custom 与 guided setup
### 6.1 Catalog schema
公开 catalog 使用:
```text
schemaVersion: 1
connectors[]
id / name / description / icon / category / tags
items[]
type: mcp | skill
required
mcpConfig | skillConfig
```
独立 metadata 文件定义 category、ecosystem 和 sort order。catalog 与 metadata 都有本地 cache、MD5 对比、启动 refresh 和每 10 分钟 refresh。
### 6.2 安装语义
market install 不直接声明连接成功,而是写入 disabled snapshot:
```json
{
"enabled": false,
"_builtinId": "...",
"_source": "market",
"_marketId": "...",
"_marketItemId": "..."
}
```
connector 如果同时包含 required skill,会同步安装 skill。用户完成 parameters、guided setup 或 OAuth 后才 enable。
### 6.3 `needs-setup`
带 `_setup` 的 config 在 setup 完成前进入 `needs-setup`,Pool 不启动 server。这个状态应与 `disabled`、`connecting`、`needs-auth`、`connected`、`error` 区分,否则 UI 会把“还没配置”错误显示为“连接失败”。
### 6.4 当前公开 market 实例
本次从本地 bundle 还原出的公开 catalog URL 验证到 Notion、Vercel、Supabase 均使用 official remote MCP endpoint:
| Connector | Endpoint | Auth |
|---|---|---|
| Notion | `https://mcp.notion.com/mcp` | OAuth |
| Vercel | `https://mcp.vercel.com` | OAuth + local callback |
| Supabase | `https://mcp.supabase.com/mcp` | OAuth |
## 7. OAuth 与 user isolation
OAuth flow 覆盖:
1. RFC authorization server metadata discovery
2. `WWW-Authenticate` 的 `resource_metadata`
3. Dynamic Client Registration
4. PKCE S256
5. random state 与 callback validation
6. localhost callback 或 app deep link
7. refresh token single-flight
8. 401 时 refresh 一次,仍失败再回到 OAuth discovery
Token persistence:
- SQLite table:`mcp_oauth_tokens_by_user`
- unique key:`(user_id, server_name)`
- payload:Electron `safeStorage`
- logout:suspend Pool、取消 pending flow、disconnect upstream
这里最重要的是 user isolation。桌面端不能只以 server name 保存 token,否则账号切换会把上一账号的授权带到新会话。
## 8. Agent adaptor 与 tool snapshot
`buildMcpServersConfig()` 不把每个上游逐个注入 Agent,而是注入单个 `qw-builtin`。每个 chat/subChat 获得对应 adaptor URL,并通过 `setMainSessionMcpToolSnapshot` 维护当前 tools。
优势:
- Agent config 稳定。
- tool list_changed、Plugin reload、OAuth reconnect 可在 host 内处理。
- active turn 保持旧 snapshot,下一 turn 使用新 snapshot。
Maka V1 目前通过 backend cache invalidation 达到相似的“下一 turn 生效”语义,但还没有可供 subprocess 共用的 loopback proxy。
## 9. MCP Apps
客户端声明 `io.modelcontextprotocol/ui` capability,识别 `text/html;profile=mcp-app` resource。host 通过 `resources/read` 加载 App,使用 AppBridge 处理 server tool call 与 model context update。
这说明 MCP 不只是一组 tools;后续若 Maka 支持 MCP Apps,必须同时设计:
- iframe/webview trust boundary
- resource CSP 与 navigation policy
- server tool call permission
- App → model context 的 size/type validation
## 10. WorkBuddy 安装 UX 的实现证据
本机 WorkBuddy 5.2.6 的 connector card 使用四态交互:
```text
idle (+)
-> connecting (spinner, disabled)
-> cancellable connecting (spinner; hover/focus 显示 red ×)
-> connected / disconnected / error
```
它在进入 `connecting` 后设置 2 秒防误触窗口,随后 `useCancellableConnecting()` 才将 connector 加入可取消集合。点击取消不是纯视觉 reset:
- abort pending server-side OAuth
- 清理 fresh-auth marker 与 user-initiated marker
- optimistic status → `disconnected`
- 调用 adapter `cancelConnector(configId)`
Maka 本次实现保留了该模式的核心:spinner hover/focus 变 `×`、可访问名称随状态变化、点击后 abort connect、序列化等待 config write 完成、remove config、reconcile manager、刷新 tool snapshot。Maka 使用较短的可感知窗口,因为 V1 market 中大量条目只是写入 disabled setup template,并没有长时间 OAuth flow。
## 11. Capability matrix
| 能力 | QoderWork 0.9.11 | Maka 当前 | 结论 |
|---|---|---|---|
| stdio | 完整 | 完整 | 已对齐 |
| Streamable HTTP | 完整 | 完整 | 已对齐 |
| SSE fallback | 完整 | 完整 | 已对齐 |
| tools/list pagination | 完整 | 完整 | 已对齐 |
| list_changed | 完整 | 完整 | 已对齐 |
| rich tool result | 完整 | 完整且有 aggregate bounds | Maka 更严格 |
| permission/event boundary | host adaptor | 统一 `MakaTool` runtime | 已对齐 |
| market templates | remote catalog + cache | bundled catalog | V2 动态化 |
| guided setup | `_setup` + `needs-setup` | disabled template + manual editor | 待补 |
| OAuth 2.1/PKCE/DCR | 完整 | 未实现 | P0 |
| encrypted token/user isolation | `safeStorage` + per-user DB | headers/env in owner-only file | P0 |
| reconnect/backoff | 分流 + jitter | manual reconnect/基本 close | P1 |
| loopback aggregate proxy | 有 | 无 | P1 |
| resources/templates | 有 | protocol contract 未做 UI | P1 |
| MCP Apps | 有 | 无 | P2 |
| Plugin `.mcp.json` | 有 | 无统一 Plugin source | P2 |
## 12. Maka roadmap
### P0:授权与 setup 真值
- OAuth metadata discovery、PKCE、state validation、local callback/deep link。
- Keychain/`safeStorage` token store,以 workspace user + server id 隔离。
- market item schema 增加 `parameters`、`setup`、`authType`、platform/version constraints。
- 状态机显式增加 `needs-setup`、`needs-auth`,禁止把未配置模板当作 runtime error。
- credential 从 process args/header JSON 迁出;stdio secret 通过受控 env 注入。
### P1:稳定 control plane
- per-server backoff、jitter、crash budget、manual retry reset。
- authenticated loopback aggregate proxy,header denylist 与 random API key。
- resources/templates browse/read/subscribe。
- remote catalog 签名或 digest 校验、atomic cache、last-known-good fallback。
- install operation idempotency、cancel/retry audit event。
### P2:生态与 UI surface
- MCP Apps sandbox 与 AppBridge。
- Plugin/Skill bundled connector manifest。
- enterprise catalog/policy overlay 与 conflict resolution。
- per-tool enable policy、trusted-server read-only policy。
- market update/version/source provenance 与 permission diff review。
## 13. Maka 本次市场实现说明
本次新增钉钉、飞书、Slack、LINE、Notion、macOS 应用、Google Calendar、Figma、Vercel、Supabase。
来源差异必须透明:WorkBuddy 当前的钉钉/飞书是其私有 CLI connector,不是可直接复用的通用 MCP server;Maka 因此采用公开 npm MCP package。Notion/Vercel/Supabase 使用公开 official remote endpoint。需要 credential/OAuth/setup 的条目安装为 `enabled: false`,用户完成配置后再启用,避免空 credential 启动和虚假“安装成功”。
Bundled catalog 对新增 npm server 固定已核验版本,避免 `latest` 静默漂移。飞书 0.5.1 已核验支持 `APP_ID`/`APP_SECRET` env,因此 credential 不写入 process args;LINE 按 package 建议默认设置 `NPM_CONFIG_IGNORE_SCRIPTS=true`。
Figma credential 使用 `FIGMA_API_KEY` env,不放在 process args。V1 仍会把显式 env 写入 owner-only `mcp.json`;在 P0 encrypted credential store 完成前,UI 必须继续提示该限制。
## 14. Evidence index
格式化 `main.js` 行号以本次样本为准:
| 范围 | 证据 |
|---|---|
| 30269–30540 | config 与 OAuth storage |
| 32590–33540 | OAuth discovery/PKCE/callback/refresh |
| 34980–37230 | `McpClientPool` lifecycle/reconnect |
| 79860–80720 | main-session tool snapshot |
| 101420–102480 | authenticated adaptor 与 header boundary |
| 102772+ | Plugin `.mcp.json` aggregation |
| 116300–117240 | market schema/cache/refresh |
| 118873–119764 | `McpService` |
| 138927–139170 | pool/adaptor lifecycle |
| 145899–146000 | runtime adaptor injection |
| 192767–192820 | startup degraded mode |
WorkBuddy 证据来自本机 5.2.6 `renderer/assets/connector-*.js` formatting 后的 `use-cancellable-connecting.ts`、`renderAvailableCard`、`cancelMcpConnector` 与主进程 connector protocol。报告只记录状态机和调用语义,不复制其产品代码或资产。
+965
View File
@@ -0,0 +1,965 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Reference Layout — UI Atlas
> Archived on 2026-07-13. This reverse-engineering snapshot is provenance, not current product design authority.
Systematic reverse-engineering pass of the external reference layout used as the
visual north star for Maka. Built from the extracted bundle at
`/tmp/qoder-extracted/out/renderer/`.
This document is the source of truth for Phase 2 reapplication. Every section is
backed by direct evidence pulled from `assets/globals-UfMzAdiO.css` (265KB
minified, beautified to `/tmp/reference-globals.css` = 8032 lines) and
`index.html`. Where an entry says "from JS chunk", it's a string literal extracted
from `index-C02cm0ok.js`.
**Use the reference's mechanics — never copy product names.** Naming hygiene
rule (memory `project_reference_layout_lane`) requires every artifact in Maka's
codebase use neutral terms ("reference layout", "upstream design"). The product
name appears in this atlas only because it's needed to identify what we're
analyzing; it must not leak into commit messages, JSDoc, or shipped CSS comments.
---
## 1. Theme System
`<html data-theme="...">` carries 8 themes:
| Theme | Loading bg | Loading fg | Notes |
|--------------------|-------------|-------------|-----------------------------------------|
| `light` | `#ffffff` | `#09090b` | Default on Windows/Linux |
| `dark` | `#09090b` | `#fafafa` | |
| `light-glass` | transparent | `#09090b` | macOS default; uses Electron vibrancy |
| `dark-glass` | `#171716` | `#eeeeeb` | |
| `classic-light` | `#fafaf9` | `#171716` | Warmer neutral |
| `classic-dark` | `#09090b` | `#fafafa` | |
| `light-parchment` | `#FAF9F6` | `#202116` | Warm bone canvas |
| `dark-parchment` | `#1E1C18` | `#FAF9F6` | |
Initial body fallback (before React mounts): `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto`.
macOS default is `light-glass` (transparent + native vibrancy). Maka does **not**
currently use Electron vibrancy — adding it is a separate platform concern.
### Glass theme implication
`light-glass`/`dark-glass` make the window content render against the system
material (translucent panels with desktop bleed-through). The whole page chrome
is calibrated to look correct on a vibrancy substrate. If we adopt `light-glass`
later, all the colors below need an alpha pass too.
---
## 2. Font System
CSS custom props in light-parchment:
| Token | Value |
|--------------------|------------------------------------------------------------------------|
| `--font-ui` | `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC'` |
| `--font-sans` | `ui-sans-serif, system-ui, sans-serif, …emoji fallbacks` |
| `--font-mono` | `ui-monospace, 'Cascadia Code', 'SF Mono', Menlo, Consolas, …` (light) / `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, …` (dark) |
| `--font-body` | `var(--font-ui)` in sans mode; `'Libre Baskerville', Georgia, 'Songti SC', …` in serif mode |
Plus a downloaded variable: `InstrumentSans-VariableFont_wdth_wght-BjF37L9x.ttf` —
not declared via `@font-face` in the snippet I checked; investigate further if a
specific surface uses Instrument Sans explicitly.
### Font weight scale
- `--font-weight-normal: 400`
- `--font-weight-medium: 500`
- `--font-weight-semibold: 600`
- `--font-weight-bold: 700`
### Type scale (Tailwind v4 defaults)
| Token | Value | Px equivalent | Line height |
|---------------|------------|---------------|------------------------------|
| `--text-xs` | `.75rem` | 12px | `calc(1 / .75)` = 16px |
| `--text-sm` | `.875rem` | 14px | `calc(1.25 / .875)` = 20px |
| `--text-base` | `1rem` | 16px | `1.5` |
| `--text-lg` | `1.125rem` | 18px | `calc(1.75 / 1.125)` = 28px |
| `--text-xl` | `1.25rem` | 20px | `calc(1.75 / 1.25)` = 28px |
| `--text-2xl` | `1.5rem` | 24px | `calc(2 / 1.5)` = 32px |
| `--text-3xl` | `1.875rem` | 30px | `1.2` |
### Runtime size controller — `data-font-size`
Set on `<html>` via `localStorage.getItem('qoder-work-ui:fontStyle')` / `'…:fontSize'`. Three steps:
| Mode | Chat input | Prose body | Code in prose |
|---------------------------------------------------|-------------|-------------|---------------|
| `[data-font-size=small]` | 13 / 24 | 13 | 12 |
| `[data-font-size=medium]` (default; `:not([…])`) | 14 / 24 | (default) | 13 |
| `[data-font-size=large]` | 16 / 28 | 16 | (larger) |
Default font-style: `sans`. `[data-font-style=serif]` swaps body to `Libre
Baskerville` (Songti SC for CJK).
Maka currently has neither `data-font-size` nor `data-font-style` runtime
controls — those are an unimplemented feature, not a styling miss.
---
## 3. Color Tokens (light-parchment as canonical reference)
Reference uses an Ant-Design-style semantic naming with `--color-bg-*`,
`--color-fill-*`, `--color-text-*`, `--color-border-*`, `--color-link-*` families.
Below is the light-parchment family — the warm-canvas theme that maka's
"reference-layout lane" targets.
### Backgrounds
- `--color-bg-base: #faf9f6` — canvas / page background (warm bone)
- `--color-bg-container: #faf9f6` — primary card background
- `--color-bg-elevated: #f7f4ef` — slightly deeper for elevated surfaces
- `--color-bg-highlight: #c9c4b8` — selected row fill
- `--color-bg-highlight-hover: #d4c9bc` — hover fill (warm taupe)
- `--color-bg-layout: #fdfcfa` — outer shell (sidebar / chrome plate; lightest)
### Fill scale (subtle tints used for chips, sub-panels)
- Tertiary fill: `var(--color-fill-tertiary)` — most subtle (think 4-5% black)
### Borders
- `--color-border` — primary
- `--color-border-secondary` — slightly lighter
- `--color-border-tertiary` — the lightest (used on the workbench card border)
### Light (default) family condensed
- `--color-bg-base: #fff`
- `--color-bg-container: #fff`
- `--color-bg-elevated: #f9f9f9`
- `--color-bg-highlight-hover: #e0e0e0` (or `#eeeeeb`)
### Dark family condensed
- `--color-bg-base: #171716`
- `--color-bg-container: #171716`
- `--color-bg-elevated: #22221f`
Maka's tokens (`maka-tokens.css`) use `oklch()` — semantically equivalent. The
gap is in **the chrome variables** (`--agents-*`) below — those don't exist in
maka.
---
## 4. Page Architecture Tokens (THE critical findings)
### Content area tokens
- `--agents-content-area-radius: 6px` — the universal corner radius for cards,
panels, content area outer ring. **Six pixels, not 10-12.**
- `--agents-content-area-gap: 4px` — the gap between sibling cards / panels.
**Four pixels.** Cards sit very close, almost touching.
- `--agents-content-area-bg` — `var(--color-bg-container)` in default/dark,
`#fff` in light, `transparent` in glass themes
- `--agents-layout-bg` — `var(--color-bg-layout)` for shell; in glass:
`#0006` (dark) / `#ffffff80` (light); in parchment: a 172° linear gradient
from `--color-fill-tertiary` to `--color-bg-container`
### The page layout DOM (inferred from CSS selectors)
```
[data-agents-page] ← root, theme-scoped
└── .agents-layout-root
└── .agents-layout-body
├── (left sidebar, sometimes glass material)
└── .agents-content-area ← right column / main pane
└── [data-agents-view=cron|skills|chat|im_hub|new_chat]
└── .agents-inner-view-clamp ← THE page card itself
├── (page header / hero)
└── content (workbench-split-root, dual-card-row, etc.)
```
### The signature card chrome
When **`light-parchment` / `dark-parchment` theme** + view is `skills` / `cron` /
`im_hub` (the three "wide" views, vs chat which is a centered conversation
column), `.agents-inner-view-clamp` paints as the lifted page card:
```css
border: 1px solid var(--color-border-tertiary); /* often 50-60% via color-mix */
border-radius: var(--agents-content-area-radius); /* 6px */
min-width: 350px;
```
### Layout primitives inside the page card
- `.workbench-split-root` / `.workbench-split-view` — split-pane container
(uses resizable panels; `gap: var(--agents-content-area-gap)` = 4px)
- `.agents-dual-card-row` — alternative dual-card row
- `.workbench-card` — content card inside the split
- `.aux-panel` — sibling card / sub-panel
- `.task-monitor-right-panel` — special right-side monitor variant (no border /
no shadow / `--color-fill-tertiary` background to recede)
### The universal card recipe
```css
border: 1px solid var(--color-border-tertiary);
border-radius: var(--agents-content-area-radius); /* 6px */
background: var(--color-bg-base);
box-shadow: 0 1px 2px #0c0c0d0a; /* black @ ~4% */
```
This is the formula for `.workbench-card`, `.aux-panel`, and
`.agents-chat-panel`. It is the **single most-reused look** in the bundle and
the gap with maka — maka's cards have 8-10px radii, 0.04 foreground shadows,
and use `var(--border)` (more like `--color-border-secondary`). Visually maka's
cards read as "bigger / softer / more isolated" than reference's tight 6px /
hairline-tertiary cards.
### The `--agents-layout-bg` (sidebar / chrome) — flat, NOT gradient (corrected 2026-06-24)
```css
background: var(--color-bg-container);
```
**This section originally claimed a 172° linear-gradient.** Corrected
2026-06-24 (WAWQAQ msg `5d3b10e5` + `1e693dee`): the reference product
actually renders a flat `--color-bg-container` here. The gradient I
extracted from the bundle CSS *was* present, but only as a leftover
in the source — the production app doesn't render it. Following the
stale extracted CSS reintroduced the gradient into maka's
`.maka-shell-2col` and `--agents-layout-bg` tokens, which WAWQAQ
flagged as wrong:
> 谁让你他妈的用渐变的啊?参考实现就没有啊
PR-CHAT-CHROME-FIX-0 (`ed292897`) reverted both maka tokens to flat
`var(--background)` / `var(--color-bg-container)`. **Future agents:
do NOT re-add this gradient based on RE evidence; the user's
observable ground truth is flat.**
---
## 5. Shadow Vocabulary (curated; 5 patterns)
The bundle has hundreds of `box-shadow:` declarations, but they reduce to these
5 patterns:
| Pattern | Use |
|----------------------------------------------------|-----------------------------------|
| `box-shadow: 0 1px 2px #0c0c0d0a` | **Universal card lift** (the only shadow on default cards) |
| `box-shadow: 0 6px 20px -12px #0c0c0d33` | Medium float (popovers, sticky panels) |
| `box-shadow: 0 8px 24px -16px #0000006b` | Deep float (dark mode equivalent) |
| `box-shadow: 0 4px 12px #00000026` | Anchored anchored alerts |
| 3-layer triple-glow (10px wing-out variants) | Composer hero glow (multiple tints) |
Notes:
- `#0c0c0d0a` = `rgba(12, 12, 13, 0.04)` — about 4% black.
- All non-glow shadows use plain hex with alpha suffix (not `rgba(…)`), keeping
the declarations short.
Maka uses `oklch(from var(--foreground) l c h / 0.04)` style alpha shadows
mostly — semantically the same lift, just expressed via oklch. The numeric gap:
maka's lift cards use 0.10 in the second shadow layer (per `da1a40f1` removal).
Reference would have used the `0 1px 2px` 4% only and let the 1px border do
the visual separation.
---
## 6. Keyframes (all 13)
| Keyframe | Spec (from earlier samples) |
|-----------------------------------|------------------------------------------------------------------------------|
| `agents-fade-in` | 200ms ease-out opacity 0→1 |
| `agents-slide-up` | 200ms translateY 8px → 0 with opacity 0→1 |
| `streaming-fade-in` | `.18s ease-out` opacity 0→1 (per-token fade on streaming Markdown) |
| `workbench-card-scale-in` | `.35s cubic-bezier(.16,1,.3,1) .15s both` scale .97→1, opacity 0→1 |
| `chat-input-glow-pulse` | `4.8s ease-in-out infinite` (composer ambient breathing — already ported) |
| `indeterminate-slide` | `1.4s ease-in-out infinite` `left -40% → 100%` (indeterminate progress bar) |
| `pointer-poke` | `4s linear infinite`, micro-shake to draw cursor attention |
| `pulse` | `2s cubic-bezier(.4,0,.6,1) infinite` (Tailwind built-in) |
| `spin` | `1s linear infinite` (Tailwind built-in) |
| `chroma-slide` | Color shift on tiled chrome (not yet sampled) |
| `connectors-lightning-pulse` | Connector banner emphasis |
| `industry-guide-marquee` | Horizontal marquee for the industry-guide page |
| `agents-chat-design-launch-narrow`| Chat hero launch (likely the empty-chat slide-up sequence) |
**Already ported into maka:**
- `agents-fade-in` → kept as ambient utility
- `agents-slide-up` → `maka-message-row-enter` (rounds 24/30) and `maka-hero-enter` (round 29/30)
- `workbench-card-scale-in` → `maka-tool-card-enter` (round 23/30)
- `chat-input-glow-pulse` → `maka-composer-glow-pulse`
**Not yet ported (good Phase 2 candidates):**
- `streaming-fade-in` — would be a real per-token fade in streamed Markdown
- `indeterminate-slide` — loading bar for retry / pending states
- `chroma-slide` / `connectors-lightning-pulse` — non-core, skip
- `industry-guide-marquee` — non-applicable (no Maka counterpart)
---
## 7. Page-Level Component Names (from JS chunks)
Strings literal-grep'd from `index-C02cm0ok.js`. These are React component or
className tokens — useful for naming Maka equivalents.
| Reference string | Likely surface |
|---------------------------------------------|------------------------------------------------|
| `agents-settings` | Settings page root |
| `agents-settings-page-heading` | Settings page H1 |
| `agents-settings-page-title-narrow` | Settings page H1 (narrow viewport variant) |
| `agents-settings-page-description` | Settings page lede |
| `workbench-aux-stack-from-panel-layout` | Aux panel stack adapter |
| `workbench-root-from-panel-layout` | Main workbench wrapper |
| `workbench-tab` | Tab control variant |
| `cron-tasks` | Cron page tasks list |
| `skills-navigation` | Skills page nav |
| `agents-rename-subchat` | Inline rename overlay |
The settings page has a dedicated heading/description block (per the H1 + lede
hierarchy below). Cron page has a `cron-tasks` content area for the actual
schedule list.
---
## 8. Data attribute selectors (state vocabulary)
Reference uses `[data-*]` heavily for state. Subset relevant to Maka:
- `[data-agents-page]` — root scope guard (theme-aware blocks)
- `[data-agents-view=cron]` / `[data-agents-view=skills]` / `[data-agents-view=chat]` / `[data-agents-view=new_chat]` / `[data-agents-view=im_hub]` — current route
- `[data-active=true]` — list selection
- `[data-disabled=true]` / `[data-disabled]` — interaction disabled
- `[data-focused=true]` — focus-within (used on the composer)
- `[data-font-size=small|medium|large]` / `[data-font-style=sans|serif]` — runtime typography
- `[data-glass-effects=off]` — disables vibrancy / backdrop blur
- `[data-composer-message]` — composer rich-text shell
- `[data-user-bubble]` — user message bubble (in chat)
- `[data-chat-launch-transition=design]` — initial chat launch animation flag
- `[data-skills-banner]` / `[data-skill-filter-menu]` — skills page chrome
- `[data-canvas-dialog]` / `[data-close-button]` — modal chrome
- `[data-gradual-blur-layer]` — blur scrim
- `[data-banner-logo]` — Settings banner logo
Maka mostly uses `[data-active="true"]` on list rows. There's no `[data-agents-view]` route attribute — module switching uses local React state, not a documented DOM contract. Adopting `[data-agents-view]` would make CSS targeting easier.
---
## 9. Specific gap analysis: Maka vs reference
| Surface | Reference recipe | Maka current | Gap to close in Phase 2 |
|----------------------|-------------------------------------------------------------|-------------------------------------------------------------|------------------------------------------------------|
| Page card outer | 1px `--color-border-tertiary` + 6px radius + `0 1px 2px #0c0c0d0a` | flat (no border / no shadow after da1a40f1) | Restore the lift, but tight 6px radius (was 10) |
| Inter-card gap | 4px (`--agents-content-area-gap`) | 10-14px gap | Tighten to 4-6px |
| Card shadow | `0 1px 2px` 4% only — no second softer layer | Used to have 2-layer (0 1px 2px + 0 8px 24px -16px 10%) | Single 4% lift; let border do the separation |
| Sidebar / chrome bg | Flat `--color-bg-container` (corrected 2026-06-24) | Flat `--background` (parity) | **None — do NOT add a gradient (see §4.5 correction)** |
| Cron page heading | (Need to extract — JS chunk has `cron-tasks` block) | h2 24px / 650 | Likely larger / display weight |
| Settings page | `agents-settings-page-heading` + lede + `narrow` variant | Modal popup (still!) | Restructure as inline page in module-main pane |
| Active list fill | `var(--color-bg-highlight)` = `#c9c4b8` (warm taupe) | `oklch(from --foreground l c h / 0.07)` (neutral gray) | Optional: warm taupe in parchment theme |
| Glass theme | Vibrancy + `data-glass-effects` toggle | Not implemented | Out of UI scope (Electron platform work) |
| `data-font-size` ctl | Runtime small / medium / large | Not implemented | Future settings surface |
| Streaming tokens | `streaming-fade-in` per-token | Bubble-level fade only | Would need ChatMarkdown renderer wrap |
---
## 10. Phase 2 plan (atlas-driven)
Goal: rewrite Maka's **Plan/Reminder** page and **Settings** surface to match
reference's signature card chrome + heading hierarchy + tight 4-6px geometry.
### 10.1 Plan/Reminder page (ScheduledTaskPanel)
- Wrap content in a `.maka-page-card` matching the universal recipe (1px
border-tertiary + 6px radius + 4% lift), with `--agents-content-area-gap`
equivalent at 4-6px between internal sections.
- Heading: change `.maka-scheduled-task-heading h2` from 24/650 to 30px / weight-600
(reference uses `--text-3xl` for page headings).
- Internal sections render as workbench-card-style sub-cards: 1px border, 6px
radius, soft lift. Card-vs-card gap = 4-6px.
- Task list = aux-panel pattern: per-row 1px divider (`border-bottom` on
`:not(:last-child)`), no per-row border or radius (cockpit-mode density per
taste skills).
### 10.2 Settings (currently a modal!)
- Convert from Base UI Dialog overlay to an inline `.maka-module-main` view.
- Header: `.maka-page-heading` (30px display) + lede paragraph + narrow variant
(`-title-narrow`) for tighter sidebar widths.
- Nav: left rail (current modal nav) → keep, but as part of a 2-column
workbench-split-root inside the page card.
- Content panel: right side workbench-card; sub-sections divided by
`border-bottom` lines, not nested cards.
### 10.3 Shared primitives (CSS-only first)
- Add `.maka-card-base` mixin via custom property: a single rule that defines
the recipe (1px border-tertiary, 6px radius, 4% lift). Compose into
`.maka-skill-library`, `.maka-scheduled-task-shell`, future settings page.
- Add `.maka-page-heading` typography class (30px / 1.2 line / weight 600).
- Optionally add `[data-maka-view=plan|skills|settings|...]` on a wrapper to
match reference's `[data-agents-view]` pattern.
### 10.4 Out of Phase 2 scope (file follow-ups)
- Glass theme + Electron vibrancy
- `data-font-size` / `data-font-style` runtime controls
- Streaming-fade-in per-token (requires Markdown renderer surgery)
- The warm taupe `--color-bg-highlight` (parchment-only)
---
## 11. Source pointers (where evidence lives)
- `/tmp/qoder-extracted/out/renderer/index.html` — DOM bootstrap, theme bootstrap, font preload
- `/tmp/reference-globals.css` — beautified CSS (multi-line)
- `/tmp/qoder-extracted/out/renderer/assets/globals-UfMzAdiO.css` — original minified CSS
- `/tmp/qoder-extracted/out/renderer/assets/index-C02cm0ok.js` — main JS bundle (component names live here)
- `/tmp/qoder-extracted/out/renderer/assets/chat-view-inner-Dgjr-1Tu.js` — chat view chunk
- `/tmp/qoder-extracted/out/renderer/assets/chat-markdown-renderer-C-R6Fd8_.js` — Markdown chunk
Grep patterns used for this pass (re-runnable):
```sh
# CSS custom props
grep -E '^[[:space:]]*--[a-z][a-z0-9-]+:' /tmp/reference-globals.css | sort -u
# All keyframes
grep -oE '^[[:space:]]*@keyframes [a-zA-Z][a-zA-Z0-9-]+' /tmp/reference-globals.css | sort -u
# All shadow patterns
grep -oE 'box-shadow:[^;}]+' /tmp/reference-globals.css | sort -u
# Page-level class strings from JS
grep -oE '"(agents|workbench|aux|cron|skills|settings)-[a-z-]+"' /tmp/qoder-extracted/out/renderer/assets/index-C02cm0ok.js | sort -u
```
---
## 12. Deep-RE Addendum (Phase 1 follow-up)
Additional findings from a second pass focused on JS chunk component naming,
secondary entry-point HTMLs, and the data-* state vocabulary. Builds on §1–§11.
### 12.1 Secondary entry-point HTMLs
The bundle ships 4 secondary HTML entry points beyond `index.html`:
| File | Lines | Role |
|----------------------------|-------|---------------------------------------------------|
| `quickpick.html` | 58 | Floating spotlight-style command bar (transparent body, vibrancy-aware, own renderer bundle `quickpick-CilslRnn.js`) |
| `voice-overlay.html` | 39 | Voice transcription overlay (Whisper-style) |
| `artifact-preview.html` | 34 | Sandboxed artifact preview surface |
| `mcp-app-preview.html` | 33 | Sandboxed MCP app preview surface |
Quickpick deserves special attention: `html, body, #root` all get
`background: transparent !important` and a system-font fallback. The window
chrome (corners, shadow) is owned by the **Electron window** itself, not CSS —
they pad nothing and rely on `setVibrancy(theme)` for material rendering. This
is a strong signal that the reference layout's "premium floating chrome" is
half CSS and half native window-level treatment. Maka has no quickpick-equivalent.
### 12.2 Data-* state vocabulary (beyond §8)
A second sweep through the JS bundle surfaced the **structural** data-attrs
the reference layout uses for layout state. These are CSS hooks the
component tree sets imperatively, not user state:
- `[data-resizable-sidebar]` — sidebar is resizable (drag handle present)
- `[data-sidebar-collapse-button]` — the collapse toggle button
- `[data-sidebar-collapsed]` — current collapse state
- `[data-sidebar-content]` — sidebar content slot
- `[data-workbench-root]` — workbench tree root
- `[data-workbench-stack]` — vertical card stack inside workbench
- `[data-workbench-card]` — individual workbench card
- `[data-workbench-fixed-panel-child]` — non-resizable child panel
- `[data-panel-name]` — named panel (for the resizable react-panel lib)
- `[data-settings-nav-column]` — Settings left-nav column
- `[data-canvas-dialog]` — canvas-style dialog (probably used for full-bleed previews)
- `[data-banner-logo]` — branded banner logo slot
The pattern: structural state is on `data-*`, theming is on
`[data-theme=…]`, route is on `[data-agents-view=…]`. CSS targets the
data-attrs heavily, components don't carry presentation classes —
they wear semantic classes (e.g. `settings-nav-column`) AND set
their parent's `data-*` for the CSS targeting layer to see.
Maka mostly uses regular className prefixes (`.maka-*`). Adopting
`data-*` state would let us write tighter CSS without prop drilling
classNames through every component.
### 12.3 Telemetry naming as a structure proxy
The JS bundle also contains telemetry event names (underscore-separated:
`cron_run_log_view_conversation`). These reveal the **surface granularity**
the reference team thinks in. A few patterns worth knowing:
- **Cron page surfaces:** `cron_page_view` / `cron_task_create` /
`cron_task_update` / `cron_toggle_enabled` / `cron_run_now` /
`cron_run_log_detail_view` / `cron_run_log_rerun` /
`cron_run_logs_filter` / `cron_run_logs_tab_view` /
`cron_sort_change` — there are multiple top-level surfaces:
task list, run logs, log detail, plus filter/sort.
- **Settings page surfaces:** `settings_tab_view` / `settings_tab` /
`settings_general_section` / `settings_preferences` /
`settings_profile_tab_view` / `settings_app_update_check_click` /
`settings_legokit_tab` / `settings_general_hidden_entry_unlock` —
Settings has *tabs* (not the single rail Maka has), with
Profile / General / Preferences / LegoKit (their term for skills/extensions)
as top-level. The hidden-entry unlock is a 5-click easter egg.
- **Sidebar:** `sidebar_chat_group_set` / `sidebar_chat_group_delete` /
`sidebar_chat_group_dissolve` / `sidebar_chat_group_toggle` /
`sidebar_workspace_group` / `sidebar_im_session` /
`sidebar_cron_group` — sidebar supports nested groups (workspace,
chats, IM channels, cron). Maka's sidebar is flat.
Maka has none of the group-nesting in its sidebar. The reference layout
keeps an entire IM-hub and a workspace-group structure that Maka would
need new IPC + data model to support — out of pure-UI scope, but worth
noting that **the reference sidebar is materially more structured**.
### 12.4 Streaming + indeterminate keyframe specs (ready to port)
Two keyframes I flagged in §6 as worth porting, with exact specs verified:
```css
@keyframes streaming-fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}
.streaming-token-fade-in {
animation: 0.18s ease-out forwards streaming-fade-in;
}
```
Per-token fade on Markdown stream — wrap each streamed token in a span
with `.streaming-token-fade-in`. Requires Markdown renderer surgery in
Maka's chat-markdown-renderer to wrap each delta in a span; skipping
for now to avoid renderer risk. Adding the keyframe + utility class
so the wrapping can happen later without another atlas pass.
```css
@keyframes indeterminate-slide {
0% { left: -40%; }
100% { left: 100%; }
}
.animate-indeterminate {
animation: 1.4s ease-in-out infinite indeterminate-slide;
}
```
Indeterminate progress bar — needs a container with `overflow: hidden`
and a child of `width: 40%; position: absolute; top: 0; bottom: 0;`
applying the animation. Useful for: retry-load pending state, action
in-flight in modal footers, scheduled-task run-now in-flight.
### 12.5 Heading + lede typography (settings page)
Three settings-specific typography classes surfaced in the JS bundle:
- `.agents-settings-page-heading` — page H1 (no exact size sampled; per
the type-scale and convention this is `--text-3xl` = 30px / 600 with
tight tracking)
- `.agents-settings-page-title-narrow` — narrow-viewport variant
(probably `--text-2xl` = 24px when sidebar is at full width)
- `.agents-settings-page-description` — lede paragraph under the H1
(probably `--text-sm` = 14px with `--color-text-secondary`)
Maka's `.settingsHeader h1` is currently 30/600 after Phase 2B — already
matches. The `-narrow` responsive variant doesn't exist in Maka; could
add via `@media (max-width: 1100px)` (atlas-style).
### 12.6 Updated Phase 2 plan (revised after deep RE)
Phase 2C (next big PR): wire the deep findings into code.
**Done so far:**
- Atlas §1–§11 (Phase 1 commit `fe74d870`)
- Plan + Skills page card + display heading + token system (Phase 2A `81b180d2`)
- Settings page chrome card recipe + display heading (Phase 2B `753ae8e0`)
**Phase 2C scope:**
- a) Port `streaming-fade-in` + `indeterminate-slide` keyframes + utility
classes (Atlas §12.4) — CSS only, no JSX wiring; future PRs can use
them on streamed tokens / loading bars.
- b) Apply page-card recipe to chat composer card outer chrome
(`.composer .maka-composer-inner` → 6px radius + hairline border +
1px inner highlight + 4% lift).
- c) Apply page-card recipe to Plan/Reminder sub-sections (alert hero,
tabs panel, template strip) so each acts as a workbench-card-style
aux-panel with 4-6px gaps between siblings (atlas `--card-gap: 4px`).
- d) Settings: add the `-narrow` responsive variant per §12.5.
- e) Settings: apply the page-card recipe to internal sub-cards
(preferences sections, profile card) so they read as aux-panels
instead of flat dividers.
**NOT in Phase 2C (deferred):**
- f) Real Settings Modal → inline-page restructure (main.tsx render tree
change, requires focus-trap / a11y rework, deserves a focused PR).
- g) `data-*` state vocabulary refactor (atlas §12.2) — too invasive,
touches every component file.
- h) Sidebar group-nesting (workspace / IM channels) — needs new IPC
+ data model, out of pure-UI scope.
- i) Wrap streamed Markdown tokens in `.streaming-token-fade-in` (needs
renderer surgery; the CSS lands in 2C so future wiring is one-line).
---
## 13. Deep-RE Round 3 — Sidebar, Composer, Banners, Resize Handle
Follow-up after WAWQAQ msg f31b4611 ("还是多研究 参考布局 的代码 和前端
设计,一定要深入挖掘他们的设计和代码"). Three days into the RE program
and most surface-level patterns are caught (atlas §1–§12). This round
focuses on the structural state machines and the smaller specialized
chrome we haven't yet decoded.
### 13.1 Sidebar — two-mode chrome system
The reference sidebar has two distinct visual modes the renderer
swaps between at run-time:
- **Standard / glass-vibrancy mode (default in glass themes):**
the sidebar renders edge-to-edge against the OS vibrancy material.
CSS rules under `[data-agents-page] [data-resizable-sidebar].agents-sidebar`
set `contain: layout paint style` for performance isolation.
- **`agents-sidebar-floating-glass` mode (parchment themes):**
opaque `--color-bg-container` background + 1px `--color-border-tertiary`
border + `box-shadow: none` + `-webkit-backdrop-filter: none`. This is
the "card-style sidebar" pattern — sidebar reads as a lifted card on
the warm parchment canvas, not a translucent panel.
This is the answer to "how to support warm parchment themes alongside
glass themes": the SAME sidebar component conditionally applies
`.agents-sidebar-floating-glass` based on `data-theme=light-parchment` /
`dark-parchment`. The class lights up via CSS theme matching, not
runtime branching.
Sidebar resize: rendered via the `react-resizable-panels` library
(`[role=separator]` + `data-resize-handle-active`).
- `[role=separator]` always has `background-color: transparent !important`
and `box-shadow: none / outline: none` on `:focus-visible`. The
resize affordance lives in JS hit-region, not visible chrome — the
user only sees the column edge.
- `[data-resizing=true]` on the sidebar drops `box-shadow` while
dragging so the "lift" doesn't get janky during column drag.
`.sidebar-section-title` (the small group label inside the sidebar)
uses `var(--color-text-quaternary)` (a faint 4th-tier text color that
maka doesn't have). In `color-mix` browsers it gets 90% of that token
+ 10% transparent, so the label fades slightly into the chrome.
### 13.2 Composer — primary glow + 3-layer wing pattern (verified)
`.chat-input-primary-glow` is the composer hero card. Two pseudo-
elements (`::before` and `::after`) each carry a 3-layer wing glow:
- **`::before`** (forward animation):
- `0 10px 30px -14px var(--color-primary)` (bottom drop, primary)
- `-18px 0 42px -24px var(--color-primary-hover)` (left wing)
- `+18px 0 42px -24px var(--color-primary-border)` (right wing)
- **`::after`** (reverse animation):
- `0 12px 34px -12px var(--color-primary)` (deeper drop)
- `+20px 0 48px -22px var(--color-primary-hover)` (right wing)
- `-20px 0 48px -22px var(--color-primary-border)` (left wing)
Both run the `chat-input-glow-pulse` keyframe at 4.8s ease-in-out
infinite. Maka has the same glow ported as `maka-composer-glow-pulse`
(2 layers via `::before` and `::after`); the wing-direction
alternation is the magic that makes the composer feel "alive" rather
than "blinking". Verifying maka's port — it uses the SAME pattern
already (looked back at styles.css), with the `--accent` token in
place of `--color-primary`. Good parity.
Composer in parchment theme has its own glow palette via:
- `--chat-input-parchment-glow` = primary glow color
- `--chat-input-parchment-halo` = mid-range halo (uses `--color-warning`
in parchment, so the glow reads as warm amber, not cold blue)
- `--chat-input-parchment-edge` = far-wing edge (uses `--color-link`)
Maka doesn't have a parchment-specific composer palette — the
composer glow uses `--accent` regardless of theme. If we adopt the
parchment theme later, the composer should get warm-amber glow
overrides per the reference.
### 13.3 Banners — three animated mascot families
Three distinct banner families, each with their own mascot animation:
- **`[data-skills-banner]`** — Skills page hero. In parchment themes:
`background-color: var(--color-fill-tertiary)` (subtle warm wash).
Hosts the `.animate-pointer-poke` SVG mascot (4s linear infinite
micro-shake — atlas §6 keyframe).
- Inner SVG attribute rewrites in parchment theme:
`[data-banner-logo], [data-skill-deco-cover]` get
`[fill*=color-primary]` → `var(--color-fill-secondary)` and
`[stroke*=color-primary-border]` → `var(--color-border-tertiary)`.
This neutralizes the brand-blue to a parchment-friendly neutral.
- **`[data-connectors-banner-visual]`** — Connectors page hero. Has
`[data-connectors-lightning]` SVG inside it pulsing on the
`connectors-lightning-pulse` keyframe (1.4s ease-in-out infinite).
Same parchment color rewrite pattern.
- **`[data-plugins-banner]`** — Plugins page hero with multi-part
mascot: `[data-plugins-banner-mascot-body]`,
`[data-plugins-banner-mascot-hand]`, `[data-plugins-banner-tags-layer]`,
`[data-plugins-banner-dynamic-visual]`. Each animates independently
for a layered character animation. Could not extract exact specs in
this pass; complex enough to deserve a dedicated note PR.
Maka has no equivalent banner mascots. They are a meaningful visual
hook the reference uses on hero pages; porting one (e.g., a Skills
mascot) would close a real "delight" gap, but requires custom SVG
illustration assets the maka team would need to commission.
### 13.4 Theme-tinted brand motion (`[data-parchment-brand-motion]`)
A specific attribute on any element that does its own brand motion
animation (Lottie / SVG): in parchment themes, get
`filter: hue-rotate(-85deg) saturate(.65)`; in dark themes (dark,
dark-glass, classic-dark), get `filter: brightness(.8)`. This is how
the reference's mascots stay on-palette across all 8 themes without
the Lottie author hand-tinting every variant.
Maka could adopt: any third-party brand motion (e.g., the loader.gif
under `apps/desktop/assets/`) could pick up a `data-brand-motion`
attribute and these filter rules so it auto-tints with theme. Tiny
patch, high payback if we adopt mascots later.
### 13.5 Selectable text marker
`.allow-text-selection { user-select: text !important }` — explicit
class for surfaces where the user must be allowed to text-select
(e.g., assistant Markdown body). Maka mostly defaults to selectable;
the reference flips the default to "non-selectable" globally
(probably for the chrome) and opts in with `.allow-text-selection`.
This is the reverse policy from Maka but probably worth aligning on
the chrome side (sidebar / nav rows / chips) since `user-select: none`
on those would prevent accidental text-select on click-drag.
### 13.6 Settings sidebar slot `.agents-settings-wide-content`
`.agents-settings-wide-content { background: var(--agents-content-area-bg);
margin: var(--agents-content-area-gap) var(--agents-content-area-gap)
var(--agents-content-area-gap) 0 }` — the content slot inside Settings
that holds each sub-page (Profile / General / Preferences / etc.).
The margins use the `--agents-content-area-gap` (4px) token so the
content slot has a 4px gap on top/bottom/right (left is 0, the nav
column is flush). This is the "Settings tab content fits inside the
page card" pattern — exactly what maka should adopt when it does the
Phase 2C Settings inline restructure.
### 13.7 Updated defer list (post-§13)
Items to consider for a Phase 3 PR after WAWQAQ verdict:
- Adopt `.agents-sidebar-floating-glass` pattern for parchment-mode
sidebar (opaque card variant) — pure CSS + a `data-theme` selector.
- Adopt `[data-parchment-brand-motion]` theme-tint pattern for any
loader / mascot we ship later — 4 lines of CSS, no JS.
- Port the `[data-banner-logo]` SVG color-rewrite pattern for any
brand-colored SVG so it auto-neutralizes in parchment themes.
- Adopt the `.allow-text-selection` opt-in policy on chrome surfaces
so click-drag on a nav row doesn't begin a text selection.
- Use `--agents-content-area-gap` (4px) consistently for Plan/Skills
sub-section gaps (we currently use 14px; the reference's 4px reads
much tighter, matching the dense-cockpit aesthetic).
---
## 14. Deep-RE Round 4 — Settings palette, popovers, Settings layout (§14)
Continuing after WAWQAQ msg 7f3ca067 ("继续学习…多解决现有前端的问题,设计 布局的问题").
### 14.1 Settings has its own primary-color palette
When the renderer sets `[data-modal=agents-settings]` on the Settings
surface, the reference applies a palette OVERRIDE:
- Light / light-glass / classic-light / **default**:
- `--color-primary: #8ee5a1` (mint green)
- `--color-fill-secondary: #ebebeb` (warm gray)
- `--color-text-on-primary: #fdfdfd`
- Light-parchment:
- `--color-primary: #202116` (deep charcoal — almost off-black)
- `--color-fill-secondary: #ede8e0` (parchment cream)
- Dark themes: tokens come from `--color-bg-container` / `--color-text`
family (no warm primary override).
**This is a meaningful design choice the reference team made:** Settings
feels visually DISTINCT from chat because its primary accent is mint
green instead of brand blue. A user clicking "Save" or "Apply" in
Settings sees a mint-green CTA — semantically distinct from a Chat
submit (brand blue). It's a subtle but premium pattern.
**Maka uses `var(--accent)` everywhere**, including Settings. Adopting
a Settings-only `data-surface=settings` token override would close
this gap. ~10 lines of CSS.
### 14.2 Sidebar background bypass for Settings
When Settings is open and the theme is `light-parchment` /
`dark-parchment` / `classic-light`:
- `aside[data-settings-nav-column].agents-sidebar > div { background: 0 0 }`
- `html[data-theme=light-parchment] .agents-layout-root .agents-sidebar
{ background: 0 0 !important }`
So the Settings nav column has NO background — it inherits from the
Settings modal palette. Maka's Settings nav has its own
`.settingsNavGroup` chrome; could simplify to inherit too.
### 14.3 Popover side-translate pattern (Tailwind data-* variants)
Reference uses Tailwind v4's data-attribute variants extensively for
popover positioning:
```
.data-[side=top]:-translate-y-1[data-side=top] { translate: 0 -4px; }
.data-[side=bottom]:translate-y-1[data-side=bottom] { translate: 0 4px; }
.data-[state=open]:bg-fill-secondary[data-state=open] { background: var(--color-fill-secondary); }
```
So Base UI (or Radix-style) primitives that set `[data-side=top]`
automatically get the right translate via the Tailwind utility class.
Maka uses Base UI primitives too, but our CSS file has hand-rolled
positioning rules instead of using Tailwind data-* variants. We have
the tooling already (Tailwind v4 is installed); shifting to data-*
variants would unify the pattern and remove ~30 lines of custom CSS.
### 14.4 Selectable text — opt-in policy confirmed
Atlas §13.5 already flagged `.allow-text-selection`. Verified in
reference: it has `user-select: text !important`. The implication
is that the reference's GLOBAL DEFAULT for chrome surfaces is
`user-select: none` — the opposite of Maka, which inherits browser
default (text everywhere). This explains why reference chat looks
"more app-like" while Maka can accidentally enter text selection
mode on chrome clicks.
**Phase 3 candidate** (atlas §13.7): apply `user-select: none` to
`.maka-session-panel`, `.maka-nav-row`, `.maka-list-row`,
`.maka-module-main-header` headings (chrome surfaces only), and
keep text selection on Markdown bodies + composer + readonly content.
### 14.5 Additional polish gaps spotted
- **Reference uses `cmd+k` global** to focus the chat input (`focus-input`
/ `agent-input-history` in JS chunk strings); Maka has `cmd+k` but
the input doesn't auto-focus on cold open. Cold-open onboarding
would be smoother.
- **Reference distinguishes `[data-glass-effects=off]` on win32 host**:
it disables all `backdrop-filter` globally. If we adopt
`[data-glass-effects=off]` toggle as a Settings option, the user
can opt out of macOS vibrancy too (some users dislike it). 4 lines
of CSS + a settings toggle.
- **Reference has `[data-canvas-dialog]`** that overrides the standard
modal background with `var(--color-bg-container)` — a full-bleed
preview / artifact dialog variant. Maka doesn't have a canvas-style
dialog; would be useful for Artifacts pane preview.
### 14.6 Phase 3 expanded scope (now 8 quick wins)
Combining §13.7 + §14:
1. `.agents-sidebar-floating-glass` for parchment-mode opaque sidebar
2. `[data-parchment-brand-motion]` global theme tint
3. SVG color-rewrite pattern (`[fill*=color-primary]` -> token)
4. `.allow-text-selection` opt-in policy reversal
5. Tighten Plan/Skill sub-section gaps 14 -> 4-6px
6. **NEW:** Settings primary-color palette override
(`data-surface=settings` → mint-green primary)
7. **NEW:** Adopt Tailwind data-* variants for popover side translate
8. **NEW:** `cmd+k` cold-open composer auto-focus
9. **NEW:** `[data-glass-effects=off]` toggle in Settings
10. **NEW:** `[data-canvas-dialog]` variant for Artifacts pane
Items 1-5 are pure CSS, can ship today. Items 6-7 need small JSX
changes. Items 8-10 are bigger (new IPC / new dialog mounting).
---
## 15. Deep-RE Round 5 — Fonts, hero, blur-layer (§15)
Following WAWQAQ msg 500eddc9 ("一定要多做、多分析…深度抄袭和学习 参考
界面 布局"). This round catches typography + visual-effect patterns I
hadn't decoded.
### 15.1 Instrument Sans variable font on hero surfaces
Reference loads a downloaded variable font:
```css
@font-face {
font-family: Instrument Sans;
src: url(./InstrumentSans-…ttf) format("truetype");
font-weight: 1 1000;
font-display: swap;
}
```
Used only on `.welcome-title` and `.login-title`:
```css
.welcome-title {
font-variation-settings: "wdth" 100, "wght" 500;
font-family: Instrument Sans, ui-sans-serif, system-ui, sans-serif;
}
```
When `[data-font-style=serif]` is set, falls back to `--font-body`
(Libre Baskerville).
**Insight**: reference distinguishes hero typography from chrome
typography via a specific variable font with bespoke axes. Maka uses
Geist Variable everywhere — adopting the same distinction WITHOUT
shipping a second font is possible: set
`font-variation-settings: "wght" 550, "wdth" 105` on hero titles to
get the "wider, slightly heavier" feel. This commit applies it to
`.maka-hero h1` / `.emptyChat h1`.
### 15.2 `[data-gradual-blur-layer]` overlay pattern
Reference has a `[data-gradual-blur-layer]` attribute marking a
backdrop-blur scrim element used to fade out content edges (e.g.
under a sticky header). It uses `-webkit-backdrop-filter` directly,
and the `[data-glass-effects=off]` toggle disables it via
`-webkit-backdrop-filter: none !important`.
**Pattern**: a fixed `pointer-events: none` overlay layer at the top
or bottom of a scroll container with a `linear-gradient(transparent
0%, var(--background) 100%)` mask + `backdrop-filter: blur(8px)`
creates a "content fades into chrome" effect. Maka has no equivalent;
adding one on sticky chat headers / sidebar scroll edges would close
a real polish gap.
### 15.3 Font-display swap + Instrument Sans local
Reference's font is shipped at the bundle, loaded with `font-display:
swap`. Best practice for variable fonts in Electron — initial paint
uses the system fallback (`ui-sans-serif, system-ui`) while the
variable font loads from local disk (fast on first run, instant on
subsequent). Maka uses Geist Variable from `@fontsource-variable/geist`
which also self-hosts and uses `font-display: swap` by default.
Aligned by accident; not a gap.
### 15.4 `[data-os=win32][data-glass-effects=off]` per-platform fallback
For Windows users who explicitly disable glass effects via Settings,
reference disables ALL `backdrop-filter` rules globally:
```css
:root[data-os=win32][data-glass-effects=off] [class*=backdrop-blur],
:root[data-os=win32][data-glass-effects=off] [data-gradual-blur-layer],
:root[data-os=win32][data-glass-effects=off] .agents-layout-root,
:root[data-os=win32][data-glass-effects=off] [data-resizable-sidebar].agents-sidebar.agents-sidebar-floating-glass {
-webkit-backdrop-filter: none !important;
}
```
Maka's `data-os` is set on `<html>` after Phase 2C (atlas §12.1).
Adopting the `data-glass-effects` toggle would let users opt out of
sidebar vibrancy on macOS too — useful for users who find blur
distracting. Pattern: add a Settings toggle that writes
`document.documentElement.dataset.glassEffects = 'off' | 'on'`, gate
the existing sidebar glass CSS rules on `:not([data-glass-effects=off])`.
### 15.5 Phase 5 candidate list (expanded)
Adding to atlas §14.6:
11. Variable-font hero typography (Geist axes) — applied in this commit
12. `[data-gradual-blur-layer]` overlay primitive — ~10 lines of CSS,
one CSS rule + apply attribute on sticky-header containers
13. `[data-glass-effects]` opt-out toggle — small Settings JSX + CSS
pattern gating
14. Login surface (Maka has no auth; future surface if we add it)
+472
View File
@@ -0,0 +1,472 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Reference Settings — full reverse-engineer (2026-06-23)
> Archived on 2026-07-13. This reverse-engineering snapshot is provenance, not the current Settings contract.
Cross-checked by 4 parallel Explore agents on the extracted bundle at
`/tmp/qoder-asar/out/renderer/assets/`.
This note captures the reference's Settings module so future Maka PRs can
align without re-doing the dig. Naming hygiene applies: do not surface
the reference product name in code, commits, or end-user copy.
---
## 1. Shell shape
Settings is a **full-pane modal** that takes over the workspace area. It
is NOT a small overlay sheet.
- `role="dialog"` + `aria-modal="true"`
- Marker attributes: `data-modal="agents-settings"`,
`data-canvas-dialog="true"`, `data-settings-layout="page"`
- Outer shell:
```
flex h-full min-h-0 w-full flex-col overflow-hidden bg-background select-none
```
- The rest of the chat surface stays visible behind a subtle backdrop;
the modal does not float over a blurred mask — it occupies the canvas.
### Two-rail layout
| Region | Width / sizing | Notes |
|----------|--------------------------------------|------------------------------------------|
| Nav rail | `w-[256px] shrink-0` | Left side, `bg-transparent`, full height |
| Content | `flex-1 min-w-0 overflow-hidden` | Right side, scrolls on the inner column |
| Gap | `var(--agents-content-area-gap)` 4px | Margin between rail and content plate |
Content pane class: `agents-settings-wide-content`:
```css
.agents-settings-wide-content {
background: var(--agents-content-area-bg);
margin: var(--agents-content-area-gap)
var(--agents-content-area-gap)
var(--agents-content-area-gap) 0;
border-radius: var(--agents-content-area-radius); /* 6px */
}
```
Content body inner wrapper (where the page lives):
```
mx-auto min-h-[200px] max-w-3xl pb-16 pt-10
```
`max-w-3xl` = 48rem / 768px. That's the optical reading column for any
settings page body.
### Responsive
| Breakpoint | Behavior |
|------------|-------------------------------------------------------|
| ≥ 1200px | Two-rail (256px rail + content) |
| 900–1199 | Two-rail, tightened |
| < 900px | Compact: rail collapses; header shows back-arrow + title |
---
## 2. Nav rail
The rail is a flat vertical list (no group headings — purely flat).
### Items shown to the end user (in order)
1. `preferences` — **Preferences** (the big aggregator)
2. `profile` — Profile / account
3. `system` — **System** (intentionally near-empty placeholder)
4. `keyboard` — Keyboard shortcuts
5. `vm` — Secure workspace / VM mode
6. `experimental` — Experimental flags
7. `permissions` — System capability toggles
8. `connector` — Integrations / connectors
9. `appshot` — App snapshot (macOS only, gated by `platforms: ["darwin"]`)
### Hidden / dev-only tabs
`appearance` (handled via the Preferences page itself), `privacy`,
`debug`, `hiddenConnector`, `awareness`, `appUpdate`, `models`,
`customModels`, `commands`, `agents`, `worktrees`, `quickpick`,
`voiceInput`, `wechat`, `beta`, `channels`, `legokit`, `archived` — all
`exposed: false`.
Tab registry lives in `index-C02cm0ok.js` around offset ~213400 (look
for `Su=[]`).
### Nav-row recipe
```html
<!-- inactive -->
<button class="squircle-md text-text-secondary/80
hover:bg-[var(--settings-nav-row-selected-bg)]
hover:text-text/80">
<Icon size=… />
<span>Label</span>
</button>
<!-- active -->
<button class="squircle-md
bg-[var(--settings-nav-row-selected-bg)]
text-text/80">
…
</button>
```
- Icon library: Lucide
- Active state is a **fill** (not border / left-bar), with
`--settings-nav-row-selected-bg` = `var(--color-fill-secondary)`
- Typography: text-sm (14px), normal weight at rest; no weight bump on
active (color/background carry the state)
- Padding (educated from sibling rows): `px-4 py-3`
- Gap between glyph + label: `gap-3`
- Border radius: `squircle-md` (custom corner, not pure rounded-md)
---
## 3. Per-tab content inventory
### 3.1 Preferences (the kitchen sink)
Globals offset ~76380–80000. This single tab carries the bulk of the
end-user controls:
| Control | Type | Notes |
|----------------------------------|-----------------|-------------------------------------------------------------|
| Language | select | UI locale |
| Extended Thinking | switch | Bigger reasoning budget, disables streaming |
| Launch at Login (autoLaunch) | switch | Auto-start |
| Close Window Behavior | radio / select | Ask / Minimize-to-tray / Quit |
| Desktop Notifications | switch | Native OS notifications |
| Sound Notifications | switch | Audio on task completion |
| Include Co-Authored-By | switch | Adds `Co-authored-by:` trailer to git commits |
| Quick Switch (Ctrl/Cmd+Tab) | radio | Workspaces vs Agents |
| Auto-advance after archive | radio | Next / Prev / Close |
| Default Mode (new agent) | radio | Agent / Plan |
| Preview Mode (artifactPreview) | radio | New window / Right sidebar |
| Expand tool calls by default | switch | Tool-block collapsed vs expanded |
| Show tool steps in IM channels | switch | Whether bot-reply surfaces include tool-call sub-steps |
| Tool execution limit | number input | Soft pause when one turn exceeds N tool calls |
| Keep System Awake (preventSleep) | switch | While Agent works |
| Network Proxy | proxy block | System / Manual; URL input + test button + scheme validation |
| Chat Settings (sub-section) | composite | Theme brightness, interface style, glass effects, font face, font size, chat width |
**Key insight**: reference does NOT split "system-level" toggles
(autoLaunch / preventSleep / notifications / proxy / closeWindowAction)
into a separate System tab. They live alongside chat/agent toggles in
ONE long Preferences page.
### 3.2 System
Title only. No fields. Placeholder for future use. The page exists in
the nav but its body is intentionally empty / "to be defined." Confirmed
by walking the bundle.
### 3.3 Profile
Two sections:
- **Avatar**: glyph (first char or emoji) + background color (preset
palette of ~11 tones)
- **Account & Subscription**: email (copy), Account UID (copy),
product UUID (copy), subscription tier (read-only), pricing-page link,
logout
### 3.4 Appearance (`appearance` i18n key — actually rendered inside
Preferences in production)
| Control | Type | Options |
|--------------------------|----------|-----------------------------------------------------------------|
| Interface theme | select | System pref / Light / Dark / Light Glass / Light Clear (win) / Dark Glass / Dark Clear (win) / Classic Dark / Parchment Light / Parchment Dark / From editors |
| Light variant | select | Per-mode theme |
| Dark variant | select | Per-mode theme |
| Workspace icon in sidebar| switch | Project glyph in the sidebar workspace list |
| Always expand to-do list | switch | Full vs compact to-do view |
### 3.5 Keyboard
- Search field across all shortcuts
- Per-action row with primary + alternative binding
- "Reset all to defaults"
- Categories: General / Chats (workspaces) / Agents
- General: Show shortcuts / Open settings / Toggle sidebar / Undo archive
- Chats: Quick switch
- Agents: Create new task / Search all tasks / Search in current task /
Prev tab / Next tab / Focus input / Toggle input focus / Send / New line
### 3.6 VM (Secure workspace)
Brief description: "dedicated space on your computer for running tasks
— faster, more reliable, on-device." Enable/disable toggle.
### 3.7 Experimental
Beta feature toggles. Per-flag rows; roster managed in the renderer
component, not visible as a literal array in the bundle.
### 3.8 Permissions
"System permissions QoderWork needs on this Mac." Capability rows
(filesystem / network / mic / screen recording / accessibility) with
"granted / not granted" status and a "request" CTA.
### 3.9 Connector
Integrations with external apps. Builtin connectors / market connectors
/ custom connectors. Each row: brand glyph, status, enable toggle,
"configure" button.
### 3.10 App Snapshot
macOS-only. Frontmost-app screenshot capture for chat context. Needs
Accessibility + Screen Recording perms. Permission status row + capture
preview.
---
## 4. Repeating visual patterns inside a page
Reference does NOT use bordered card containers around groups of
controls. The default presentation is a **flat row list inside the
`max-w-3xl` column**.
### 4.0 Content-column padding — WAWQAQ msg `2c810f2d` 2026-06-23
WAWQAQ called out the left/right whitespace as visibly wider in
reference than in Maka. Concrete numbers from a second RE pass:
- The content column is `mx-auto max-w-3xl pt-10 pb-16` (= 768px wide,
40px top, 64px bottom). No explicit `px-N` is applied inside the
column — the side whitespace comes from the column NOT filling the
right pane.
- At the typical desktop window width (~1280px viewport, 256px nav
rail), the right pane is ~1024px. A 768px centered column leaves
~128px of whitespace on each side. That's the "wider padding" effect.
- Inside each row, padding is `px-5 py-4` (20px / 16px). Rows do NOT
carry their own border, background plate, or shadow — separation is
pure spacing.
**Maka delta found 2026-06-23**:
- `.settingsStructuredPage` had `gap: 16px` but NO `max-width` / `margin: 0 auto` — rows stretched to fill the right pane. Fixed to `max-width: 768px; margin: 0 auto; padding: 40px 24px 64px`.
- `.settingsFormRow` had `border-bottom: 1px solid var(--border)` and `padding: 6px 0` — both wrong. Reference uses spacing-only and `px-5 py-4`. Fixed to `padding: 16px 20px`, no border-bottom.
- `.settingsRow` was sharing `.providerCard` chrome (bordered, tinted, lifted). Provider cards keep their chrome (they ARE cards); `.settingsRow` flattened to a flex row.
- Label was 12px / 600 weight; bumped to 14px / 500. Sub-text 11px → 12px. Matches reference `text-sm font-medium` + `text-xs text-text-quaternary`.
### 4.1 Row
```
flex items-center gap-3 px-4 py-3 /* group */
├── label column
│ ├── truncate text-sm font-medium text-text (title)
│ └── mt-0.5 truncate text-xs text-text-quaternary (hint)
└── control (right-aligned)
```
- No border / no card background per row by default
- Visual separation between rows is achieved by spacing alone (no
border-bottom)
- Hover state moves via the parent `.group` (e.g., shows a kebab menu)
### 4.2 Sub-section heading inside a tab
```
text-sm font-semibold text-text
mb-4
```
### 4.3 Switch
- ~20×20px wrapper, `rounded-md`
- On: `bg-primary/20 ring-1 ring-primary/40 scale-[0.97]`
- Off: subtle gray fill (`bg-gray-100 dark:bg-neutral-600`) with inset shadow
- Transition: `transition-all duration-200`
### 4.4 Input
```
w-full px-3 py-2 text-sm bg-background border rounded-md
border-border-tertiary/80
focus:outline-none focus:ring-2 focus:ring-primary/50
```
- Error: `border-error-border`
- Password / icon suffix: `pr-9`
### 4.5 Button
- Primary: `bg-primary text-primary-foreground`
- Secondary: `bg-muted text-foreground`
- Outline: `border border-border text-foreground hover:bg-muted`
- Destructive: `text-error hover:bg-error-bg`
- Icon-only sizes: `size-7 rounded-md` (28px square), `size-8 rounded-full` (32 round)
### 4.6 Chip / badge
```
rounded bg-fill-tertiary px-1.5 py-0.5 text-[10px] font-medium text-text-tertiary
```
### 4.7 Content scroll
The page body owns its scroll:
```
min-h-0 flex-1 overflow-y-auto scrollbar-none
```
Custom scrollbar is hidden. The inner content column has
`pb-16` bottom padding so the last row never hugs the modal edge.
---
## 5. Tokens used inside Settings
| Token | Value |
|----------------------------------|----------------------------------------------------------------|
| `--agents-content-area-bg` | `var(--color-bg-container)` |
| `--agents-content-area-gap` | `4px` |
| `--agents-content-area-radius` | `12px` |
| `--agents-layout-bg` | `var(--surface-canvas)` (flat neutral shell — **DO NOT** paint a 172deg gradient here, see warning below) |
| `--settings-nav-row-selected-bg` | `var(--color-fill-secondary)` |
> ⚠️ **DO NOT paint a 172deg gradient on the layout/shell background.**
>
> The RE notes here historically said `--agents-layout-bg` is
> `linear-gradient(172deg, var(--color-fill-tertiary) 19.61%, var(--color-bg-container) 81.35%)`. That value was an **artifact of the bundle CSS we copied**, not what the live reference product actually paints.
> WAWQAQ has called this out four times in a row (msgs `1e693dee` /
> `5d3b10e5` / `486b5611` / `4a1b8c13`) — "谁让你他妈的用渐变的啊?
> 参考实现就没有啊". Every time we re-added the gradient because
> some atlas/RE note claimed it was canon.
>
> Source-side enforcement: `apps/desktop/src/main/__tests__/chat-chrome-no-gradient-contract.test.ts` fails if any of `.appFrame`, `html[data-os="darwin"] .maka-session-panel`, or the floating panel reintroduces a gradient / border-right / smaller-than-12px radius. If a future RE iteration finds a *new* place where the reference product genuinely does paint a gradient, please update the contract test with the new exception in the same PR, don't silently lift the assertion.
>
> Use a **flat neutral shell** (`var(--surface-canvas)`) behind the
> white content surface. Do not collapse the shell to the same white
> as the content card: that removes the visible lower radius and lets
> the bug come back even without a border or gradient.
>
> Rendered-pixel enforcement: after capturing
> `sidebar-long-sessions/light-1280-motion`, run
> `npm --workspace @maka/desktop run screenshots:chat-chrome:check`.
> It verifies the actual PNG has a visible shell/content delta, no dark
> one-pixel seam, and readable bottom surface corners.
>
> Surface radius is **12px** (was historically noted as 6px). 6px was
> geometrically there but optically invisible — bottom corners of the
> floating content card didn't read against a same-color shell.
Color tokens consumed inside settings:
- `--color-text` (primary text)
- `--color-text-secondary`
- `--color-text-tertiary` (hints)
- `--color-text-quaternary` (de-emphasized)
- `--color-border` (strong divider)
- `--color-border-tertiary` (input borders)
- `--color-fill-secondary` (active row fill)
- `--color-fill-tertiary` (chip background)
- `--color-bg-container` (page surface)
- `--color-primary`, `--color-primary/20`, `--color-primary/40` (focus, switch on, ring)
---
## 6. Routing, state, behaviors
- Active tab is stored in the URL: `?view=settings&tab=preferences`.
No localStorage for the active tab — round-tripping the URL restores
the page.
- Deeplink scheme: external triggers can deep-link to a specific tab:
`qoder-work://settings/preferences`. Maka's equivalent could be
`maka://settings/<tab>`.
- Default tab when opened with no `tab` param: `preferences`.
- Modal close is escape-key + click-outside backdrop; focus is trapped
inside `[role="dialog"]`.
- Telemetry: `settings_tab_view` fires when a tab is selected;
`settings_heartbeat` ticks every 300s with a feature-flag snapshot.
- Saves: per-control, no explicit batch / "save" button visible.
Renderer dispatches IPC writes per change; toasts surface failures.
---
## 7. Implications for Maka
This RE matters because WAWQAQ's ask — "add a 系统设置 section" — is
ALMOST CORRECT but the deeper truth from the reference is:
**Reference does NOT scatter system-level toggles into their own
narrow pages.** It collapses them into ONE long `Preferences` page,
and reserves a near-empty `system` tab for future expansion.
Maka today has 17 nav items in Settings (some of them — `网络`,
`健康`, `开放网关`, `语音模型`, `每日回顾`, `记忆` — are single-purpose
pages that mirror exactly the "wasteful single-purpose page" pattern
WAWQAQ flagged for `网络`).
The closer-to-reference move is to **consolidate**, not add another
section. Concrete proposal:
### Proposed consolidated Maka Settings nav (target: ~9 items)
| Group | Item | Holds |
|-------------|-------------|------------------------------------------------------------|
| 基础 | 通用 | Aggregator: 隐身模式, 启动行为, 新对话模式, 默认模型, 通知, 网络代理, 保持唤醒, 自动更新, 关闭窗口行为 |
| 基础 | 外观 | 个性化 + theme + palette + font (merge 主题 + 个性化) |
| AI | 模型 | Provider connections + OAuth + per-model config |
| AI | 记忆与回顾 | Merge `记忆` + `每日回顾` |
| AI | 语音与网关 | Merge `语音模型` + `开放网关` |
| 集成 | 机器人对话 | Telegram / WeChat / Slack bot config |
| 集成 | 联网搜索 | Tavily provider config |
| 数据/系统 | 数据 | Export / import / wipe |
| 数据/系统 | 权限与能力 | Capability toggles + filesystem allowlist |
| 其他 | 健康 | Diagnostic / status / logs |
| 其他 | 关于 | Version + license + acknowledgements |
Specifically: **drop the standalone `网络` page; move proxy into 通用**.
That's the minimum WAWQAQ asked for, but ride the same PR to merge
`主题 + 个性化` into `外观` per reference, since 外观 is reference's
canonical name for that grouping.
### Settings page-body recipe to copy
For ANY settings page body, switch to:
- `max-w-3xl mx-auto pt-10 pb-16` inner column
- Flat row list (`flex items-center gap-3 px-4 py-3` per row)
- `text-sm font-medium` title + `text-xs text-text-quaternary` hint
- No bordered card around row groups
- Sub-section heading: `text-sm font-semibold mb-4`
- Switches / inputs / chips per §4 above
This is the visual "reference 真好看" pattern WAWQAQ asked us to study —
the page is uncluttered because rows have NO chrome, only spacing.
---
## 8. PR plan suggestion
Two PRs, ship in sequence:
**PR-SETTINGS-ALIGN-IA-0** — Information architecture consolidation
- Drop standalone `网络` page; fold proxy into a "网络代理" sub-section of 通用
- Merge `主题 + 个性化` → `外观`
- Merge `记忆 + 每日回顾` → `记忆与回顾`
- Merge `语音模型 + 开放网关` → `语音与网关`
- Update SETTINGS_NAV + section render switch + all deeplinks (`maka://settings/...`)
- Pin via contract test: nav id allowlist + deeplink mapping
**PR-SETTINGS-PAGE-BODY-0** — Visual pattern conversion
- Convert every page body from `settingsStructuredPage / SettingRow`
card chrome to the flat-row + `max-w-3xl` recipe
- Add `.maka-settings-row`, `.maka-settings-page` primitives in styles.css
- Sweep all SettingsPage components to use them
Tackle them in this order so the IA stops shifting while we're polishing
the bodies.
+298
View File
@@ -0,0 +1,298 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Runtime Kernel Extraction
> Archived on 2026-07-13. This is a point-in-time extraction record, not current architecture authority. Start with `ARCHITECTURE.md`.
This document explains the runtime-kernel work in this change set: what changed,
why it was needed, what stayed stable, and how it was verified.
## Summary
Maka already had the pieces of a local desktop coding agent: sessions, model
streams, tool calls, permission prompts, abort handling, usage telemetry, bot and
gateway entry points, and persisted session messages. The problem was that the
core execution responsibilities were still concentrated in a few large runtime
paths, especially `AiSdkBackend` and `SessionManager.sendMessage()`.
This change set keeps the product surfaces stable but introduces clearer
internal runtime boundaries:
```text
SessionManager
-> AgentRun
-> AiSdkBackend
-> ModelAdapter
-> ToolRuntime
-> RunTrace
-> AgentRunStore
```
The intent is not to rewrite the runtime or replace the Vercel AI SDK. The goal
is to make the existing runtime easier to reason about, easier to recover after
interruption, and easier to extend with future backends or workflow integrations.
## What Changed
### ToolRuntime
`ToolRuntime` is now the internal boundary for the lifecycle of model-requested
tools. The extracted runtime owns the work that used to be interleaved inside the
AI SDK backend:
- validate tool input before execution
- evaluate permission policy
- wait for parked permission decisions with timeout behavior
- pause and resume the stream watchdog around tool execution
- propagate abort signals into tools
- classify tool failures
- record tool telemetry and artifacts
- emit tool and permission trace events
`AiSdkBackend` still bridges model stream behavior, but it no longer needs to own
every detail of tool execution.
### ModelAdapter
`ModelAdapter` is the provider-facing stream and error normalization layer. It
keeps AI SDK-specific stream chunks, provider setup, usage normalization, and
provider error mapping out of the higher-level backend orchestration shell.
This makes the boundary explicit:
```text
provider / AI SDK details
-> ModelAdapter
-> Maka runtime events and usage records
```
That separation matters because future providers should not need to duplicate
permission, tool, run, or session-state behavior.
### RunTrace
`RunTrace` is an internal best-effort trace path for runtime events. It records
milestones such as:
- turn started
- model resolved / stream started / stream completed / stream failed
- tool started / completed / failed
- permission requested / decided / failed
- usage recorded
- abort requested
Trace recorder failures are intentionally non-fatal. A failed trace write must
not alter model or tool execution.
### AgentRun Types and Store
The core package now defines internal AgentRun contracts:
- `AgentRunHeader`
- `AgentRunEvent`
- `AgentRunStatus`
- `AgentRunStore`
The storage package adds a file-backed run store:
```text
sessions/<sessionId>/runs/<runId>/run.json
sessions/<sessionId>/runs/<runId>/events.jsonl
```
The store provides:
- atomic `run.json` writes
- append-only run event JSONL
- same-run write serialization
- session run listing
- corrupt committed event-line recovery via `event_corrupt`
- malformed unterminated tail tolerance
This ledger is separate from the existing session message JSONL so runtime
diagnostics and recovery state do not pollute user-visible conversation history.
### AgentRun Execution
`SessionManager.sendMessage()` now delegates the heavy turn lifecycle to
internal `AgentRun.execute()`.
`AgentRun` owns:
- generating and recording run identity
- appending the user message
- writing initial turn state
- locking the connection snapshot for the run
- ensuring the active backend exists
- driving backend stream events
- projecting session status changes
- writing turn completion, failure, abort, or permission-wait states
- recording run started/completed/failed/cancelled status
- routing `RunTrace` events into the durable run ledger
- preserving abort source diagnostics such as `renderer.stop_button`
`SessionManager` remains the public runtime API and continues to own session
CRUD, backend registry orchestration, active run lookup, and legacy recovery
entry points.
### Active Run Registry
The old active stream counters and turn-id side maps were replaced with:
```ts
activeRuns: Map<string, AgentRun>
turnToRunId: Map<string, string>
```
This makes overlapping run behavior explicit. It also avoids masking active
stream accounting bugs with defensive counter clamping.
### Startup Recovery
`recoverInterruptedSessions()` now prefers the AgentRun ledger when run rows are
available. It scans persisted run headers and events, classifies stale
non-terminal runs, repairs them, and then converges the existing session/turn
projection.
Recovered cases include:
- stale `created` or `running` runs
- runs whose last event is `run_started` or `model_stream_started`
- stale tool tails after `tool_started`
- stale permission waits after `permission_requested`
- `model_stream_completed` without a terminal run event
- corrupt run event lines represented as `event_corrupt`
Legacy sessions without run ledger rows still use the prior message and
turn-state recovery path.
## What Stayed Stable
This work intentionally does not change:
- `window.maka.*` preload API
- Electron IPC channel names
- renderer-visible `SessionEvent` behavior
- user-visible permission modes
- session message JSONL compatibility
- builtin tool names or public tool behavior
- model provider settings UI
- bot and OpenGateway public entry points
- Rive workflow integration semantics
The runtime kernel is internal. Public behavior should remain compatible while
the internals become more explicit and recoverable.
## Why This Was Needed
Before this work, a single turn execution was spread across session management,
backend stream handling, tool wrapping, permission policy, telemetry, and abort
logic. That made several questions difficult to answer after a failure:
- Did the model stream start?
- Which backend/model/connection did the turn use?
- Was the run waiting for permission?
- Which tool was running when the app exited?
- Did the user press stop?
- Was usage recorded?
- Did a stale session status reflect a real active run or a crashed process?
The AgentRun ledger gives the runtime a durable fact record for those answers.
ToolRuntime and ModelAdapter then reduce the amount of model/tool/provider logic
that has to be understood at once.
## Recovery Model
The new recovery path is conservative. It does not replay model streams or tools.
Instead, it repairs stale state into deterministic terminal states so the app
does not reopen with sessions permanently stuck in `running` or
`waiting_for_user`.
When a stale non-terminal run is recovered, the runtime:
1. Updates the AgentRun header to terminal state.
2. Appends a durable recovery event such as `run_failed` or `run_completed`.
3. Writes the existing session `turn_state` projection.
4. Updates the session header out of active running/waiting states.
The failure class for app-restart recovery is recorded as `app_restarted`.
Diagnostics are limited to small reason-code fields such as `recovered`,
`failureClass`, `recoveryReason`, `lastEventType`, and `eventCorrupt`; raw user
text and raw event payloads are not copied into recovery diagnostics.
## Files Added
- `packages/runtime/src/tool-runtime.ts`
- `packages/runtime/src/model-adapter.ts`
- `packages/runtime/src/run-trace.ts`
- `packages/core/src/agent-run.ts`
- `packages/storage/src/agent-run-store.ts`
- `packages/runtime/src/agent-run.ts`
- `packages/runtime/src/agent-run-recovery.ts`
## Tests Added or Expanded
The runtime test suite now covers:
- ToolRuntime permission allow/block/prompt/timeout paths
- watchdog pause/resume behavior around tools
- tool abort and failure classification behavior
- tool telemetry and artifact recording behavior
- ModelAdapter stream, usage, and provider error normalization
- RunTrace recording and best-effort failure behavior
- overlapping AgentRuns
- backend build failure after user-message append
- permission handoff without stuck run state
- stop button abort source preservation
- late complete after stop not overwriting aborted state
- durable trace redaction
- AgentRunStore create/read/update/list behavior
- same-run AgentRun event append serialization
- corrupt AgentRun event JSONL recovery
- startup recovery from stale AgentRun ledger states
- legacy startup recovery fallback
- terminal AgentRun recovery idempotency
## Verification
The changes were verified with:
```sh
npm --workspace @maka/core run typecheck
npm --workspace @maka/storage run test
npm --workspace @maka/runtime run typecheck
npm --workspace @maka/runtime run test
npm --workspace @maka/desktop run build:main
git diff --check
```
The final runtime suite included 315 passing tests after the AgentRun recovery
work.
## Follow-Up Work
This PR establishes the internal runtime-kernel shape, but it does not finish
every possible cleanup. Good follow-up slices are:
- merge usage and cost accounting more tightly with the run ledger
- expose an internal run-inspection/debug read model
- reduce the remaining `SessionManager` hook surface used by `AgentRun`
- consider Gateway/Bot policy unification through the same run policy layer
- consider Rive workflow/run mapping only after there is a product need
@@ -0,0 +1,353 @@
---
title: Maka Runtime 主线教学手册
document: MAKA_RUNTIME_MAINLINE_TEACHING_MANUAL
version: 1.0
status: architecture teaching manual
date: 2026-06-24
audience: "第一次理解 Maka runtime 的工程师、后续 runtime 架构维护者"
scope: "用设计动机和心智模型解释当前 Maka runtime 为什么分层、每层解决什么问题、少一层会怎样;源码入口只作为文末简要索引。"
primary_spine:
- SessionManager
- RuntimeKernel
- AgentRun
- RuntimeRunner
- AiSdkFlow
- AiSdkBackend
- ModelAdapter
- ToolRuntime
---
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
> Archived on 2026-07-13. The Runtime discussion is historical; the Headless architecture it describes was retired by #2598. Start with `ARCHITECTURE.md`.
# Maka Runtime 主线教学手册
Maka runtime 的核心设计,不是把一条用户消息直接送给模型,而是把“一次对话回合”拆成可展示、可运行、可追溯、可恢复的几类事实。
如果只用一句话概括:
> Maka 用 `SessionManager -> RuntimeKernel -> AgentRun -> RuntimeRunner -> AiSdkFlow -> AiSdkBackend` 这条主线,把“用户看到的一轮对话”和“系统内部一次可记录的模型/工具运行”分开管理。
这句话里的重点不是类名,而是分工。用户看到的是一条消息进去,一串文本、工具调用、权限请求、完成状态出来。系统内部看到的不是一件事,而是至少三件事:这条消息属于哪个 session;这次运行是不是一个独立的 run;这次 run 产生了哪些可以以后 replay、检查、恢复或投影回 UI 的 runtime facts。
如果把这些事都塞进一个大函数,短期能跑,长期会很难回答几个问题:为什么 UI 还要展示旧格式事件?为什么模型历史不能只靠聊天记录字符串?为什么 headless、autonomous、Harbor 不能都各自重写一套 agent loop?为什么未来做 checkpoint / warm retry 时,不能只保存最后一段文本?
这篇手册尝试用一个新工程师能顺着读下去的方式解释这些问题。
## Runtime 到底在解决什么问题
Maka runtime 解决的不是“怎么调用一次 LLM API”这么窄的问题。如果只是调用模型,代码可以很简单:拿用户输入,拼历史,调用 provider,流式返回文本。但 Maka 要处理的是 agent 产品里的完整运行问题。
一个用户 turn 可能会产生模型文本、thinking、工具调用、工具结果、权限请求、token usage、错误、取消、完成状态。它还可能运行在桌面交互、headless benchmark、autonomous task、Harbor container 这些不同外壳里。
这些场景有共同的核心:都需要模型/工具循环。它们也有不同的外层责任:桌面关心 UI stream,headless 关心 workspace 和 verifier,autonomous 关心 attempt/retry,Harbor 关心 container 和 benchmark reward。Maka runtime 的分层,就是为了让“共同核心”可以复用,同时让“外层责任”不要污染模型循环。
这也是为什么当前主线看起来比普通聊天应用复杂:
`SessionManager -> RuntimeKernel -> AgentRun -> RuntimeRunner -> AiSdkFlow -> AiSdkBackend -> ModelAdapter/ToolRuntime`
每一层都在回答一个不同问题。`SessionManager` 回答外部调用者怎么发起一轮对话;`RuntimeKernel` 回答这轮对话怎么被组织成一次 runtime 执行;`AgentRun` 回答这次执行在持久世界里是谁、状态是什么、怎么收尾;`RuntimeRunner` 回答一次 invocation 的开始、结束、错误和 terminal 语义是什么;`AiSdkFlow` 回答旧 backend 产出的 UI 事件怎么变成新的 runtime facts;`AiSdkBackend` 回答真正的 provider request、stream、工具、权限、usage 怎么跑。
少掉任何一层,系统不是立刻不能工作,而是会把某类责任挤到错误的位置。
## 三个大心智模型
读 Maka runtime 前,最好先建立三个词:turn、run、event ledger。这三个词容易被混在一起,但它们不是同一件事。
### Turn:用户视角的一轮
turn 是用户和 UI 最容易理解的单位。用户发一句话,系统回复一轮,这就是一个 turn。从产品视角看,turn 很自然。用户不关心 run id,也不关心 invocation id。用户只会问:我刚才这句话的回答在哪里?这轮是不是还在跑?能不能停止?能不能重试?
所以 Maka 需要保留 session/turn 这套外部接口。如果没有 turn 这个心智模型,runtime 会很容易变成内部 run ledger 的展示工具,UI 和用户操作会被迫理解太多底层概念。
### Run:系统视角的一次执行
run 是系统内部更精确的单位。一轮用户 turn 需要被真正执行一次,这次执行要有自己的身份、开始时间、状态、backend/model/cwd/permission 信息、失败原因、完成状态。这就是 `AgentRun` 想表达的东西。
run 不是单纯的 message,也不是单纯的 assistant reply。它是“这次 agent 运行”的 envelope。如果没有 run,系统会很难回答:这次运行有没有开始?它是完成、失败、取消,还是进程崩了之后留下的 stale 状态?它用了哪个 backend 和 model?它产生的 runtime events 属于哪次运行?它以后能不能作为 replay 历史的一部分?
### Event Ledger:可重建的事实账本
event ledger 是 runtime 的长期方向。聊天记录是给人看的,但 runtime facts 是给系统重建历史用的。
例如,模型说了一段文本,这当然可以进入聊天记录。但模型还发起了一个 tool call,工具返回了结构化结果,中间出现了 permission decision,最后 invocation 以某种 terminal status 结束。这些都不是普通文本能完整表达的。
所以 Maka 逐渐把 runtime 事实写成 `RuntimeEvent`。这些事件以后可以被投影回 UI,也可以用于模型 replay,也可以帮助恢复、检查和调度。如果只有聊天记录字符串,很多语义会被压扁:工具调用变成一段文本,thinking 是否可 replay 不清楚,terminal 状态和 verifier 状态容易混在一起。
这就是为什么 runtime-event ledger 很重要。
## 一条消息进入 Maka 后发生了什么
现在可以回到主线:
`SessionManager -> RuntimeKernel -> AgentRun -> RuntimeRunner -> AiSdkFlow -> AiSdkBackend -> ModelAdapter / ToolRuntime`
这条链不只是 call graph。它更像一组逐渐收窄的责任边界:外层越靠近产品和 session,内层越靠近 provider、stream 和工具执行,中间几层负责把两边翻译成稳定契约。
## 第一层:SessionManager 稳住外部入口
`SessionManager` 是外部世界最应该认识的 runtime 门面。桌面 UI、generic headless runner、测试入口、上层调用者,都不应该一上来就知道 `AgentRun`、`RuntimeRunner`、`RuntimeEventStore` 的细节。它们需要的是一个稳定动作:给某个 session 发送一条消息。
所以 `SessionManager.sendMessage()` 的意义不是“这里写了很多 runtime 逻辑”。恰恰相反,它应该尽量薄。它把外部调用者维持在 session API 上,然后把真正的 turn orchestration 交给 `RuntimeKernel.startTurn()`。
如果少了这一层,上层调用者会直接依赖 runtime 内部结构。今天可能还能跑,明天只要 run ledger 或 runtime-event replay 改了,桌面、headless、Harbor 入口就可能一起被迫改。
`SessionManager` 的教学重点是:它不是 runtime brain,而是 runtime 的公共门面。一个好的门面会让外部 API 慢变,让内部实现快变。
## 第二层:RuntimeKernel 组织一轮运行
`RuntimeKernel` 开始进入内部世界。它要处理的不是 provider stream,而是这轮运行怎么被组织起来。
它知道当前 session 有没有 active backend,知道如何创建 `AgentRun`,知道 stop 和 permission response 应该转发给谁,也知道怎么把 child run、session status、active run 注册这些状态接起来。这类状态放在 backend 里不合适。backend 应该专心处理模型和工具,不应该管理“这个 session 是否已经有 active backend”或“这个 run 怎么注册到 kernel 的 active maps 里”。
这类状态也不应该放在 `SessionManager`。门面如果背太多状态,就会变成无法替换的巨大 runtime brain。所以 `RuntimeKernel` 是 orchestration boundary。它把一轮 turn 从“外部 API 调用”变成“内部可执行的 run”。
如果少了这一层,状态会向两边泄漏:上层要懂 backend lifecycle,backend 要懂 session/run orchestration。后面想换 flow、换 backend、做 child run 或统一 headless,就会更困难。
## 第三层:AgentRun 给一次执行套上 Durable Envelope
`AgentRun` 的名字容易让人误会,以为它负责“跑 agent”。更准确地说,它负责让一次 agent 运行在持久系统里有身份、有状态、有记录。
它在开始时确定 run id、session id、turn id、lineage。它创建 operational run record。对需要 session projection 的顶层 run,它还会写用户消息和 running turn state。它会构造 prior runtime context,确保后端能看到该看的历史。
运行过程中,它接收 legacy `SessionEvent` 和新的 `RuntimeEvent`,把它们写到正确的地方。结束时,它更新 session header,写 terminal turn state,注销 active run,并把 run 标成完成或失败。这层的核心是“生命周期”,不是“模型算法”。
如果少了 `AgentRun`,一次 turn 产生的各种事实会散落在 session store、backend stream、runtime event store、run status 更新里。系统还能输出文本,但很难可靠地恢复、审计、重试或投影。
一个新工程师可以把 `AgentRun` 理解成执行护照:这次运行是谁,从哪个 session 和 turn 来,运行到哪里了,结束时该留下什么记录,以后读历史时能不能信它。这个比喻只到这里为止。它不是业务对象的花名,而是 durable lifecycle boundary。
## 第四层:RuntimeRunner 定义 Invocation 语义
`RuntimeRunner` 是很多人第一次读时最容易跳过的一层,因为它看起来像“又包了一层 flow”。但它解决的是一个关键问题:不同 backend 或 flow 都可以产生事件,但一次 invocation 的基本语义必须一致。
例如,初始 user runtime event 应该什么时候出现?flow 产出的 events 怎样收集?没有 terminal event 算什么?abort、error、permission、finish reason 怎样归类?invocation result 应该怎样结构化返回?这些语义如果交给每个 backend 自己决定,未来就很难做统一 read model、checkpoint、调度、测试和恢复。
`RuntimeRunner` 因此是 backend-neutral invocation shell。它不直接写 storage,不关心 provider,不执行工具。它定义“一次 invocation 应该如何开始、如何遍历 flow、如何判断结束、如何归类结果”。
如果少了这一层,`AiSdkBackend` 可能直接 stream 给 UI,同时顺手定义 terminal 语义。短期少一层,长期会把 provider 细节和 runtime 语义绑死。这也是它对未来很重要的原因:checkpoint / warm retry 需要先知道 invocation 的边界在哪里。没有统一 invocation 边界,就很难说“恢复到哪一步”。
## 第五层:AiSdkFlow 做迁移适配
`AiSdkFlow` 不是新的模型 backend。它是一个 adapter。
当前 Maka 已经有 `AiSdkBackend` 能产生 renderer-facing `SessionEvent`。这些事件服务 UI 和旧 session projection 很久了,不能一下子全部丢掉。但新 runtime 需要 canonical `RuntimeEvent`。
`AiSdkFlow` 的工作就是把旧 backend stream 接入新 runtime 世界:它调用 `backend.send()`,把每个 `SessionEvent` 映射成 `RuntimeEvent`,同时继续让 legacy event 往外流。
这是一种迁移设计。它让系统不必一次性重写整个模型/工具循环,也能开始积累 runtime-event ledger。如果少了 `AiSdkFlow`,要么旧 backend 直接被迫产出新 runtime events,要么 runtime 只能继续依赖旧 UI event。这两种都不理想:前者迁移风险大,后者无法建立新的语义账本。
所以 `AiSdkFlow` 的价值是过渡而不是炫技。它承认旧世界还存在,同时把旧世界翻译给新世界。
## 第六层:AiSdkBackend 仍是模型和工具循环的大块核心
读到这里,不能误以为 runtime 已经把 AI SDK backend 抽空了。当前真正的 provider/model/tool loop 仍然主要在 `AiSdkBackend.send()` 里。
它要做很多事:解析 provider 和 model;决定本轮模型可见哪些工具;构造 prior messages 和当前 user message;选择 runtime-event replay、text-only replay 或 stored-message fallback;启动 AI SDK stream;处理 text、thinking、finish reason、usage;让 `ToolRuntime` 执行工具、处理权限、写 tool call/result;记录 request shape、usage、cost、telemetry。
这不是简单的“调用模型”。它更像一次 provider request 的总装线。
`ModelAdapter` 把 AI SDK 和 provider 差异包住,负责调用形状、stream chunk 归一化、usage 字段归一化。`ToolRuntime` 把工具执行和权限包住,负责 tool call、permission coroutine、tool result、异常归一化、输出 delta 和工具 telemetry。`ToolAvailabilityRuntime` 控制工具面,决定哪些工具对模型可见,并在执行边界继续防守。
但这些组件的顺序仍然由 `AiSdkBackend` 组织。如果过早把它拆碎,最危险的不是代码编译不过,而是改变模型看到的 request shape:工具 schema 顺序、history replay 策略、context budget、permission 事件、usage 归因都可能悄悄变化。
所以后续拆分应该先保护执行顺序,再谈文件大小。
## 三套存储为什么要分开
Maka runtime 当前最容易让新人困惑的地方,是为什么同时有 `SessionStore`、`AgentRunStore`、`RuntimeEventStore`。直觉上,一个系统不是应该只有一份聊天记录吗?问题在于,它们回答的问题不同。
### SessionStore:给 Session 和 UI 的投影
`SessionStore` 保存的是 session JSONL:第一行 session header,后面是 `StoredMessage`。它对 UI、旧 API、active run 展示、兼容读写都很重要。用户看到的消息、assistant 文本、tool rows、permission decision、token usage、turn state,都可以在这个世界里出现。
所以 `SessionStore` 不是已经被废弃。它仍然在写,仍然在读,仍然是迁移期重要的 compatibility projection。但它不适合作为唯一的语义来源,因为 `StoredMessage` 更像展示和兼容格式。它可以表达很多东西,但不是 runtime 的 canonical fact model。
如果只保留 `SessionStore`,后续 replay 会经常面对“这段展示消息背后到底是 tool call、tool result、permission 还是 terminal fact”的问题。
### AgentRunStore:给 Run 生命周期的账本
`AgentRunStore` 保存 run header 和 operational events。它关心的是一次 run 的生命周期:created、started、running、completed、failed、usage、permission、tool operational facts。
它适合回答:这个 run 是什么状态?它什么时候开始?它是否 stale?恢复时应该把它归为什么状态?它和哪个 session/turn/backend/model 相关?
这不是 UI transcript,也不是模型 replay 历史。如果没有 `AgentRunStore`,系统也许还能展示聊天,但很难可靠管理 run 状态。崩溃恢复、任务调度、超时归因、attempt 记录都会变得含糊。
### RuntimeEventStore:给语义 Replay 的账本
`RuntimeEventStore` 保存 canonical `RuntimeEvent`。这是最接近 runtime 真相的一层。
它记录 initial user event、model text/thinking、function call、function response、permission/token usage actions、terminal runtime facts。它的目标不是替代 UI,而是让系统以后能从语义事实重建历史。
这对模型 replay 尤其关键。文本 transcript 可以告诉模型“之前说了什么”。runtime events 还能告诉系统“之前调用过什么工具、工具返回是什么、thinking 是否可作为 provider-native 语义保留、terminal 状态是什么”。
如果没有 `RuntimeEventStore`,Maka 只能继续依赖 legacy projection。那会让 provider-native replay、read model、checkpoint high-water mark 都缺少可靠基础。
### 三者的关系
可以这样理解:`SessionStore` 是给人和旧系统看的投影,`AgentRunStore` 是给运行生命周期看的账本,`RuntimeEventStore` 是给语义 replay 和未来恢复看的账本。
三者都重要,只是权威性取决于你问的问题。问 UI 当前要展示什么,`SessionStore` 仍然重要。问 run 是否完成,`AgentRunStore` 更重要。问 completed run 的模型历史怎么 replay,`RuntimeEventStore` 是更好的起点。
当前 Maka 是迁移态,所以会双写、会 fallback、会比较 projection。这不是设计混乱,而是为了在不打断现有产品路径的情况下,把语义权威逐步迁到 runtime events。
## Runtime-event Replay 为什么不是简单重放日志
`RuntimeEventStore` 很重要,但不能被误解成“把 runtime-events.jsonl 从头到尾喂给模型就行”。Maka 的 replay 是有 gate 的。
它只把 prior top-level terminal runs 纳入默认 replay。child runs 默认不混进顶层 transcript。它要求 terminal prior run 有 runtime-event ledger,并且有 terminal runtime fact。它会丢弃 partial events,诊断 action-only facts、terminal facts、unsupported content、unmatched tool results 等情况。它会根据 provider 能力选择 provider-native replay、text-only replay 或 stored-message fallback。
这套 gate 的意义是:不要把不完整或 provider 不支持的语义假装成完整历史。例如,tool call/result 如果能以 provider-native 形式 replay,语义最好;如果 provider 不支持,那就不能盲目把它塞成普通文本还假装等价。系统可以 fallback,但应该知道自己损失了语义。
所以 runtime-event replay 的目标不是“永远不用 StoredMessage”。它的目标是:有可靠语义时用语义;语义不完整或不支持时,明确降级。
## Headless、Autonomous、Harbor 为什么在 Runtime 外面
理解完核心 runtime,再看外层会清楚很多。headless、autonomous、Harbor 都不是另一套 runtime。它们是在不同运行环境里包住 runtime。
### Generic Headless:最薄的外壳
generic headless runner 基本复用交互式路径。它准备 workspace,创建 `SessionManager`,创建 session,然后 drain `sendMessage()`。
runtime 完成之后,它才进入 headless 自己的世界:freeze workspace、准备 scoring workspace、跑 verifier、调用 scorer。这说明 verifier/scorer 不是 runtime terminal。runtime terminal 只说明模型/工具 invocation 怎么结束。benchmark pass/fail 是 runtime 之后,基于 workspace 和 verifier 计算出来的结果。
如果把 verifier 塞进 runtime,runtime 就会变得只服务 benchmark,而不是通用 agent execution。
### Task Controller:复用核心组件,但有自己的任务账本
task controller 更复杂。它要记录 task-run、attempt、workspace lease、isolation facts、permission grants、runtime feedback、verifier result、score result。这些都不是普通 interactive session 的责任。
所以它没有简单走 `SessionManager` facade,而是手动组装 `AgentRun -> AiSdkFlow -> RuntimeRunner` 这条核心链。这带来一点重复:`runRuntimeAttempt()` 和 `RuntimeKernel.runAgentTurn()` 很像。
但重复背后有原因。task path 需要非 streaming attempt result,需要 task-run WAL,需要 isolated backend lifecycle,需要 verifier/scorer 边界。这不是说重复永远应该保留。未来可以抽一个 shared turn runner。
教学上要先理解:task controller 不是在重新实现模型循环,它是在 runtime 周围加任务评测语义。
### Autonomous:多 Attempt 策略,不是 Invocation Resume
autonomous loop 又往外一层。它关心 attempts、预算、feedback、自检、verifier feedback、retry/continue decision。每个 attempt 调一次 `runTaskOnce()`。
如果要继续或重试,它会开始新的 attempt,并用 instruction override 或 feedback prompt 把上轮结果带过去。这不是 same-session checkpoint resume,也不是把中断的 `RuntimeRunner` 从半路接起来。它是 task-level retry/continue。
如果把 autonomous retry 误写成 invocation resume,后续 checkpoint 设计会站在错误基础上。
### Harbor:Benchmark / Container 外壳
Harbor 更外层。host-side runner 负责生成 Harbor job config、启动 `harbor run`、收集 reward 和 artifacts。
Python `MakaAgent` 是 Harbor installed agent。它在 Harbor 环境里拿到任务指令,必要时启动 host-side Node cell,并把模型可见的工具执行通过 HTTP executor 转回 Harbor task container。
真正的 Maka runtime session 在 Node cell 里创建。cell 创建 stores、backend registry、`SessionManager`、session,然后 drain `sendMessage()`,最后写 `runtime-events.jsonl` 和 `maka-cell-output.json`。
Harbor 因此不是 runtime backend。它是 benchmark/container/tool-execution boundary。这个边界很重要:LLM key 可以留在 host,工具命令可以在 container 里执行,runtime events 可以作为 cell artifact 出来,verifier reward 由 Harbor 产生。
如果把 Harbor 当成 runtime backend,就会混淆 credential、container、tool executor、runtime session、verifier reward 这几层责任。
## 为什么这套设计对未来 Checkpoint / Scheduling 有意义
当前 Maka 还没有把所有 checkpoint / warm retry 问题都解决。但现在的分层已经在为这些问题铺路。
未来如果一个长任务超时,系统至少要分清几个层级:invocation 是否有 terminal runtime fact;run header 是 completed、failed、cancelled,还是 stale;runtime-event ledger 写到了哪个 high-water mark;workspace 有没有可恢复 snapshot;外层 task attempt 是不是已经 verifier 过;Harbor container 和 reward 状态是否还存在。
这些问题不属于同一层。`RuntimeRunner` 帮你定义 invocation 边界。`AgentRunStore` 帮你定义 run lifecycle。`RuntimeEventStore` 帮你找到语义事件高水位。task controller 帮你定义 attempt、workspace、verifier/scorer。Harbor 帮你定义 container trial 和 benchmark artifacts。
如果没有这些分层,checkpoint 设计会很容易变成一句笼统的“保存上下文并重试”。但真正难的是保存哪种上下文。
只保存文本 transcript,可能丢 tool semantics。只保存 runtime events,可能缺 workspace snapshot。只保存 workspace snapshot,可能不知道模型已经看过什么、工具已经返回什么。只保存 task attempt,可能只能 cold retry,不能 warm resume。
所以未来设计要把 runtime high-water mark、run status、workspace snapshot、task/Harbor 状态配对起来。这篇手册不把 checkpoint 当成已完成能力来描述,只说明为什么当前 runtime 分层让这个问题有机会被严肃建模。
## 新人最容易误解的地方
第一个误解:`SessionManager` 是 runtime 的全部。更准确地说,`SessionManager` 是外部入口。它让调用者不用知道内部运行结构。
第二个误解:`AgentRun` 就是模型循环。`AgentRun` 是 durable run envelope。真正的模型/工具循环主要还在 `AiSdkBackend`。
第三个误解:`RuntimeRunner` 只是多包了一层。它定义 invocation 语义。没有它,terminal、error、abort、missing terminal 等语义会散在 backend 里。
第四个误解:`AiSdkFlow` 已经替代了 `AiSdkBackend`。没有。`AiSdkFlow` 是迁移 adapter,负责把 legacy `SessionEvent` 转成 canonical `RuntimeEvent`。
第五个误解:`SessionStore` 已经可以删。不能。active UI、兼容 projection、旧读写路径仍然依赖它。
第六个误解:runtime-events 可以无条件 replay。不能。partial、terminal fact、provider-native support、unsupported diagnostics 都要经过 gate。
第七个误解:autonomous retry 就是 checkpoint resume。当前更像新 attempt 加反馈 prompt,不是从同一个 invocation 中点恢复。
第八个误解:Harbor 是 Maka runtime 的一种 backend。不是。Harbor 是 benchmark/container/tool-execution 外壳;Maka runtime session 在 Node cell 里。
第九个误解:runtime 成功等于 benchmark 成功。runtime 成功只说明 invocation 完成。benchmark 还要看 workspace、verifier、scorer 和 reward。
第十个误解:以后拆 `AiSdkBackend` 只要按文件大小拆。不行。要先保护 provider request shape、tool visibility、history replay、permission path、usage/cost 归因这些顺序契约。
## 设计取舍和仍未完全回答的问题
这套设计的第一个代价是迁移期复杂。旧 `SessionEvent` / `StoredMessage` 不能立刻消失,新 `RuntimeEvent` 又必须开始承担 replay/read-model 语义。于是系统会同时写投影、run ledger、runtime ledger。改事件映射时,维护者要考虑 UI、session projection、run status、runtime replay、read model 多个面。
第二个代价是 `AiSdkBackend` 仍然很重。它承担了 provider resolution、tool availability、history replay、context budget、stream pump、tool execution、usage/cost、telemetry 等编排。这个模块未来值得拆,但拆分需要设计顺序依赖图。
第三个代价是 task controller 复制了一段 kernel turn shell。它这样做有任务评测上的理由,但长期看可能需要 shared turn runner。问题是这个 shared API 必须允许 task path 注入 task-run WAL、permission intervention、isolated backend lifecycle、非 streaming result 和 verifier/scorer 后处理。
第四个代价是 Harbor 横跨 host、container、runtime 三个边界。这对 benchmark isolation 有价值,但 timeout/cancel/tool executor failure/credential handling 的 taxonomy 会更复杂。未来 warm retry 要清楚区分 runtime failure、tool executor failure、Harbor infra failure、verifier failure。
第五个开放问题是 child run replay。当前默认 top-level transcript/read-model 排除 child runs,避免子代理内部细节污染主会话。未来如果要把子代理成果纳入主历史,可能需要显式 subagent summary 或 source-bearing replay policy,而不是直接混入所有 child runtime events。
第六个开放问题是 `SessionStore` 的长期地位。它可能逐渐变成 derived projection,也可能因为 UI/compatibility 需要长期保留。现在不能提前假设它已经过时。
## 最后总结
Maka runtime 当前的设计可以从一个简单问题开始理解:一条消息进入 Maka 后,为什么不直接丢给模型?
答案是:因为 Maka 需要的不只是模型回复,还需要 session 兼容展示、run 生命周期、invocation 语义、runtime-event replay、工具/权限执行、headless/task/Harbor 外壳,以及未来可恢复和可调度的事实边界。
`SessionManager` 稳住外部入口。`RuntimeKernel` 组织一轮运行。`AgentRun` 记录一次 durable run。`RuntimeRunner` 定义 invocation 语义。`AiSdkFlow` 把旧事件流翻译成新 runtime facts。`AiSdkBackend` 驱动模型和工具循环。
`SessionStore`、`AgentRunStore`、`RuntimeEventStore` 分别承担 session projection、run lifecycle、semantic replay ledger。headless、autonomous、Harbor 则在 runtime 外面处理 workspace、attempt、verifier、scorer、container、reward。
这套结构的价值,不是让调用链变长,而是让每一类事实有自己的归属。只有归属清楚,未来才可能安全地做 read model、checkpoint、warm retry、调度集成和 backend 拆分。
## 简要来源索引
相关文档:
- `docs/archive/runtime-kernel.md`
- `docs/archive/runtime-v2-architecture-evolution.md`
- `docs/archive/runtime-v2-implementation-notes.md`
少量源码证据入口:
- `packages/runtime/src/session-manager.ts`
- `packages/runtime/src/runtime-kernel.ts`
- `packages/runtime/src/agent-run.ts`
- `packages/runtime/src/runtime-runner.ts`
- `packages/runtime/src/ai-sdk-flow.ts`
- `packages/runtime/src/ai-sdk-backend.ts`
- `packages/runtime/src/model-adapter.ts`
- `packages/runtime/src/tool-runtime.ts`
- `packages/runtime/src/tool-availability.ts`
- `packages/runtime/src/model-history.ts`
- `packages/runtime/src/runtime-read-model.ts`
- `packages/storage/src/session-store.ts`
- `packages/storage/src/agent-run-store.ts`
- `packages/core/src/runtime-event.ts`
- `packages/headless/src/runner.ts`
- `packages/headless/src/task-agent-controller.ts`
- `packages/headless/src/autonomous-agent-loop.ts`
- `packages/headless/src/harbor-task-runner.ts`
- `packages/headless/src/harbor-cell.ts`
- `packages/headless/harbor/maka_agent.py`
@@ -0,0 +1,681 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Runtime v2 Architecture Evolution
> Archived on 2026-07-13. This proposal records the route to the current runtime; it is not current architecture authority. Start with `ARCHITECTURE.md`.
This document proposes a long-term evolution path for Maka's agent runtime. It
is written for developers who need to understand why the runtime needs another
architecture pass, what should stay stable, and how the codebase can move there
without a big-bang rewrite.
The goal is not to replace the Vercel AI SDK. The goal is to make Maka own its
runtime semantics while continuing to use the AI SDK as the main model/tool
stepping engine.
## Executive Summary
Maka already has a working local coding agent runtime:
- Electron desktop entry points
- session JSONL storage
- model streaming through the AI SDK
- tool execution and permission prompts
- abort handling
- bot and OpenGateway entry points
- tool artifacts and usage telemetry
- an internal `AgentRun` ledger
The current architecture is better than the original monolithic path, but the
runtime still lacks one stable center of gravity. The same run is currently
represented through several related but separate structures:
- `StoredMessage` in the session JSONL
- renderer-facing `SessionEvent`
- `AgentRunEvent` in the run ledger
- best-effort `RunTraceEvent`
- telemetry records
That split makes the system hard to evolve. It also makes important questions
harder than they should be:
- Which facts are the real runtime truth?
- Which objects are only UI projections?
- Which events should be replayed into the next model request?
- Which events are diagnostic-only?
- Where should permission, tool, recovery, and telemetry semantics live?
Runtime v2 should introduce a canonical invocation/event spine:
```text
RuntimeRunner
-> InvocationContext
-> AgentFlow
-> AiSdkFlow
-> AI SDK streamText
-> ToolRuntime
-> RuntimeEvent ledger
-> projections
```
The key shift is:
```text
Today:
SessionManager + StoredMessage/SessionEvent are close to the runtime center.
Target:
Invocation + RuntimeEvent + AgentFlow are the runtime center.
StoredMessage, renderer SessionEvent, AgentRunStore, RunTrace, and telemetry
become projections or ledgers derived from canonical runtime facts.
```
## Design Inputs
This proposal is based on the current Maka implementation and on the runtime
structure documented in the Google ADK Go reading materials. The useful lesson
from ADK is not to copy type names directly. The useful lesson is the layering:
```text
Runner -> Agent -> Flow -> Model/Tool -> Event -> Session
```
In that model:
- `Runner` owns the invocation shell and persistence boundary.
- `Agent` owns lifecycle and routing.
- `Flow` owns the model/tool loop.
- `Event` is the shared runtime fact language.
- `Session` stores durable history and scoped state.
- tool, callback, plugin, instruction, workflow, entrypoint, and telemetry
layers attach around that axis without becoming the axis themselves.
Maka has different product constraints, so the evolution should be adapted:
- keep the AI SDK as the long-term main flow implementation;
- preserve Electron and existing user-visible session behavior;
- preserve existing JSONL compatibility;
- reuse `ToolRuntime`, `AgentRunStore`, and `RunTrace`;
- avoid a flag day where all storage and UI projections change at once.
## Current Runtime Path
Today, one user turn approximately flows like this:
```text
renderer / bot / gateway
-> desktop main / entrypoint code
-> ensureSessionCanSend(...)
-> SessionManager.sendMessage(sessionId, input)
-> new AgentRun(...)
-> AgentRun.execute()
-> append user StoredMessage
-> append turn_state=running
-> lock connection snapshot
-> ensureActive() / build AiSdkBackend
-> register active run
-> AiSdkBackend.send()
-> PermissionEngine.beginTurn()
-> RunTrace
-> ModelAdapter.resolveModel()
-> build AI SDK tool map
-> materialize prior StoredMessage[] into AI SDK messages
-> AI SDK streamText({ tools, stopWhen: stepCountIs(maxSteps) })
-> AI SDK owns model -> tool -> model stepping
-> tool.execute(...) calls ToolRuntime
-> pump fullStream into SessionEvent queue
-> append assistant StoredMessage
-> append token_usage StoredMessage
-> queue complete/error/abort
-> AgentRun updates SessionHeader / turn_state / AgentRunStore
-> cleanup active run
```
This split has some good properties:
- `SessionManager.sendMessage()` no longer owns the whole hot path.
- `AgentRun` gives a durable run lifecycle record.
- `ToolRuntime` is a real boundary for tool permission, execution, artifacts,
and tool telemetry.
- `ModelAdapter` isolates provider/AI SDK stream and error normalization.
But it also has structural limits:
- Maka does not have its own explicit `Flow.Run`; the model/tool loop is
implicit inside `AiSdkBackend.send()` plus the AI SDK.
- Cross-turn model history is built from selected `StoredMessage` types, not
from a canonical runtime event history.
- tool calls and tool results are persisted for UI/history, but their role in
future model context is an implicit policy inside `materializePriorMessages()`.
- permission and tool events are represented across `StoredMessage`,
`SessionEvent`, `RunTraceEvent`, and telemetry rather than one canonical fact.
- entrypoints still own some runtime readiness semantics, such as connection
readiness/rebind checks.
## Target Architecture
The target architecture keeps the AI SDK as the primary model/tool stepping
engine, but wraps it in Maka-owned invocation and event semantics.
```text
┌────────────────────────────────────────────────────────────────────┐
│ Entrypoint Adapters │
│ │
│ Desktop IPC Bot Gateway OpenGateway │
│ - parse input - parse input - parse HTTP │
│ - validate shape - auth/rate limit - SSE/JSON │
│ - broadcast output - send bot reply - gateway output │
│ │
│ Entrypoints are protocol adapters. They do not own runtime loop, │
│ readiness, recovery, or model/tool policy. │
└──────────────────────────────┬─────────────────────────────────────┘
│ RuntimeRequest
▼
┌────────────────────────────────────────────────────────────────────┐
│ RuntimeRunner │
│ │
│ - RuntimeGate preflight │
│ - create invocation/run context │
│ - append user RuntimeEvent │
│ - resolve active agent/flow policy │
│ - run AgentFlow │
│ - persist canonical events │
│ - drive projections │
└──────────────────────────────┬─────────────────────────────────────┘
│ InvocationContext
▼
┌────────────────────────────────────────────────────────────────────┐
│ AgentFlow Interface │
│ │
│ interface AgentFlow { │
│ run(ctx, input): AsyncIterable<RuntimeEvent> │
│ } │
│ │
│ AiSdkFlow is the default long-term implementation: │
│ │
│ - build AI SDK messages from runtime history projection │
│ - build AI SDK tool map │
│ - call streamText({ tools, stopWhen: stepCountIs(...) }) │
│ - map text/thinking/tool/usage/finish/error to RuntimeEvent │
│ - delegate tool execution to ToolRuntime │
└──────────────────────────────┬─────────────────────────────────────┘
│ tool.execute seam
▼
┌────────────────────────────────────────────────────────────────────┐
│ ToolRuntime │
│ │
│ - declaration/schema/registry │
│ - permission evaluation and parked decisions │
│ - tool implementation execution │
│ - output deltas and artifacts │
│ - tool telemetry │
│ - RuntimeEvent(tool_call/tool_result/permission/artifact) │
│ - returns result to AI SDK so streamText can continue stepping │
└────────────────────────────────────────────────────────────────────┘
```
Shared services sit beside this path:
```text
RuntimeEventLedger
canonical invocation facts
ProjectionManager
RuntimeEvent -> StoredMessage JSONL
RuntimeEvent -> renderer SessionEvent stream
RuntimeEvent -> TurnRecord / SessionHeader
RuntimeEvent -> AgentRunStore / RunTrace
RuntimeEvent -> TelemetryRepo
SessionService
event history + scoped state
legacy JSONL compatibility
RuntimeGate
readiness / rebind / blocked/running/waiting guards
Plugin / Callback / Instruction
prompt injection / audit / retry-reflect / telemetry hooks
```
## Canonical RuntimeEvent
Runtime v2 needs a single internal fact model. A sketch:
```ts
type RuntimeEvent = {
id: string;
invocationId: string;
runId: string;
sessionId: string;
turnId: string;
ts: number;
author: 'user' | 'agent' | 'tool' | 'system';
role: 'user' | 'model' | 'tool' | 'system';
branch?: string;
partial: boolean;
content?: {
text?: string;
thinking?: string;
functionCall?: {
id: string;
name: string;
args: unknown;
};
functionResponse?: {
id: string;
name: string;
result: unknown;
isError?: boolean;
};
error?: {
code?: string;
reason?: string;
message: string;
};
};
actions?: {
stateDelta?: Record<string, unknown>;
artifactDelta?: Record<string, string | number>;
permissionRequest?: PermissionRequest;
permissionDecision?: PermissionDecision;
transferToAgent?: string;
endInvocation?: boolean;
tokenUsage?: TokenUsage;
};
refs?: {
storedMessageId?: string;
traceEventId?: string;
toolCallId?: string;
providerEventId?: string;
};
};
```
This event is not a UI event and not a trace event. It is the internal runtime
fact. Other records should either be written from it or be explicitly linked to
it.
## Consumption Paths
### Desktop User Message
```text
Renderer
-> preload IPC
-> main IPC handler
-> normalize input
-> validate attachment shape
-> RuntimeRunner.run({ source: 'desktop', sessionId, text, attachments })
-> RuntimeGate.preflight()
-> create invocation
-> append RuntimeEvent(user)
-> ProjectionManager writes user StoredMessage and renderer append event
-> AiSdkFlow.run(ctx)
-> build AI SDK messages from runtime history projection
-> streamText(...)
-> partial model chunks -> RuntimeEvent(partial model text)
-> final model output -> RuntimeEvent(final model text)
-> tool calls go through ToolRuntime
-> ProjectionManager writes:
-> assistant StoredMessage
-> TurnRecord
-> SessionHeader
-> AgentRun ledger status
-> telemetry usage/tool records
-> main process streams projected SessionEvents back to renderer
```
The renderer consumes a projection stream. It does not consume the runtime core
directly.
### Tool Call and Permission
```text
AiSdkFlow
-> AI SDK asks tool.execute({ args, toolCallId })
-> ToolRuntime.executeTool()
-> RuntimeEvent(tool_call)
-> StoredMessage tool_call
-> renderer tool_start
-> PermissionEngine.evaluate()
├─ allow
│ -> run tool.impl
│ -> RuntimeEvent(tool_result)
│ -> return result to AI SDK
│
├─ block
│ -> RuntimeEvent(tool_result isError synthetic)
│ -> return synthetic error result to AI SDK
│
└─ prompt
-> RuntimeEvent(permission_request)
-> renderer permission modal
-> session waiting_for_user projection
-> park tool execution
-> respondPermission(...)
-> RuntimeEvent(permission_decision)
-> allow/deny branch
```
Permission is a runtime action, not just a UI event. It should be represented in
`RuntimeEvent.actions`.
### Model History Construction
Today, model history is built from stored messages and skips several runtime
message types. Runtime v2 should make this an explicit projection:
```text
RuntimeEvent history
-> ModelHistoryProjector
include:
user text events
model final text events
selected system/instruction events
function call events when required by the provider protocol
tool function response events
exclude:
partial chunks
token usage
trace diagnostics
UI-only system notes
permission ack unless deliberately exposed to the model
-> AI SDK messages
```
This makes the "what does the next model call see?" policy reviewable and
testable.
### Startup Recovery
```text
App startup
-> RuntimeRecovery.scan()
-> read invocation/run ledger
-> classify non-terminal invocations by latest RuntimeEvent:
- model stream started with no terminal event -> failed app_restarted
- tool started with no result -> failed interrupted_tool
- permission_request pending -> waiting_for_user or failed by policy
- final model event persisted but run header not completed -> complete it
-> append recovery RuntimeEvent
-> ProjectionManager repairs:
- TurnRecord
- SessionHeader
- StoredMessage system note if needed
- AgentRun terminal status
```
Recovery should reason from canonical invocation facts first and then repair
projections. It should not independently guess from multiple stores unless it is
handling legacy data.
### Bot and OpenGateway
```text
Bot / OpenGateway
-> normalize external protocol
-> RuntimeRunner.run({ source: 'bot' | 'gateway', ... })
-> same RuntimeGate
-> same AiSdkFlow
-> same ToolRuntime
-> projected output:
bot: collect final response and send bot reply
gateway: format JSON/SSE response
desktop: optional fan-out when the same session is visible
```
The runtime should not have three subtly different send paths for desktop, bot,
and gateway.
## Source of Truth Boundaries
The intended boundary is:
```text
RuntimeEventLedger
runtime fact
StoredMessage JSONL
user-visible conversation projection and legacy read model
renderer SessionEvent
transport projection for UI streaming
AgentRunStore
invocation/run lifecycle ledger, eventually closely linked with RuntimeEvent
RunTrace
diagnostic-only projection; failures must not affect execution
TelemetryRepo
economic and operational projection
```
This boundary is the main reason to do the work. Without it, adding more
features will keep increasing the number of places that need to agree about the
same run.
## Proposed Module Shape
One possible file layout:
```text
packages/runtime/src/
runtime-event.ts
runtime-event-projection.ts
invocation-context.ts
runtime-runner.ts
runtime-gate.ts
runtime-recovery.ts
flows/
agent-flow.ts
ai-sdk-flow.ts
model/
model-adapter.ts
model-history.ts
tools/
tool-runtime.ts
tool-registry.ts
permission-events.ts
projections/
stored-message-projection.ts
session-event-projection.ts
turn-record-projection.ts
telemetry-projection.ts
session-manager.ts
```
This is not a required final file layout. The important part is the direction:
`SessionManager` becomes a facade and session CRUD owner, not the runtime brain.
## Migration Plan
### Phase 1: RuntimeEvent RFC and Adapters
Add the canonical event types and adapters without changing the run path.
Deliverables:
- `RuntimeEvent` and `RuntimeEventActions`
- mapping tests for current `StoredMessage`, `SessionEvent`, `AgentRunEvent`,
and `RunTraceEvent`
- documentation of which events are facts and which are projections
Reason:
This gives the team a shared language before changing control flow.
### Phase 2: RuntimeRunner Shell
Introduce `RuntimeRunner.run()` and make `SessionManager.sendMessage()` delegate
to it while still using current `AgentRun.execute()`.
Deliverables:
- `RuntimeRunner`
- `InvocationContext`
- `RuntimeGate` placeholder for readiness policy
- backward-compatible `SessionManager` facade
- tests proving current UI-visible behavior remains stable
Reason:
This moves invocation ownership out of `SessionManager` without changing the
AI SDK or storage path all at once.
### Phase 3: AgentRunStore to Invocation Ledger
Upgrade the run ledger to carry invocation/event linkage.
Deliverables:
- run headers linked to `invocationId`
- run events linked to canonical runtime event ids where possible
- recovery tests that reason from invocation facts and repair projections
Reason:
The current run ledger is already useful. It should become part of the runtime
spine instead of a parallel diagnostic store.
### Phase 4: AiSdkFlow Formalization
Extract/formalize the current `AiSdkBackend.send()` loop into `AiSdkFlow`.
Deliverables:
- `AgentFlow` interface
- `AiSdkFlow` implementation
- `ModelHistoryProjector`
- `AiSdkFlow` emits canonical `RuntimeEvent`s
- `AiSdkBackend` becomes configuration/factory shell rather than the runtime
loop owner
Reason:
The AI SDK remains the main engine, but Maka gains explicit flow input/output
semantics.
### Phase 5: ToolRuntime Event Actions
Make tool calls, tool results, permission requests, permission decisions, state
deltas, and artifact deltas first-class runtime event actions.
Deliverables:
- `tool_call` / `tool_result` runtime events
- permission request/decision runtime actions
- artifact delta linkage
- tests for allow/block/prompt/deny/abort paths through canonical events
Reason:
Tools are where model intent becomes local side effects. That boundary must be
auditable and replayable.
### Phase 6: Entrypoint Cleanup
Move readiness and rebind policy out of desktop main and into runtime gates.
Deliverables:
- `RuntimeGate` owns connection readiness/rebind/session blocked/running guards
- desktop/bot/gateway entrypoints call the same runner APIs
- contract tests proving the same session state behaves the same across
desktop, bot, and gateway paths
Reason:
Entry points should translate protocols; they should not own runtime policy.
### Phase 7: Model History Correctness
Build future model prompts from runtime event history projection.
Deliverables:
- explicit policy for which runtime events enter model history
- tests for tool-result replay into the next model call
- tests excluding partial chunks, telemetry, trace, and UI-only notes
- provider compatibility tests around AI SDK message shapes
Reason:
The next model call must see the right history for the right reason. This
should be a tested runtime policy, not incidental stored-message filtering.
## What Not To Do
Avoid these failure modes:
- Do not rewrite all provider streaming logic from scratch.
- Do not remove the AI SDK as the main flow engine.
- Do not delete session JSONL compatibility.
- Do not move UI concerns into `RuntimeEvent`.
- Do not let `RunTrace` become a success/failure source of truth.
- Do not let desktop main remain the owner of readiness/rebind runtime policy.
- Do not introduce a second tool runtime beside the current `ToolRuntime`;
evolve it into the event/action model.
## Success Criteria
Runtime v2 is working when these statements are true:
- A developer can answer "what happened in this turn?" by reading one
invocation/event ledger.
- UI messages, turn records, run records, trace rows, and telemetry can be
traced back to canonical runtime event ids.
- desktop, bot, and gateway entry points share the same runtime readiness and
execution semantics.
- model history construction is an explicit tested projection from runtime
events.
- tool permission and tool result behavior is visible as runtime actions, not
scattered side effects.
- `SessionManager` is mostly session CRUD plus backward-compatible facade
methods.
- the AI SDK remains a supported first-class flow implementation through
`AiSdkFlow`.
## Open Questions
- Should canonical `RuntimeEvent` be stored as a separate JSONL immediately, or
first mirrored into `AgentRunStore` events?
- Should `StoredMessage` projection be synchronous with event append, or should
projection failures be recoverable/replayable?
- How much of current `RunTrace` should be folded into `RuntimeEvent` refs
versus left as diagnostic-only rows?
- Should permission prompts be model-visible events, UI-only events, or
configurable by flow policy?
- How should branch/agent transfer be represented before Maka has a full
multi-agent tree?
- What is the compatibility strategy for old sessions that have no runtime
event ledger?
These should be answered in the RFC before implementation starts.
@@ -0,0 +1,122 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Runtime v2 implementation notes
> Archived on 2026-07-13. These notes describe the initial Phase 1–4 skeleton; the backend architecture chapters and current source now describe the active runtime.
Status: Phase 1–4 skeleton landed (compile-safe, tested). The production
`SessionManager.sendMessage` hot path is **unchanged**; the v2 seam exists
in parallel so future work can migrate onto it incrementally.
Source plan: `docs/archive/runtime-v2-architecture-evolution.md`.
## What landed
### Core contract (`@maka/core`)
- `packages/core/src/runtime-event.ts` — the canonical `RuntimeEvent` fact
model (role / author / status enums, content discriminated union, actions,
refs, pure helpers `isTerminalRuntimeEvent` /
`runtimeEventHasModelVisibleContent` / `createRuntimeEventId`).
- `packages/core/src/__tests__/runtime-event.test.ts` — focused contract
tests.
- New subpath export `@maka/core/runtime-event`. The later public-API cleanup
removed the package root barrel, so this subpath is now the only public route.
### Runtime v2 seam (`@maka/runtime`)
Five new modules, each importable via its canonical subpath AND re-exported
(selectively) from the runtime barrel:
| Module | Subpath | Role |
|---|---|---|
| `model-history.ts` | `@maka/runtime/model-history` | Policy-driven `buildModelHistoryFromRuntimeEvents()` replacing ad-hoc `StoredMessage` filtering. |
| `invocation-context.ts` | `@maka/runtime/invocation-context` | `InvocationRequest` / `InvocationContext` spine, injectable `newId`/`now` providers, `InvocationResult` envelope. |
| `runtime-runner.ts` | `@maka/runtime/runtime-runner` | `RuntimeRunner.run()` collecting shell: preflight gate → context → user event → flow dispatch → terminal collection. |
| `agent-flow.ts` | `@maka/runtime/agent-flow` | Formal `AgentFlow` / `AgentFlowControl` / `FlowInput` seam. |
| `ai-sdk-flow.ts` | `@maka/runtime/ai-sdk-flow` | `AiSdkFlow` wrapping an `AgentBackend`; `mapSessionEventToRuntimeEvent()` placeholder mapping. |
Each module shipped with co-located tests for the initial migration seam.
### Exports consolidated by the steward
- `packages/core/package.json` — added `"./runtime-event"`.
- `packages/runtime/package.json` — added the runtime-v2 subpath exports.
- The later public-API cleanup removed both package root barrels in favor of
explicit domain subpaths.
## Reconciled: single `InvocationContext` type
`InvocationContext` is now owned by `invocation-context.ts` and reused by the
formal flow seam:
- `invocation-context.ts` — the canonical runner/flow spine (required
`source`, `startedAt`, `request`, `newId`, `now`).
- `agent-flow.ts` — imports and re-exports that canonical type for the
`AgentFlow.run(ctx, input)` contract.
The runtime barrel re-exports the canonical `InvocationContext` from
`invocation-context.ts`; the previous duplicate flow-local context was removed
so runner and flow code share the same identity/provider spine.
## What remains (by phase)
- **Phase 5 — Tool-event actions:** promote `tool_output_delta` /
`tool_progress` `SessionEvent`s to a dedicated tool-progress runtime
action (currently partial tool-role heartbeats). Refine
`mapSessionEventToRuntimeEvent` role/author policy.
- **Phase 6 — RuntimeGate:** implement the real preflight (connection
readiness/rebind, blocked/running/waiting guards) behind `RuntimeGate`
and inject it into desktop + bot/gateway entrypoints.
- **Phase 7 — Projection:** drive `StoredMessage` / `TurnRecord` /
`SessionHeader` / `AgentRunStore` / `RunTrace` / `TelemetryRepo` writes
from `InvocationResult.events`. Wire
`buildModelHistoryFromRuntimeEvents()` into the live
`AiSdkBackend.materializePriorMessages` path.
- **SessionManager delegation:** replace the body of
`SessionManager.sendMessage` with `RuntimeRunner.run(...)` behind a
feature flag, mapping `InvocationResult.events` → existing
`SessionEvent` projection. A streaming `async *stream()` variant may be
added then if the renderer needs live deltas. Today `RuntimeRunner.run()`
is **collecting** (returns `Promise<InvocationResult>`), not streaming.
- **`abort` + `complete` coalescing:** `AiSdkFlow` is a faithful translator
(the backend emits `abort` then a trailing `complete`, and the flow emits
both). Coalescing into a single terminal event is a runner/projection
concern.
- **Flow runnable surface:** `RuntimeRunner` depends on the centrally owned
`RunnableAgentFlow` (`Pick<AgentFlow, 'run'>`) so it remains decoupled from
flow metadata (`kind`, `sessionId`) while sharing the formal `AgentFlow.run`
signature.
## Verification snapshot
All commands run from the repository root (`$RIVE_WORKSPACE`):
```
npm run build # all workspaces — clean
npm run typecheck # all workspaces — clean
npm --workspace @maka/core run test # 613 pass / 0 fail
npm --workspace @maka/runtime run test # 384 pass / 0 fail
git diff --check # clean
```
No production source (`session-manager.ts`, `ai-sdk-backend.ts`,
`agent-run.ts`, `materializer.ts`) was modified. The v2 seam is purely
additive.
@@ -0,0 +1,93 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Search Service Threat Model
> Archived on 2026-07-13. This document records the PR-SEARCH-0/1.5 contract boundary, not the current search product implementation. Source and focused tests own the active contract.
PR-SEARCH-0 is a contract-only package. It must not add a real provider, browser engine, renderer network call, cache writer, or runtime search execution path.
PR-SEARCH-1.5 (this revision) adds the `SearchResultTarget` closed discriminated union (currently only `{ kind: 'thread'; sessionId: string; turnId?: string }`) so source-kind-specific navigation hints stay typed. Adding a new variant (memory / activity / etc.) is an explicit contract change. Thread navigation deliberately does NOT use `maka://session/<id>` — `packages/ui/src/maka-uri.ts:24` defers that scheme until a real session navigation contract exists. Renderers consume `SearchResult.target` for navigation; `SearchResult.url` stays reserved for `web` / `web_fetch` external results.
## Source Separation
Search is not one source. Maka must keep these sources typed and visibly separate:
- `thread`: local session/thread text search.
- `memory`: local or provider-backed semantic memory search.
- `activity`: future OCR/activity search, gated by Screen Recording and retention settings.
- `tool`: tool discovery, bounded by existing tool permission mode.
- `web`: external web search through an explicit provider.
- `web_fetch`: URL fetch/extraction, treated as active untrusted content.
Local private sources (`thread`, `memory`, `activity`) must not be silently mixed with external `web` results. UI and tool outputs should show source kind.
## Assets
- search query text;
- result titles, URLs, snippets, summaries, and markdown;
- cookies and browser session state;
- provider credentials and base URLs;
- chat history, memories, and future OCR/activity text;
- cache entries and embeddings;
- citation indexes injected into LLM context.
## Boundaries
1. Renderer to main: renderer can request search only through typed IPC.
2. Main to provider/site: query/result data may leave the machine.
3. Browser page to app: HTML/JS/CSS is untrusted active content.
4. Result to renderer: no raw HTML rendering.
5. Local private stores to LLM: memory/activity results may be prompt-injected only with explicit source semantics.
6. Cache/persistence: results and embeddings can become long-lived local data.
## Required Gates Before Web Search
- `normalizeSearchQuery`, `normalizeSearchLimit`, `normalizeSearchDomainList`, and `normalizeSearchUrl` before provider calls.
- URL scheme allowlist: only `http:` and `https:`.
- Redirect/private-network policy before URL fetch.
- Domain allow/block suffix matching; blocked wins.
- Result and total-output byte caps.
- AbortSignal and wall-clock timeout.
- No query/snippet/markdown bodies in telemetry.
- Cache policy with TTL, refresh bypass, and no authenticated page caching by default.
- Browser mode must use isolated partitions by default; no defaultSession cookie reuse.
- CAPTCHA/challenge returns `needs_human_browser`, not indefinite hidden-browser waiting.
## What Not To Copy From reference implementation
- Hidden Google scraping as the default Search Service.
- `defaultSession` browser/cookie reuse.
- Cookie sync from debug windows into search/fetch sessions.
- Cookie import/export without typed payload, user action, and audit log.
- Raw HTML rendering in the app.
- One untyped result list that mixes local thread/memory/activity and external web.
## Minimum Test Matrix
- query trim, empty reject, type reject, length cap;
- limit default/clamp/reject;
- domain canonicalization, dedupe, suffix matching;
- URL scheme allowlist and tracking parameter stripping;
- freshness rewrite for latest/today/now/今天/最新 queries;
- no rewrite for historical/archive/since/过去 queries;
- citation indexes monotonic once execution exists;
- timeout/abort returns structured error once execution exists;
- renderer has no direct network primitive for search once IPC exists;
- telemetry omits snippets and markdown bodies once execution exists.
@@ -0,0 +1,88 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# UI Convergence Map (2026-07-09)
> Archived on 2026-07-13. The campaign is complete; current ownership lives in the frontend READMEs, source, and contract tests.
Maintainer complaint: shared UI solutions re-implemented per call site. This map inventories
every duplicated recipe family and the extraction sequence. Produced by a read-only audit
agent; token layer (--state-hover-bg/--state-selected-bg, Chip alphas, tabular-nums) is
already contract-governed — the work is moving STRUCTURE onto shared primitives.
## Prioritized extraction sequence
1. **Chip expansion + CSS-label migration** — 7+ hand-rolled tone→alpha tables collapse into
primitives/chip.tsx. Order: primitive-free CSS labels first (maka-skill-library-status-label,
maka-scheduled-task-run-status, maka-daily-review-archive-status), then settingsAuthActionPill
(connection.css:160-227), settingsBotStatusPill (bot.css:189-213, add `dot` prop),
providerCatalogBadge.is-state (models.css:193-198). chip-converge-contract.test.ts already
pins the target alphas (/12 /14 /18 /15). Badge stays pill-role (badge-converge contract).
2. **Item adoption for list rows** — primitives/item.tsx already encodes 4%/8% fills; add a
`selected` prop → --state-selected-bg; migrate enabledConnRow, providerCatalogRow,
maka-skill-library-row, daily-review archive/session rows, settingsOsPermissionRow.
HIGH contract risk: sidebar-topbar-rail / model-oauth-section / skills.test.ts pins;
state-token-governance-499 rejects any non-token fill introduced during migration.
3. **PageHeader primitive** — module h2 shells (maka-module-main-header, maka-scheduled-task-hero) +
settings h3 intros (settingsPermissionIntro, settingsHealthIntro, settingsFeatureStatusHero,
settingsAboutHero). API: title/subtitle/icon/eyebrow/as('h2'|'h3')/actions/meta. Keep class
hooks for renderer-module-styles + tailwind-compile contracts.
4. **StatTile** — permission.css:547 tile ≈ health.css:118 tile (near-identical); fold
settings-metric-card.tsx MetricCard + daily-review totals cell. outline|filled emphasis.
Must bake in tabular-nums (tabular-nums-converge contract).
5. **SectionHeader + ActionRow (+ EmptyState inline variant)** — three section-header dialects
(maka-skill-section-row / maka-daily-review-section-title with ::before accent bar /
settingsPermissionSectionHeader); action rows: settingsActionRow already shared, fold
maka-module-main-actions / maka-scheduled-task-top-actions / maka-daily-review-actions.
## Status
- [x] 1 Chip — SHIPPED #681 (5 recipes retired, dot prop added, contract extended)
- [~] 2 Item rows — round 2 shipped on feat/item-row-convergence: Item gained
`selected` (→ --state-selected-bg) + `interactive` gate. Migrated
enabledConnRow (data-default → selected) and daily-review archive rows
(UiButton → Item). providerCatalogRow already on Item. DEFERRED with
reasons: enabledProviderTrigger (accordion header, not a row),
settingsOsPermissionRow (static grid + stripe, pinned grid/div-actions),
daily-review session rows (composite: preview sibling outside the button),
maka-skill-library-row (bespoke grid + parent-li hover, skills.test.ts
pin — interactive={false} support landed for a future adoption).
- [x] 3 PageHeader — SHIPPED on feat/page-header-convergence: new
primitives/page-header.tsx (title/subtitle/eyebrow/icon/badge/
as('h2'|'h3')/actions/meta + contentClassName/iconClassName/
headingRowClassName/subtitleClassName/as_wrapper escape hatches).
Migrated all 6 call sites: skills-panel maka-module-main-header,
scheduled-task-panel maka-scheduled-task-hero (contentClassName=maka-scheduled-task-heading),
permission-center settingsPermissionIntro (meta), health-center
settingsHealthIntro (meta + <strong> subtitle), voice-settings
settingsFeatureStatusHero (icon + 本地自检 badge), about-settings
settingsAboutHero (icon + version/channel badge fragment + h2 scale).
NO CSS re-pin needed: every wrapper's typography CSS uses DESCENDANT
selectors (`.wrap h2/h3/p`) that keep matching through the primitive's
slot divs; radius/pruning contracts untouched (wrapper blocks
unchanged). check-a11y: added `Maka` to the brand allow-list (About
title prop tripped english-aria-label). Verified: ui+desktop builds,
2293 desktop / 46 ui tests, dead-css clean, alignment audit clean,
5 CDP captures in notes/round3-page-header-captures/.
- [x] 4 StatTile — SHIPPED #701 (inline after agent cancellation; --font-size-stat ladder step added)
- [x] 5 SectionHeader (#703) + EmptyState inline (#704). ActionRow intentionally CLOSED: settingsActionRow already shared across 9 pages; the 3 module clusters carry distinct pinned layouts where a primitive adds a hook without removing duplication.
CAMPAIGN COMPLETE 2026-07-10: every family has one primitive + contract pins + the CI alignment auditor.
(Details per family — implementations with file:line, divergences, risks — are in the audit
agent's report; the essentials are inlined above. Update checkboxes as rounds ship.)
+543
View File
@@ -0,0 +1,543 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Maka UI Quality Plan (one-month delivery contract)
> Archived on 2026-07-13. This month-one rollout plan and coverage snapshot are no longer current; use `docs/frontend-css-governance.md`, the local frontend READMEs, source, and contract tests.
This is the **UI-side delivery contract** complementing
the archived full-product roadmap (`docs/archive/full-product-test-plan-2026-05.md`, @xuan's) and
@kenji's `notes/maka-one-month-completion-test-plan.md` (cross-cutting
test/release matrix). Where those define "what features ship and how the
overall test matrix is laid out", this document defines the UI-specific
quality bar each visible surface must clear before its PR can merge.
Treat the contracts here as merge gates. A UI PR that violates any §3
invariant or skips a §5 testing layer for its surface does not merge.
---
## 0 — Scope
**UI = everything inside `apps/desktop/src/renderer/` and
`packages/ui/`.** This covers:
- Visual composition (layout, tokens, light/dark themes)
- Interaction (mouse, keyboard, touch later)
- Motion (durations, easings, reduced-motion)
- Accessibility (ARIA, focus management, screen reader)
- UX text (labels, errors, empty states, copy stance)
- Visual smoke fixtures + screenshots
- Renderer-only IPC helpers (`window.maka.*`)
Out of scope (covered elsewhere):
- IPC handlers in `apps/desktop/src/main/**` (xuan + kenji own)
- Backend runtime + telemetry (xuan)
- Storage + migration (xuan)
- Cross-cutting release process (kenji)
---
## 1 — Per-surface gate (the "is this surface done?" checklist)
Every UI surface MUST clear **all** of these gates before merging:
| # | Gate | What it means | Where it lives |
|---|---|---|---|
| 1 | **Contract** | Surface listed in `docs/design-system.md` §9.x with state machine + boundary rules | `docs/design-system.md` |
| 2 | **Pure helper test** | All derived state extracted into pure function + node:test cases covering state transitions | `apps/desktop/src/renderer/*.ts` + `apps/desktop/src/main/__tests__/*.test.ts` |
| 3 | **Component contract** | Component declared in `packages/ui/src/components.tsx` with typed props; no untyped `any` props; no implicit DOM events leaking up | `packages/ui/src/components.tsx` |
| 4 | **Fixture scenario** | At least one `MAKA_VISUAL_SMOKE_FIXTURE=...` scenario seeds the surface with realistic data | `apps/desktop/src/main/visual-smoke-fixture.ts` |
| 5 | **Smoke path** | Step-by-step manual verification with explicit Pass / Fail signals | `apps/desktop/tests/smoke.md` |
| 6 | **Light + dark screenshots** | Both theme variants captured in the fixture | screenshots dir (TODO PR-IR-01) |
| 7 | **Narrow viewport screenshot** | ≤ 990 px width captured to verify mobile-ish layout | screenshots dir |
| 8 | **Failure / empty state** | If the surface has a failure or empty state, that's a separate fixture + screenshot | fixtures + screenshots |
| 9 | **a11y assertion** | ARIA roles, labels, keyboard navigation declared + tested per §3 | component + tests |
| 10 | **Motion contract** | Animations respect `prefers-reduced-motion`; durations from token catalog | `packages/ui/src/maka-tokens.css` + component CSS |
| 11 | **i18n contract** | Visible strings are Chinese by default; no English fallback in user-facing copy | component source |
| 12 | **Security contract** | No raw user input rendered back; no secret in any path; redactSecrets applied | component + tests |
Skipping any single gate is a release-no-go (see §11).
---
## 2 — UI testing layers (the "what kind of test" matrix)
Use **the minimum sufficient** testing layer for each concern. Over-testing
adds maintenance burden and slows merges.
| Concern | Layer | Tool | Example |
|---|---|---|---|
| Pure derivation (badge tone, ordering, filtering) | node:test on pure helper | `node:test` | `deriveChatHeaderAlert` |
| CSS contract (no rule hides X, opacity restores on active) | grep-style assertion on styles.css | `fs.readFile + regex` | `stale-sessions.test.ts` |
| Component prop wiring (renderer → IPC → backend) | preload + ipc shape test | node:test on type | preload.ts shape check |
| User flow (rename, archive, send) | smoke.md manual path | manual | `smoke.md` Path 4 |
| Visual rendering (light/dark/narrow) | screenshot diff | TODO (PR-IR-01) | fixture screenshots |
| a11y semantics (ARIA, kbd) | smoke.md path with explicit Tab order assertion + AT spot check | manual | `smoke.md` Path 9-style |
| Motion (reduced-motion) | smoke.md path with system pref toggled | manual | dedicated path |
| Cross-platform (macOS / Win / Linux) | smoke.md per-platform run | manual | smoke.md preamble |
| Performance (cold start, scroll FPS) | smoke.md + dedicated metric | manual + future telemetry | smoke.md release section |
### What we do NOT test in the UI workspace
- React rendering via JSDOM (over-fragile; pure helper + smoke covers it)
- Backend logic (lives in @maka/runtime / @maka/storage workspaces)
- Network calls (mocked in pure helpers when needed; live calls live in
@maka/runtime tests)
---
## 3 — Cross-surface invariants (always-on quality contract)
These are **non-negotiable** rules every UI PR must respect. They're the
same kind of "always-on" gate that linters enforce, except codified
prose because they're judgement calls.
### 3.1 Focus management
- Every modal MUST trap focus via `useModalA11y(ref, onEscape?)`.
- Every modal MUST restore focus to the previously-focused element on
close.
- Focus ring MUST use `:focus-visible`, never bare `:focus`.
- All interactive elements MUST have `aria-label` if they have no visible
text (icon-only buttons, etc.).
### 3.2 Keyboard
- All clickable controls MUST be reachable by Tab.
- All Tab stops MUST be in DOM order (no positive `tabIndex` except where
documented in the design system).
- Roving tabindex pattern (Arrow keys) MUST be used for radio groups,
toolbars, listboxes, menus — verified by node:test on the pure helper
(see `model-table-keyboard.test.ts`).
- Cmd/Ctrl+K opens command palette (PR31). Esc closes it.
- ⌘/Ctrl+, opens Settings (PR98 menu).
- ⌘/Ctrl+F focuses session search (PR32).
- ? or ⌘/Ctrl+/ opens keyboard help (PR22).
- Enter sends message in composer; Shift+Enter newline (PR3).
- Esc cancels rename / dismisses modal / closes drawer.
### 3.3 Motion
- All durations use tokens from `--ease-out-strong / --ease-in-out-strong / --ease-drawer`.
- `@media (prefers-reduced-motion: reduce)` caps every animation to ~0.01ms
(already global cap; do not override).
- Use `@starting-style` for entrance animations where supported.
- Animate `transform` and `opacity` only; never `width / height / top`.
### 3.4 Spacing + theme
- Every component MUST use stable spacing that matches its local
information density; Maka does not expose a global UI density toggle.
- Every component MUST render correctly under light + dark theme via
`.dark` class.
- Tokens MUST come from `maka-tokens.css`; **no hardcoded color** in
component CSS (PR5 sweep; enforced by visual review).
- Theme picker preview tiles MUST mirror the actual chat surface (PR79).
### 3.5 Text + i18n
- All user-facing copy is **Chinese by default**.
- Errors go through `generalizedErrorMessage()` from `@maka/core` (PR58,
PR74).
- UI-level `redactSecrets()` runs on any string that may contain user
input or provider responses (PR60).
- **NEVER expose internal enums** ("演示版", "FakeBackend", reason codes,
slugs) in user-visible labels. Put technical detail in tooltip.
- Empty / loading / error states are **first-class**; no surface ships
with only a happy path.
### 3.6 Boundaries (renderer ↔ main)
- Renderer NEVER assembles absolute paths from a `relativePath`. (Artifact
contract §9.1.5.)
- Renderer NEVER inspects credential storage directly. Provider secret presence
goes through `window.maka.connections.hasSecret(slug)`; subscription OAuth
safeStorage remains main-process only.
- Renderer NEVER opens a URL via `window.open(url)`. Goes through
`shell.openExternal` in main via `setWindowOpenHandler` (PR96).
- Renderer NEVER drops a file into the DOM. `did-finish-load` blocks
dragover/drop globally (PR96 follow-up).
### 3.7 Trust hierarchy (UI redactor + backend redactor)
- Backend `redactSecrets` from `@maka/core` is **authoritative**.
- UI `redactSecrets` is a **second layer** for runtime strings (tool
output banner, copy-to-clipboard).
- Never display a raw error message from any IPC — always run through
`cleanErrorMessage(error)` first.
---
## 4 — Per-surface coverage matrix (current state + month-1 plan)
Rows are **all UI surfaces** in Maka. Columns are §1 gates 1–12.
✅ = met. ❌ = gap. ⚙️ = partial.
| Surface | C | PH | CC | F | S | L+D | Nar | E/F | A11y | M | i18n | Sec |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| **Shipped (P0/V0.2)** | | | | | | | | | | | | |
| Sidebar session list | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Chat header banner | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Composer | ✅ | ⚙️ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| EmptyChatHero | ✅ | ⚙️ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| OnboardingHero | ✅ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Settings · 模型 | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Settings · 账号 | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Settings · 数据 | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ |
| Settings · 个性化 | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ |
| Settings · 网络 | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ |
| Settings · 机器人对话 | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ |
| Settings · 关于 | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ⚙️ | ✅ | ✅ | ✅ |
| Settings · 主题 | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ⚙️ | ✅ | ✅ | ✅ |
| Settings · 4 Coming Soon | ✅ | n/a | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ⚙️ | ✅ | ✅ | ✅ |
| Permission dialog | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tool result renderer | ✅ | ✅ | ✅ | ⚙️ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Turn summary chips | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Thinking block | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Sidebar streaming dot | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Sidebar stale pill | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Command palette | ✅ | ⚙️ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Toast | ✅ | ⚙️ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Keyboard help modal | ✅ | n/a | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Error boundary | ✅ | n/a | ✅ | ❌ | ⚙️ | ❌ | ❌ | ✅ | ⚙️ | ✅ | ✅ | ✅ |
| Artifact pane | ✅ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Session status grouping | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Session status icon + chat header badge | ✅ | ✅ | ✅ | ✅ | ✅ | ⚙️ | ⚙️ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Pending (month-1)** | | | | | | | | | | | | |
| Quick Chat (§9.7) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Workstation shell (§9.8) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Turn control (§9.9) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Sources/Skills/Auto (§9.10) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Health Center (§9.11) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| First-run stepper (§9.12) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| ModelCatalog ext | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Memory drawer | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Voice composer | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Search service | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Open gateway admin | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| MCP client UI | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
Legend: C = Contract, PH = Pure helper, CC = Component contract, F =
Fixture, S = Smoke path, L+D = Light + Dark, Nar = Narrow, E/F = Empty +
Failure states, A11y = a11y assertions, M = Motion, i18n = Chinese
default, Sec = Security (redact + no raw input).
**Total gap count: 13 shipped surfaces × 1–3 ⚙️ gates ≈ 30 cells. All
pending surfaces are full ❌.**
---
## 5 — Required testing infra (PR-IR-XX series)
These are infrastructure PRs the UI side owes the rest of the project.
Without them, the gates in §1 can't be enforced uniformly.
### PR-IR-01 — Screenshot capture pipeline
**What.** A playwright (or Electron-native + tape-recorder) runner that
boots Maka under each `MAKA_VISUAL_SMOKE_FIXTURE=...` scenario, applies
light + dark + narrow viewport variants, and captures PNGs to
`apps/desktop/tests/screenshots/<scenario>/<variant>.png`.
**Why.** Right now `smoke.md` paths require a human to eyeball the
screen. Screenshots provide a regression baseline.
**Gate it unlocks.** L+D and Nar columns in §4 can flip from ⚙️ to ✅.
**Owner.** @yuejing (this is UI infra, not feature).
### PR-IR-02 — Screenshot diff CI gate
**Status (2026-05-22).** Stage 1 is live as
`scripts/diff-screenshots.mjs`. The committed baseline in
`apps/desktop/tests/screenshots-baseline/` covers 3 stable scenarios × 8
variants = 24 PNGs: `artifact-pane`, `first-run`, and `artifact-errors`.
`npm --workspace @maka/desktop run screenshots:diff:stable` exits 1 on
hard failures: missing PNG, corrupt PNG, file < 1 KB, or wrong
dimensions. Size drift is a soft warning.
**Scope catches.**
- Capture pipeline regression: renderer crashes before paint, capture
IPC breaks, or fixture seed throws.
- Viewport misconfiguration: `MAKA_VISUAL_SMOKE_WIDTH/HEIGHT` not
honored by the main process.
- Truncated or empty PNGs.
- Scenario/variant matrix drift between capture and diff scripts.
**Out of scope.**
- Pixel-level UI regressions. Electron/font rasterization drift makes
byte-level SHA/SHA256 equality unsuitable as a blocker (~70/88 PNGs
change between runs even with @xuan's PR108k fixture clock + Date.now
freeze).
- Layout shifts that keep the same viewport dimensions.
- Color, contrast, opacity, typography, and spacing regressions.
**PR-IR-02 v3 plan.** Add `pixelmatch` + `pngjs` with calibrated
tolerance and ignored dynamic regions. Pilot only on the stable subset
(`artifact-pane` / `first-run` / `artifact-errors`) before expanding to
all 18 scenarios. Configure per-scenario tolerance based on observed
drift.
**Owner.** @yuejing. Baseline rollout coordinated with @xuan.
### PR-IR-03 — A11y assertion library
**What.** A small node:test helper that, given a fixture HTML snapshot,
verifies:
- Every `<button>` either has visible text or `aria-label`.
- Every modal has `role="dialog"` and `aria-labelledby` / `aria-label`.
- Every `<input>` / `<select>` has an associated `<label>` (via `for` or
wrapping).
- No `tabIndex` > 0 in the snapshot.
**Why.** Today a11y is checked manually in smoke.md; this adds an
automated floor.
**Owner.** @yuejing.
### PR-IR-04 — Reduced-motion fixture variant
**What.** Add `MAKA_VISUAL_SMOKE_REDUCED_MOTION=1` env var that forces
`prefers-reduced-motion: reduce` regardless of OS setting. Screenshot
pipeline (PR-IR-01) captures a `reduced-motion.png` variant per scenario.
**Why.** Verifies the motion contract per surface.
**Owner.** @yuejing.
### PR-IR-05 — i18n string extractor
**What.** Build-time script that walks `packages/ui` + `apps/desktop` for
all string literals in JSX text nodes and `aria-label` / `title`
attributes; flags any that contain only ASCII letters as "untranslated
English fallback".
**Why.** Locks down the "Chinese-default" contract.
**Owner.** @yuejing.
### PR-IR-06 — Strict component prop typing audit
**What.** ESLint rule (or tsc strict + grep) that bans `any` /
`Record<string, unknown>` / unsealed object types in component props.
**Why.** Component contracts must be enforced by the type system, not
docs.
**Owner.** @yuejing.
---
## 6 — UI PR template (every UI PR must answer)
```markdown
## Contract
- Surface: <name + design-system.md §9.x reference>
- State machine: <list states>
- Boundary: <renderer ↔ main contract this PR touches>
## User Flow
1. <step>
2. <step>
3. <observable signal>
## Tests
- [ ] Pure helper test added in `apps/desktop/src/renderer/<name>.ts` +
`apps/desktop/src/main/__tests__/<name>.test.ts`
- [ ] CSS contract test if styles touched
- [ ] Fixture scenario seeded in `visual-smoke-fixture.ts`
- [ ] Smoke path added in `smoke.md` with Pass + Fail signals
- [ ] Light + dark + narrow screenshots captured (or noted as PR-IR-01
blocker)
- [ ] a11y assertions: <list — ARIA roles, kbd nav, focus trap, screen
reader label>
## Security
- [ ] No raw user input rendered back without `redactSecrets`
- [ ] No secret in any IPC path (uses `hasSecret` envelope when needed)
- [ ] No absolute path in renderer (uses `relativePath` + storage helper)
- [ ] No `dangerouslySetInnerHTML` outside the artifact pane sandboxed
iframe
## Not Included
- <explicit list of things out of scope this PR, link follow-ups>
```
If a UI PR doesn't answer all five sections, the PR doesn't merge.
Empty "Not Included" is fine; missing is not.
---
## 7 — Release no-go conditions (UI-specific)
A release is **blocked** if any of these is true at tag time:
### 7.1 Functional
- Any P0 user flow regressed (send / open settings / pick model / archive).
- Old sessions can block sending when a ready default exists (the P0 we
just fixed; verify via smoke Path 12 + new chat from old session).
- "无法发送" / "已过期" banners appear with no actionable CTA.
### 7.2 Visual
- A surface ships with no light/dark or no narrow-viewport baseline.
- Screenshot diff CI red on main without an explicit baseline-update
commit.
- Tokens drift detected (hardcoded color in component CSS).
### 7.3 Accessibility
- Any modal regresses focus trap or focus restoration.
- Any icon-only button without `aria-label`.
- Reduced-motion fixture variant shows animation > 0.1s.
### 7.4 Text / privacy
- Any user-facing string contains "fake" / "演示版" / internal enum reason
codes.
- Any error toast contains raw user input or unredacted secret.
- Any tooltip / aria-label is English when the visible label is Chinese
(mixed-language fallback is worse than fully-translated).
### 7.5 Boundary
- Any renderer-side absolute filesystem path.
- Any `window.open` outside the `setWindowOpenHandler` route.
- Any IPC channel returning `errno` / native errors without going through
`generalizedErrorMessage`.
---
## 8 — Cross-platform gates
UI works on macOS by default (that's the primary dev box). Before any
release tag:
| Platform | Owner | Smoke run | Required passes |
|---|---|---|---|
| macOS arm64 | @yuejing | All 12 smoke paths | 12/12 |
| macOS x86_64 | TBD | First-run + send + permission | 3/3 |
| Windows | TBD | First-run + send + permission | 3/3 |
| Linux (Ubuntu LTS) | TBD | First-run + send + permission | 3/3 |
Cross-platform owners TBD — @WAWQAQ to assign when CI infrastructure is
ready.
---
## 9 — Performance gates
These are budget ceilings; we don't optimize past them without need.
| Metric | Budget | How measured |
|---|---|---|
| Cold start to first paint | < 1.5s on M1 air | manual stopwatch |
| Composer key-to-glyph latency | < 16ms (one frame) | manual eye check |
| Streaming text render | 60fps (no jank) | DevTools perf panel |
| Sidebar scroll with 200 sessions | 60fps | manual scroll test |
| Settings modal open | < 300ms (post @starting-style) | manual eye check |
| ⌘K palette open | < 100ms | manual eye check |
Regressions investigated, not blocked, unless the regression > 2x budget.
---
## 10 — Surface delivery checklist template
When picking up a pending surface (e.g. Health Center, Quick Chat), the
pattern is:
1. **Read** `docs/design-system.md` §9.x for the target surface.
2. **Write** pure derivation helper(s) under `apps/desktop/src/renderer/`
with explicit input/output types. Locate by `<surface>.ts`.
3. **Write** node:test cases in `apps/desktop/src/main/__tests__/<surface>.test.ts`
covering all state transitions + edge cases + invariants.
4. **Write** React component(s) under `packages/ui/src/components.tsx` (or
a new file if surface is large enough). Wire the pure helper.
5. **Wire** the component into `apps/desktop/src/renderer/main.tsx` (or
the matching App-level mount point). Pass IPC handles via
`window.maka.*`.
6. **Add** styles to `apps/desktop/src/renderer/styles.css` using tokens
only. Include light / dark / motion variants.
7. **Add** ARIA roles + keyboard navigation per §3.
8. **Add** fixture scenario in `visual-smoke-fixture.ts` with realistic
data including the failure / empty state.
9. **Add** smoke path in `apps/desktop/tests/smoke.md` with Precondition /
Steps / Pass signal / Fail signal.
10. **Capture** light + dark + narrow + reduced-motion screenshots once
PR-IR-01 lands.
11. **Update** §4 matrix in this file to reflect the new ✅ row.
12. **PR description** fills the §6 template.
A surface typically needs 3–8 PRs (component scaffold → wire IPC →
states → keyboard → smoke + fixture). Avoid mega-PRs > 1000 LOC; split
along the checklist boundary.
---
## 11 — Owner directory
| Domain | Primary | Backup |
|---|---|---|
| Per-surface UI components | @yuejing | (none yet) |
| Visual smoke fixture seeding | @xuan (data) + @yuejing (consumption) | — |
| Screenshot pipeline (PR-IR-01) | @yuejing | — |
| Cross-platform smoke | TBD | TBD |
| UX text + i18n | @yuejing | @WAWQAQ for stance reviews |
| a11y contract | @yuejing | @kenji for invariant review |
| Theme tokens (`maka-tokens.css`) | @yuejing | — |
| @maka/ui package exports | @yuejing | @xuan (build chain) |
---
## 12 — Open questions / pending decisions
- Cross-platform: who owns Win + Linux runs? Need a CI runner or a
human on those machines.
- Screenshot diff tooling: playwright vs custom Electron runner? See
PR-IR-01.
- Visual regression baselines: stored in repo (git LFS?) or external
bucket?
- Accessibility testing: native AT (VoiceOver, NVDA, Orca) coverage —
manual + cadence?
- Performance regression: do we instrument or just rely on manual?
These get filed as `notes/open-questions.md` after this doc lands and
get answered as the month-1 work progresses.
---
## 13 — How this doc evolves
- Each new surface adds a row to §4.
- Each new gate violation discovered in PR review adds a rule to §3.
- Each new infra need adds a PR-IR-XX row to §5.
- Release-no-go conditions grow when a regression makes it to main
uncaught — that's a sign the corresponding gate was missing.
This doc is **a living contract**, not historical record. If a rule
becomes obsolete (a feature got cut, an invariant got automated away),
delete the rule and link to the commit that removed it.
Last updated: 2026-05-22 — initial publication after the P0 stale-session
wave (PR108e/g shipped, send-path rebind by @xuan landed).
@@ -0,0 +1,127 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# UI Skills Deep Read — 2026-06-23 (yuejing)
> Archived on 2026-07-13. This research snapshot is not current product design authority.
3 parallel Explore agents deep-read 5 design-skill collections per WAWQAQ
ask `f424c6b5`. Synthesis below — converged rules + maka delta + 1 final
ship list.
## Sources
| Source | Type | Repo |
|--------|------|------|
| Impeccable | 26 commands + 44-rule SLOP catalog | github.com/pbakaus/impeccable |
| UI Skills | 15-skill collection (baseline-ui, fixing-a11y, fixing-motion-performance, 12-principles-of-animation, fixing-metadata, etc.) | github.com/ibelick/ui-skills + github.com/raphaelsalaja/skill |
| Taste-Skill | taste / redesign / soft variants | github.com/Leonxlnx/taste-skill |
| UI Design Brain | 60+ component best practices | github.com/carmahhawwari/ui-design-brain |
| Better Icons | MCP + skill, 200k icons | github.com/better-auth/better-icons |
## Convergent rules across all 5 sources
These showed up in **3+ sources** independently. Highest signal.
| Rule | Sources | Maka delta |
|------|---------|-----------|
| Body text ≥ 14-16px, weights 400/500/600 with semantic contrast | Impeccable /typeset, baseline-ui, Taste-Skill, UIDB | hint 13px → 14, no display face |
| **Tactile :active = `scale(0.98)` or `translateY(1px)`** | Taste-Skill, UIDB, 12-principles | rows/nav/back have no `:active` |
| Compositor-only motion (`transform`/`opacity` only, ≤ 200ms) | baseline-ui, fixing-motion-perf, Impeccable /animate | Maka may transition `background-color` on hover (paint) — acceptable but verify nothing animates layout |
| Reduced-motion guards | All 5 | ✅ done round 11 |
| Single accent, saturation < 80%, OKLCH neutrals tinted toward brand | Taste-Skill, Impeccable /colorize | maka uses raw `%foreground` overlays, no brand tint |
| 8px grid; double whitespace as redesign default | Taste-Skill, UIDB, Impeccable /layout | row padding 20/24 off-grid (should be 16/24 or 24/24) |
| Toggle/touch target ≥ 44px hit area | Taste-Skill, UIDB | switch 26×46 visual, no hit padding |
| Sidebar primary nav 5-7 items, distinct active state | UIDB, Taste-Skill | maka has more than 7 in "AI 与集成" group — could collapse; active state is light-fill, no accent bar |
| One CSS variable for all motion timings (no scattered ms values) | 12-principles "timing-consistent" | Maka has scattered 150ms values, no `--motion-fast` token |
| `text-balance` on heading, `text-pretty` on body | baseline-ui | not used |
| `tabular-nums` on data/numeric labels | baseline-ui, Impeccable /typeset | not used; value cells `已启用 / zai-live` would benefit |
| No display tracking modification unless explicit | baseline-ui | h2 has `-0.012em` (intentional, OK) |
| Em-dash ban | Taste-Skill, redesign | grep needed |
| Inter/system as default font flagged as "AI tell" | Taste-Skill, redesign, soft | maka is system-ui default — flagged |
| Lucide-as-default flagged | Taste-Skill | maka is lucide-react — flagged, Better Icons proposed |
| Card OR border, not both | Impeccable rule 36, UIDB, Taste-Skill | mostly ✅ (.settingsRows is border-only) but `.settingsConnectionRow` still has border + shadow |
| Visible focus ring (`:focus-visible` 2px) | fixing-a11y, Impeccable /audit | ✅ done round 11 |
| Page header height 56-72px | UIDB | maka uses padding stack, not a true bar — partial |
## Anti-patterns triggered by maka today
- **Impeccable rule 11 (card overuse / nested cards):** every settings page renders multiple `.settingsRows` blocks stacked, each with own card chrome. Could be one card with section dividers (per /distill).
- **Impeccable rule 25 (raw neutrals not tinted toward brand):** `.settingsRows` border is `oklch(from var(--foreground) l c h / 0.08)`. Tinting toward `--accent` at 0.005-0.01 chroma would carry brand into chrome.
- **Taste-Skill banned: border-b on every row.** ❌ — wait, maka DOES use `border-bottom` hairlines between rows. WAWQAQ asked for this explicitly in PR-GROUPED-CARD-0 ("内容紧密、用分隔线划开"), so this disagreement stays unresolved between Taste-Skill and WAWQAQ. Defer to WAWQAQ.
## 10 highest-value action items for maka
Ranked by impact / effort:
1. **Tactile `:active` scale** on rows, nav items, back button.
- `.settingsFormRow:active, .settingsRow:active, .settingsNavItem:active, .settingsBackButton:active { transform: scale(0.985); transition: transform 100ms cubic-bezier(0.16, 1, 0.3, 1); }`
- Reduced-motion gate already in place — fires inside the `@media (no-preference)` branch.
2. **Sidebar active accent bar** (Redesign-Skill).
- `.settingsNavItem[data-active="true"] { box-shadow: inset 3px 0 0 var(--accent); }`
- Carries brand into the most-used surface.
3. **`tabular-nums` on value cells**.
- `.settingsRow > span { font-variant-numeric: tabular-nums; }`
- Numeric values like `zai-live`, version strings, counts align cleanly.
4. **`text-balance` / `text-pretty`** on page header.
- `.settingsPageHeader h2 { text-wrap: balance; }`
- `.settingsPageHeaderDescription { text-wrap: pretty; }`
5. **Hint floor 13 → 14px**.
- `.settingsFormRow small, .settingsField small { font-size: 14px; }` (currently 13)
- Crosses the 14px legibility floor that baseline-ui + UIDB + Impeccable converge on.
6. **Switch hit-area shim**: 44×44 invisible hit target around the 26×46 visual.
- Wrap `<Switch>` in a `min-width:44px; min-height:44px;` flex container.
7. **One motion token**.
- `--motion-fast: 160ms` + `--motion-ease-out: cubic-bezier(0.16, 1, 0.3, 1)`.
- Reuse across nav / row / switch / input transitions.
8. **OKLCH brand tint on settings chrome**.
- `.settingsRows` border from `from var(--foreground) ... / 0.08` to `oklch(from var(--accent) l c h / 0.08)`.
- Subtle brand carry without changing perceived neutrality.
9. **Em-dash + AI-slop copy grep**.
- Audit `apps/desktop/src/renderer/settings/SettingsModal.tsx` for `—`, "Seamless", "Elevate", "Unleash".
- Quick mechanical cleanup.
10. **Better Icons MCP install (deferred ship)**.
- Add to `.mcp.json`. Don't migrate icons in this lane — let it learn preferred families per surface first. Lucide stays default for now.
## Defer / out-of-lane
- **Font swap** (Geist/Cabinet Grotesk/Satoshi instead of system-ui): meaningful but needs font files in the bundle, license check, and broad visual regression. Worth its own PR.
- **Nav consolidation to ≤7 in big group**: would re-split AI 与集成. Defer; WAWQAQ already approved 3 groups in round 7.
- **Card overuse / merge stacked cards**: contradicts WAWQAQ's grouped-card direction. Don't change.
## Round 15 (final) PR plan
Bundle action items 1, 2, 3, 4, 5, 7 (highest-impact, single-CSS-file). Skip 6 (component wrap), 8 (brand tint risk), 9 (audit), 10 (config) — those need their own PRs.
PR-SETTINGS-MOTION-TYPO-TOKENS-0:
- `:active` scale on rows/nav/back (item 1)
- accent bar on active nav (item 2)
- tabular-nums on row value (item 3)
- text-balance / text-pretty on header (item 4)
- hint 13→14 (item 5)
- shared `--motion-fast` token (item 7)
@@ -0,0 +1,119 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# UI Skills Deep Read — 2026-06-24 (yuejing, round 2)
> Archived on 2026-07-13. This research snapshot is not current product design authority.
WAWQAQ msg `802dda4c` asked me to deeply learn ALL skills + 6 external
URLs he sent at msg `cdfd68eb`. This is the round-2 synthesis,
extending [[ui-skills-deep-read-2026-06-23]] from yesterday.
## New external sources (WAWQAQ msg `cdfd68eb`)
| URL | Type | Notes |
|-----|------|-------|
| github.com/shadcn-ui/ui/tree/main/skills/shadcn | shadcn skill | Composition + token rules. Critical: FieldGroup/Field for forms, `size-*` not `w-*/h-*`, `truncate` shortcut, Avatar must have Fallback, Skeleton not animate-pulse |
| github.com/Jakubantalik/transitions.dev | transitions catalog | 18 named transitions (number pop-in, badge, text-state-swap, panel reveal, success check), namespaced `t-*` classes, mandatory @media (prefers-reduced-motion) |
| github.com/carmahhawwari/ui-design-brain | UI design brain | 8px baseline, body ≥ 14px (prefer 16), micro-interaction 150–250ms ease-out, toast 4–6s, skeleton after 300ms delay, tabular-nums for numbers, single accent |
| github.com/better-auth/better-icons | icon CLI | **INACCESSIBLE** — both candidate paths 404'd. Skipped |
| github.com/pbakaus/impeccable | 26 cmds + 44-rule SLOP | Cap body at 65–75ch; `text-wrap: balance` on h1–h3, `pretty` on long prose; display headings letter-spacing ≥ -0.04em; OKLCH for color; ease-out exp curves, **no bounce, no elastic**; **bans:** ghost-card (1px border + 16px+ shadow), over-rounding > 16px, gradient text via background-clip, identical card grids |
| emilkowal.ski/ui/agents-with-taste | Emil Kowalski blog | Start scale from 0.95 not 0; exit 20% faster than entrance; UI animations < 300ms; `will-change: transform` to kill shake; animate child not parent to fix hover flicker; `transform-origin` at trigger; `:active` → `scale(0.97)`; cap body at 65ch; tabular-nums for prices; loose letter-spacing on uppercase labels |
## Local skill collections (deep re-read)
| Skill | Top 5 actionable rules | Top banned anti-pattern |
|-------|------------------------|------------------------|
| high-end-visual-design | Double-Bezel nested architecture, `py-24` minimum section padding, custom cubic-bezier (no `linear`/`ease-in-out`), scroll-entry `translate-y-16 blur-md opacity-0` 800ms+, GPU-safe (transform/opacity only) | Harsh drop shadows (`shadow-md`), generic 1px borders, sticky navbar glued to top |
| design-taste-frontend | Metric-based DESIGN_VARIANCE/MOTION_INTENSITY/VISUAL_DENSITY=8/6/4 baseline, Display `text-4xl tracking-tighter` + body `text-base leading-relaxed max-w-[65ch]`, perpetual micro-interactions on dashboard, Framer Motion `layout`/`layoutId` for smooth reorder, staggered cascade via `calc(var(--index) * 100ms)` | Inter font, purple/blue AI gradient, neon outer glows, centered Hero (variance > 4) |
| minimalist-ui | Warm monochrome + ultra-desat pastels (`#FDEBEC` / `#E1F3FE`), body off-black `#111` or `#2F3437` never `#000`, cards = exactly `border: 1px solid #EAEAEA` + 8–12px radius, scroll-entry `translateY(12px) + opacity:0 → 0 + 600ms`, keystroke badges pill-shaped uppercase wide-tracking | Heavy shadows, gradients, 3D glassmorphism, emojis, generic placeholder names |
| gpt-taste | Python-driven seeded randomization, AIDA structure (Nav → Hero → Bento → GSAP scroll → CTA), Hero ≤ 3 lines + `max-w-5xl/6xl` containers, gapless bento `grid-auto-flow: dense`, GSAP ScrollTrigger pinning + stacking | Meta-labels ("SECTION 01"), floating stamp icons on hero, pill tags under hero, raw stats in hero |
| stitch-design-taste | Density spectrum (1–10) + Variance spectrum (1–10) encoded in DESIGN.md, inline typography images at type-height, spring physics default (stiffness 100 damping 20), perpetual loops on active components | Overlapping elements, pure `#000`, neon glows, 3-column equal grids, AI copywriting |
| image-taste-frontend | IMAGE-FIRST workflow (generate before coding), deep image analysis (text + ratios + spacing), fresh regeneration (don't crop old), Hero 1–3 line headlines, media in fixed-aspect bounded containers | Tiny multi-section collages, generic stock imagery, vague vibe-only analysis, design drift during implementation |
| industrial-brutalist-ui | Swiss Industrial Print OR Tactical Telemetry/CRT (pick one), macro-typography `clamp(4rem,10vw,15rem)` ultra-tight `-0.03em` to `-0.06em`, micro-typo monospace 10–14px uppercase generous `0.05em` tracking, color `#F4F4F0` bg + `#050505` fg + `#E61919` accent, grid `gap: 1px` for razor dividers | Rounded corners (90° only), gradients, soft shadows, lowercase, serif |
| redesign-existing-projects | Audit chain: Font → Color → Hover/Active → Layout/Spacing → Replace generic components → Loading/Empty/Error → Polish, max 1 accent saturation < 80%, max-width container 1200–1440px, CSS Grid over flexbox math, every component needs hover/active/focus/loading/empty/error | Centering bias, 3-equal-card rows, `h-screen`, pure `#000`, warm+cool gray mixing |
| shadcn | Use existing components first (compose, don't reinvent), critical rules: FieldGroup + Field for forms, data-icon + no sizing on icons, `gap-*` not `space-y-*`, full Card composition (Header/Title/Description/Content/Footer), validation via data-invalid + aria-invalid | Manual z-index on overlays, space-x/y-* (use gap), hardcoded colors, floating labels |
| shadcn-baseui | Use `render` prop NOT `asChild` for triggers, Accordion uses boolean `multiple` not `type`, `nativeButton={false}` when wrapping Link/custom, Select `alignItemWithTrigger={false}` replaces `position="popper"` | Radix `asChild` patterns in Base UI project |
| web-perf | Core Web Vitals: LCP < 2.5s INP < 200ms CLS < 0.1, animate ONLY transform/opacity never top/left/width/height, grain/noise fixed pseudo-element only, `backdrop-blur` only on fixed/sticky never scrolling | Continuous GPU repaints, `window.addEventListener('scroll')`, render-blocking JS/CSS without async/defer |
## Cross-reference: what's already covered in Maka source
The 11+ external + 13 local skills converge on a tight set of rules.
Maka coverage (audit done 2026-06-24):
| Rule | Maka source ref | Status |
|------|----------------|--------|
| transform + opacity only animations | All keyframes (maka-list-row-enter, maka-tool-card-enter, maka-message-row-enter, maka-onboarding-child-enter, maka-hero-enter, maka-composer-stream-bounce, maka-composer-permission-pulse) | ✅ |
| Reduced-motion guards | ~12 `@media (prefers-reduced-motion: reduce)` blocks | ✅ |
| Custom cubic-bezier (`--ease-maka` = `cubic-bezier(0.16, 1, 0.3, 1)`) | All easing references | ✅ |
| Tactile `:active scale(0.97-0.98)` | onboarding-card, skill-row, prompt-chip, plan-new-task-button, palette-item, composer-tool-button, composer-send-button, list-row, artifact-row, model-switcher-trigger | ✅ (shipped this round) |
| No 172deg gradient anywhere | 4 contract tests pin invariant | ✅ (shipped this round) |
| ARIA semantic `<ul>`/`<li>` not `<div role="list">` | Global walker contract test | ✅ |
| `text-wrap: pretty` on body, `balance` on headings | `.maka-bubble-assistant p` (pretty), h1-h4 (balance) | ✅ (shipped this round) |
| Body max-width 65-75ch | `.maka-bubble-assistant { max-width: 72ch }` + escape valves on pre/table/code | ✅ (shipped this round) |
| h1/h2 negative tracking | `letter-spacing: -0.012em` on h1/h2 | ✅ (shipped this round) — conservative for CJK mix |
| `tabular-nums` on numeric text | `.maka-bubble-assistant code`, `.maka-artifact-row-meta`, `.maka-list-row-meta`, `.maka-tool-duration` | ✅ (shipped this round) |
| Nested-bezel avatar | `.maka-message-avatar` 24×24 full-circle + 3-layer box-shadow | ✅ (shipped this round) |
| Stagger entrance on hero | `.maka-onboarding > *:nth-child(n)` with 0/60/120/180/220ms cascade | ✅ (shipped this round) |
| Perpetual micro-interaction on pending state | `.maka-composer-permission-dot` 1.4s pulse + `.maka-composer-streaming-dot` 1.05s bounce | ✅ (shipped this round) |
| `<details>` chevron rotation + body fade-in | `.maka-turn-thinking` summary::before chevron + body @starting-style opacity ramp | ✅ (shipped this round) |
| BAN harsh `rgba(0,0,0,0.5)` shadows | Audit found none — all shadows use oklch alpha | ✅ |
| BAN `transition: all` | Audit found none | ✅ |
| BAN transitioning width/height/top/left | Only 1 place (turn-footer secondary action max-width hover reveal) — acceptable | ✅ |
| Single accent < 80% saturation | `--accent` oklch tokens per palette | ✅ |
| Focus-visible rings | 65 `focus-visible` rules audited | ✅ |
| Native macOS cursor convention (`pointer` only on links) | Contract test pins | ✅ |
| Code blocks: language pill + copy button + 10px radius | `.maka-code-block` matches | ✅ |
| `noopener` on all `target="_blank"` | Global walker contract test | ✅ |
## Remaining deltas (defer or out-of-scope)
- **Lucide → Phosphor icons swap**: skill-banned but would touch ~80+ icon usages across the app. High risk, low visible value for power users. Defer until a focused PR is requested.
- **Heavy display typography (`clamp(4rem, 10vw, 15rem)`)**: industrial-brutalist style, doesn't match Maka's calm desktop aesthetic. Skip.
- **Inline typography images at type-height**: stitch pattern, not applicable to a chat tool.
- **Footer with legal links**: Maka is a desktop app, no public footer. Skip.
- **GSAP scroll-trigger paradigms**: Maka doesn't have marketing scroll surfaces. Skip.
- **Skeleton-after-300ms delayed render**: requires JS state pattern not pure CSS. Sized but deferred.
- **Per-tab transitions.dev catalog of 18 named transitions**: would require adopting a new namespacing convention. Defer.
## Skills round task #116 — ship list this round
10 PRs shipped applying skills (all merged to shared/origin main):
1. `e20662d1` — PR-CHAT-MESSAGE-AVATAR-BEZEL-0 (high-end-visual-design §2 nested-shell)
2. `120f72f3` — PR-REASONING-PANEL-SMOOTH-EXPAND-0 (design-taste-frontend §5 spring)
3. `e22035da` — PR-SKILL-SEARCH-DOUBLE-BOX-0 (feedback_use_component_library)
4. `f946f86b` — PR-REFERENCE-SETTINGS-GRADIENT-CORRECTION-0 (notes correction to prevent regression cycle)
5. `6a58e303` — PR-MESSAGE-BODY-READING-RHYTHM-0 (pbakaus impeccable + agents-with-taste)
6. `fb26e391` — PR-ONBOARDING-HERO-STAGGER-0 (Emil agents-with-taste)
7. `d761e1a8` — PR-ARTIFACT-ROW-TACTILE-0 (Emil `:active scale`)
8. `5d9585f9` — PR-SESSION-ROW-TACTILE-0
9. `033d6a9a` — PR-COMPOSER-PERMISSION-PULSE-0 (design-taste-frontend §8.3 perpetual)
10. `05b4d084` — PR-MODEL-SWITCHER-TACTILE-0
11. `b7a0cd2a` — PR-MESSAGE-BODY-MAX-WIDTH-0 (impeccable 65-75ch)
Tests at 1482/1482 throughout. Current shared/origin main: `b7a0cd2a`.
## When to re-read this note
Before ANY future "polish UI" / "use the skills" pass. Cross-check
new ideas against the "already covered" table above before
proposing them — most generic polish ideas have already been
shipped, so the next round needs to find genuinely new structural
deltas, not redo what's done.