Skip to content

Extract runtime kernel boundaries and durable AgentRun recovery - #3

Merged
likun666661 merged 4 commits into
mainfrom
runtime-kernel-upstream
Jun 14, 2026
Merged

Extract runtime kernel boundaries and durable AgentRun recovery#3
likun666661 merged 4 commits into
mainfrom
runtime-kernel-upstream

Conversation

@likun666661

Copy link
Copy Markdown
Member

Summary

This PR extracts Maka's runtime hot path into clearer internal kernel boundaries while keeping the public desktop, renderer, IPC, settings, bot, gateway, and session-message surfaces stable.

It builds on the recently merged hardening work and focuses on the next architectural step: making a single agent turn explicit, observable, and recoverable.

What changed

ToolRuntime

  • Extracted internal ToolRuntime around tool execution.
  • Centralized tool input validation, permission checks, watchdog pause/resume, abort handling, telemetry, artifact recording, and failure classification.
  • Kept builtin tool names and public tool behavior stable.

ModelAdapter

  • Extracted minimal ModelAdapter for provider-facing stream/error/usage normalization.
  • Kept AiSdkBackend as the compatibility/orchestration shell.
  • Reduced provider-specific details in the higher-level backend path.

RunTrace

  • Added best-effort runtime trace events for model, tool, permission, abort, and usage milestones.
  • Recorder failures remain non-fatal and do not change model/tool execution.
  • Trace data is internal and does not change renderer-visible SessionEvent behavior.

AgentRun ledger

  • Added core AgentRunHeader, AgentRunEvent, status, and AgentRunStore contracts.
  • Added file-backed AgentRunStore under:
    • sessions/<sessionId>/runs/<runId>/run.json
    • sessions/<sessionId>/runs/<runId>/events.jsonl
  • Supports atomic run header writes, same-run serialized appends, corrupt event-line recovery, and truncated-tail tolerance.

AgentRun execution

  • Moved the heavy turn lifecycle out of SessionManager.sendMessage() and into internal AgentRun.execute().
  • AgentRun now owns user-message append, turn state, backend stream drive, status projection, abort/failure handling, and durable trace writes.
  • SessionManager remains the public runtime API and keeps session orchestration / backend registry responsibilities.

Startup recovery

  • recoverInterruptedSessions() now prefers AgentRun ledger state when available.
  • Stale non-terminal AgentRuns are deterministically repaired and get durable recovery events.
  • Legacy session-message / turn-state fallback remains for old sessions without run ledger rows.

Why

Before this PR, a single turn's execution state was spread across SessionManager, AiSdkBackend, tool wrapping, permission logic, telemetry, abort handling, and session JSONL projection. That made it difficult to answer operational questions such as:

  • Did the model stream start?
  • Which backend/model/connection did this turn use?
  • Was the run waiting for permission?
  • Which tool was running when the app exited?
  • Did a user stop action cause the abort?
  • Is a running session actually running, or just stale after restart?

This PR introduces an internal durable run ledger and explicit runtime boundaries so Maka can explain and recover its agent turns without changing user-facing surfaces.

Documentation

Added docs/runtime-kernel.md, which explains:

  • the new runtime layering
  • what changed and what stayed stable
  • recovery semantics
  • test coverage
  • follow-up work

Updated CHANGELOG.md with a Runtime kernel extraction section.

Verification

Run on the clean PR branch based on current upstream/main:

  • npm --workspace @maka/core run typecheck
  • npm --workspace @maka/storage run test = 76/76
  • npm --workspace @maka/runtime run typecheck
  • npm --workspace @maka/runtime run test = 315/315
  • npm --workspace @maka/desktop run build:main
  • git diff --check

Scope notes

Intentionally not included:

  • no renderer/preload API changes
  • no public IPC channel changes
  • no settings UI changes
  • no bot/gateway behavior changes
  • no Rive scheduler mapping
  • no checkpoint/replay implementation
  • no migration of existing session JSONL format

@likun666661
likun666661 merged commit 1daac3d into main Jun 14, 2026
Astro-Han added a commit that referenced this pull request Jun 17, 2026
…mantics

PR #30's P2 #3 redefines promptSegment.toolCount as the model-visible
(active) tool count, not the full providerTools registry (which includes
the invalid fallback plus any deferred-unloaded / economy-hidden schemas
that never reach the wire). After rebasing onto Tool Source Economy
(PR #34), three economy tests still asserted the old providerTools.length
values; update them to the active counts, which now equal
modelToolNames().length in each case.
jackwener pushed a commit that referenced this pull request Jun 21, 2026
Extract runtime kernel boundaries and durable AgentRun recovery
jackwener pushed a commit that referenced this pull request Jun 21, 2026
…mantics

PR #30's P2 #3 redefines promptSegment.toolCount as the model-visible
(active) tool count, not the full providerTools registry (which includes
the invalid fallback plus any deferred-unloaded / economy-hidden schemas
that never reach the wire). After rebasing onto Tool Source Economy
(PR #34), three economy tests still asserted the old providerTools.length
values; update them to the active counts, which now equal
modelToolNames().length in each case.
jackwener pushed a commit that referenced this pull request Jun 21, 2026
Extract runtime kernel boundaries and durable AgentRun recovery
jackwener added a commit that referenced this pull request Jun 21, 2026
…doc-merge)

1. **iframe sandbox/CSP language tightened** (@kenji #1):
   - Remove the "separate webContents partition + permissive CSP for
     iframe" promise — DOM `<iframe>` doesn't get an independent
     partition without `<webview>` / BrowserView. Don't promise what
     the DOM can't deliver.
   - Add `allow-forms` and `allow-modals` to the not-allowed list
   - Switch from `src="file://..."` to `srcdoc={await readText()}` so
     content delivery goes through the path-guard read helper
   - Note that global CSP `default-src 'self'` stays locked — sandbox
     attribute carries all isolation

2. **Symlink-aware path guard** (@kenji #2):
   - Both `realpath(artifactRoot)` AND `realpath(target)` required,
     then containment check
   - Mirror PR56 open-path-guard which already has symlink-escape +
     `..` + URL-scheme test coverage

3. **readBinary MIME allow-list** (@kenji #3):
   - Allowlist: image/png/jpeg/gif/webp/svg+xml, application/pdf
   - MIME sniffed at read time, NOT trusted from record.kind or
     record.mimeType
   - Unknown → `reason: 'unsupported_mime'` + UI shows metadata +
     "在 Finder 中打开" / "另存为" actions (no raw bytes to renderer)

4. **Deleted tombstone blocks reads** (@kenji #4):
   - `status: 'deleted'` records immediately return `reason: 'deleted'`
     from readText / readBinary — even if the underlying file still
     exists on disk
   - Surface table updated: deleted row preview shows "此 artifact 已
     删除,预览已停止"
   - Permanent path purge is a separate Settings · 数据 flow

5. **PR108c runtime hook scope** (@kenji #5):
   - First version does NOT do LLM extractor
   - Only deterministic outputs: Write→file record, Edit→diff record,
     Bash with explicit redirect → file record
   - stdout/stderr NOT auto-promoted (noise)
   - LLM extractor (parse ```html``` from assistant message) deferred
     to PR109+

Also: type rename `storagePath` → `relativePath` to enforce contract
in the type system; full reason union widened on both read helpers.

doc total: 1416 lines.
jackwener added a commit that referenced this pull request Jun 21, 2026
Consumes the turn-level contract from PR109c. Implements two of @kenji's
remaining PR109d review gates:

**(d) Failed turn AlertOctagon banner** (@kenji gate #3)
TurnView renders a destructive-tone banner above the assistant body
when `turn.status === 'failed'`. Distinct from PR58 tool-error banner
because it's turn-scoped (the whole turn failed, not just a tool).

Copy is generalized Chinese phrase translated from `turn.errorClass`
via new pure helper `describeTurnErrorClass()` in
`session-status-presentation.ts`:

  timeout                → 请求超时
  auth / 401 / 403       → 鉴权失败
  rate_limit / rate_*    → 触发模型速率限制
  network / fetch / econn → 网络错误
  provider_unavailable / 5xx → 模型服务暂不可用
  tool_failed            → 工具调用失败
  permission_required    → 等待权限确认
  (unknown / empty)      → 未知错误

10 node:test cases lock the mapping invariant: never leak raw enum
identifier, always Chinese, case-insensitive, fallback to "未知错误".

**(e) Lineage badges** (@kenji gate #4)
- Forward badges sit at the top of the new turn:
  "重试自 turn ${shortId}" / "重新生成自 turn ${shortId}".
  Source: `turn.retriedFromTurnId` / `turn.regeneratedFromTurnId` on
  the turn record itself.
- Reverse badges sit at the bottom (above the footer actions):
  "已重试 → turn ${shortId}" / "已重新生成 → turn ${shortId}".
  Source: derived UI-side via existing `deriveTurnLineageMap()` —
  NOT persisted in storage (@kenji review on PR109c kept this side
  immutable).
- Both directions share the same TurnLineageBadge type; UI
  distinguishes via `data-direction="forward"|"reverse"` for tone
  (forward = info-tinted, reverse = success-tinted).
- Click handler scrolls the target turn into view via
  `data-turn-id` attribute querySelector (smooth, block center).

Files:
- `packages/ui/src/components.tsx`:
  - `TurnView` gains `failedReasonLabel` + `lineageBadges` +
    `onLineageBadgeClick` props
  - New `TurnLineageBadge` exported interface
  - Renders failed banner + forward/reverse badge rows
  - `ChatView` gains `turnFailedReasonLabels` +
    `turnLineageBadgesByTurn` + `onLineageBadgeClick` props
- `apps/desktop/src/renderer/main.tsx`:
  - Consolidates `turnFooterActionsByTurn` + `turnFailedReasonLabels`
    + `turnLineageBadgesByTurn` into a single useMemo (one pass over
    materialized turns)
  - `handleLineageBadgeClick()` scrolls target turn via querySelector
- `apps/desktop/src/renderer/session-status-presentation.ts` —
  new `describeTurnErrorClass()` helper
- `apps/desktop/src/renderer/styles.css` — `.maka-turn-failed-banner`
  (destructive), `.maka-turn-lineage-row` + `.maka-turn-lineage-badge`
  (info-tinted forward, brand-deep-tinted reverse)

Per @kenji review (PR109e thread): lineage badges only do scroll/link
— no IPC, no pending mask needed. If a future "retry from badge"
shortcut is added, it MUST reuse the same `pendingTurnActions`
channel from PR109d.

Tests: 234 → 243 desktop passing (+9 describeTurnErrorClass).
check-a11y 0 violations.

Deferred to PR109f:
- Branched session banner ("分自 ${parentSessionName}" + aborted
  starting-point copy "从中断前分支")
- `turn-control-history` fixture + smoke Path 15
jackwener added a commit that referenced this pull request Jun 21, 2026
@kenji PR110a review #3: the slug carried by
needs_connection_credentials and needs_default_model MUST be the
current default's slug. The UI uses it to focus a setup panel;
pointing it at an alt would route the user to a provider they did
not choose.

  case 18: default missing_api_key + alt empty_model_list
           → needs_connection_credentials { 'conn-default' }
           (NOT 'conn-alt' even though alt is also broken)
  case 19: default + alt both have model_not_enabled
           → needs_default_model { 'conn-default' }

Both assert the slug equals the default and (for 18) explicitly
notEqual to the alt's slug.

core 94 / desktop 273 = total 483 pass.
jackwener added a commit that referenced this pull request Jun 21, 2026
Two blocker findings from @kenji's contract-consumer review of A2
addressed:

**Blocker #1: user-facing copy no longer leaks internal PR / IPC
terms.** Every JSX-rendered string referencing `PR-CU-*`, `PR-HC-*`,
`PR-REAL-*`, `typed action IPC`, or `ToolOutputDelta` is replaced
with product language. PR-tracking references survive only in
JSDoc / code comments, where they belong.

  - Permission Center footnote: "正在 PR-CU-0 / PR-CU-1(Computer Use
    原生 helper)路上,落地后会替换这里的只读视图。" →
    "权限引导模块(Computer Use 原生 helper)接入后会替换这里的只读视图。"
  - Permission Center action hint titles:
    "操作入口等 PR-CU-0/1 typed action IPC 接入后可用" →
    "权限引导模块接入后提供"
  - Permission Center audit slot empty state:
    "审计日志将在 PR-REAL-3 接入后显示。" → "审计日志接入后显示。"
  - Health Center hard-rule p tag:
    "凭据测试只属于 validation 层,运行态需要 PR-REAL-4 接入实测探测。" →
    "凭据测试只属于 validation 层,运行态需要运行态探测接入后实测。"
  - Health Center footnote:
    "操作入口等 PR-HC-2 typed action IPC 接入后可用。所有真正的运行态探测
    (PR-REAL-4 ToolOutputDelta + send/stream/abort smoke)落地后会自动
    出现在「运行态探测」层。" →
    "运行态修复操作接入后显示。所有真正的运行态探测落地后会自动出现
    在「运行态探测」层。"

**Blocker #2: CAPABILITY_READINESS_COPY.enabled.detail no longer
overpromises runtime probe.** A2 is a read-only consumer; whether
`runtimeProbe.state` is `not_available` / `not_run` / `passed` is
already shown separately in the per-layer breakdown below the
summary pill. The summary copy must not synthesize a conclusion
the snapshot didn't make.

  - "配置、权限、运行态探测都已通过。" →
    "当前快照标记为可用,具体层级见下方。"

The runtime probe layer remains the single source of truth for
"是否运行态通过"; the summary pill's `detail` now defers to it
rather than asserting it.

@kenji non-blocking follow-up #3 (`toLocaleString()` host-locale
drift) deferred to when Permission/Health pages enter visual smoke
— mirrors the `MAKA_VISUAL_SMOKE_LOCALE` deterministic-formatter
pattern. Not blocking A2 since A2 carries no screenshot baseline.

332 desktop tests pass; typecheck + check-a11y + check-console
clean. No behavior change beyond copy.
jackwener added a commit that referenced this pull request Jun 21, 2026
…thread (SEARCH-MODAL-REAL-0 absorbed)

Addresses WAWQAQ msg `e0dbad11` blocker #3 + kenji msg `2844f64f`
blocker #3: the Search modal can no longer ship as an empty
placeholder shell. This commit wires it to the existing
`window.maka.search.thread` IPC + the `@maka/core/search` contract
that's been shipped since PR-SEARCH-2.

## Implementation

SearchModal now takes two new optional props:
- `deps: { searchThread(request) }` — injected IPC binding so
  tests can pass an in-memory fake. main.tsx binds production to
  `(request) => window.maka.search.thread(request)`.
- `onNavigateToSession(sessionId, turnId?)` — navigation callback
  from the app shell. Modal does NOT construct `maka://session`
  URIs (per kenji SEARCH gate).

Internal state:
- `query` is local React state ONLY. NO localStorage, NO IPC echo,
  NO history.
- `results` / `error` / `pending` track the latest response. An
  inflight `ticketRef` discards stale responses (typing fast
  cancels the previous query's UI update).
- Debounce: ~180ms after the user stops typing. Empty query clears
  state without an IPC roundtrip.

## SEARCH gate compliance (kenji `2844f64f`)

| Gate | How it's satisfied |
|---|---|
| No query persistence | `query` is `useState`, not localStorage; modal unmounts on close, state goes with it. |
| No `maka://session` fallback | `onNavigateToSession(sessionId, turnId?)` callback. Renderer routes via the existing session-pane state (`setActiveId`). |
| Incognito blocked state | `error.reason === 'incognito_active'` renders a dedicated `<div data-tone="info">` panel with `隐私模式已关闭搜索。` — NOT a fake disabled toggle. |
| Plain text snippet | `SearchResult.snippet` is rendered as `<div>{result.snippet}</div>`. No markdown render, no `<img>`, no `<a href>`. The IPC layer already redacts secrets + caps to `SNIPPET_MAX_CODE_POINTS`. |
| No Command Palette wiring | Modal is the sole search entry. No `cmd-palette.tsx` integration. |
| Selection navigates + scrolls | Click result → `onNavigateToSession(sessionId, turnId?)` + `props.onClose()`. main.tsx wires to `setActiveId(sessionId)` (turnId is plumbed through the contract but the renderer doesn't scroll-to-message yet — that's a follow-up since it requires ChatView scroll-anchor plumbing). |

## States the modal handles

- No `deps` injected → "当前环境无法连接搜索后端,请稍后重试。"
  (degraded, never crashes).
- Empty query → "开始输入以按关键词查找历史对话。结果只包含会话内容
  文本,不进入网络。" (informative).
- Loading (180ms debounce + IPC roundtrip) → "正在搜索…"
- No matches → "没有匹配的会话内容。换个关键词试试。"
- Incognito blocked → info-tone card.
- Other IPC errors (provider_error, etc.) → warning-tone card with
  the error envelope's `message` field.
- Results → scrollable list of buttons, each with title + bounded
  3-line snippet clamp.

## CSS additions

- `.maka-search-modal-input-row` + `.maka-search-modal-input-icon`
  + `.maka-search-modal-input` for the input row.
- `.maka-search-modal-state[data-tone="info"|"warning"]` for
  blocked / error cards.
- `.maka-search-modal-results` + `.maka-search-modal-result` for
  result list rendering. 3-line snippet clamp via `-webkit-line-clamp`.
- Modal grid-template-rows changed `auto minmax(0, 1fr)` →
  `auto auto minmax(0, 1fr)` to accommodate the input row.

## Contract test updates (minimal relax)

- `search-modal-lifecycle-contract.test.ts`: regex anchor relaxed
  from `<SearchModal\s+onClose=` to `<SearchModal\s+on[A-Z]` so
  multi-line JSX with `onClose / deps / onNavigateToSession` props
  still satisfies the conditional-mount contract. Semantic
  invariant unchanged.
- `visible-copy-hygiene-contract.test.ts`: `incognito` needle
  tightened from `/incognito/i` to `/incognito(?![_a-zA-Z])/i` so
  contract enum names (`incognito_active`) and camelCase
  identifiers (`incognitoBlocked`) don't false-positive. The gate
  still catches `incognito` as a standalone English word in JSX
  text / copy.

## Gates

- typecheck clean, UI + renderer build clean
- desktop tests: 647 pass / 0 fail (both updated contract tests
  passing; no test regressions)
- core tests: 392 pass
- `sidebar-search-modal-open` capture shows the new real-search
  modal with input row + empty-state hint copy.

## Out of scope (deferred follow-ups)

- turnId-scoll-to-message: the SearchResult target carries
  `turnId?` per `SearchResultTarget` but the chat-pane scroll-to-
  turn anchor requires ChatView plumbing not yet present. A
  follow-up PR will land that integration.
- Search highlight in result list (visual hit emphasis): bounded
  by what `SearchResult.snippet` provides; if we want bolding,
  the backend would need to emit `matchStart/matchEnd` ranges, a
  separate contract change.
- Command Palette result merging: explicitly OUT of scope per
  kenji gate (the modal is the sole search entry).
jackwener added a commit that referenced this pull request Jun 21, 2026
…nd-path deferred to PR-OAUTH-SUBSCRIPTION-1)

WAWQAQ msg `f6d38739` driver. Gate doc: `notes/pr-oauth-subscription-0-gate.md`
(11 hard gates: kenji `cf41871b` × 6 + xuan `2c5aa125` × 5).

## borrow
- alma `~/Downloads/alma-re/readable/main.js:15913-16400` Claude
  subscription PKCE flow, refresh, quota fetch, encrypted token
  persistence, cloaked-request shape.
- Endpoints / client_id / scope strings reproduced verbatim from
  alma since they're Anthropic-server-registered and not negotiable.

## diverge
- Cloaked request headers behind env flag MAKA_CLAUDE_SUBSCRIPTION_CLOAK
  (default OFF) — alma defaults ON. Until product/legal decides on
  Claude Code CLI impersonation, default OFF.
- Refresh failure does NOT auto-logout — alma deletes the token file
  on first refresh fail; we leave it and surface refresh_failed
  state so the user can retry / re-login deliberately.
- Token file gets explicit mode 0o600 on create AND on every save
  (alma uses default mode).
- No telemetry / logs of token lifecycle events (alma logs to stdout).

## risk
- Cloaked CLI identity headers (UA, Stainless, Claude Code system
  prefix) may violate Anthropic ToS. The cloak path is in a
  separate module `cloaked-request.ts` that's never statically
  imported by the default request path; contract test
  `claude-subscription-cloak-flag.test.ts` enforces this.
- On Linux without libsecret, safeStorage.isEncryptionAvailable()
  returns false and tokens persist as plaintext JSON. Card needs a
  future warning (deferred to follow-up PR).
- PKCE verifier in main-process memory only; cleared on
  completeAuthorization success, cancelAuthorization, or TTL prune.

## gate
- 11 hard gates locked in `notes/pr-oauth-subscription-0-gate.md`.
- Implemented contract tests (all PASSING):
  - `oauth-subscription.test.ts` (core, 26 tests): PKCE algorithm,
    URL builder, paste parse, constant-time compare, no token-
    shaped field declarations in the contract.
  - `claude-subscription-cloak-flag.test.ts`: cloak module exists,
    is NOT statically imported, contains impersonation strings,
    service references MAKA_CLAUDE_SUBSCRIPTION_CLOAK env flag.
  - `claude-subscription-ipc-boundary.test.ts`: 6 forbidden field
    keys (access_token / refresh_token / id_token + camelCase)
    forbidden in preload, renderer/, packages/ui/src/.
- Send-path tests + real-window smoke deferred to
  PR-OAUTH-SUBSCRIPTION-1 (xuan G-X5: `requireReadyConnection`
  rejects subscription connections until SUBSCRIPTION-1 wires the
  send path with runtime smoke).

## Implementation summary

NEW files:
- `packages/core/src/oauth-subscription.ts` — closed types + pure PKCE helpers
- `packages/core/src/__tests__/oauth-subscription.test.ts` — 26 tests
- `apps/desktop/src/main/oauth/claude-subscription-service.ts` — main-process service
- `apps/desktop/src/main/oauth/cloaked-request.ts` — flag-gated, NOT statically imported
- `apps/desktop/src/main/__tests__/claude-subscription-cloak-flag.test.ts`
- `apps/desktop/src/main/__tests__/claude-subscription-ipc-boundary.test.ts`

EDITED:
- `packages/core/src/index.ts` — re-export OAuth types + helpers
- `apps/desktop/src/main/main.ts` — wire 8 IPC handlers
- `apps/desktop/src/preload/preload.ts` — bridge (NO token fields exposed)
- `apps/desktop/src/global.d.ts` — window.maka.claudeSubscription type
- `apps/desktop/src/renderer/settings/SettingsModal.tsx` — ClaudeSubscriptionCard
  in Settings · 账号 (kenji decision #3 location)
- `apps/desktop/src/renderer/styles.css` — card state styles

## Verification

- core build clean; runtime build clean; UI build clean
- desktop typecheck clean
- core tests: 418 pass / 0 fail (+26 OAuth subscription tests)
- desktop tests: 657 pass / 0 fail (+10 new contract assertions)
- a11y gate clean; console gate clean

## Out of scope (PR-OAUTH-SUBSCRIPTION-1 follow-up)

- Subscription send-path: actual inference calls to api.anthropic.com
  via `claude-subscription` provider type. Currently blocked by
  `requireReadyConnection` (xuan G-X5 boundary).
- Runtime smoke for the send-path (cloak ON variant + cloak OFF
  variant returning a clean policy error).
- Device-ID file linkage into send-path (service exposes
  `getOrCreateDeviceId()` ready for SUBSCRIPTION-1 consumption).
- Other providers (Codex, Gemini, Copilot) — one per future PR.
jackwener added a commit that referenced this pull request Jun 21, 2026
borrow
- xuan's transparent local MEMORY.md MVP (c06e13f) — two-switch
  shape (enabled + agentReadEnabled), HTML-comment metadata,
  fail-open parser, 0700/0600 perms, 128 KB cap.
- kenji's boundary (19b0996f): local transparent file MUST NOT
  become implicit durable memory; agent-read default OFF.
- kenji's 3-way confirmation (7749c411 #3): no plugin-provider
  abstraction yet — alma is built-in, pilotdeck is single
  provider with embedding selector, only Hermes is true plugin
  registry. Two-of-three is NOT convergence; defer abstraction.

diverge
- This is a contract anchor, NOT code. Freezes field names + status
  semantics so V0.2 / V0.3 PRs extend predictably without breaking
  V0.1 disk format. If reality changes, edit here first.

risk
- None. Documentation only. No file Maka reads/writes references
  this note at runtime.

What
- `notes/maka-memory-whitebox-contract.md`:
    - V0.1 (live): xuan's settings shape, file format, parse output
      with the `status` enum invariants.
    - V0.2 (forward): origin/status/decay/tags/stable-id extensions
      to the HTML-comment metadata. Parser MUST stay fail-open on
      unknown fields so V0.1 readers don't break on V0.2 files.
    - V0.3 (open): provider abstraction / vector search / cross-
      session recall / Dream Mode / multi-workspace memory.
- Decisions log table with date + decision + source-msg pointers.
- Pointers section linking xuan implementation + Hermes / pilotdeck
  deep-dive notes.

Notes
- The `extract_memory` agent tool design is intentionally deferred
  to V0.2; when shipped it is `permissionRequired: true`, gated on
  both `agentReadEnabled` AND a new `extractToolEnabled` switch
  (you cannot extract memory the agent cannot read).
- Contract test surface §"Contract test surface" lists the 5
  invariants the V0.2 implementation will need to lock down.
jackwener pushed a commit that referenced this pull request Jun 21, 2026
Extract runtime kernel boundaries and durable AgentRun recovery
jackwener pushed a commit that referenced this pull request Jun 21, 2026
…mantics

PR #30's P2 #3 redefines promptSegment.toolCount as the model-visible
(active) tool count, not the full providerTools registry (which includes
the invalid fallback plus any deferred-unloaded / economy-hidden schemas
that never reach the wire). After rebasing onto Tool Source Economy
(PR #34), three economy tests still asserted the old providerTools.length
values; update them to the active counts, which now equal
modelToolNames().length in each case.
jackwener added a commit that referenced this pull request Jun 21, 2026
…doc-merge)

1. **iframe sandbox/CSP language tightened** (@kenji #1):
   - Remove the "separate webContents partition + permissive CSP for
     iframe" promise — DOM `<iframe>` doesn't get an independent
     partition without `<webview>` / BrowserView. Don't promise what
     the DOM can't deliver.
   - Add `allow-forms` and `allow-modals` to the not-allowed list
   - Switch from `src="file://..."` to `srcdoc={await readText()}` so
     content delivery goes through the path-guard read helper
   - Note that global CSP `default-src 'self'` stays locked — sandbox
     attribute carries all isolation

2. **Symlink-aware path guard** (@kenji #2):
   - Both `realpath(artifactRoot)` AND `realpath(target)` required,
     then containment check
   - Mirror PR56 open-path-guard which already has symlink-escape +
     `..` + URL-scheme test coverage

3. **readBinary MIME allow-list** (@kenji #3):
   - Allowlist: image/png/jpeg/gif/webp/svg+xml, application/pdf
   - MIME sniffed at read time, NOT trusted from record.kind or
     record.mimeType
   - Unknown → `reason: 'unsupported_mime'` + UI shows metadata +
     "在 Finder 中打开" / "另存为" actions (no raw bytes to renderer)

4. **Deleted tombstone blocks reads** (@kenji #4):
   - `status: 'deleted'` records immediately return `reason: 'deleted'`
     from readText / readBinary — even if the underlying file still
     exists on disk
   - Surface table updated: deleted row preview shows "此 artifact 已
     删除,预览已停止"
   - Permanent path purge is a separate Settings · 数据 flow

5. **PR108c runtime hook scope** (@kenji #5):
   - First version does NOT do LLM extractor
   - Only deterministic outputs: Write→file record, Edit→diff record,
     Bash with explicit redirect → file record
   - stdout/stderr NOT auto-promoted (noise)
   - LLM extractor (parse ```html``` from assistant message) deferred
     to PR109+

Also: type rename `storagePath` → `relativePath` to enforce contract
in the type system; full reason union widened on both read helpers.

doc total: 1416 lines.
jackwener added a commit that referenced this pull request Jun 21, 2026
Consumes the turn-level contract from PR109c. Implements two of @kenji's
remaining PR109d review gates:

**(d) Failed turn AlertOctagon banner** (@kenji gate #3)
TurnView renders a destructive-tone banner above the assistant body
when `turn.status === 'failed'`. Distinct from PR58 tool-error banner
because it's turn-scoped (the whole turn failed, not just a tool).

Copy is generalized Chinese phrase translated from `turn.errorClass`
via new pure helper `describeTurnErrorClass()` in
`session-status-presentation.ts`:

  timeout                → 请求超时
  auth / 401 / 403       → 鉴权失败
  rate_limit / rate_*    → 触发模型速率限制
  network / fetch / econn → 网络错误
  provider_unavailable / 5xx → 模型服务暂不可用
  tool_failed            → 工具调用失败
  permission_required    → 等待权限确认
  (unknown / empty)      → 未知错误

10 node:test cases lock the mapping invariant: never leak raw enum
identifier, always Chinese, case-insensitive, fallback to "未知错误".

**(e) Lineage badges** (@kenji gate #4)
- Forward badges sit at the top of the new turn:
  "重试自 turn ${shortId}" / "重新生成自 turn ${shortId}".
  Source: `turn.retriedFromTurnId` / `turn.regeneratedFromTurnId` on
  the turn record itself.
- Reverse badges sit at the bottom (above the footer actions):
  "已重试 → turn ${shortId}" / "已重新生成 → turn ${shortId}".
  Source: derived UI-side via existing `deriveTurnLineageMap()` —
  NOT persisted in storage (@kenji review on PR109c kept this side
  immutable).
- Both directions share the same TurnLineageBadge type; UI
  distinguishes via `data-direction="forward"|"reverse"` for tone
  (forward = info-tinted, reverse = success-tinted).
- Click handler scrolls the target turn into view via
  `data-turn-id` attribute querySelector (smooth, block center).

Files:
- `packages/ui/src/components.tsx`:
  - `TurnView` gains `failedReasonLabel` + `lineageBadges` +
    `onLineageBadgeClick` props
  - New `TurnLineageBadge` exported interface
  - Renders failed banner + forward/reverse badge rows
  - `ChatView` gains `turnFailedReasonLabels` +
    `turnLineageBadgesByTurn` + `onLineageBadgeClick` props
- `apps/desktop/src/renderer/main.tsx`:
  - Consolidates `turnFooterActionsByTurn` + `turnFailedReasonLabels`
    + `turnLineageBadgesByTurn` into a single useMemo (one pass over
    materialized turns)
  - `handleLineageBadgeClick()` scrolls target turn via querySelector
- `apps/desktop/src/renderer/session-status-presentation.ts` —
  new `describeTurnErrorClass()` helper
- `apps/desktop/src/renderer/styles.css` — `.maka-turn-failed-banner`
  (destructive), `.maka-turn-lineage-row` + `.maka-turn-lineage-badge`
  (info-tinted forward, brand-deep-tinted reverse)

Per @kenji review (PR109e thread): lineage badges only do scroll/link
— no IPC, no pending mask needed. If a future "retry from badge"
shortcut is added, it MUST reuse the same `pendingTurnActions`
channel from PR109d.

Tests: 234 → 243 desktop passing (+9 describeTurnErrorClass).
check-a11y 0 violations.

Deferred to PR109f:
- Branched session banner ("分自 ${parentSessionName}" + aborted
  starting-point copy "从中断前分支")
- `turn-control-history` fixture + smoke Path 15
jackwener added a commit that referenced this pull request Jun 21, 2026
Two blocker findings from @kenji's contract-consumer review of A2
addressed:

**Blocker #1: user-facing copy no longer leaks internal PR / IPC
terms.** Every JSX-rendered string referencing `PR-CU-*`, `PR-HC-*`,
`PR-REAL-*`, `typed action IPC`, or `ToolOutputDelta` is replaced
with product language. PR-tracking references survive only in
JSDoc / code comments, where they belong.

  - Permission Center footnote: "正在 PR-CU-0 / PR-CU-1(Computer Use
    原生 helper)路上,落地后会替换这里的只读视图。" →
    "权限引导模块(Computer Use 原生 helper)接入后会替换这里的只读视图。"
  - Permission Center action hint titles:
    "操作入口等 PR-CU-0/1 typed action IPC 接入后可用" →
    "权限引导模块接入后提供"
  - Permission Center audit slot empty state:
    "审计日志将在 PR-REAL-3 接入后显示。" → "审计日志接入后显示。"
  - Health Center hard-rule p tag:
    "凭据测试只属于 validation 层,运行态需要 PR-REAL-4 接入实测探测。" →
    "凭据测试只属于 validation 层,运行态需要运行态探测接入后实测。"
  - Health Center footnote:
    "操作入口等 PR-HC-2 typed action IPC 接入后可用。所有真正的运行态探测
    (PR-REAL-4 ToolOutputDelta + send/stream/abort smoke)落地后会自动
    出现在「运行态探测」层。" →
    "运行态修复操作接入后显示。所有真正的运行态探测落地后会自动出现
    在「运行态探测」层。"

**Blocker #2: CAPABILITY_READINESS_COPY.enabled.detail no longer
overpromises runtime probe.** A2 is a read-only consumer; whether
`runtimeProbe.state` is `not_available` / `not_run` / `passed` is
already shown separately in the per-layer breakdown below the
summary pill. The summary copy must not synthesize a conclusion
the snapshot didn't make.

  - "配置、权限、运行态探测都已通过。" →
    "当前快照标记为可用,具体层级见下方。"

The runtime probe layer remains the single source of truth for
"是否运行态通过"; the summary pill's `detail` now defers to it
rather than asserting it.

@kenji non-blocking follow-up #3 (`toLocaleString()` host-locale
drift) deferred to when Permission/Health pages enter visual smoke
— mirrors the `MAKA_VISUAL_SMOKE_LOCALE` deterministic-formatter
pattern. Not blocking A2 since A2 carries no screenshot baseline.

332 desktop tests pass; typecheck + check-a11y + check-console
clean. No behavior change beyond copy.
jackwener added a commit that referenced this pull request Jun 21, 2026
…nd-path deferred to PR-OAUTH-SUBSCRIPTION-1)

WAWQAQ msg `f6d38739` driver. Gate doc: `notes/pr-oauth-subscription-0-gate.md`
(11 hard gates: kenji `cf41871b` × 6 + xuan `2c5aa125` × 5).

## borrow
- alma `~/Downloads/alma-re/readable/main.js:15913-16400` Claude
  subscription PKCE flow, refresh, quota fetch, encrypted token
  persistence, cloaked-request shape.
- Endpoints / client_id / scope strings reproduced verbatim from
  alma since they're Anthropic-server-registered and not negotiable.

## diverge
- Cloaked request headers behind env flag MAKA_CLAUDE_SUBSCRIPTION_CLOAK
  (default OFF) — alma defaults ON. Until product/legal decides on
  Claude Code CLI impersonation, default OFF.
- Refresh failure does NOT auto-logout — alma deletes the token file
  on first refresh fail; we leave it and surface refresh_failed
  state so the user can retry / re-login deliberately.
- Token file gets explicit mode 0o600 on create AND on every save
  (alma uses default mode).
- No telemetry / logs of token lifecycle events (alma logs to stdout).

## risk
- Cloaked CLI identity headers (UA, Stainless, Claude Code system
  prefix) may violate Anthropic ToS. The cloak path is in a
  separate module `cloaked-request.ts` that's never statically
  imported by the default request path; contract test
  `claude-subscription-cloak-flag.test.ts` enforces this.
- On Linux without libsecret, safeStorage.isEncryptionAvailable()
  returns false and tokens persist as plaintext JSON. Card needs a
  future warning (deferred to follow-up PR).
- PKCE verifier in main-process memory only; cleared on
  completeAuthorization success, cancelAuthorization, or TTL prune.

## gate
- 11 hard gates locked in `notes/pr-oauth-subscription-0-gate.md`.
- Implemented contract tests (all PASSING):
  - `oauth-subscription.test.ts` (core, 26 tests): PKCE algorithm,
    URL builder, paste parse, constant-time compare, no token-
    shaped field declarations in the contract.
  - `claude-subscription-cloak-flag.test.ts`: cloak module exists,
    is NOT statically imported, contains impersonation strings,
    service references MAKA_CLAUDE_SUBSCRIPTION_CLOAK env flag.
  - `claude-subscription-ipc-boundary.test.ts`: 6 forbidden field
    keys (access_token / refresh_token / id_token + camelCase)
    forbidden in preload, renderer/, packages/ui/src/.
- Send-path tests + real-window smoke deferred to
  PR-OAUTH-SUBSCRIPTION-1 (xuan G-X5: `requireReadyConnection`
  rejects subscription connections until SUBSCRIPTION-1 wires the
  send path with runtime smoke).

## Implementation summary

NEW files:
- `packages/core/src/oauth-subscription.ts` — closed types + pure PKCE helpers
- `packages/core/src/__tests__/oauth-subscription.test.ts` — 26 tests
- `apps/desktop/src/main/oauth/claude-subscription-service.ts` — main-process service
- `apps/desktop/src/main/oauth/cloaked-request.ts` — flag-gated, NOT statically imported
- `apps/desktop/src/main/__tests__/claude-subscription-cloak-flag.test.ts`
- `apps/desktop/src/main/__tests__/claude-subscription-ipc-boundary.test.ts`

EDITED:
- `packages/core/src/index.ts` — re-export OAuth types + helpers
- `apps/desktop/src/main/main.ts` — wire 8 IPC handlers
- `apps/desktop/src/preload/preload.ts` — bridge (NO token fields exposed)
- `apps/desktop/src/global.d.ts` — window.maka.claudeSubscription type
- `apps/desktop/src/renderer/settings/SettingsModal.tsx` — ClaudeSubscriptionCard
  in Settings · 账号 (kenji decision #3 location)
- `apps/desktop/src/renderer/styles.css` — card state styles

## Verification

- core build clean; runtime build clean; UI build clean
- desktop typecheck clean
- core tests: 418 pass / 0 fail (+26 OAuth subscription tests)
- desktop tests: 657 pass / 0 fail (+10 new contract assertions)
- a11y gate clean; console gate clean

## Out of scope (PR-OAUTH-SUBSCRIPTION-1 follow-up)

- Subscription send-path: actual inference calls to api.anthropic.com
  via `claude-subscription` provider type. Currently blocked by
  `requireReadyConnection` (xuan G-X5 boundary).
- Runtime smoke for the send-path (cloak ON variant + cloak OFF
  variant returning a clean policy error).
- Device-ID file linkage into send-path (service exposes
  `getOrCreateDeviceId()` ready for SUBSCRIPTION-1 consumption).
- Other providers (Codex, Gemini, Copilot) — one per future PR.
jackwener added a commit that referenced this pull request Jun 21, 2026
borrow
- xuan's transparent local MEMORY.md MVP (c06e13f) — two-switch
  shape (enabled + agentReadEnabled), HTML-comment metadata,
  fail-open parser, 0700/0600 perms, 128 KB cap.
- kenji's boundary (19b0996f): local transparent file MUST NOT
  become implicit durable memory; agent-read default OFF.
- kenji's 3-way confirmation (7749c411 #3): no plugin-provider
  abstraction yet — alma is built-in, pilotdeck is single
  provider with embedding selector, only Hermes is true plugin
  registry. Two-of-three is NOT convergence; defer abstraction.

diverge
- This is a contract anchor, NOT code. Freezes field names + status
  semantics so V0.2 / V0.3 PRs extend predictably without breaking
  V0.1 disk format. If reality changes, edit here first.

risk
- None. Documentation only. No file Maka reads/writes references
  this note at runtime.

What
- `notes/maka-memory-whitebox-contract.md`:
    - V0.1 (live): xuan's settings shape, file format, parse output
      with the `status` enum invariants.
    - V0.2 (forward): origin/status/decay/tags/stable-id extensions
      to the HTML-comment metadata. Parser MUST stay fail-open on
      unknown fields so V0.1 readers don't break on V0.2 files.
    - V0.3 (open): provider abstraction / vector search / cross-
      session recall / Dream Mode / multi-workspace memory.
- Decisions log table with date + decision + source-msg pointers.
- Pointers section linking xuan implementation + Hermes / pilotdeck
  deep-dive notes.

Notes
- The `extract_memory` agent tool design is intentionally deferred
  to V0.2; when shipped it is `permissionRequired: true`, gated on
  both `agentReadEnabled` AND a new `extractToolEnabled` switch
  (you cannot extract memory the agent cannot read).
- Contract test surface §"Contract test surface" lists the 5
  invariants the V0.2 implementation will need to lock down.
jackwener pushed a commit that referenced this pull request Jun 21, 2026
Extract runtime kernel boundaries and durable AgentRun recovery
jackwener pushed a commit that referenced this pull request Jun 21, 2026
…mantics

PR #30's P2 #3 redefines promptSegment.toolCount as the model-visible
(active) tool count, not the full providerTools registry (which includes
the invalid fallback plus any deferred-unloaded / economy-hidden schemas
that never reach the wire). After rebasing onto Tool Source Economy
(PR #34), three economy tests still asserted the old providerTools.length
values; update them to the active counts, which now equal
modelToolNames().length in each case.
jackwener added a commit that referenced this pull request Jun 24, 2026
…scape hatch (#214)

Kenji aesthetic-audit reminder 9 (msg `bacde601` 2026-06-24,
findings #2 + #3): two chrome design-system holes still leaking
gradients / non-token blur into the app.

## #2 — darwin sidebar footer gradient

`html[data-os="darwin"] .maka-session-panel-footer` was setting:
`background: linear-gradient(180deg, transparent 0%, oklch(.../0.30) 100%)`

The panel above already has a 50% glass base + 8px backdrop blur,
so the footer doesn't need any additional material — but the
180deg fade IS the same "上面灰下面白" pattern WAWQAQ called out
twice before in chat sweep (`1e693dee` / `5d3b10e5`). Replaced
with flat `oklch(...) / 0.15` + hairline border-top.

## #3 — `[data-gradual-blur-layer]` block

A 56-line block that let any element opt into:
- `linear-gradient(to bottom/top, var(--background) → transparent)`
- `backdrop-filter: blur(6px)`
- raw `z-index: 5`

just by wearing the `data-gradual-blur-layer` attribute. Kenji
called it a "blur escape hatch" — a future surface could re-
introduce blur and a non-token z-index without going through
any review of the design system.

`grep -rn data-gradual-blur-layer --include=*.tsx` confirms zero
JSX consumers — the CSS is dead. Removing the entire block kills
the escape hatch + retires one of the four raw z-index numbers
kenji flagged in finding #4.

`notes/reference-atlas.md` references stay — those describe what
reference upstream does, not what we ship.

## Diff

1 file changed, 18 insertions(+), 60 deletions(-) net delete.
No JSX touched. No tests touched (no contract pinned this attr).
Footer change is darwin-only so non-darwin platforms unaffected.

Far from PR #202 styles.css edit area (onboarding ~1500-1700 +
cubic-bezier sweep on transition rules). PR #210 touches main.tsx
debounce, not styles.css. No merge conflicts.
jackwener added a commit that referenced this pull request Jun 24, 2026
…ji audit #3+#4) (#219)

* test(contract): lock drawer.tsx + tabs.tsx motion escape hatches (kenji audit #3 + #4)

Sibling of PR-FE-BUG-HUNT-12 (which locked ui.tsx). Closes kenji's
audit reminder 4-6 findings #3 + #4 (msg `6cc0e04d` 2026-06-24):

## drawer.tsx escape hatches (5)

- `cubic-bezier(0.32,0.72,0,1)` ×2 — iOS-style settle curve, raw on
  backdrop + popup. Should move to `--ease-drawer` token.
- `duration-450` ×2 — drawer settle duration. Doesn't match any
  current `--duration-*` token. Should tokenize.
- `transition-[transform,box-shadow,height,background-color]` ×1 —
  animating `height` is layout-trigger. Intentional because drawer
  snap points (peek / half / full) drive variable height and
  `transform: scaleY` would distort children. Layout-property
  transition is acknowledged here.
- `backdrop-blur-sm` ×1 — same finding as kenji #6.
- `z-50` ×1 — same convention as ui.tsx; pending tokenization.

## tabs.tsx escape hatches (3)

- `transition-[width,translate]` ×1 — animating `width` is layout-
  trigger. Cleaner refactor is `translate + scaleX` with measured
  base width, but that needs measurement infrastructure not in
  place. Layout transition acknowledged.
- `duration-200` ×1 — matches `--duration-base` (200ms) by value
  but uses bare Tailwind utility.
- `ease-in-out` ×1 — generic Tailwind easing, not project's
  `--ease-out-strong`.

## Approach

Same as PR-FE-BUG-HUNT-12 (ui.tsx contract): pin EXACT count of
each allowlisted pattern. Adding new sites OR removing stale
allowlist entries both fail. Plus a "no other layout transitions"
sweep that verifies no NEW `transition-[<bracketed-list>]` showed
up beyond the allowlisted ones.

Not touching source — touches primitive wrappers; each tokenization
needs kenji / WAWQAQ review per primitive. Contract locks perimeter.

## Diff

1 new file, ~140 lines test. No source change, no visual change.
No overlap with PR-FE-BUG-HUNT-12 (different file scope) or any
other open PR.

## Verification

Disk still ~100%, couldn't run tests. Greenfield contract is
self-validating — first run confirms counts match current file
state. The allowlist counts were derived from grep + manual read.

* test(self-review): fix wrong occurrence counts in primitives contract

Verified counts via `grep -o ... | wc -l` before pushing PR #219 to
review and caught my own miscounts:

- drawer.tsx `z-50`: 1 → actual 3 (gesture wrapper, backdrop scrim,
  cushion overlay all stack at z-50)
- drawer.tsx had TWO additional `transition-[<bracketed>]` patterns
  I missed: `transition-[transform,box-shadow,height,background-color,
  margin,padding]` (drawer popup bottom-edge — worse layout trigger
  than the base, adds margin+padding) and `transition-[background-
  color,box-shadow]` (drawer-internal switch handle — paint-only,
  safe)
- tabs.tsx had ONE additional `transition-[color,background-color,
  box-shadow]` pattern (tabs trigger paint transition — paint-only,
  safe)

All five additions added to the allowlist with explicit reason. The
`no unexpected transition-[<bracketed>]` sweep rewritten to subtract
the full allowlist from the found set (set-difference) instead of
naively iterating, so it doesn't false-fail when there are multiple
paint-only patterns.

Pre-push count verification is now baked into the test contract
itself — if a future maintainer changes a count without re-running,
the test will fail loudly.
jackwener added a commit that referenced this pull request Jun 24, 2026
…k-run buttons (#227)

WAWQAQ msg `0951e3b1` + `3871e56b` 2026-06-25, two related bugs in
the Daily Review surfaces:

## #2 — 分析模型 Select 特别长

`.settingsRow` is a 2-column grid `minmax(150px, 0.36fr) 1fr`. On a
wide settings page the right control column can be 500px+. With
`w-full` on the `<SelectTrigger>`, the dropdown stretched the entire
right column — hence "特别长,完全就不对".

Fix: add a `.settingsRow > .settingsBaseSelectTrigger` rule that
caps `max-width: 320px` and `justify-self: end`, mirroring the
existing `.settingsField[data-orient="horizontal"]` rule that
already caps inputs on the parallel horizontal-row layout. The
Select now visually sits in the same right-edge position as the
Switch toggles in adjacent rows.

## #3 — 生成 buttons 直接靠近文本

`.maka-daily-review-quick-runs` was `display: flex` with no
`justify-content`, so the 生成每日回顾 / 生成深度分析 buttons sat
flex-start (left-aligned), hugging the explanatory paragraph
directly above them.

Fix: `justify-content: flex-end` pushes them right, matching the
panel's right-action convention (the 复制 / 粘到 / 保存 row below
already sits right via its `1fr auto` grid).

## Diff

1 file, +18 / -1. CSS-only.
jackwener added a commit that referenced this pull request Jun 25, 2026
Per @kenji audit `26a221be` against #237 main `4e0247f6`:

**#1 — bottom 「想先看看效果?」 hero buttons left-aligned.**
PR #236 right-aligned the top 「打开每日回顾」 button via
`.settingsFeatureStatusHeroActions` but the bottom 「生成每日回顾 /
生成深度分析」 row still used inline `style={{ display: 'flex', gap:
8, marginTop: 8 }}`, so it stayed glued to the paragraph margin.
Route the bottom row through the same class. Add `gap: 8px` to the
class so multi-button clusters keep proper spacing.

**#2 — 执行时间 disabled when the master switch is off.**
The disable condition was `formDisabled || savingKey === 'executeTime'
|| !(effectiveConfig?.enabled ?? false)`. That last clause blocked the
common UX of "pick a time first, then turn it on". Drop it — the time
value is harmless to edit while disabled (it doesn't fire until the
switch flips), and the disabled state was misread as "broken UI".

**#3 — 分析模型 default-row label still verbose.**
WAWQAQ's directive: "所有模型选择只用模型名". PR #236 went from
`使用对话默认模型(Codex OAuth · email · gpt-5.5)` to
`对话默认(gpt-5.5)` — better, but the model id parenthetical still
mixes "selectable model" with "follow chat default". Per @kenji's
"don't conflate the two semantics", the default row becomes plain
`跟随对话默认`; the explicit options carry the model id.

**#4 — duplicate model ids across connections looked unselectable.**
When two enabled connections both expose `gpt-5.5`, the flat
`model`-only labels read as two identical entries. Detect the collision
at build time and append `· ${connection.name}` ONLY to the colliding
entries; unique model ids stay terse per WAWQAQ's directive.

Deferred (with @kenji concurrence):
- **#5** (`data-control-width` semantic API replacing CSS-selector width
  patches) — pure refactor, no end-result change. WAWQAQ explicitly
  said 「只看最后的结果」.
- **#6** (collapse Daily Review hero card pile into a single grouped
  settings surface) — bigger visual restructure judgment call; needs
  WAWQAQ sign-off before swinging at the whole page layout.
jackwener added a commit that referenced this pull request Jun 25, 2026
@kenji audit thread `#my-ai:4821a792` msg `e4cfbfb0` vs main `71d88454`.
WAWQAQ msg `782a1663` + `继续`: bundle these, don't split.

**#1 — Memory page top control rows had no outer card surface.**
The three control rows (本地 MEMORY.md / 模型上下文可读取 / 项目指令
文件) were direct children of `.settingsStructuredPage`, which is
transparent. `.settingsFormRow` only carries a hairline bottom border,
so users saw three floating rows without the grouped-card visual every
other settings surface uses. Wrap them in `.settingsRows` so they
inherit the 12px outer border + radius + overflow:hidden treatment.

**#2 — Plan Reminder delivery picker showed text-only IM channels.**
Settings → 机器人对话 switched to real brand logos (Iconify simple-icons)
in #BOT-SETTINGS-UI-0, but the chat-side Plan Reminder delivery flow's
`平台` select was still rendering plain Chinese labels. Same channels
should read the same everywhere.
- Move `BOT_BRAND` to `packages/ui/src/bot-brand.ts` so both surfaces
  share a single brand-metadata source instead of one duplicating the
  other.
- Extend `PlanReminderSelect` to accept a third tuple slot (optional
  `ReactNode` icon); `PlanReminderPanel` now passes an `<IconifyIcon
  icon={brand.iconifyId}>` per provider with the existing offline
  glyph fallback.
- Update `SettingsModal.tsx` to import `BOT_BRAND` from `@maka/ui`
  instead of declaring a local copy.

**#5 — Two inline `style={{ marginTop }}` spacing patches.**
- `Alert variant="error" style={{ marginTop: 12 }}` → `className=
  "settingsSurfaceAlert"`.
- `<div className="settingsActionRow" style={{ marginTop: 8 }}>` →
  `className="settingsActionRow settingsNoticeAction"`.
The new classes pin the spacing decision in CSS where the design
tokens live, instead of inline JSX.

Deferred per kenji's own scope note + WAWQAQ 「只看最后的结果」:
- **#3** (`RadioCard` / `ChoiceCard` primitive for theme/palette picker)
  — pure design-system refactor, no visible delta on Settings → 外观.
- **#4** (`SettingsSelect` uplift to rich-option contract) — bigger
  cross-cutting refactor; current fix covers the immediate model-name
  and disambiguation issues kenji raised in round 1.
- **#6** (layout-property transitions in sidebar/tabs/accordion) —
  kenji explicitly flagged these as visual-smoke-gated; will land
  alongside the smoke config in a separate PR.
jackwener added a commit that referenced this pull request Jun 25, 2026
… dvh (#250)

@kenji audit msg `232aec0f` (`#my-ai:c28a6293`). Round 3 against
`af66ebb7` post-#249.

**#2 — Plan Reminder select collapsed state was text-only.**
PR #247 added a brand icon to each `<SelectItem>`, but `<SelectValue
/>` defaulted to rendering just the label string — open dropdown
showed a logo, collapsed trigger showed plain text. Build a
`value → { label, icon }` lookup inside `PlanReminderSelect` and pass
a function-child to `SelectValue` so the picked state renders the same
icon + label row the dropdown items render.

**#6 — `100vh` residue in renderer CSS.**
Four sites still on `100vh`:
- `maka-tokens.css:760` `.maka-shell` `height`
- `maka-tokens.css:1941` `.maka-modal` `max-height: calc(100vh - 80px)`
- `styles.css:5028` `.maka-help-modal` `max-height: calc(100vh - 96px)`
- `styles.css:13121` `.maka-onboarding-stack` `min-height: calc(100vh - 84px)`

Replace each with `100dvh` so Electron browser frames with dynamic
chrome (or a future mobile-style viewport) don't get half a viewport
of height calculation drift. Other renderer surfaces already use
`100dvh`; this brings the legacy four into line.

Deferred per kenji's own scope:
- **#1** feishu/dingtalk official-brand-kit sourcing — separate PR.
- **#3 / #4** SettingsSelect / Segmented / ChoiceCard primitive
  unification — design-system refactor; no end-result delta on its
  own.
- **#5** layout-property transitions in sidebar/tabs/accordion —
  kenji explicitly gated on visual smoke harness.
jackwener pushed a commit that referenced this pull request Jul 6, 2026
…ete (#520 PR8) (#562)

* feat(ui): migrate SearchModal result list to Base UI Autocomplete (#520 PR8)

SearchModal's hand-rolled roving-focus result list (activeResultIndex /
moveActiveResult / jumpActiveResult / keyboardSelectionHandledRef /
handleResultKeyDown / data-active) is replaced by Base UI Autocomplete in
activedescendant mode:

- Autocomplete.Root inline + mode="none" + autoHighlight="always" +
  filter={null}: the list renders inline in the modal body (no floating
  popup), Autocomplete does not re-filter the server-side IPC results, and
  the first result is always highlighted so Enter works without an extra
  ArrowDown.
- Autocomplete.Input renders the input via the shared InputGroupInput
  primitive (render prop); ArrowUp/Down/Enter/Escape keyboard nav is owned
  by Autocomplete (floating-ui useListNavigation).
- Autocomplete.List + Autocomplete.Item replace the hand-rolled
  <ul role=listbox> + <li><button role=option>; item onClick fires
  selectResult for both pointer click and Enter on the highlighted item.
- aria-activedescendant on the input is now managed by Autocomplete.

selectResult navigation (sessionId + turnId, restoreFocus: false), the
debounced IPC search, the inflight ticket guard, the unmount invalidation,
the clear button, the snippet rendering, and all copy/states are unchanged.

The roving-focus kbd-nav interaction (ArrowDown moved focus to the result
button) becomes activedescendant (input keeps focus, active item reflected
via aria-activedescendant). a11y is more standard; the interaction habit
shifts. Home/End now move the input cursor (Base UI ComboboxInput default);
jump-to-first/last result is not bound for now (to be confirmed by manual
testing per PR8 plan).

search-modal-lifecycle-contract: the kbd-nav it-block is rewritten to lock
the Autocomplete shape (Root props + Item onClick + selectResult navigation)
instead of the roving-focus implementation; the focus-policy it-block drops
the activeResultIndex/moveActiveResult assertions; the empty-query it-block
repoints onChange -> onValueChange. CSS .maka-search-modal-result[data-active]
-> [data-highlighted] (Autocomplete item highlighted state).

Verification: typecheck clean, @maka/desktop 2076/2076, @maka/ui 43/43,
sidebar-search-modal-open screenshot AE=2655 (fuzz 5%, RMSE 0.0006) vs main.

* feat(ui): migrate CommandPalette to Base UI Autocomplete (#520 PR8)

CommandPalette's hand-rolled activedescendant result list (highlight
state + onInputKeyDown + reset useEffect + <div role=listbox> +
<Button role=option data-active>) is replaced by Base UI Autocomplete:

- Autocomplete.Root inline + mode="none" + autoHighlight="always" +
  filter={null}: the list renders inline in the modal body, Autocomplete
  does not re-filter the palette's own fuzzy + content-search combined
  list, and the first command is always highlighted so Enter works
  without an extra ArrowDown.
- Autocomplete.Input renders the input via the shared InputGroupInput
  primitive (render prop); ArrowUp/Down/Enter/Escape keyboard nav is
  owned by Autocomplete. aria-controls + aria-activedescendant are
  managed by Autocomplete — no manual wiring.
- Autocomplete.List + Autocomplete.Group + Autocomplete.GroupLabel +
  Autocomplete.Item replace the hand-rolled <div role=listbox> +
  <div group> + <Button role=option>. Autocomplete.Item fires onClick
  for both pointer click and Enter on the highlighted item, so commit()
  covers both paths.
- The CornerDownLeft cursor hint is now CSS-driven
  (.maka-palette-cursor visibility via [data-highlighted]) instead of
  the JS `!cmd.hint \&\& active` conditional, since the hand-rolled
  highlight state is gone.

commit() (commitPendingRef + committedCommandId + await run + finally
close), the fuzzy filter, useThreadSearch content-search, grouped
rendering, and all copy/states are unchanged.

The kbd-nav interaction stays activedescendant (input keeps focus,
active item reflected via aria-activedescendant) — same mode as before,
now owned by Autocomplete. Home/End now move the input cursor (Base UI
ComboboxInput default); jump-to-first/last command is not bound for now
(to be confirmed by manual testing per PR8 plan).

Contracts: command-palette-a11y-copy-contract #1 (listbox) rewritten to
lock the Autocomplete shape; #2 import regex drops Button (no longer
used); #3 CSS data-active -> data-highlighted; #6 commit-gate block
boundary is commit() (onInputKeyDown gone); #7 highlight reset is now
autoHighlight="always" (no hand-rolled state). renderer-utility-primitives
row assert repoints <Button role=option> -> <Autocomplete.Item>. CSS
.maka-palette-item[data-active] -> [data-highlighted] + .maka-palette-cursor.

Verification: typecheck clean, @maka/desktop 2076/2076, @maka/ui 43/43,
command-palette-open screenshot AE=7250 (fuzz 5%, RMSE 0.0017) vs main.

* fix(ui): Autocomplete inline open + item-press guard (#562 review)

P1 (blocker): both Autocomplete.Root used `inline` without `open`. Per
Base UI docs, `inline` requires `open` so the list is treated as visible:
"Specify open unconditionally in conjunction with this prop so the list is
considered visible: <Autocomplete.Root inline open>". Without `open`,
defaultOpen=false -> the input is not data-popup-open and keyboard nav /
activedescendant do not work. Add `open` to SearchModal + CommandPalette.

P2-a: object items (<Autocomplete.Item value={result/cmd}>) had no
itemToStringValue, and onValueChange did not filter item-press. Add
itemToStringValue (result.title / cmd.label) so item-press never writes
[object Object] into the query, and skip onValueChange when
details.reason === 'item-press'. In inline mode selectionMode='none' + no
Popup means shouldFillInput is currently false (popupRef.current null), so
this is defensive — but correct regardless of future Popup changes.

P2-b: contract tests now lock open + itemToStringValue + onValueChange
item-press-filter on Autocomplete.Root (catches P1/P2-a regressions), plus
the existing inline/mode/autoHighlight/List/Item shape.

Verified via CDP probe (command-palette-open fixture): input has
data-popup-open + aria-controls + aria-activedescendant; first item
data-highlighted (autoHighlight); ArrowDown 0->1->2, ArrowUp 2->1.
typecheck clean, @maka/ui 43/43, @maka/desktop 2076/2076.
command-palette-open AE=48000 (RMSE 0.011) vs main — larger than pre-fix
AE=7250 because open now correctly renders the first-item highlight bg +
input data-popup-open state (the bug state hid these). search-modal-open
AE=2655 (RMSE 0.0006, unchanged).

* test(ui): lock Home/End input-cursor decision (#562 P2-c)

P2-c decision: accept Base UI ComboboxInput's default — Home/End move the
input cursor, not the highlight. The old roving-focus jumpActiveResult
(SearchModal) / hand-rolled onInputKeyDown highlight jump (CommandPalette)
must not return. Lock via doesNotMatch on jumpActive( / onInputKeyDown.

* fix(ui): keepHighlight + empty-state inside Autocomplete.List (#562 review P2)

P2-1 (keepHighlight): both Roots had autoHighlight="always" but no
keepHighlight. keepHighlight=false (default) sets resetOnPointerLeave=true
(AriaCombobox.js:855), so pointer leave clears activeIndex, then the
autoHighlight="always" effect (line 678-682) re-highlights the first item
-> hover item[2] -> leave -> Enter ran the first item, not the hovered one.
Add keepHighlight so pointer leave preserves the hovered item.

P2-2 (empty-state): CommandPalette rendered a standalone <div> for empty
and <Autocomplete.List> only when non-empty, so the input lost its listbox
reference with no matches. Unify on Autocomplete.List always, with the Empty
primitive inside. Autocomplete.Empty is not used: filter={null} + mode="none"
keeps filteredItems non-empty (the palette's fuzzy filter is external), so
Autocomplete.Empty would never trigger.

Contracts lock keepHighlight on both Roots + empty-state-must-not-use-
standalone-div.

keepHighlight verified by source (resetOnPointerLeave = !keepHighlight) +
Base UI docs. CDP hover-leave probe could not reliably trigger floating-ui
useListNavigation's hover highlight (synthesized pointermove does not fire
its hover detection), so pointer-leave preservation is source-guaranteed,
not probe-verified. typecheck clean, 2076/2076, screenshots unchanged
(command-palette-open AE=48000, search-modal-open AE=2655 vs main — same as
prior commit, keepHighlight/empty-state don't affect non-empty visuals).
anaconda110 added a commit to anaconda110/maka-agent that referenced this pull request Aug 3, 2026
…pache#3)

res/values/styles.xml uses Theme.AppCompat.DayNight.NoActionBar but
app/build.gradle dependencies did not declare androidx.appcompat, causing
aapt2 to fail with 'resource style/Theme.AppCompat.DayNight.NoActionBar not
found' during resource compilation. Add implementation 'androidx.appcompat:appcompat:1.7.0'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant