源码快照 @ 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
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:
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
@@ -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 子进程,成本高)。
|
||||
@@ -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.
|
||||
@@ -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。报告只记录状态机和调用语义,不复制其产品代码或资产。
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.)
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user