Compare commits

...
177 Commits
Author SHA1 Message Date
mesalogo ff61da28a0 test: wait for workspace readiness during cache rotation
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-10-04 12:42:53 +08:00
mesalogo 362fe50402 test: align supervisor settings regressions with grouped tabs
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Select the review tab before saving and reopening its settings, use the current model heading and visible screenshot anchor, and allow the 1000-row activity regression 60 seconds on shared CI. Preserve all saved-value, paging, trimming and layout checks.
2026-10-04 11:58:05 +08:00
mesalogo 273a7fa7a5 chore: prepare desktop 0.15.10 release
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Document unified navigation, Supervisor controls, activity and desktop performance changes. Keep Agent 0.15.3 and record schema 58 backup requirements, NORMAL durability tradeoffs, and bounded real Host evidence.
2026-10-04 11:33:33 +08:00
mesalogo 74efbcdc17 fix(workbar): reduce divider spacing to 4px
Reduce the workspace left margin and resize target from 9px to 4px so the divider takes less space without overlapping workspace content.
2026-10-04 10:12:40 +08:00
mesalogo e89db96b85 feat(workspace): unify projects and conversations in one menu
Replace separate project and activity navigation with a fixed-height two-line button and a shared workspace menu. Browse local, remote and channel projects, preview sessions on hover, restore the last opened conversation, and show ten recent conversations alongside active work.

Preserve project settings, recovery actions, draft leave guards and completion reminders. Reuse conversation summaries and windowed lists without hover fetches or polling.

Validation: focused component and App tests, real Electron layout and interaction checks, typecheck and lint passed. The earlier full suite timed out and reported Supervisor settings/layout failures.

Release note: 左上角统一项目与活动入口,支持分类查找、悬停预览和最近 10 个会话;双行按钮整体点击,保留项目设置与跨项目会话切换。
2026-10-04 09:53:11 +08:00
mesalogo ae4c6d2423 perf(disk): remove runtime caches left by earlier versions after startup 2026-10-04 09:45:50 +08:00
mesalogo 91edcf5e8b docs: add the desktop performance phase 1 report and refresh the principle gaps 2026-10-04 09:26:31 +08:00
mesalogo 0dd4d1d642 perf(db): relax frequent local writes, checkpoint WAL off Main, index startup recovery (schema 58) 2026-10-04 03:07:16 +08:00
mesalogo 75e50fc194 fix(activity): summarize every loaded conversation instead of the first 5,000 2026-10-04 03:07:16 +08:00
mesalogo 29a134b804 fix(workbar): keep scroll arrows clear of tab close buttons
Reserve space on both sides of the tab viewport for overflow arrows so they cannot cover tab close controls. Keep that space while arrows are hidden or disabled to avoid layout shifts.

Add Electron coverage for arrow bounds and native closing at the right viewport edge; update tab layout documentation.

Validation: 19 focused tests passed; typecheck and lint passed. Full suite did not complete before interruption and reported failures in supervisor settings and supervisor layout tests.

Release note: Fix workbar scroll arrows covering tab close buttons when many tabs are open.
2026-10-04 01:14:41 +08:00
mesalogo 59d36c2258 docs: add module conventions for store, selectors, sync and views to the performance principles 2026-10-04 00:35:45 +08:00
mesalogo 39fc15efe0 docs: record activity history incremental save and paging (P4) 2026-10-04 00:33:19 +08:00
mesalogo 5d0ca44b22 test: compare upgraded schema versions with ASSISTANT_DATABASE_SCHEMA_VERSION
The supervision upgrade tests asserted the latest version literally (56);
schema 57 (activity history order and indexes) made them fail.
2026-10-04 00:26:10 +08:00
mesalogo 78beade289 fix(composer): clarify DSH actions and keep stop confirmation inline
Name the browser and service controls explicitly for DSH. Keep confirmation and cancellation together in the composer action row, retain the interruption warning as a tooltip and accessible description, and show stopping progress on the confirmation button.

Validation: 20 focused tests passed; typecheck and lint passed.

Release note: 明确 DSH 网页和服务按钮名称,停止确认与取消按钮同行显示,避免确认区单独撑开聊天输入区。
2026-10-04 00:17:51 +08:00
mesalogo bf9850a6a9 perf(activity): keep only loaded activity pages in the renderer, driven by Main's summary
The renderer no longer holds the whole history. Startup sends queued
changes, lets Main reconcile leftover pending/running records against the
stored tasks (same localized interruptedOnRestart line), then loads the
first page (200). The Activity page loads a full visible batch (500, as
before paging); "load more" shows the next batch and loads the next page
behind it; a filter switch refetches that filter's first page; leaving the
page trims to one batch. Counts, project/conversation groups, titles,
conversation status and the clear count come from activityHistory.summary
(refreshed on entry, by the page timer, the refresh button and after load
more / filter changes).

Records that are not loaded are still updated incrementally: the update
contract gains content-matched changes (upsert-call, update-request,
resolve-approval, settle-request, remove-request, remove-call) that Main
applies in SQL to shown rows; the store applies the same semantics to its
pages through the shared reference. update returns Main's stored call
records so a call whose earlier record was not loaded keeps one id. Task
refresh reconciles via Main SQL. Legacy localStorage records migrate as
incremental changes; the renderer no longer calls replace.

Tests: randomized sequences of every action, page loads, trims, flushes and
injected failures against the former full-list code on a real database;
first page <= 200 after startup; flush payload depends only on changed
records (including unloaded ones); ActivityPanel output with pages + summary
equals the full-list rendering for randomized datasets with duplicates;
restart interrupts leftover records with the same detail; clear empties the
database, pages and summary; SQL change semantics equal the reference.
2026-10-04 00:03:33 +08:00
mesalogo 0e9c9d4927 fix: align supervisor settings layout and default model naming
Group settings tabs and forms in a single surface with consistent spacing. Rename the model section to cover model selection and execution limits, and use Default model for the default connection option.

Release note: 修复监督者设置页签与表单贴边的问题,统一设置布局,并将默认模型选项名称改为默认模型。
2026-10-03 23:43:58 +08:00
mesalogo 352da66dbd perf(activity): page, summarize and reconcile activity history in Main by index
Schema 57 rebuilds activity_history_records with order_seq (newer = larger;
a move to the front assigns a new maximum) and stored generated columns
(record_id, occurrence, status, status_group, kind, request_id,
conversation_id, project_id, created_at) with indexes. The order is migrated
from record_order_json exactly; legacy duplicates keep their occurrence and
only the first is shown. record_order_json is no longer written.

ActivityHistoryRepository owns the SQL: page({before, limit <= 500, filter,
projectId, conversationId}) newest first by index, summary() with the counts
and conversation titles/status the Activity page derived from the full list,
reconcile() joined with tasks with a renderer-localized detail line, update()
applied directly in SQL without reading the history, replace() for the
legacy migration, clear(). New activity-history:page/summary/reconcile IPC
with zod validation; page and summary run on the readonly worker.
perf:main measures 24,000 records: startup load 106 ms on Main (14 MB) ->
first page + summary 13.5 ms on the worker (0.6 ms loop block, 114 KB).
2026-10-03 21:42:52 +08:00
mesalogo 6aab78b9fa perf(activity): save activity history incrementally through an activity store and sync queue
The renderer no longer sends the whole history on every change. Domain
actions on an external activity store update the list and queue the matching
contract changes; activity-sync.ts flushes them every 250 ms via
activityHistory.update (<= 20,000 per call), keeps failed changes queued in
order with backoff retry, and flushes on quit and unmount. replace remains
only for the legacy localStorage migration; clearing uses the new
activityHistory.clear IPC. App's activity state is gone; ActivityPanel reads
a selector. update is now required in the desktop API.
2026-10-03 20:45:51 +08:00
mesalogo 2adee13638 fix(composer): keep first focus on the expert menu when opening options
48d7526 widened the options dialog first-focus selector to inputs, so the new story graph switch above the menus took initial focus. Keyboard users no longer landed on the expert / work mode menus like the model picker. Restore the button-only target; the switch stays reachable with Tab.
2026-10-03 20:04:55 +08:00
mesalogo 942fe3206c docs: record PERF-13 composer, route boundary, task store and conversation switch results 2026-10-03 19:54:49 +08:00
mesalogo 6b634c5d6b chore(lint): set the App.tsx max-lines cap to 8,000 after the supervisor commit 2026-10-03 19:17:56 +08:00
mesalogo 739f26702b chore(lint): lower the App.tsx max-lines cap to 7,900 2026-10-03 19:16:53 +08:00
mesalogo 48d7526b7e feat: configure supervisor model and story graph use per conversation
Add a supervisor text-model selector using existing connections and a per-conversation story graph switch above knowledge settings. Hide the switch when Supervisor is disabled and enforce conversation preferences at local and remote tool execution boundaries.

Release note: Choose a dedicated supervisor model and control whether each conversation can query existing stories and experiences. Enabling graph access does not start a review or automatically insert context.
2026-10-03 19:16:35 +08:00
mesalogo 492f1d208a perf(renderer): render App once per conversation switch and keep the titles map stable 2026-10-03 19:14:54 +08:00
mesalogo 651475d17a perf(renderer): move assistant tasks and artifacts into a task store with narrow selectors and a sync module 2026-10-03 18:58:01 +08:00
mesalogo 2e3d2e77a9 perf(renderer): memoize keep-alive routes, workspaces and sidebars behind stable callbacks 2026-10-03 17:59:20 +08:00
mesalogo 41be9f8076 perf(renderer): extract the chat composer into a memoized Composer with stable actions and a popup store 2026-10-03 17:34:57 +08:00
mesalogo 010aafb6f4 perf(startup): load canvas, node-pty, ssh2, html-to-text and the OpenCode SDK on first use
Main module evaluation drops from ~420 ms to ~330 ms. Types stay as import type; the DeepSeek Harness chunks stay static because the protocol chunk shares code with Main startup paths.
2026-10-03 17:15:13 +08:00
mesalogo 68cc0792e1 perf(startup): build the local tool environment in the background and gate its consumers
Runtime creation, MCP launches, Obsidian, native clients and the tool-environment IPC await the initialization promise instead of startup awaiting it before the window path. A failure still fails startup with the same diagnostic.
2026-10-03 16:56:13 +08:00
mesalogo b28ddd4ba8 chore: commit pending workspace changes (supervisor settings tabs, conversation keep-alive budget, PR article updates) 2026-10-03 16:22:42 +08:00
mesalogo 805acc3fce fix: restore the jump-to-bottom glide and keep user state, selections and scroll anchors in windowed lists
Pause scroll writes while the jump-to-bottom smooth scroll runs; keep rows with pending questions, approvals, recent interactions and selection ends mounted; cache Mermaid results; copy text spanning unmounted rows; anchor the sidebar on id changes; disable native overflow-anchor for windowed lists; write terminal output on the leading edge and before clear; forward remote events every 16 ms; flush streaming text before copy/export; add performance principle P10 (no UX regressions).
2026-10-03 16:19:56 +08:00
mesalogo a620c22676 fix: keep the notes list accessible in narrow workspaces
Default the notes list to 200px with a 160px minimum and hide it automatically only below 600px. Keep the detail toolbar toggle available to open a drawer, with dismissal and note-switching support. Restore reading positions after rich text content effects populate the record.

Include the existing document-table and file-tree scroll anchoring styles in this file-level commit. Validation: 143 related tests, Electron layout regression, typecheck and changed-file lint passed; repository lint is blocked by unrelated App.tsx changes.

Release note: The notes list takes less space and remains accessible through a drawer in narrow windows. Existing saved list widths are preserved.
2026-10-03 16:02:27 +08:00
mesalogo 10065da8ac docs: record the third parallel performance round 2026-10-03 13:54:24 +08:00
mesalogo b2eb86a0b4 perf: window the notes grid, workspace file tree and knowledge documents table 2026-10-03 13:54:24 +08:00
mesalogo f4458afe4b perf: coalesce terminal output in Main and write it to xterm once per frame 2026-10-03 13:54:24 +08:00
mesalogo 10c5162811 perf: parse, chunk and hash documents in a worker thread and add startup and document-parse benchmarks 2026-10-03 13:54:24 +08:00
mesalogo 9d03217f99 docs: add GoodBuddy performance principles and review checklist 2026-10-03 13:54:24 +08:00
mesalogo 55f554d13c fix: keep composer context usage on the action row by letting it shrink 2026-10-03 13:21:04 +08:00
mesalogo 9527402003 release: prepare Desktop 0.15.9 and Agent 0.15.3
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
Document shared review timelines, stories and experiences, spiral navigation, conversation performance work, and Execute configuration behavior. Include schema 56 backup and cost boundaries, Agent tool-schema compatibility, and four successful current-source real Host calls with checkpoint persistence. Full release validation runs in CI.
2026-10-03 10:45:54 +08:00
mesalogo 50481deaa1 fix: let story graph views fill the canvas column with compact controls 2026-10-03 10:23:09 +08:00
mesalogo baa6defc0c docs: record what this performance round deferred and why 2026-10-03 06:28:13 +08:00
mesalogo 775763eee0 perf: keep sidebar ids stable during streaming and measure marquees from the resize observer 2026-10-03 06:28:12 +08:00
mesalogo 82107ad86a docs: record second parallel performance round 2026-10-03 05:53:33 +08:00
mesalogo b6c7de5f91 perf: move conversation persistence, refresh and agent events out of App
Persistence and history loading, refresh scheduling and the agent event handler work against the conversation store outside React; App.tsx drops to 9,566 lines and the lint ceiling to 9,600.
2026-10-03 05:53:33 +08:00
mesalogo 663aa85e1a perf: window the sidebar, cache Markdown trees and window chat scrolling once per frame
The chat pane decides re-windowing once per frame from scrollTop and cached row heights instead of reading layout per scroll event. Rendered Markdown hast trees are cached, so remounted rows skip parsing. Sidebar lists of 60+ conversations mount only rows near the viewport. The perf driver scrolls the windowed sidebar to reach a row.
2026-10-03 05:53:10 +08:00
mesalogo 3c31f0b499 perf: read conversation lists in the readonly worker and diff activity history saves
conversations:list-summaries and conversations:get run on the readonly query worker. Activity history replace validates and serializes only changed records; a new activity-history:update IPC writes one record. Adds npm run perf:main.
2026-10-03 05:53:10 +08:00
mesalogo 169941ceda docs: record conversation store, timeline windowing and read worker results 2026-10-03 02:53:51 +08:00
mesalogo fdf7d83b20 perf: move conversations into an external store and window the message timeline 2026-10-03 02:53:51 +08:00
mesalogo f571301764 perf: run knowledge, story graph and conversation search reads in a read-only worker 2026-10-03 02:53:51 +08:00
mesalogo 53b9ecbcae chore: add architecture lint ratchets and perf regression thresholds
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
ESLint allowlists (only to shrink) for DatabaseSync, synchronous fs in src/main and direct window.goodbuddy subscriptions in components, plus a max-lines ratchet for App.tsx; docs demos and vendored scripts are no longer linted. perf:check (or GB_PERF_CHECK=1 perf:app) fails a non-throttled benchmark that exceeds build/perf-thresholds.json. Records the results in the PERF plan.
2026-10-02 23:11:11 +08:00
mesalogo d859cbfdeb perf: persist remote runtime events in one transaction per checkpoint
Remote semantic events were written with one or two synchronous SQLite transactions each before being forwarded; recoverable conversations also rewrote the whole assistant message per event. Events now commit together at each remote checkpoint (before the Agent can acknowledge), and before non-remote, question and terminal events, on a size cap or a 50 ms timer. Only newly stored events are forwarded, in order; a failed batch rolls back and blocks later commits. Applies to interactive, background and recovery runs.
2026-10-02 23:11:11 +08:00
mesalogo 855b941aeb perf: catch up new conversation panes in small steps and keep pane order in state
The first message batch now grows by 20 per frame after the first paint instead of one large commit, keeping the reader at the bottom or in place. The cached pane order is reconciled into state by a pure helper instead of reading and writing a ref during render (react-hooks/refs).
2026-10-02 23:11:11 +08:00
mesalogo a1a1d24022 fix: release the spiral view's WebGL context and keep its failures local
Each open of the time spiral created a WebGL context that was never released, so repeated use hit the browser limit and lost the oldest context. The view now releases it on close, shows a local retry on creation failure or context loss, and the lazy chunk retries once before an in-canvas error.
2026-10-02 22:58:30 +08:00
mesalogo 9d8477b728 fix: fit the story graph to the window and scroll each column on its own 2026-10-02 22:22:03 +08:00
mesalogo 0fd2f7e3c8 fix: retry unusable model output in smaller review batches
Leaf batches retry at half and a quarter of the text budget, navigation merges retry once; provider errors are not retried. Adds the long-session acceptance script and records the results.
2026-10-02 21:48:29 +08:00
mesalogo 7aad78752b docs: correct PERF-13 attribution and record architecture review of concurrent changes
Opening a long conversation got faster because of the two-step pane render (09c415b), not the live message store, and its catch-up frame is now a long frame. Records which concurrent changes follow the batch rules, and adds Story Graph search and batched remote event writes to PERF-15/16 and the list-order state to PERF-14.
2026-10-02 21:04:47 +08:00
mesalogo 6ced760c1a test: add long-session acceptance script for the production review
Runs the review on an isolated copy of a real database with DeepSeek, injects one provider failure, resumes, and checks coverage and zero-call reruns. Credentials come from an env file passed on the command line.
2026-10-02 20:07:37 +08:00
mesalogo 09c415bd41 perf: render a newly opened conversation in two steps
A freshly mounted pane first renders the trailing 20 messages and the rest of its 80-message batch right after the first paint, keeping the bottom or the reader's place. Saved scroll positions and note targets still render the full batch at once. Keeps the write-on-change guard for --chat-context-height.
2026-10-02 20:06:44 +08:00
mesalogo e97263c79e feat: help agents find stories and experiences on their own
Search matches query terms (CJK pairs) instead of only the whole phrase, previews show readable fields first, and the tool description says when to call it and when to stop.
2026-10-02 20:02:07 +08:00
mesalogo 02a6f5c4e7 fix: remove redundant confirmation for configuration changes
Configuration apply previously required a separate native confirmation even in Agent Runtime Execute mode, and failed in clients without an approval callback. Use existing work-mode and tool authorization instead, preserving Ask read-only access and request-scoped plan validation. Update regression tests, tool descriptions and documentation.

Validation: 267 focused tests passed; typecheck and changed-file lint passed. Full tests timed out with failures in other modules; repository lint reported errors in unchanged files.

Release note: Execute 模式下通过 Agent Runtime 修改 GoodBuddy 配置不再额外弹出确认框,Ask 模式仍保持只读。
2026-10-02 19:39:44 +08:00
mesalogo dc635d185e perf: record PERF-13 streaming and draft store results; allow a custom fake model port 2026-10-02 19:27:36 +08:00
mesalogo 4d1ce38dad fix: record an experience application only when it touches the same knowledge 2026-10-02 19:08:05 +08:00
mesalogo c3fa262832 fix: keep area commits and tests in their own story, not in release 2026-10-02 19:00:18 +08:00
mesalogo 0ce9606c89 test: cover five-second conversation ordering and interaction pauses
Add App regression coverage and document five-second sidebar sorting, live row updates, and deferred reordering during interaction. The ordering hook and App wiring were already included in ee4eb73. Focused validation: 23 tests passed.
2026-10-02 19:00:10 +08:00
mesalogo ee4eb737e0 feat: show experiences outside the time spiral 2026-10-02 18:49:24 +08:00
mesalogo 1f420cd042 feat: organize the work review by story 2026-10-02 18:42:28 +08:00
mesalogo 43be465286 feat: suggest stalled stories and reusable experiences, and let agents read stories through MCP
Also includes pending workspace changes: unsaved-change guard, keep-alive cache and live message store, knowledge workspace dialogs and perf driver updates.
2026-10-02 18:23:08 +08:00
mesalogo 5f1ff24333 fix: soften chat scroll-to-bottom button border 2026-10-02 18:11:03 +08:00
mesalogo d773f5e961 chore: upgrade Electron to 44.5.1 and adapt clipboard APIs 2026-10-02 17:53:42 +08:00
mesalogo 90ea33ef5c perf: skip redundant composer measurement, cache i18next language codes and follow streaming once per frame 2026-10-02 16:57:29 +08:00
mesalogo 983cdd4e05 feat: extract reusable experiences from supervisor stories 2026-10-02 16:21:53 +08:00
mesalogo bf6c4a91a2 fix: keep scroll position when switching between cached conversations 2026-10-02 15:52:48 +08:00
mesalogo 1e10935d22 perf: add CPU-throttled, memory-growth and knowledge retrieval benchmarks
perf:app gains GB_PERF_CPU_THROTTLE (CDP renderer throttling) and a keep-alive memory scenario with post-GC heap samples; perf:knowledge times synchronous FTS, vector and hybrid search at 5k/20k/50k chunks. Results and conclusions recorded in the performance plan (PERF-05, PERF-09, PERF-12).
2026-10-02 15:51:33 +08:00
mesalogo 5dc65457d9 feat: shared supervisor timeline, story assignment and time-spiral view
Each source version is extracted once for every review scope; events carry spans and projects from their sources, and attention density is counted from messages. After publication, events are assigned to project features and sub-threads (cross-project stories only when enabled), with user rename, merge, remove, move and undo. The story graph gains a lazily loaded Three.js time-spiral view. Includes the 3D concept demo, real-data DeepSeek check scripts and docs.
2026-10-02 15:21:48 +08:00
mesalogo 3d4e9577b5 perf: keep Markdown segmentation for single-line display math and ship KaTeX fonts as files 2026-10-02 14:41:56 +08:00
mesalogo 80579ee421 perf: add full-app interaction benchmark and UI architecture plan 2026-10-02 13:42:40 +08:00
mesalogo 8280158edc test: preserve native focus sequence when discarding note drafts
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Wait for detail navigation and continued editing focus, and focus the cancel button before synthetic clicks so confirmation restores the actual opener. Keep draft discard and final entry focus assertions unchanged. Validate in CI without touching parallel work.
2026-10-02 12:14:33 +08:00
mesalogo 5368e0ecf4 test: align fixtures with image tools and enabled browser defaults
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Isolate Obsidian tests from the default browser capability, check the additional image-tool card, and center native overlay click targets below the frosted header. Retain existing permissions, identity, hit-testing and keyboard assertions; preserve unrelated uncommitted work.
2026-10-02 11:53:21 +08:00
mesalogo 20317c21b5 release: prepare Desktop 0.15.8 and Agent 0.15.2
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Release committed image-save tools, note editing improvements, and enabled-by-default browser and frosted appearance preferences. Include bounded Linux Host save evidence and bilingual Desktop/Agent notes. Preserve all pre-existing uncommitted design and article files; full validation and packaging run in CI.
2026-10-02 11:31:43 +08:00
mesalogo b1129cedec fix: enable frosted glass effect by default 2026-10-02 10:40:16 +08:00
mesalogo 2e5c348dce fix: enable built-in browser by default 2026-10-02 10:27:58 +08:00
mesalogo f4d8fe7f06 fix: add new-entry cancel, full-width note content, and restore focus in magic notes
- Add a Cancel action to the new-entry composer with discard confirmation
- Remove the 880px cap so note content matches the toolbar width
- Focus the chat composer only after a deferred workspace leave commits
- Return focus to a sensible target after cancelling new/edited entries and switching entry type
2026-10-02 10:20:04 +08:00
mesalogo dd29f309f1 feat: add save_image tool and list image MCP tools
Expose save_image across direct model, MCP gateway, DeepSeek Harness and remote Agents (written on the remote host via chunked reads), and show the image MCP tool list in settings.
2026-10-02 10:08:42 +08:00
mesalogo b02ce31886 test: align native composer hints and bound Markdown corpus checks
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Update the Electron probe for the two-line shortcut placeholder after greeting removal. Preserve all prefix-equivalence checks and give only the exhaustive Markdown corpus test 15 seconds. Validate through CI per user instruction.
2026-10-01 22:43:33 +08:00
mesalogo 7cb74cce78 test: account for sticky settings headers in layout checks
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Scroll settings controls into the unobscured area below the measured sticky header, retaining native interaction and hit-testing assertions. Give the full legacy outbox migration fixture a scoped 15-second timeout. Product code and bilingual release notes are unchanged; validation remains in CI.
2026-10-01 22:13:42 +08:00
mesalogo 5e77118508 fix: remove redundant chat input placeholder greeting 2026-10-01 22:01:47 +08:00
mesalogo 227507de8f release: prepare Desktop 0.15.7
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Document streaming rendering improvements, on-demand activity refresh, local custom MCP failure isolation, managed model source selection, and opt-in device sharing. Agent remains 0.15.1 and schema remains 52. Per user instruction, validate the exact candidate through CI without additional local tests.
2026-10-01 21:54:32 +08:00
mesalogo c8f327fc4e perf: reduce streaming render cost and refresh activity page on demand
- Segment streaming Markdown so completed blocks are reused and only the growing tail is re-parsed; fall back to whole-document parsing for document-wide constructs
- Memoize conversation sidebar rows and cache per-conversation derived activity, ordering and pending approvals
- Activity page loads on entry, refreshes every 15 s while visible, and has a single header refresh button
- Include pending settings, model download source, device sharing and knowledge gateway changes in the working tree
- Add Electron streaming input benchmark and regression tests
2026-10-01 21:49:09 +08:00
mesalogo 016f987026 test: assert note focus before opening creation form
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Check detail navigation focus before the auto-focused creation input takes over. Preserve return-to-list and reading-position assertions, and include the reviewed sharing first-read test and independent validation record. Validation runs in CI per user request.
2026-10-01 09:16:15 +08:00
mesalogo 2c5300d031 chore: finalize expanded desktop 0.15.6 release scope
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Include device-sharing metadata and the persistent New note entry in bilingual release notes, align five-app documentation, and update the configuration fixture. Per user instruction, validation proceeds through CI without more local tests. Agent remains 0.15.1.
2026-10-01 01:43:53 +08:00
mesalogo a3b41bf2b1 feat: add device sharing metadata application 2026-10-01 01:31:44 +08:00
mesalogo 0a02a17cb6 fix: keep New note in the magic notes page header 2026-10-01 01:31:43 +08:00
mesalogo d2ceff8db5 test: allow the long-history warm-switch regression to finish
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Scope a 15-second budget to the 161-message switching scenario while retaining all render-count, DOM reuse, draft, attachment, and scroll assertions. The full App suite, typecheck, and lint pass.
2026-10-01 01:15:10 +08:00
mesalogo 8201c9c27e test: wait for theme transitions before comparing glass styles
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The Linux full suite sampled a window-control color during its transition. Wait for CSS transitions to finish while retaining exact style, contrast, geometry, and native interaction assertions.
2026-10-01 00:48:35 +08:00
mesalogo cc451f698d release: prepare Desktop 0.15.6
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Include the pending sidebar footer style correction and document frosted appearance, chat rendering, image previews, settings layout, and local MCP recovery. Record schema 52 backup guidance and 886 passing focused tests; Agent remains 0.15.1.

Release note: 新增可选磨砂外观,改善聊天输入、图片预览和本地工具连接恢复,并整理设置与笔记布局。
2026-10-01 00:23:54 +08:00
mesalogo 9cc5dc1244 fix: retire uncertain MCP names so late OpenCode adds cannot replace live connections 2026-10-01 00:08:50 +08:00
mesalogo f7f501a8e3 feat: add frosted glass appearance and improve local MCP recovery
Add an opt-in persistent frosted glass setting for the top bar and runtime checklist, with chat scrolling behind the header while preserving scroll anchors and window controls.

Retry transient local MCP initialization failures once after cleanup, include redacted connection diagnostics in feedback, and index lifecycle events to reduce execution statistics scans.

Release note: Add optional frosted glass appearance with chat visible behind the title bar; improve local tool connection recovery and diagnostic feedback.
2026-09-30 23:37:26 +08:00
mesalogo e4f7b8d993 fix: group settings navigation and align switches to the right 2026-09-30 23:33:25 +08:00
mesalogo b6d24ada87 fix: preview draft and queued image attachments directly
Make draft thumbnails and queued images open the shared image viewer without parsing. Keep queued attachment menus read-only and remove OCR source and settings entries while preserving explicit text extraction in drafts.

Validation: 14 focused tests, typecheck and lint passed. Full suite: 5534 passed, 3 failures outside this change in conversation scrolling and frosted-glass descriptions.

Release note: 草稿和队列附件中的图片现在支持点击放大查看,无需 OCR;队列只读菜单不再显示解析来源和解析设置。
2026-09-30 23:18:45 +08:00
mesalogo c23dca65e8 fix: reduce chat typing and cached conversation switching overhead
Keep unchanged history panes and task strips from rerendering during typing, and stabilize message callbacks across cached conversation switches. Preserve streaming updates, scroll positions, drafts, and asynchronous attachment ownership.

Add App regressions for render isolation, warm switches, retry, and image recovery. Focused tests, typecheck, and lint passed; the full suite encountered unrelated settings failures.

Release note: 减少任务运行时输入和切换已缓存长会话的重复渲染,保留实时输出、滚动位置和会话草稿。
2026-09-30 22:59:25 +08:00
mesalogo bf64181c85 fix: align magic note headers and editor spacing
Align the notes list, detail toolbar and comment direction row, removing stacked top padding. Let text editing controls fill the record card while preserving text and action padding and canvas spacing.

Add Electron geometry regressions and document the spacing rules. Release note: Magic Notes now aligns pane headers and removes excess gaps around text editing toolbars and input areas.
2026-09-30 22:31:30 +08:00
mesalogo 3be55348a2 fix: locate note entries after their content mounts
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Quick Notes could open the full workspace before its selected record was mounted, losing the scroll request. Perform entry navigation after React commits and retain a deterministic regression. Align the manual-compaction fixture with layered model selection and loaded conversation state.

Release note: 从快速笔记打开完整记录时,等待目标内容挂载后再定位,避免偶发只打开页面却未滚动到所选记录。
2026-09-30 19:46:13 +08:00
mesalogo 89fc100d4f release: refresh Desktop 0.15.5 candidate
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Include note tags and layout, workspace controls, and OpenCode launch/tool-name updates in bilingual notes. Update schema 51 guidance and align the Electron Supervisor regression with the implemented suggestions UI.
2026-09-30 19:18:09 +08:00
mesalogo 6edfc0046b feat: add magic note tags and a two-pane notes layout
Keep the notes list beside the open note, compact the detail view with a full-width sticky toolbar, and make Back to overview return to a full-width all-notes grid. Add manual note tags with AND filtering, a tag manager for rename, merge and delete, schema 51 tag tables, and tag support in the note Agent tools.
2026-09-30 19:04:47 +08:00
mesalogo ab85e72f01 fix: keep workspace file controls visible while scrolling
Scroll the file list independently so view selection, refresh, creation actions and directory navigation remain visible. Keep the selected Files entry available in projects without Git.

Add component coverage and 16 Electron layout scenarios for themes, window sizes and scroll retention. Focused tests, typecheck and lint passed; the full test run timed out after 200 seconds.

Release note: Workspace file controls remain visible while scrolling, and projects without Git retain the Files entry.
2026-09-30 17:33:16 +08:00
mesalogo 80edd15438 fix: shorten OpenCode temporary MCP tool names and keep them stable per conversation
Use gbd-/gbc- prefixes with a 6-char conversation hash instead of 20-char per-request hashes, lease per-conversation slots so parallel requests never share a name, and expose gateway custom tools as <server hash>_<name> within the 64-char provider limit.
2026-09-30 17:32:26 +08:00
mesalogo 7fb23d9b6b fix: speed up opening OpenCode in the terminal
Every 'Open in terminal' launch for OpenCode copied the bundled plugin dependency tree (about 3,600 files, 52 MB) into a temporary directory, adding 2.2-2.4 s on Windows before the TUI started. The tree is now copied once into terminal/opencode-config under user data and reused until its bundled version marker changes. Skills, Ask-mode plugins and model configuration are still generated per launch and cleaned up on close. OpenCode's own TUI startup (about 3 s) is unchanged; the remote Agent path is unaffected.

Release note: 在终端中打开 OpenCode 时不再每次复制插件依赖,第二次及以后的打开速度明显加快。
2026-09-30 16:59:13 +08:00
mesalogo 925d410247 release: prepare Desktop 0.15.5
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Document incremental Smart heartbeat reviews, evidence-backed suggestions, interrupted-turn history, floating checklists, and note save behavior. Record schema 50 backup requirements and real Host cancellation validation; Agent remains 0.15.1.

Release note: 智能心跳按变化回顾并提供有依据的建议,修复取消后续聊、清单展开和笔记保存体验;升级前请备份数据库及笔记。
2026-09-30 11:23:45 +08:00
mesalogo 2d8e58e995 fix: exit note editing after saving changes
Return text and canvas records to read-only content after a successful save. Automatic analysis no longer reopens the editor; failed saves preserve drafts for retry. Update regression coverage and UI behavior documentation.

Validation: 118 MagicNotesWorkspace tests, typecheck and lint passed. Full test suite exceeded the 120-second timeout.

Release note: 笔记中的文字和画布记录保存修改成功后自动退出编辑状态,自动评论完成后不再重新打开编辑器;保存失败时保留草稿以便重试。
2026-09-30 10:57:25 +08:00
mesalogo 6293ef105d feat: make heartbeat trigger incremental reviews and suggest from graph changes
Heartbeat no longer writes its own report; it triggers the shared incremental review and, per plan intervention, derives evidence-backed suggestions from the review's graph changes. Manual review is incremental by default with explicit reanalysis. The Supervisor tab is renamed Smart heartbeat with heartbeat plans, and the redundant inner plan frame is removed.
2026-09-30 10:45:22 +08:00
mesalogo d30f794184 fix: expand runtime checklists without shifting chat content
Render checklist details in an anchored floating panel with bounded scrolling, outside dismissal and Escape focus restoration. Reuse the shared overlay mechanism and update layout documentation and regression coverage.

Validation: 25 focused tests, Electron checklist layout and keyboard scenarios, and lint passed. Full-suite and typecheck blockers outside this change were reported separately.

Release note: Runtime execution checklists now expand in a floating panel without pushing conversation content down.
2026-09-30 02:36:50 +08:00
mesalogo 608a9a5fec fix: keep cancelled turns as interrupted instead of merging into the next request
- Build runtime history per turn: a cancelled or failed reply keeps its partial
  text plus an interruption marker (or a marker-only placeholder), so its user
  message is never left unanswered and merged into the next request.
- Use the same history for send, manual compaction and Main compact validation.
- OpenCode: keep a cancelled prompt in the reused native session and prefix the
  next turn with a one-time synthetic interruption notice; failed prompts are
  still removed.
- Add an opt-in DeepSeek live test covering direct model and OpenCode.
2026-09-30 02:18:33 +08:00
mesalogo a662b16f5e fix: remove the conversation runtime override dot
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Keep the Runtime icon and label clear after a conversation-specific selection. Preserve override details and the reset action inside the picker, with regression coverage and bilingual release notes.

Release note: 移除会话 Runtime 按钮图标与名称之间的圆点,保留菜单内的单独选择提示与恢复项目默认操作。
2026-09-29 22:50:43 +08:00
mesalogo 911eed5e67 test: wait for runtime readiness before question submission
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Wait for the enabled send control rather than its initial presence. Preserve the question rounds, saved answers, skip, and reload assertions; production behavior and bilingual release notes are unchanged.
2026-09-29 22:38:08 +08:00
mesalogo ed47e2c2a5 chore: prepare desktop 0.15.4 and agent 0.15.1 releases
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Document layered Runtime and model selection, separate system-task usage, and Continue history handling. Record schema 49 backup requirements and current-source Host validation with explicit failed-provider attempts.

Release note: Desktop 0.15.4 supports layered Runtime/model choices and separate system-task usage; update Desktop before Agent 0.15.1 to receive the remote Continue history fix.
2026-09-29 22:10:20 +08:00
mesalogo 9c88ca20f3 feat: layer runtime and model selection across conversation, project and global
- Store runtime selection as { provider?, model? } layers and resolve them per
  field (conversation -> project -> global); drop the legacy auto provider.
- Migrate stored selections (database v49) and clear deleted profile ids.
- Re-resolve selections in Main per request and at queue dispatch.
- Add ProjectRuntimeSelector and a two-column RuntimeModelPicker; manage
  link opens model connections; restore per-runtime default model tabs.
- Stop duplicating trusted instructions and history across OpenCode,
  Continue and DeepSeek Harness runtimes.
- Document the layered design, global provider removal plan and planned
  purpose-specific models.
2026-09-29 21:33:06 +08:00
mesalogo d1b92deff3 docs: update unreleased article through 86347af 2026-09-29 21:32:41 +08:00
mesalogo 9a9170441e feat: separate system task token usage and add usage refresh
Add a System tasks tab to run history that splits background usage (supervisor, auto-supervision, magic notes, knowledge base, delegation) from conversation usage, with a manual refresh button and automatic refresh when usage is recorded. Record previously dropped usage from supervisor reviews, image regeneration, knowledge graph extraction, and remote embeddings.
2026-09-29 10:38:37 +08:00
mesalogo 3c9229ac7a release: prepare Desktop 0.15.3
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Document restored direct-model compaction, request-lifetime tool access, clearer cancellation and sidebar controls, reduced redundant startup work, and slimmer package inputs. Keep published Agent 0.15.0 and prior release notes unchanged. Review both feature matrices and retain Office editing as planned work. Typecheck, lint and bilingual notes validation passed; 85 focused tests passed and one POSIX-only test was skipped on Windows. Full source validation and production build remain CI gates before tagging.
2026-09-28 22:52:26 +08:00
mesalogo 86347af0b1 fix: refine sidebar search and composer shortcut hints 2026-09-28 22:38:35 +08:00
mesalogo 4baf4ec142 fix: restore manual context compaction for direct models
Local direct-model text conversations could not use manual context compaction,
and disabling automatic compression prevented saved summaries from being reused.
Restore the composer action regardless of the automatic setting or threshold,
use the selected summary connection, and persist summaries for subsequent
requests while retaining recent turns and full conversation history.

Image connections, remote channel conversations, and SSH projects remain outside
the local direct-model compaction workflow. Add focused regressions, a local
Electron end-to-end test, and an opt-in two-request live probe.

Validation: staged snapshot passed typecheck, changed-file ESLint, 13 focused
tests, and the Electron end-to-end test with two local HTTP fixture requests.
No additional real-model calls were made during commit verification.

Release note: 修复本地直连文本模型无法手动压缩上下文的问题。现在关闭自动压缩或尚未达到触发阈值时,也可点击“压缩上下文”;摘要会随会话保存并用于后续对话,同时保留最近轮次和完整历史。
2026-09-28 22:21:47 +08:00
mesalogo 1603871f7c fix: keep tool authorization valid throughout long requests
Remove the fixed gateway capability lifetime so active requests retain knowledge, notes, browser, configuration, and custom MCP access. Preserve token validation and request cancellation or cleanup revocation, and replace the misleading knowledge expiry error with a tool authorization error.

Remove native-client clock overrides and cover long-running scoped and HTTP MCP calls. Validation: 34 focused tests passed; changed-file lint passed. Repository typecheck and lint remain blocked by unrelated manual-compaction and sidebar probe files.

Release note: Long-running Agent requests no longer lose tool access after ten minutes, including when the knowledge-base switch is off. Request cancellation and completion still revoke access.
2026-09-28 21:36:41 +08:00
mesalogo 393a07ccdf fix: keep note capture actions compact and right-aligned
Remove initial generic validation instructions, show size errors beside their fields, and expose capture scope through contextual help. Keep save errors beside actions where space allows and preserve content-sized Save buttons in narrow note workspaces.

Release note: Note capture now uses a compact action bar with right-aligned buttons, on-demand scope help, and field-specific validation. Narrow workspaces no longer stretch the Save button.
2026-09-28 21:30:03 +08:00
mesalogo 50e8fde64c fix: soften sidebar dividers and tighten spacing 2026-09-28 20:54:56 +08:00
mesalogo 13ff182858 fix: reduce redundant work during chat and note startup
Avoid hidden activity renders, no-op message saves and attachment scans when pinning conversations. Reuse note body inspection and skip unchanged attachment reference writes while preserving recovery and cleanup behavior.

Skip unnecessary Markdown normalization and memoize subagent lookups without delaying live rich text. Add storage operation counts and real Electron rendering equivalence benchmarks; full-render speedup remains unproven.

Validation: 5397 tests passed, 84 skipped; typecheck passed. Lint passed excluding two existing untracked sidebar probe files.

Release note: Reduce repeated rendering and disk work during chat, conversation pinning and note startup while preserving existing content and interactions.
2026-09-28 20:53:43 +08:00
mesalogo 594222a2b5 fix: reduce desktop package size and loose files
Exclude validation outputs from desktop packages, ship renderer-only dependencies through Vite bundles, and omit DS Web type declarations, source maps, and PDB symbols. Preserve runtime assets and licenses without changing dependency versions.

Validate builder filters, production build and type checks, Windows packaged runtime smoke paths, and App page loading. Record measured size reductions and remaining validation limits in BUILD.md.

Release note: 减少桌面安装包中的重复依赖和非运行文件,降低安装后的磁盘占用与零散文件数量。
2026-09-28 20:10:19 +08:00
mesalogo 1c6264dd87 fix: group worktree diff actions with the file path
Move file actions above the divider alongside the path, allowing wrapping in narrow panels. Hide the redundant worktree heading unless staged changes also need a separate section.

Release note: Worktree diffs now group file actions above the divider and omit the redundant heading when only unstaged changes are shown.
2026-09-28 19:27:52 +08:00
mesalogo 120076a13d fix: group conversation search and standardize sidebar spacing
Move search to the top of the conversation area, remove its visible heading, and use a white surface in the light theme. Keep the list independently scrollable and sidebar controls reachable in short windows. Standardize footer divider spacing to 16px on both sides and update layout tests and design guidance.

Validation: 307 focused tests, typecheck, targeted ESLint, and four Electron layout scenarios passed.

Release note: Conversation search now sits above the conversation list in a distinct themed area, with consistent divider spacing and improved short-window scrolling.
2026-09-28 19:06:06 +08:00
mesalogo d8f0ef1301 fix: make browser Go button icon-only
Remove the visible Go label and use the toolbar icon-button width while preserving the tooltip and accessible name.
2026-09-28 19:00:44 +08:00
mesalogo 10f3b4f2c1 fix: center sidebar footer divider and correct Apps arrow
Use equal-width Apps and Settings buttons so the divider stays centered. Increase divider visibility and point the Apps arrow upward to match the menu opening direction.

Release note: Make the sidebar footer divider clearer and centered, and align the Apps arrow with its upward-opening menu.
2026-09-28 18:38:01 +08:00
mesalogo 58e6808f1f fix: deduplicate terminal notices and distinguish cancellation
Keep cancellation and failure details in message status while preserving existing replies. Render cancellation neutrally, restore terminal metadata from history, and offer Edit again only when input can be restored.

Release note: Cancelled and failed requests show a single notice; cancellation no longer appears as an error, and Edit again accurately describes the recovery action.
2026-09-28 18:10:10 +08:00
mesalogo 8650907b75 fix: distinguish Apps and Settings in the sidebar footer
Keep the horizontal footer layout with visible labels, matching button heights, a subtle divider, and an Apps dropdown indicator. Use a layout icon for Manage Apps so it is distinct from global Settings.

Release note: Make the bottom-left Apps and Settings entries easier to distinguish with labeled buttons and separate visual boundaries.
2026-09-28 18:08:52 +08:00
mesalogo c11f204b17 fix: align conversation list scrollbar with notes
Use a thin native scrollbar with a transparent track and theme colors for the conversation list on fine-pointer devices. Preserve system defaults for coarse pointers and forced colors.

Release note: The conversation list now uses the same subtle scrollbar style as notes, with native touch and high-contrast behavior preserved.
2026-09-28 18:06:20 +08:00
mesalogo be23c1346b docs: define live Office editing requirements and acceptance
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Specify same-session Agent edits, visible changes, and Word, Excel, and PowerPoint acceptance scenarios. Record centralized deployment and ONLYOFFICE API licensing prerequisites as design requirements, not implemented capabilities.
2026-09-28 16:59:51 +08:00
mesalogo 47ba54f47d test: derive MCP settings expectations from the catalog
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
Include Story Graph in the shared settings fixture and verify every catalog server by identity, rather than filtering buttons against an outdated list of five names. Preserve the existing count and interaction assertions. Complete SettingsPanel tests, typecheck, lint and bilingual notes checks passed; the full suite remains delegated to CI as requested.
2026-09-28 15:23:31 +08:00
mesalogo 76e781a0a8 release: prepare Desktop 0.15.2 and Agent 0.15.0
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Add bilingual notes for Story Graph reads, full follow-up editing, note capture, diagnostics and website updates. Preserve published 0.15.1 history and include reviewed Host/cloud plans as explicitly unimplemented documentation. Agent remains 0.15.0 and requires Desktop 0.15.2. Typecheck, lint, notes and website validation passed; current-source Host runtime checks made four real model requests. User requested full-suite validation in CI rather than waiting for the interrupted local run.
2026-09-28 12:55:10 +08:00
mesalogo 0cdf04261a docs: update announcement draft with story graph access
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-09-28 12:48:45 +08:00
mesalogo 66f77b83c2 feat: let agents read supervisor story graphs
Expose saved decisions, timelines and source evidence through read-only Story Graph tools across Model, Harness and local/remote OpenCode and Continue. Supervisor enablement and runtime assignments govern discovery and calls; reads do not invoke models or modify stored evidence. Remote use requires matching Desktop and Agent changes.

Make Continue discussion show the complete editable message with a separated destination and shared action layout. Sending uses the visible draft without reattaching removed context.

Validation: existing focused MCP, runtime, controlled Linux Host, discussion component and Electron layout checks plus typechecks; staged diff check passed. Cross-scope fact reuse, historical reconstruction and full package acceptance remain deferred.

Release note: Agents can consult saved supervisor decisions and their sources when continuing work. Continue discussion now lets you review and edit the complete message before sending.
2026-09-28 12:38:00 +08:00
mesalogo 83a3ea261e fix: clarify site positioning and keep downloads prominent
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Highlight domestic platform support and built-in coding agents, replace placeholder demos with task workflows, and add bilingual setup FAQs. Keep downloads directly after the hero and improve mobile demo layout.

Release note: Update the bilingual website with clearer product copy and demos, prominent downloads, and setup guidance. DeepSeek Harness and LoongArch retain their preview labels.
2026-09-28 12:10:22 +08:00
mesalogo 013e72f69e fix: simplify note capture controls and entry points
Remove duplicate quick-note shortcuts, redundant scope text and visible content labels. Right-align note action groups while retaining accessible input names and validation feedback.

Release note: Simplify the notes panel and align save and source actions to the right. Open notes from the workbar or conversation capture actions.
2026-09-28 11:45:52 +08:00
mesalogo 61590395ef fix: retain renderer exit details in diagnostic logs
Persist Electron renderer exit reasons and exit codes through diagnostic writes, reads, and exports so abnormal window exits can be investigated. Keep older records readable and existing log bounds unchanged.

Validation: 18 focused tests, typecheck, lint, and diff checks passed. Full test run timed out after 60 seconds with a SettingsPanel test failure.

Release note: Diagnostic logs now retain renderer exit reasons and exit codes to help distinguish out-of-memory exits from other window failures.
2026-09-28 11:36:01 +08:00
mesalogo 4e5239256d fix: keep bundled DS Web links relocatable
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Preserve npm relative symlinks while preparing the desktop DS Web resources.
The previous copy rewrote CLI links to build-machine absolute paths, causing
both macOS targets to fail strict code-signature verification. The regression
runs the production preparation and builder matcher, removes the installation
and staging trees, and verifies the final link remains readable.

Recover the unpublished Desktop 0.15.0 as 0.15.1 without moving its tag.
Agent stays 0.15.0 and requires Desktop 0.15.1. Carry the unpublished notes
forward once and align the bilingual feature matrices and upgrade guidance.

Validation: complete frozen Linux tree 5372 passed, 51 skipped; Windows
packaging checks 45 passed and one POSIX-only test skipped. Typecheck, lint,
and bilingual release-note validation passed. Native macOS signing remains
an independent release check. Agent Runtime implementations are unaffected.

Release note: 修复本地 DS Web 随包依赖的链接复制,避免 macOS 安装包签名校验失败。
2026-09-28 09:51:01 +08:00
mesalogo 65a21390de test: retain created knowledge bindings across polling
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Model persisted library state in the external-binding fixture instead of returning pre-creation data to background refreshes. Keep all selected and excluded library assertions, without assuming the last snapshot request belongs to the new library. Windows App: 300 passed; complete frozen Linux suite: 5371 passed, 51 skipped. Typecheck, lint and bilingual notes validation passed.
2026-09-28 04:22:13 +08:00
mesalogo 706726bad6 test: wait for completed navigation before opening menus
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Require the actual message article to own focus instead of matching body text. Wait for project runtime initialization before opening dismiss-on-blur composer options. Preserve all capture and native command assertions. Complete frozen Linux snapshot: 5371 passed, 51 skipped; focused Windows, typecheck and lint passed.
2026-09-28 03:47:29 +08:00
mesalogo 5cdffee9ee release: validate the complete 0.15.0 branch snapshot
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Account for concurrent composer, conversation-order, desktop-notification,
and Supervisor discussion changes in bilingual notes and feature matrices.
Correct the removed entity-writeback description in the companion article.

Align the configuration fixture with the default-on notification setting,
scope note-source UI queries and wait for completed actions, and preserve
all real Obsidian child-process checks with a 15-second scenario allowance.

Validation: frozen complete tree passed 5371 Linux tests with 51 skipped;
19 focused Windows tests, typecheck, lint, and release-note validation passed.
The older 5345-test Host result did not cover all concurrent branch changes
and is not treated as validation of this complete candidate.
2026-09-28 03:06:41 +08:00
mesalogo d9e1e42288 release: finalize 0.15.0 validation and companion materials
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Run conversation capture acceptance in a visible focused Electron window
and report concrete focus state on failure. Trigger Continue pre-spawn
cancellation at its existing environment callback instead of racing a timer.
Product implementation and approved bilingual note text remain unchanged.

Include the user's companion article updates and three reviewed draft images
as authorized. Keep their draft status and unverified build provenance clear.
Set the release date to September 28 and record the final real Linux result:
5345 passed, 51 skipped; Windows focused, typecheck, lint, and notes passed.
2026-09-28 02:26:06 +08:00
mesalogo d47f967781 fix: simplify continuing work from supervisor reviews
Let users continue discussions without opening sources first, edit a question, and send source-linked review context to the target conversation. Add explicit conversation navigation and remove redundant context disclosure and entity-writeback controls. Hide empty automatic supervision metrics until activity exists.

Release note: Continue work directly from supervision feedback or graph sources with relevant context, fewer controls, and clearer automatic supervision empty states.
2026-09-28 01:51:23 +08:00
mesalogo f35198df01 feat: add desktop notification toggle to general settings
Add a default-on persisted desktop notification switch that takes effect immediately for background task results while leaving in-app notifications unchanged. Group General settings into shortcut, conversation and notification, and workspace and download cards.

Validation: 756 related tests passed, 1 skipped; typecheck and lint passed. Full suite timed out with three supervision preview failures; native OS notification delivery was not manually verified.

Release note: 通用设置新增默认开启的桌面通知开关,可随时关闭系统任务提醒并保留应用内提示;设置项按用途分组,方便查找。
2026-09-28 01:43:21 +08:00
mesalogo 8e607366ca fix: keep conversation order stable during streaming
Sort pinned and unpinned conversations by their latest message creation time so parallel replies no longer repeatedly swap sidebar positions. Include the same timestamp in lightweight summaries while preserving updatedAt for persistence and snapshot merging.

Validation: 13 focused tests, typecheck and ESLint passed. The full test run timed out after ten minutes and reported configuration-summary and Agent offline-install failures.

Release note: Conversation order stays stable while replies stream or tools report progress; new messages still move conversations forward, with pinned conversations first.
2026-09-28 01:40:01 +08:00
mesalogo ee1bf8a187 fix: unify composer tool button styles
Use matching button and icon sizes, spacing, and hover feedback for attachment, voice input, and composer options while preserving recording and expanded states.

Release note: 修复输入框附件、语音和设置按钮的样式不一致问题,统一尺寸、间距和悬停效果。
2026-09-28 01:08:54 +08:00
mesalogo b8b6007b51 test: synchronize narrow notes keyboard acceptance on Linux
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The real Linux Host reproduced Escape being sent to the collapsed workbar
trigger after resize. Wait for the narrow viewport, open the workbar through
its existing control, and confirm the append field owns focus before input.

Keep the migration fixture and all IO assertions, with a 15-second allowance
for its 10000-record scenario. Product behavior and approved notes are unchanged.

Validation: real Linux Host full suite 5345 passed, 51 skipped; Windows focused
3 passed; complete typecheck, lint, and release-note verification passed.
2026-09-28 00:49:24 +08:00
mesalogo 12594961fe test: stabilize conversation capture checks in release CI
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Focus the real Electron window before native keyboard input and give the
two long-history App scenarios the existing 15-second per-test allowance.
Keep all capture, source navigation, persistence, and keyboard assertions.

Record the initial Linux CI failures and the revised local validation:
5317 passed, 79 skipped, with typecheck, lint, and release notes verified.
Product behavior and approved Desktop/Agent 0.15.0 notes are unchanged.
2026-09-28 00:00:19 +08:00
mesalogo 5a6ba6c14f release: prepare Desktop and Agent 0.15.0
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Align Desktop and Agent candidates at 0.15.0 and include the approved
Chinese and English release notes. Document Obsidian, conversation capture,
local native-client preview boundaries, and knowledge and Supervisor fixes.

Desktop upgrades storage to schema 48. Agent 0.15.0 requires Desktop 0.15.0;
remote telemetry opt-outs apply to newly started Runtimes. Keep the two
release channels independent and exclude the experimental LoongArch build.

Validation: 5317 tests passed, 79 skipped; typecheck, lint, and release-note
verification passed. Current-source Linux x64 OpenCode and Continue passed
Ask/Execute and telemetry-environment checks. Record preliminary probe
limitations and model-call accounting in the release preparation document.

Release note: Desktop 与 Agent 同步准备 0.15.0,新增 Obsidian、会话笔记采集及本地原生客户端预览,并修复知识库、监督者、界面与 Runtime 启动体验;升级前请备份完整用户数据,远程用户先升级桌面端再更新 Host。
2026-09-27 23:01:02 +08:00
mesalogo 0f8bf405db fix: remove persistent DS service hint from composer
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Remove the persistent independent-session and service-lifecycle description beneath the native client action. Keep the stop action and existing service lifecycle unchanged, and align the proposal documentation.

Release note: 移除 DS 浏览器入口旁常驻的会话和服务说明,简化输入区;停止服务操作保持可用。
2026-09-27 22:16:47 +08:00
mesalogo 5ecc19dfe0 fix: include DS Web dependencies in portable packages
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Copy the prepared DS node_modules contents directly so electron-builder retains the CLI entry and nested Web assets. Fail portable output validation when the DS entry is absent, preventing browser launch from failing with ENOENT.

Add regression coverage using the actual builder matcher and include the pending Continue/OpenCode article screenshots.

Validation: 3 focused packaging tests passed; staged whitespace checks passed. No new full-suite or remote Host validation was performed for this commit.

Release note: 修复 Windows portable 包遗漏 DS Web 依赖导致无法在浏览器中打开的问题;构建时会检查启动入口是否完整。
2026-09-27 22:09:47 +08:00
mesalogo 4a2986ca5d test: correct HeartbeatCenter nested navigation assertions
Fix the two Chinese and English test failures by scoping page and graph tabs to their accessible names. Cover selection, tab/panel associations, keyboard focus and graph content while preserving non-graph navigation assertions.

Clarify the top-level navigation design and record the follow-up validation in the quality and workbar docs. No production code changes.

Prior validation: 96 tests passed across HeartbeatCenter (42), SupervisorWorkspace and WorkspacePrimitives; npm run typecheck and npm run lint passed. The full repository suite was not rerun. Commit-time staged diff review and whitespace checks passed.
2026-09-27 22:09:04 +08:00
mesalogo 2713a81bd9 fix: restore consistent controls, usable overlays and narrow layouts
Resolve 15 shared-control, overlay and responsive-layout issues plus the MCP editor being hidden behind settings. Restore theme contrast, danger and disabled states, visible keyboard focus, and consistent Continue fields. Keep the workbar add button reachable and model protocol values readable in narrow containers.

Make nested MCP and image dialogs usable above their parents with focus restoration, keep project Escape handling within the active modal, release native browser views for overlapping help, and restore keyboard access and Escape in sandboxed HTML previews without relaxing sandbox permissions. Add unit and real Electron regression coverage and update design and validation documentation.

Release note: 修复深浅主题下控件边框、危险操作、禁用状态和键盘焦点显示不一致的问题。MCP 编辑器与图片预览可在父弹窗上正常操作,HTML 全屏预览支持键盘进入和 Escape 返回;窄工作栏的添加按钮保持可用,模型连接协议文字完整可读。

Validation: the full suite recorded 5309 passed, 3 failed and 83 skipped. The ProjectSwitcher initial-focus regression was then fixed, and App, ProjectSwitcher and overlay Electron tests passed all 331 cases. The other two HeartbeatCenter failures were reproduced on isolated HEAD 11facf0 and remain pre-existing. Typecheck and lint passed after the fix; staged whitespace checks passed. The full suite was not rerun after the focused fix.

Limits: Electron evidence is from Windows development runs, with fixtures for unrelated services and native occlusion throttling disabled in some drivers. No production build, packaging, macOS/Linux, remote Agent host or external MCP connection validation; skipped tests and the documented scaling and native-select coverage gaps remain.
2026-09-27 21:57:35 +08:00
mesalogo 11facf0392 fix: restore destructive dialog buttons and document recent updates
Destructive confirmation buttons lost their styling outside the notes page, including the draft-discard Portal. Share button sizing, typography, disabled feedback and theme-aware hover colors across callers, with Electron and contrast regression coverage and design documentation.

Include the update article draft covering v0.13.8 through v0.13.16 and the separately identified 5116613 follow-up, its verification record and article index. The article remains unpublished with screenshots and release details pending.

Validation: 37 focused tests passed, including real Electron capture; typecheck, lint and staged whitespace checks passed. No model calls. The previously documented unrelated HeartbeatCenter full-suite failure was not rerun.

Release note: Destructive confirmation buttons, including the discard-note-draft dialog, now display consistent styling, keyboard focus and disabled feedback in light and dark themes.
2026-09-27 15:34:01 +08:00
mesalogo 0f0b6ac21d fix: skip Continue update status when updates are disabled
Continue still displayed Checking for updates even when version lookup was disabled. Skip the update service before it emits checking state and retain the lower-level lookup guard in the shared local and Agent adapter.

Add a regression for disabled and normal update paths and record real terminal and production Electron validation. Seven real model requests returned HTTP 200; Linux validation covered startup only. The reported UI freeze was not reproduced, full Agent/model acceptance remains open, and full-suite failures are documented.

Release note: 修复 Continue 禁用自动更新后仍显示 Checking for updates 的问题,终端现在保持正常空闲状态。
2026-09-27 15:23:41 +08:00
mesalogo eb53c827c6 fix: apply runtime telemetry settings to web and remote clients
DS Web and remote Agent launch paths omitted shared telemetry opt-out settings. Reuse one environment definition for local launches, remote runtime profiles, and the remote model bridge helper while preserving existing update controls and user-requested networking.

Validation: 16 focused tests passed at commit time. Prior local real-model validation passed; modified Agent real-Host validation and background network monitoring remain outstanding.

Release note: 补齐 DS Web 与远程 Runtime 启动时的遥测关闭配置,保留现有自动更新关闭设置,不影响模型、MCP 和用户主动联网请求。
2026-09-27 13:57:26 +08:00
mesalogo 8b15399577 fix: prevent workbar scroll arrow from overlapping add button
Hide disabled scroll arrows at tab strip boundaries for hover, keyboard focus, and touch input. Update the workbar specification to match.

Release note: Hide the workbar scroll arrow at the end of the tab strip so it no longer overlaps the add button.
2026-09-27 13:30:22 +08:00
mesalogo 67dedc2ed2 fix: simplify knowledge action menus
Remove repeated document names from action labels, shorten the reparse label, and narrow knowledge menus to 220px while preserving other menu widths and viewport bounds. Keep object context on menu triggers and removal confirmations.

Validation: 72 focused and Electron layout tests, typecheck, lint, and diff checks passed.

Release note: 知识库操作菜单去掉重复文件名,将操作简写为重新解析,并收窄菜单宽度,减少换行。
2026-09-27 13:11:09 +08:00
mesalogo ceb275bdb8 fix: clarify knowledge document actions and task history
Restore URL identity in merged rows and replace crowded document actions with a primary entry and accessible menu. Organize preview navigation and maintenance actions, and show task history as batches with matching counts and filters.

Keep preview task scope consistent for multi-document sources and track pending operations independently so one source does not block another source's pause action. Include responsive Electron layout and behavioral regressions, bilingual copy, and design documentation.

Validation: focused regressions, typecheck, lint, and diff checks passed. Electron fixtures cover light/dark and wide/narrow layouts. Full tests reported two previously known HeartbeatCenter assertions; real import and parsing integration was not rerun.

Release note: 知识库文档列表保留来源网址,统一查看入口和更多操作,并整理解析结果页。任务历史按批次展示,筛选计数与当前对象一致,不同来源的操作互不阻塞。
2026-09-27 12:55:50 +08:00
mesalogo 44cd944601 refactor: extract chat history pane and clarify knowledge ownership 2026-09-27 10:12:10 +08:00
mesalogo 40c108da1a fix: organize story graph lists into tabs
Replace the stacked event, entity and relation lists with shared tabs and an independently scrolling panel. Preserve graph selection when switching tabs and follow canvas selections in the sidebar. Update translations, UI documentation and focused interaction and Electron layout coverage.

Release note: Browse story graph events, entities and relations in separate sidebar tabs, with counts and synchronized canvas selection.
2026-09-27 10:06:35 +08:00
mesalogo 5177b47aeb fix: unify knowledge documents and sources
Merge single-document sources into one row, keep grouped directory documents and failed sources visible, and share search and task actions. Compact the drop zone and move secondary actions into a disclosure.

Include the existing knowledge workspace component extraction required by this implementation, focused regression tests, bilingual copy, and design documentation.

Validation: 94 focused tests, typecheck, lint, and diff checks passed. Full tests timed out after 200 seconds; two unrelated HeartbeatCenter tablist failures were reproduced separately. Electron visual acceptance remains unverified.

Release note: 知识库的内容来源与文档索引合并为统一列表,减少重复记录和操作;支持统一搜索,并保留目录分组和失败来源的恢复入口。
2026-09-27 10:05:50 +08:00
mesalogo 4edf06b685 refactor: extract model settings IPC handlers 2026-09-27 09:03:17 +08:00
mesalogo f81f342cde feat: open local runtime clients from the composer
Add composer shortcuts for local Continue and OpenCode terminals and the official DeepSeek Harness Web client, using the current project and model bridge. Manage browser service reuse, stop actions and terminal cleanup. Upgrade DeepSeek Harness to 0.1.7-rc.2 and Cordis to 4.0.4.

Include the coordinated IPC handler extraction and related tests and documentation. Remote native-client coordinator wiring, managed Node consolidation, Ask MCP mappings and full desktop/package acceptance remain unfinished; this commit records the implemented local workflow and supporting SSH groundwork.

Validation: typecheck and lint passed; 267 focused tests passed and 5 skipped. No new real-provider calls were made for this commit. Prior real-model evidence and remaining acceptance work are documented in the feature progress files.

Release note: 本地项目选择 Continue 或 OpenCode 后,可从输入框下方打开原生终端;选择 DS 后,可打开使用当前项目和模型的官方 Web 客户端。客户端使用独立会话,DS 网页关闭后服务继续运行,可在 GoodBuddy 中停止;远程入口尚未完成。
2026-09-27 08:38:00 +08:00
mesalogo 1094433a7d feat: connect conversations with Magic Notes
Let users retain useful assistant replies or complete conversations without
leaving their chat workflow. Preview and edit captured text, choose an existing
note or atomically create a note with its first entry, and return to the source
conversation or message from saved records.

Add an application-scoped Notes workbar tab with title/body search and quick
text appends. Preserve drafts across conversation switches and guard closing,
replacement and disabling. Schema 48 stores optional source metadata without
rewriting file-backed bodies; source deletion retains the saved note. Older
clients cannot reopen an upgraded database without a pre-upgrade backup.

Release note: 可将助手回答或完整会话预览、编辑后加入魔法笔记,并从笔记返回原会话或消息。右侧快速笔记支持搜索和追加文字,切换会话保留草稿;采集本身不调用模型,保存后自动评论仍遵循已有设置。

Validation: isolated HEAD-based candidate passed 22 files / 930 focused tests,
plus 20 gateway tests after including its create-result fixture adjustment.
Real Electron App/preload/IPC/SQLite acceptance passed for all 502 historical
messages, exact-entry/source navigation, native keyboard input in a 214px
panel, reopen and source-deletion retention, with zero capture model calls.
Repository lint and web TypeScript passed. Cached whitespace and secret-pattern
checks passed; native-client changes and unrelated IPC extraction are excluded.

Validation limits: the isolated source tree reuses installed node_modules,
where a parallel task upgraded DSH from the committed 0.1.2-rc.1 to 0.1.7-rc.2.
Full typecheck reports 14 DSH errors in three unchanged files; the full-suite
attempt with --bail=1 also fails in deepseek-harness-host.test.ts. These are not
all-green repository or release-package results. Earlier execution-time full
suite failures and fixture repairs remain recorded in the feature progress doc.
2026-09-27 08:17:54 +08:00
mesalogo 55efca3ee1 feat: connect local Obsidian vaults to conversations
Users can now read, search, and update existing local Obsidian notes through
built-in tools without configuring an external MCP server or Obsidian plugin.
Add vault discovery, an optional folder scope, and independent test, save,
enablement, and runtime assignment controls in MCP settings.

Bundle MCPVault 0.16.0 and its runtime dependencies, exposing 19 shared tools
to direct models and managed local OpenCode and Continue conversations.
Obsidian is disabled by default. Ask remains read-only; Execute permits
writes. Scope and assignment changes revoke existing Obsidian grants.
DeepSeek Harness and remote execution remain unsupported.

Validate the isolated staged snapshot with its own dependency installation:
typecheck, lint, production build, and 540 tests across 14 files passed.
No paid model calls were repeated. Physical-offline, installer, and other
platform acceptance remain pending as documented in the feature record.

Release note: Connect local Obsidian vaults in MCP settings to find notes and
write conversation results back to them. Test all registered vaults or a
chosen folder before enabling access; no Obsidian plugin is required.
2026-09-27 00:37:46 +08:00
mesalogo 5116613615 fix: keep supervisor controls usable and prevent overlapping reviews
Allow only one active review across manual starts, automatic runs and resume. Add cancellation that holds admission through runtime cleanup, preserves saved batches and prevents cancelled runs from resuming.

Keep review controls usable during execution, show a dismissible progress notice and remove redundant filter labels. Include focused regressions and lifecycle documentation.

Release note: Supervision reviews can now be cancelled from Activity records. Starting another review shows the active review instead of duplicating work, while scope, time and refresh controls remain available.
2026-09-26 15:55:50 +08:00
mesalogo 0759d5dcff release: prepare Desktop 0.13.16
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Package approved bilingual notes for history upgrade reclamation, continuous
Supervisor reviews, and clearer review/activity layouts. Align feature lists
and retain published notes. Agent remains 0.13.4; no schema bump or LoongArch.

Update two stale layout assertions to match removed repetitive copy.
Record candidate validation and the isolated Mermaid rerun without claiming
a clean full-suite result. Require main CI and production build before tagging.
2026-09-26 13:43:53 +08:00
mesalogo 325b702934 fix: clarify supervisor review and activity layouts
Work Review could place its empty state beside the controls, while redundant copy and secondary activity statistics obscured progress. Use a full-width vertical review flow with a separate run status, connected review stages, prominent saved-batch coverage, and expandable secondary details.

Validation: 33 component tests, 40 Electron fixture layout cases, typecheck, targeted ESLint, and diff checks passed. No model calls.

Release note: 修复工作回顾空态布局,精简顶部控件与重复说明。活动记录通过相连阶段和已保存批次展示进度,次要统计可在详情中展开。
2026-09-26 12:53:10 +08:00
mesalogo 4c7193e1ea fix: avoid history reclamation during structure-only upgrades
Ordinary database schema upgrades previously ran VACUUM on free pages and
showed misleading history-cleanup progress. Separate structure upgrades from
required legacy conversions, retaining reclamation intent only for retries
within the same startup. Preserve released data without new schema versions
or persistent migration markers; Agent and normal writes remain unchanged.

Add real SQLite and Worker regressions, bilingual operation-specific startup
copy, and migration documentation. Synchronize native Electron drag input
with actual resize state without weakening layout assertions.

Validation: 47 focused migration tests passed; typecheck, lint and diff checks
passed. Final full suite: 5137 passed, 74 skipped, 2 unrelated Electron failures.
Both failing Electron tests passed an isolated rerun; full CI remains unverified.

Release note: 修复普通数据库结构升级再次整理历史和回收空间的问题;仅在需要转换旧数据时执行相关处理,启动页准确显示当前升级操作。
2026-09-26 12:42:08 +08:00
mesalogo e6ac0734de fix: keep supervisor reviews running until completion
Remove elapsed execution-budget pauses from extraction and navigation so long reviews no longer require repeated manual continuation. Remove the pause-duration setting and update activity copy and design documentation. Preserve explicit pause, saved batches, model timeouts and concurrency limits.

Validation: recorded 13 focused tests, typecheck and targeted ESLint passed; staged diff check passed. Tests were not rerun for this commit.

Release note: 监督回顾现在持续分批处理至完成,不再每隔固定时长自动暂停;仍可主动暂停并从已保存进度继续。
2026-09-26 11:02:30 +08:00
mesalogo e8d93b5bb6 release: prepare Desktop 0.13.15 recovery
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Carry the approved unpublished 0.13.14 release notes into 0.13.15 after
stabilizing the CI UI tests. Remove the duplicate unpublished entry and
update the bilingual feature matrices and recovery evidence.

Keep published Agent 0.13.4 unchanged. Its minimum Desktop requirement is
0.13.14; users should install this recovery release before updating Hosts.
Schema 47 still requires a pre-upgrade backup for rollback.

Release-note verification and nine focused tests passed. Require this
candidate main-branch validation and production build before tagging.

Release note: 承接监督回顾、待办布局、原生搜索和远程恢复更新;请先升级 Desktop 0.13.15,再更新 Host 至 Agent 0.13.4。
2026-09-26 02:05:59 +08:00
mesalogo cd27163660 test: stabilize asynchronous UI checks in release CI
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Control the native inventory retry response and wait for the complete
browser-close result instead of asserting on intermediate renders.
Run native note-layout input in a visible isolated Electron window,
scan the fixture dependencies up front, and report timeout explicitly.
Keep all behavior and geometry assertions and Electron sandboxing.

The three focused regressions, full typecheck, and lint pass locally.
Linux CI must confirm the release failures are resolved. No product,
Agent, version, or release-tag changes are included.
2026-09-26 01:53:04 +08:00
665 changed files with 88051 additions and 20839 deletions
+31
View File
@@ -103,6 +103,13 @@ npm run build
中间构建输出位于 `out`。该目录为生成内容,应修改源文件后重新构建,不要直接编辑。
Obsidian 接入的构建钩子将锁定的 npm `@bitbonsai/mcpvault@0.16.0` 及递归运行依赖、
许可证和原生辅助资源复制到 `out/main/obsidian-mcpvault`,桌面打包时解包到
`app.asar.unpacked`。运行时复用现有工具执行环境的 Node,不执行在线 npm 安装。
生产组件自动化、Electron 托管 Node、Windows 设置与聊天 UI,以及受限网络下的
Windows x64 解包应用已验证;物理断网、安装器及其他系统验收待完成。
验证记录见 [Obsidian 进度](./docs/features/obsidian/progress.md)。
## 图标生成
应用图标源文件位于 `icons`。修改源图或图标处理逻辑后运行:
@@ -175,6 +182,30 @@ npm run dist:linux:arm64
已由 Vite 打包的 `@antv/g6` 归类为构建期依赖,不会再作为独立 Node 模块复制进桌面
安装包。
Desktop packaging includes only `out/main`, `out/preload`, and `out/renderer`;
validation builds elsewhere under `out` are not shipped. Renderer-only React,
Xterm, Mermaid, Fabric, Quill, i18next, and Markdown/math dependencies are build-time
dependencies: Vite ships their browser bundles, while the existing license
resources remain included. Install development dependencies before building;
do not prune them before bundling and copying license resources.
The desktop DS Web resource filter excludes JavaScript/TypeScript source maps,
`.d.ts` / `.d.mts` / `.d.cts` declarations, and `.pdb` debug symbols. It preserves
the installed dependency layout, plugin configuration, browser assets, licenses,
native binaries, and non-code `.map` assets. This filter does not change the
separately built remote Agent or Runtime payloads.
Windows x64 development validation on 2026-09-28 compared the existing 0.15.2
portable directory (excluding `data`) with a fresh unpacked test build:
`app.asar` decreased from 609.4 to 350.5 MiB, and DS Web decreased from 28,701
files / 463.4 MiB to 14,984 files / 357.0 MiB. Total measured size decreased from
1,758.3 to 1,438.4 MiB despite the test build retaining 45.6 MiB more Electron
locales than the portable build. These are unpacked sizes, not installer download
sizes. Builder-copy regression tests, production build/type checks, packaged
Harness/OpenCode/npm smoke checks, DS Web authentication/page scripts/workspace
creation, and packaged App/settings/notes-page loading passed. Full editor/canvas
interaction and other platform packages were not revalidated in this check.
以上 `dist*` 与 `portable` 命令用于普通本地桌面打包。它们会携带
`agent-runtime-lock.json`、`remote-runtime-lock.json` 与公开的
`agent-release-keys.json`,但所有桌面构建路径(包括本地 `portable`)都不会嵌入
+267 -49
View File
@@ -52,13 +52,21 @@ records are listed separately and do not introduce another feature status.
longer stop at 100 conversations, and consecutive sends no longer truncate
saved history beyond 500 messages. Model context limits still apply;
previously lost messages cannot be restored.
Pinned and unpinned groups order by the latest message creation time,
keeping positions stable during streaming text and tool updates.
- [x] **Optional desktop task notifications**: A default-on switch under
Platform Features / General immediately controls system task reminders.
In-app notifications remain available independently; General settings group
shortcuts, conversations and notifications, and the working directory.
- [x] **On-demand contextual help**: Supplementary page and settings
explanations use shared title- or field-adjacent help controls with mouse
and keyboard access. Important operation consequences and errors remain visible.
- [x] **Compact conversation controls**: Composer options share a compact
settings panel while Runtime, mode, sending, and queue controls remain
available. Conversation search has an inline clear action that restores the
scoped list and returns focus to the input.
scoped list and returns focus to the input. New conversation and expandable
search share one row. Composer shortcut hints reflect successful registration
and saved settings, without being overwritten by late startup responses.
- [x] **On-demand conversation history**: Lists load lightweight summaries
rather than every conversation's process metadata. Opened and active
conversations retain full details; search, copy, export, continued requests,
@@ -135,15 +143,18 @@ records are listed separately and do not introduce another feature status.
models, approval policy, budgets, and timeouts in a Project, with reusable
templates.
- [x] **Application center and navigation**:
Clicking the bottom App Center opens a lightweight upward anchored popup with enabled apps,
The footer shows equal-height Apps and Settings buttons with visible labels and a subtle vertical divider.
Clicking Apps, marked with an upward chevron, opens a lightweight upward anchored popup with enabled apps,
regardless of pinning or opening history, and no modal backdrop. Clicking an app row closes
the popup; Local Inference Monitor opens a separate modal preserving the workspace, while other apps
open their main content pages. Manage Apps opens searchable cards with settings details;
open their main content pages. Manage Apps uses a LayoutGrid icon and opens Application Center with searchable cards and settings details;
layout and interaction rules are defined in the [UI design](./docs/features/application-tool-navigation/ui-design.md).
All four application cards support arrow and drag reordering regardless of enablement or pinning.
All five application rows support arrow and drag reordering regardless of enablement or pinning.
Cards, the launcher, and the filtered sidebar share one persisted order. Knowledge
is always enabled and pinned, with Open and reorder controls. Supervisor, Notes,
and Local Inference Monitor have enablement and pinning settings. Supervisor uses
and Local Inference Monitor have enablement and pinning settings. Device Sharing
is optional and off by default during testing, with an enable switch but no pinning.
Supervisor uses
the existing Heartbeat identity and defaults to off when no preference is saved;
explicit saved choices are preserved. Disabling it retains plans and history,
blocks new reviews, and hides sidebar feedback; in-flight work may finish.
@@ -162,6 +173,16 @@ records are listed separately and do not introduce another feature status.
local-engine checks, and remaining platform/package coverage. Historical
failures are not the current full-suite result. See the
[validation progress](./docs/features/application-tool-navigation/progress.md).
- [x] **Local device-sharing metadata** (implemented in source): Enable Device Sharing
in Application Center first; it is off by default during testing. Saved explicit
enablement is preserved. Disabling leaves the page without clearing catalog records
or revoking publications. Open it from Apps or Application Center, save a service URL and device name, register the
device, browse returned records, publish capability or knowledge metadata, and
revoke this device ID's entries. The default service is `http://127.0.0.1:8787`.
This unauthenticated local service uses IDs for identification only. Server source
mode does not upload or sync content, and independent knowledge search/read/download
flags are declarations, not content access or remote execution. See
[scope and validation](./docs/features/device-sharing/README.md).
- [ ] **Privately deployable application marketplace** (planned): Extends
application management with an organization-owned catalog and application
distribution. Current management covers built-in apps only, not marketplace
@@ -194,7 +215,9 @@ records are listed separately and do not introduce another feature status.
reopen the upgraded database. The schema-35 repair released in 0.13.2 also handles
repeated tool blocks written after upgrading to 0.13.1, without deleting events.
Terminal tasks release their write caches; duplicate remote replay does not
rebuild a released cache. See
rebuild a released cache. Structure-only upgrades do not reconvert history or
reclaim ordinary free pages; the startup page distinguishes structure updates,
legacy conversion, and actual space reclamation. See
[storage and upgrade behavior](./docs/features/assistant-workbar/execution-history-storage.md).
- [x] **Direct model Runtime**: Supports question answering, knowledge
@@ -322,7 +345,7 @@ records are listed separately and do not introduce another feature status.
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
database. Successful tool START/END events appear exactly once, with no
Prompt, provider, or tool replay observed. The current Agent source lock is
`0.13.4`, while the current Desktop release candidate is `0.13.14`; formal
`0.15.3`, paired with the current Desktop release candidate `0.15.10`; formal
publication status follows the separate Agent and Desktop
release channels. Previous macOS validation covered native package installation,
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
@@ -371,7 +394,8 @@ records are listed separately and do not introduce another feature status.
compatible connection without requiring a duplicate selection. Settings
displays the actual administrator or fallback model source. Ask permits
only real Host-registered `read` and `skill` tools plus Main-managed Web
Search/Fetch proxies, and rejects plugin impersonation of those names.
Search/Fetch and enabled Story Graph proxies, and rejects plugin impersonation
of those names.
Execute allows all enabled built-in and plugin tools with the current user's
permissions. Image input follows the selected model connection's declared
capability: text models reject images before Host or model invocation, while
@@ -391,6 +415,22 @@ records are listed separately and do not introduce another feature status.
user's authorization for all tools, processes, network access, and writable
paths available to the current local or SSH account, including paths outside
the workspace and native subagent work.
- **Local native Runtime clients (preview)**: The composer opens Continue or
OpenCode in a workbar terminal, or the official DS Web client in the system
browser, using the current local project and model. These are independent
sessions, without imported GoodBuddy conversation history. Closing a DS page
does not stop its service; use GoodBuddy's stop action. Remote shortcuts are
not connected. Continue/OpenCode Ask now map Main-bound read tools through
native permissions. Managed standard Node delivery and full cross-platform
package acceptance remain incomplete;
existing Windows real-model evidence does not establish full capability parity.
See the [implementation and remaining work](./docs/features/assistant-workbar/progress.md#2026-09-27-local-native-client-validation).
- **Runtime telemetry opt-out**: Local DS Web and remote Runtime/model-bridge
launches share the telemetry opt-out environment without blocking model, MCP,
or user-requested networking. Continue skips disabled automatic-update checks.
Remote changes require a newly started Runtime from the matching Agent;
current-source Linux x64 Agent/model validation is recorded in the
[0.15.0 preparation record](./docs/development/release-preparation-0.15.0.md).
- [x] **Efficient direct-model workspace tools**: Direct models use bundled
ripgrep with native arguments for file discovery and content search, read large
UTF-8 files by line, and apply multi-file patches in Execute. Ask searches stay
@@ -418,6 +458,8 @@ records are listed separately and do not introduce another feature status.
waiting for another approval. See the [interaction boundaries](./docs/features/assistant-workbar/runtime-interactions.md).
- [x] **Native execution checklists**: OpenCode and Continue update a
read-only checklist above the conversation, with progress saved in history.
Details expand in an anchored, scrollable floating panel without shifting chat
content; outside clicks close it and Escape restores focus to its trigger.
Explicit clears and remote replay retain request ownership; cancellation
does not mark unfinished items complete. Remote delivery requires the matching
Agent package. See the
@@ -442,10 +484,19 @@ records are listed separately and do not introduce another feature status.
direct models report their own backoff phase. Footer dots are static, and
unsupported Runtime retry details are not invented. Conversation-list
indicators and send/stop behavior remain unchanged.
- [x] **Simplified Runtime selection**: Conversation and project menus no
longer enumerate every Runtime/model combination. Configure Runtime models
in system settings; saved fixed project selections remain usable and
explicitly display their fixed model.
Cancellation and failure retain existing replies and use a single status;
cancellation is neutral, and Edit again appears only when input can be restored.
Model-visible history preserves interrupted partial replies or placeholders
so cancelled questions are not merged into later requests. Reused local
OpenCode sessions receive a one-time interruption notice.
- [x] **Layered Runtime and model selection**: Resolve Runtime and model per
field through conversation, project, and global defaults. The two-column
composer picker shows sources and supports overrides or restoring project
defaults; project and channel settings share the same rules. Main resolves
each request again, including queued dispatch. Deleted or incompatible
connections fall back, while missing credentials remain explicit errors.
Schema 49 migrates stored choices. Purpose-specific models remain planned;
see the [selection design](./docs/features/model-connections/runtime-selection-design.md).
- [x] **Role-bound model connections**: Each role can inherit the default model
or select an independent text-model connection. Invalid connections safely
fall back to the default; the synthesis role always inherits the default.
@@ -470,7 +521,12 @@ records are listed separately and do not introduce another feature status.
fields take precedence, and these ordinary connection settings must not be
used to store API keys or other secrets. See
[model request customization](./docs/features/model-connections/README.md).
- [x] **Context usage and automatic compaction**: Direct models update usage
- [x] **Context usage and compaction**: Local direct text-model conversations
also support manual compaction below the automatic threshold or with automatic
compaction disabled. The selected summary model may incur charges; recent turns
and full stored history remain available, and saved summaries are reused.
Image connections and remote direct-model entry points are excluded.
Direct models update usage
from each successful provider call. Images and tool rounds use the same
accounting, with estimation only when the provider omits usage. The UI
distinguishes This Model Call from Post-Compaction Conversation Estimate,
@@ -575,10 +631,22 @@ records are listed separately and do not introduce another feature status.
the applicable checks pass.
- [x] **On-demand built-in MCP**: Knowledge, Magic Notes, GoodBuddy
configuration, and built-in browser MCP can be enabled independently and assigned to direct
models, GoodBuddy-managed OpenCode, and Continue. Settings explicitly marks
DeepSeek Harness unsupported. Built-in MCP uses short-lived local authority
models, GoodBuddy-managed OpenCode, and Continue. DeepSeek Harness remains
unsupported for these servers; Story Graph supports it through the Main proxy.
Built-in MCP uses short-lived local authority
for the current request, and user configuration cannot loosen Ask/Execute
read/write boundaries.
- **Obsidian integration (implemented in source, acceptance in progress)**:
Disabled by default, with all locally registered vaults as the default scope
and an optional folder that can be tested before saving or enabling.
Bundled MCPVault 0.16.0 provides all upstream abilities through 19 shared tools.
Automated production-component checks, Electron managed-Node tests, Windows
settings and chat UI checks, and restricted-network Windows x64 unpacked-app
checks passed. Physical-offline, installer, and other-platform acceptance
remain pending. DeepSeek
Harness and remote execution remain unavailable. See the
[feature docs and validation record](./docs/features/obsidian/README.md);
this is not a release claim.
- [x] **MCP Tools**: Explicitly enabled custom MCP can be assigned to direct
models, GoodBuddy-managed OpenCode, Continue Agent Execute, and DeepSeek
Harness, and loads only in Execute. Agent child processes receive only
@@ -599,6 +667,10 @@ records are listed separately and do not introduce another feature status.
retrieval parameters are collapsed by default. Documents distinguish ready,
processing, and failed states, with source opening, retry, and confirmation
before source removal.
Sources and indexed documents share one searchable list: single-document
sources do not duplicate rows, while directories retain grouped documents and
failed sources retain recovery actions. Task history and secondary operations
remain reachable from the consolidated controls.
- [x] **External knowledge-base connections**: Manage Dify, FastGPT,
and RAGFlow instances from the Knowledge page, then discover remote
knowledge bases or enter their IDs and verify bindings with provider-specific
@@ -648,7 +720,9 @@ records are listed separately and do not introduce another feature status.
records without replacing editors. Successful saves establish the editor's
normalized baseline; only unsaved changes require confirmation on leaving. Opening
a note shows a blank text/canvas composer above the history, and creation resets it
for the next record. Explicit editing updates a saved record. The separate To-dos view offers search, status filters,
for the next record. Explicit editing updates a saved record and exits after a
successful save; automatic comments do not reopen the editor, and failed saves
retain drafts. The separate To-dos view offers search, status filters,
grouped tasks and direct completion, with instructions, source entries and AI
comments in a right detail pane beside a stable task list. Narrow views offer
on-demand detail with an explicit return to the list. Wide views support a
@@ -661,6 +735,17 @@ records are listed separately and do not introduce another feature status.
navigation can show the incomplete-todo count, and create, save, and comment
results use application-wide notifications. Agent/MCP note writes automatically
refresh the open workbench while preserving selection and unsaved drafts.
- **Conversation capture and quick notes** (implemented; real Electron acceptance passed):
Preview and edit an assistant response or complete conversation text before
appending to an existing note or creating one with its first entry. The compact
Notes workbar tab supports title/body search, quick text appends and full-workspace
navigation. Saved sources link back to conversations and messages; drafts survive
conversation switches. Real App/preload/IPC/SQLite acceptance passed, including
complete 502-message history and native keyboard use in a 214px panel, with zero
model calls. The [feature validation record](./docs/features/magic-notes/progress.md)
separates focused passes, fixture repairs and earlier execution-time failures;
the [0.15.0 candidate validation](./docs/development/release-preparation-0.15.0.md)
passed the full suite, typecheck and lint.
- [x] **Paged canvas notes**: The integrated PeopleLib Fabric + Quill editor
combines flowing body text with pen/highlighter, object selection/transforms,
floating text, images and paper templates. Editors and read-only viewers support
@@ -683,13 +768,13 @@ records are listed separately and do not introduce another feature status.
invalidated by layout changes and reanalyzed in after-save-auto mode. MCP
rejects plain-text replacement of canvas entries. Implementation is complete;
final validation status is tracked in the [feature progress record](./docs/features/magic-notes/progress.md).
The first upgrade migrates stored bodies into files and raises the database
schema to 39 together with attachment changes. Older clients cannot reopen
Historical body migration moved stored bodies into files; schema 48 now adds
conversation-source metadata. Older clients cannot reopen
that database; rollback requires a pre-upgrade backup.
- [ ] **MCP Server Control Plane** (planned): Unified MCP lifecycle, health
checks, reconnection, schema cache, isolation, approval, and audit.
- [ ] **Traceable note excerpts and AI editing** (planned): Collects sourced
excerpts from conversations, knowledge, and the web and provides
- [ ] **Knowledge/web excerpts and AI editing** (planned): Extend capture beyond
the implemented conversation-text path to knowledge and web excerpts, with
confirmation-based summarization, rewriting, and organization.
### Work management, long-term collaboration, and workflows
@@ -705,7 +790,11 @@ records are listed separately and do not introduce another feature status.
model and normalizes the different OpenAI-compatible and Anthropic Messages
cache-reporting semantics when showing cache hit rate. Project and conversation
usage groups show collapsed totals by default and expand to Runtime/model child
rows; model grouping remains flat. Activity is grouped
rows; model grouping remains flat. A separate System tasks tab groups
background usage by source, with manual refresh and updates when usage is
recorded. Supervisor reviews, image regeneration, graph extraction, and remote
embeddings feed usage records; missing historical/provider data is not reconstructed.
Activity is grouped
by conversation and collapsed by default so long histories do not fill the
page.
- [x] **Compact conversation tool records**: Expand individual tool records
@@ -737,31 +826,56 @@ records are listed separately and do not introduce another feature status.
[Scheduled Task PRD](./docs/features/task-and-job/scheduled-task-prd.md).
Status counts and filters identify running tasks and those needing attention;
task approvals remain actionable in their task cards.
- [x] **Memory and Smart Heartbeat**: Provides periodic review, suggested
memories, insights, follow-up tasks, and auditable run history.
- [x] **Automatic supervision settings and scope**: Supervisor / Automatic supervision is
the sole authoritative plan configuration, automatic report, and suggestion entry. Daily or weekly plans can be
created and edited for Global or one or more selected Projects. Legacy
single-project settings migrate without loss, and project-level memory and
action output must explicitly target a Project in scope. Task Center and
Settings no longer duplicate the form. Partition-aware review, candidate
generation, and recall triggers remain to be designed. See the
[Smart Heartbeat PRD](./docs/features/smart-heartbeat/prd.md).
- [x] **Memory and Smart Heartbeat**: Plans trigger shared incremental reviews
instead of separate heartbeat reports. Choose memory/graph updates only or
evidence-backed suggestions for open items, disagreements, revisions, and
candidate conventions. Accepted open items create paused tasks; confirmed
conventions become long-term background. Suggestion failures can be retried
independently without undoing reviews.
- [x] **Smart heartbeat settings and scope**: Supervisor / Smart heartbeat
manages daily or weekly Global or selected-Project plans and suggestions.
Task Center and Settings do not duplicate this form. Pending suggestions are
not repeated; dismissed suggestions can return with changed evidence.
Project/global processing progress share one source-version timeline; real-time conversation
intervention remains planned. See the
[implemented rules](./docs/features/conversation-supervision/logic-design.md).
- [x] **Stories, experiences, and time-spiral navigation**: Reviews organize
events into project features, threads, and optional cross-project stories.
Users can rename, merge, reassign, remove, and undo adjustments. Experiences
retain formation/application evidence and human-edit markers; heartbeat
suggestions can identify stalled stories or potentially useful experiences.
Flat and lazy-loaded WebGL spiral views share selection and sources, with
a flat-view fallback and graphics cleanup. Organization and suggestions may
incur model costs and require source verification. See the
[implementation evidence](./docs/features/conversation-supervision/progress.md).
- [x] **Supervisor reviews, story graphs, and activity**: Review a selected scope
and period, read dated supervision results and history in Work review, inspect saved result graphs and sources, confirm or revise entities,
and preview local knowledge entity writes. Sidebar feedback follows or pins a
and continue a discussion with source-linked review context. Sidebar feedback follows or pins a
Conversation or Task and opens its matching result. It shows target names with
compact pin/refresh buttons and preserves long-title readability and keyboard
focus in narrow sidebars. Activity combines heartbeat
and downstream review stages, including failures and links to older results.
Automatic stages process only new, changed, or unprocessed source portions;
no-change checks skip model calls. Manual review pages through the selected history
and saves batches with their facts and sources. Runs can pause and resume from
saved progress; settings expose concurrency, timeouts, batch sizes, execution
budgets, and response capacity. Activity shows execution stages and saved batch
details. Work review and activity use the available panel width.
no-change checks skip model calls. Manual review is also incremental by default;
explicit reanalysis rereads the period without changing shared progress.
Automatic reviews do not resume user-paused runs. Reviews save batches with
their facts and sources. Runs can pause and resume from
saved progress, without fixed-duration automatic pauses; settings expose
concurrency, per-request timeouts, batch sizes, and response capacity.
Activity emphasizes connected stages and saved-batch coverage, with secondary
statistics in expandable details. Work review uses a vertical full-width
flow with a separate run status and correctly placed empty state.
Calls remain read-only without tools and may incur model charges. See the
[implementation and evidence](./docs/features/conversation-supervision/progress.md).
Manual starts, automatic triggers, and resume share one active review;
cancellation from Activity preserves saved batches and holds admission until
cleanup finishes. Cancelled reviews cannot resume. Story-graph events, entities,
and relations have separate counted tabs synchronized with canvas selection.
Continue discussion opens directly from feedback or graph sources, with an
editable question and explicit target-conversation navigation. The UI no
longer exposes knowledge-entity writeback controls or redundant context
explanations. Smart heartbeat shows actionable suggestions rather than report
metrics and trends.
- [ ] **Further supervision capabilities** (planned): Full event-by-event replay,
Experiment targets, chronological manual-edit audits, event
triggers, and broader execution observation remain incomplete. Current automatic
@@ -771,6 +885,16 @@ records are listed separately and do not introduce another feature status.
- [ ] **Batch runs and comparison lab** (planned): Compares model, Prompt,
role, and workflow configurations in batches and summarizes quality,
duration, tokens, cost, failure rate, and artifact differences.
- **Story Graph read tools (Desktop 0.15.9 / Agent 0.15.3)**: Agents can search saved work,
read current claims or timelines, and page through source evidence. Supervisor
enablement and Runtime assignment control discovery and calls, including old
sessions. Model, local Harness, and local/remote OpenCode and Continue share
the desktop reader. Queries make no model calls and do not infer replacement
from newer reviews. Shared source-version facts, stories, and experiences are
available across permitted scopes; `as_of` remains deferred. Keyword matching
and tool guidance support model-selected lookups without automatic full-graph
context injection; experiences remain advice, not instructions. See the
[design and validation](./docs/features/conversation-supervision/story-graph-mcp-design.md).
- [ ] **Temporal memory and fact-conflict detection** (planned): Adds validity
periods, current/expired/conflicting fact detection, fact checking, and
evidence tracing to memory and the knowledge graph.
@@ -820,9 +944,12 @@ records are listed separately and do not introduce another feature status.
- [x] **WeCom and DingTalk connections**: Main-only encrypted settings,
read-only environment-variable overrides, connection tests, dynamic
enable/disable, sender scope, and status diagnostics.
- [x] **Managed local-model download source**: Platform Features / General
selects ModelScope (default) or Hugging Face globally for future speech-input
and OCR model downloads. A source without a complete verified file is
- [x] **Managed local-model download source**: The Model Connections header shows
the current source and a secondary button, right-aligned on the same row as the
model-type tabs below Save. Its dialog selects ModelScope (default) or Hugging Face
globally for future managed local-model downloads; changes save only on Confirm,
Cancel leaves the saved source unchanged, and errors allow retry. Embedding, speech,
and OCR source links open this dialog directly. A source without a complete verified file is
explicitly unavailable and never silently falls back or combines files.
- [x] **Optional local speech-model management**: Model weights are not bundled.
Provides verified downloads, progress and cancellation, source links, ZIP or
@@ -851,13 +978,97 @@ records are listed separately and do not introduce another feature status.
### Open source, builds, and releases
- Current source candidates are Desktop `0.13.14` and Agent `0.13.4`, with
- The current Desktop candidate is `0.15.10`; published Agent `0.15.3` requires Desktop `0.15.9`, with
OpenCode pinned to `1.18.29` and Continue to `1.5.47`. Publication status follows the independent
Desktop and Agent release channels.
- Desktop `0.13.14` improves paged Supervisor reviews and resume behavior,
- Desktop `0.15.10` unifies project/conversation navigation with ten recent
conversations, adds independent Supervisor model selection and a default-on
per-conversation Story Graph switch. Indexed activity paging and complete
summaries, document Workers, lazy startup, narrower render subscriptions,
windowed lists, terminal batching and old Runtime cache cleanup reduce repeated
work without promising universal speedups. Schema 58 requires a pre-upgrade
database and notes backup. Frequent local writes use NORMAL and can lose a
recent suffix on power/system failure; remote acknowledgments, channel delivery
and task lifecycle writes retain FULL. Agent stays at 0.15.3. Four real Host
model requests and a 1,000-line Unicode SSH terminal check passed; exact-source
CI and six native packages remain release gates, and reported remote resume
failures are not claimed fixed. See the
[preparation record](./docs/development/release-preparation-0.15.10.md).
- Desktop `0.15.9` adds shared review progress, stories, experiences, and spiral
graph navigation, with schema 56 backup/rollback requirements and possible
model costs. Conversation/message windowing, Markdown caching, read-only
Workers, incremental activity writes, and checkpoint event batches reduce
repeated work without promising universal speedups. In-page find excludes
unmounted rows, rapid scrolling can show placeholders, and unavailable Workers
can fall back to synchronous reads. Execute configuration changes no longer
require duplicate native confirmation. Electron is 44.5.1. Four current-source
real Host model requests passed; exact-candidate CI and packaging remain required.
See the [preparation record](./docs/development/release-preparation-0.15.9.md).
- Desktop `0.15.8` and Agent `0.15.2` add Execute-only saving of conversation
images to absolute local or Host paths, with explicit overwrite and no WebP
conversion. MCP settings list image tools without promising availability in
every mode. Notes gain new-entry cancellation, edit-focus restoration and full
available content width. Missing browser and frosted-appearance preferences
default to enabled; saved choices remain unchanged. Schema stays at 52.
Real Linux x64 Host tool-layer evidence and remaining exact-candidate CI and
package checks are separated in the
[preparation record](./docs/development/release-preparation-0.15.8.md).
- Desktop `0.15.7` reuses completed streaming Markdown blocks and conversation
derived values without changing the default streaming cadence. Activity records
load on entry, refresh every 15 seconds while visible, and offer manual refresh.
Local custom MCP discovery failures are isolated per service. Managed model
source selection moves to a confirmation dialog in Model Connections, and Device
Sharing becomes an opt-in test feature. Schema remains 52. Exact-candidate CI
and packaging are still required; component benchmarks are not full-App results.
See the [preparation record](./docs/development/release-preparation-0.15.7.md).
- Desktop `0.15.6` adds optional, persisted frosted-glass header/checklist surfaces
(off by default, without OS desktop transparency), reduces repeated chat renders,
and supports direct previews for draft/queued images. Settings are grouped with
right-aligned switches, and note headers/editor spacing are tighter.
Local OpenCode retries explicit transient MCP transport failures once after
cleanup and retires uncertain names; it does not replay prompts or tool calls.
Redacted MCP diagnostics support feedback. Schema 52 adds execution-statistics
indexes; back up the database and notes before upgrading.
See the [preparation record](./docs/development/release-preparation-0.15.6.md).
- Desktop `0.15.5` updates Smart heartbeat and incremental review, preserves
cancelled-turn history, floats checklist details, and exits note editing after
saving. It also adds manual note tags with AND filtering and rename/merge/delete,
a persistent two-pane note layout with a full-width overview, sticky workspace
file controls, cached OpenCode terminal dependencies, and shorter MCP names.
Schema 51 adds tag storage alongside the migration of pending legacy memory
proposals into unconfirmed conventions; back up the database and adjacent notes before upgrading.
See the [preparation record](./docs/development/release-preparation-0.15.5.md).
- Desktop `0.15.4` introduces layered Runtime/model choices and separate
system-task usage, and reduces duplicate Runtime instructions and history.
Schema 49 migrates project/conversation choices; rollback requires the complete
pre-upgrade database and adjacent notes backup. Agent `0.15.1` requires Desktop
`0.15.4` and fixes remote Continue history wrapping without changing locked
Runtime versions. See the [0.15.4 preparation record](./docs/development/release-preparation-0.15.4.md).
- Desktop `0.15.3` restores local direct-model manual compaction, retains tool
authorization for the request lifetime, clarifies cancellation and sidebar/note
controls, avoids redundant startup/storage work, and trims duplicated package
dependencies and development files. It does not claim a proven overall rendering
speedup. Office live editing remains a design, not a delivered capability.
See the [0.15.3 preparation record](./docs/development/release-preparation-0.15.3.md).
- Desktop `0.15.1` carries forward the unpublished `0.15.0` Obsidian integration, conversation capture into Magic
Notes, and preview local native clients, plus knowledge, Supervisor, and UI
fixes. Schema 48 adds conversation-source metadata; older clients require a
complete pre-upgrade backup for rollback. Agent `0.15.0` requires Desktop
`0.15.2`, retains Node `24.19.0`, and updates remote telemetry opt-out and
disabled Continue update-check behavior without adding remote client shortcuts.
- Desktop `0.15.1` completed all six native release targets and GitHub/OSS publication.
Desktop `0.15.2` adds read-only Story Graph tools and a fully editable
Supervisor follow-up message, simplifies note capture, and improves exit diagnostics.
Remote Story Graph requires the paired Agent `0.15.0`; planned Host and cloud
management applications remain unimplemented. Current candidate evidence is in the
[0.15.2 preparation record](./docs/development/release-preparation-0.15.2.md).
- Desktop `0.13.15` carries forward the unpublished `0.13.14` changes to paged Supervisor reviews and resume behavior,
native ripgrep searches, to-do layouts, and explicit remote follow-up after an
uncertain result. Database schema 47 requires a complete pre-upgrade backup
for rollback to older clients.
- Desktop `0.13.16` separates structure upgrades from legacy history reclamation,
removes fixed-duration review pauses, and clarifies Supervisor layouts.
It adds no schema change over `0.13.15` and requires no Agent update.
- Agent `0.13.3` reclaims idle Runtimes and retires displaced Agents after existing
work drains. Its packages require Desktop `0.13.12`;
update Desktop first, then the Host environment. Node remains `24.19.0`.
@@ -865,13 +1076,14 @@ records are listed separately and do not introduce another feature status.
preserves completed results when idle Runtime cleanup fails, and removes verified
inactive Agent payloads and owned orphan processes after environment updates.
History and active or uncertain installations are retained. Its independent
packages require Desktop `0.13.14`; upgrade Desktop first, then update the Host.
- Release preparation requires the full local test suite, notes verification,
packages require Desktop `0.13.14` or later. Since `0.13.14` did not publish,
upgrade to Desktop `0.13.15` first, then update the Host.
- Release preparation requires the full test suite, notes verification,
typecheck, and lint. Candidate main-branch CI, its production build, native
packaging, and publication verification remain separate acceptance steps.
No local production build, packaging, or LoongArch preview is requested.
Current validation status is recorded in the
[release preparation record](./docs/development/release-preparation-0.13.14.md).
[release preparation record](./docs/development/release-preparation-0.15.0.md).
- Desktop `0.13.12` and Agent `0.13.3` have completed their native release jobs;
this does not replace development-time real-Host scenario coverage.
- Validation records remain separate from implementation status. Current-source
@@ -921,7 +1133,7 @@ records are listed separately and do not introduce another feature status.
Windows desktop-to-Linux checks cover installation, checklist updates, long lists,
cancellation and restart recovery. Execute session MCP delivery passed real
text-model Host checks with a substituted image service, not real image generation.
Candidate Agent `0.13.4` requires Desktop `0.13.14`; publication and native
Published Agent `0.13.4` requires Desktop `0.13.14` or later; publication and native
platform acceptance remain separate from Linux development evidence. See the
[validation record](./docs/features/remote-host/runtime-checklist-validation.md).
@@ -952,9 +1164,15 @@ records are listed separately and do not introduce another feature status.
offline GoodBuddy transfer release acceptance. Until these gates pass, this
path must not be described as published or as having passed formal release
acceptance.
- [ ] **Multi-cloud remote sandbox Agents** (planned): Manages dedicated Linux
sandboxes through cloud-provider APIs and SSH Agents. Credentials remain in
Main, and high-risk control-plane operations receive separate confirmation.
- [ ] **Host and cloud environment management** (planned): Move host management
into App Center with a list/detail workspace. A sibling cloud application
reuses host connections and remote execution. The first-version adapter plan
covers AWS, Azure, Alibaba Cloud, Tencent Cloud, VMware vCenter / vSphere,
and ZStack, delivered in batches with real instance discovery, import, and
execution acceptance. Instance creation, start/stop, deletion, and network
orchestration follow later. Provider APIs are not implemented; existing cloud
servers can already be added manually when SSH requirements are met. See the
[product scope](./docs/features/remote-host/prd.md).
## Roadmap Principles
+46 -21
View File
@@ -22,6 +22,8 @@
- [x] **跨项目会话活动**:统一入口汇总各项目正在运行和需要处理的对话,保留仍有活动的较早会话。锚定的“项目 → 会话”菜单支持键盘导航和精确打开对话,不再使用整块活动弹窗;空闲时隐藏摘要,长会话子菜单独立滚动,项目选择器继续显示活动数量。
后台会话成功完成后,在本次客户端运行期间保留完成提示,直到对应聊天可见且加载完成;提示不跨重启保留。
- [x] **会话置顶与完整历史**:常用会话可置顶,重启后保留。列表和搜索不再限于最近 100 个会话,连续发送不再截断超过 500 条的已保存历史;模型上下文限制仍有效,已丢失的历史无法恢复。
置顶与非置顶分组按最近一条消息的创建时间排序,流式文本和工具进度更新不再反复改变位置。
- [x] **可选桌面任务通知**:“平台功能 / 通用设置”提供默认开启、保存后立即生效的系统任务提醒开关,应用内提示独立保留;通用设置按快捷键、会话与通知、工作目录分组。
- [x] **按需上下文帮助**:页面和设置中的补充说明使用标题或字段旁的共享帮助入口,支持鼠标与键盘;关键操作后果和错误提示保持可见。
- [x] **紧凑会话控件**:输入选项集中在紧凑设置面板,Runtime、工作模式、发送和队列操作保持可用;会话搜索提供内嵌清除按钮,清除后恢复当前范围列表并把焦点返回输入框。
- [x] **会话历史按需加载**:列表只加载轻量摘要,不再一次读入全部会话的过程元数据;已打开和活动会话保留完整详情,搜索、复制、导出、续聊和远程待答问题保持可用。图片合并不会重新发送未变化消息,已确认保存的空闲详情随现有视图缓存释放。详见[历史读取与保留规则](./docs/features/assistant-workbar/execution-history-storage.md#会话列表读取与前端保留)。
@@ -34,14 +36,15 @@
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
- [x] **助手工作栏、多终端与可调布局**:工作栏开关位于明暗主题按钮旁;右侧工作栏使用持久“+”能力目录和应用 Tab,任务中心与工作区为不可关闭的单实例,浏览器可打开多个独立实例。任务中心可切换当前项目和所有项目,范围随布局保存;无项目任务在所有项目中标注“未绑定项目”。用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记编辑器的 AI 评论栏支持鼠标与键盘调宽;AI 评论栏记住显示状态及宽度,窄窗口下移至编辑器底部。
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
- [x] **应用中心与导航**:底部应用中心向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻筛选。点击应用行关闭菜单,本机推理监控打开保留底层工作区的独立 Modal,其余应用打开主内容区;底部“管理应用”打开可搜索卡片及设置详情,布局与交互见 [UI 设计](./docs/features/application-tool-navigation/ui-design.md)。四张应用卡片均支持上下移动和拖动排序,不受启用或常驻状态限制;卡片、菜单和经过过滤的侧栏共用一份保存顺序。知识库始终启用并常驻,提供“打开”和排序;监督者、魔法笔记和本机推理监控可设置启用与常驻。监督者沿用智能心跳身份,缺失启用设置时默认关闭,已明确保存的选择保留;关闭后保留计划和历史,拒绝新回顾并隐藏侧栏反馈,在途运行可完成。启用应用不会自动创建计划。旧应用顺序补齐默认项并保留已有相对顺序。设置持久化后的事件同步各入口,包括工具配置写入;重开中心刷新期间锁定修改,新快照覆盖迟到的旧读取。`local-inference` Modal 直接显示服务列表,不展示任务历史或外部连接;向量服务显示独立进程 CPU 与工作集内存,ASR/OCR 说明共享进程无法独立统计的原因。ASR 就绪状态未知,不从活动请求推断运行中。受支持的服务操作保留影响确认,失败后刷新列表并要求重新确认;TTS 显示不可用。取消中的推理在 Worker 确认或退出前仍计入活动任务及服务停止影响。实现已接入生产 App;验收记录分别列出完整 App、组件 fixture、真实本地引擎和剩余平台/安装包覆盖,不以历史失败代替当前全量结果,详见[验证进度](./docs/features/application-tool-navigation/progress.md)。
- [x] **应用中心与导航**:底部“应用”和“设置”为显示文字的等高按钮,中间使用弱竖分隔线。“应用”带向上箭头,点击后向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻筛选。点击应用行关闭菜单,本机推理监控打开保留底层工作区的独立 Modal,其余应用打开主内容区;底部“管理应用”使用 `LayoutGrid` 图标,打开标题仍为“应用中心”的可搜索列表及设置详情,布局与交互见 [UI 设计](./docs/features/application-tool-navigation/ui-design.md)。五个应用行均支持上下移动和拖动排序,不受启用或常驻状态限制;列表、菜单和经过过滤的侧栏共用一份保存顺序。知识库始终启用并常驻,提供“打开”和排序;监督者、魔法笔记和本机推理监控可设置启用与常驻。设备共享在测试阶段默认关闭,提供启用开关,不提供常驻开关。监督者沿用智能心跳身份,缺失启用设置时默认关闭,已明确保存的选择保留;关闭后保留计划和历史,拒绝新回顾并隐藏侧栏反馈,在途运行可完成。启用应用不会自动创建计划。旧应用顺序补齐默认项并保留已有相对顺序。设置持久化后的事件同步各入口,包括工具配置写入;重开中心刷新期间锁定修改,新快照覆盖迟到的旧读取。`local-inference` Modal 直接显示服务列表,不展示任务历史或外部连接;向量服务显示独立进程 CPU 与工作集内存,ASR/OCR 说明共享进程无法独立统计的原因。ASR 就绪状态未知,不从活动请求推断运行中。受支持的服务操作保留影响确认,失败后刷新列表并要求重新确认;TTS 显示不可用。取消中的推理在 Worker 确认或退出前仍计入活动任务及服务停止影响。实现已接入生产 App;验收记录分别列出完整 App、组件 fixture、真实本地引擎和剩余平台/安装包覆盖,不以历史失败代替当前全量结果,详见[验证进度](./docs/features/application-tool-navigation/progress.md)。
- [x] **本地设备共享元数据**(源码已实现):测试阶段默认关闭,先在应用中心启用,再从应用菜单或应用中心打开;已明确保存的启用选择保留,关闭会退出页面,但不清除目录或撤销已发布记录。可保存服务地址和设备名称、注册本机、浏览服务返回的目录、发布能力或知识元数据并撤销本机 ID 的条目。默认服务为 `http://127.0.0.1:8787`。服务未认证,ID 只用于识别;服务器来源不上传或同步内容,知识搜索/读取/下载标志是独立声明,不提供正文访问或远程执行。详见[范围与验证](./docs/features/device-sharing/README.md)。
- [ ] **私有化应用市场**(规划中):在应用管理基础上提供组织自己的目录与应用分发。当前只管理内置应用,尚无市场浏览或安装能力,详见 [FR-15](./docs/features/application-tool-navigation/prd.md#fr-15-私有化市场与-yaml-交换后续)。
- [ ] **通用助手工作栏与执行空间后续能力**(规划中):在现有工作栏和多终端基础上继续加入更完整的执行监督、统一 Runtime 监控、受管进程、可固定目标的工作区/浏览器/成果实例、底部停靠与独立窗口。Task Center 保持 Task 的单例索引,附件与知识库继续由会话输入区管理,未来的记忆与历史执行上下文归入关联 Task。详见 [Feature PRD](./docs/features/assistant-workbar/prd.md)。
- [ ] **ShareServer Office 协同编辑**(设计中):通过可选 ShareServer 集成 ONLYOFFICE Docs,在助手工作栏中以文件名打开多个文档 Tab,支持 DOCX、XLSX、PPTX 的人工编辑、保存、撤销、源文件冲突保护和后续 AI 选区修改。Office 正文会由所选 ShareServer 和编辑引擎处理;Desktop 不内置或启动 Document Server,未配置服务时不宣称离线编辑。详见[设计文档](./docs/features/office-document-editing/README.md)。
### Agent Runtime 与模型连接
- [x] **子任务历史去重与空间回收**:本机与托管 SSH 子任务事件只存变化,不再反复保存完整进度。首次升级自动转换旧记录并回收空间,显示进度且支持退出重试;保留聊天、执行详情、结果和远程事件去重。已随 0.13.2 发布的 schema 35 补修还会整理升级到 0.13.1 后重复写入的工具块,不删除事件;终态及时释放写入缓存,远程重复重放不重建已释放的缓存。旧客户端不能打开升级后的数据库,具体规则见[执行记录存储与升级回收](./docs/features/assistant-workbar/execution-history-storage.md)。
- [x] **子任务历史去重与空间回收**:本机与托管 SSH 子任务事件只存变化,不再反复保存完整进度。首次升级自动转换旧记录并回收空间,显示进度且支持退出重试;保留聊天、执行详情、结果和远程事件去重。已随 0.13.2 发布的 schema 35 补修还会整理升级到 0.13.1 后重复写入的工具块,不删除事件;终态及时释放写入缓存,远程重复重放不重建已释放的缓存。普通结构升级不重新转换历史或回收正常空闲页,启动页区分结构更新、旧内容转换与实际空间回收。旧客户端不能打开升级后的数据库,具体规则见[执行记录存储与升级回收](./docs/features/assistant-workbar/execution-history-storage.md)。
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行、图像生成,以及通过支持 OpenAI 兼容图片编辑接口的服务商进行参考图编辑。连续请求自动复用最近一次成功生成的图片和文字历史,已保存会话重开后仍可继续修改,本轮显式附图优先;上游明确不支持编辑时继续按文字生成,仅在回复底部提示未使用参考图。编辑使用 multipart 上传图片,结果仍只接受经过校验的内联图片,不下载服务商返回的图片 URL。详见[图片生成与连续修改](./docs/features/image-generation/README.md)。
- [x] **当前会话图片工具**:为图片模型开启会话调用后,支持工具调用的聊天模型可在 Execute 中生成图片、修改上传图片或历史成果,无需另开会话或手动切换图片模型。直连文本模型、本地 OpenCode、Continue、DeepSeek Harness 和托管远程 OpenCode 复用统一服务,Runtime 分配由模型设置派生;图片请求可能产生费用,编辑需服务商支持。详见[实现与验证记录](./docs/features/conversation-media-generation/progress.md)。
@@ -50,25 +53,29 @@
- [x] **高效直连模型工作区工具**:直连模型使用随包 ripgrep 的原生参数发现文件和搜索内容,按行分页读取大型 UTF-8 文件,并在 Execute 中应用多文件补丁。Ask 限于工作区只读搜索和读取,Execute 按当前账号权限搜索;用户无需另行安装 ripgrep。搜索保留原生输出与退出码,大结果通过 `output_read` 完整续读,可处理的参数错误允许模型调整后继续。
- [x] **长上下文与工具输出分页**:历史消息、长回复和文档解析文本不再采用旧的固定截断限制。直连模型可分页续读已保存的搜索、命令和 Subagent 输出;所选模型的上下文窗口及传输边界仍然有效。
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、启动与控制请求时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;本机 GoodBuddy 管理的 OpenCode 在该被动检查中只确认所选路径和模型凭据,不启动用后即毁的 Server,设置中的显式连接测试和原生能力清单仍执行完整启动及健康检查,原生能力清单随后与首次实际请求复用同一个执行 Runtime,不再重复冷启动;执行 Runtime 仍按项目复用,但 OpenCode 配置依赖和按内容摘要生成的 Skill 快照在所有本机项目间全局复用,会话数据、工具输出及请求级 MCP 状态继续隔离。本机 GoodBuddy 管理的 OpenCode 只对同一会话内的请求保持顺序,不同会话即使属于同一项目也可并行,不同项目同样可并行;请求级动态 MCP 通过默认通配禁用和当前请求精确放行保持隔离。托管的本机和远端 OpenCode 跳过界面未使用的自动 Git 快照,避免同步差异计算拖住会话,文件工具、子代理与工作区 Git 差异保留;Continue 持续排空不用展示的宿主 stdout,防止控制台输出填满管道。这些调整随新启动的 Runtime 生效,不改写外部 OpenCode Server 配置。OpenCode 与 Continue 不再按固定工具调用数或活动数中止单次运行,已观察到的工具与 Subagent 活动全部保留在本地会话,不再在原生任务继续执行时丢弃后续详情。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
- [x] **托管 SSH OpenCode 与 Continue**:该能力由“设置 > 平台功能”中的独立“远程项目”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 Linux x64、Linux arm64 和 macOS arm64 Host,不支持 Intel Mac;通过 SSH 按需启动 detached Agent,不要求开机服务。Ask 在 Runtime 工具边界保持只读,Execute 使用所选 SSH 账号的完整权限。Agent 持有 Prompt、模型和工具轮次、Runtime 进程及语义 transcript,Desktop 断开后重新 Attach 原操作而不重发 Prompt;模型凭据仅在当前操作内存中使用,不写入 Renderer、SSH 参数、环境或磁盘。当前 Agent 可以常驻,更新替换的旧 Agent 排空活动任务后退出。任务完成后回收空闲 Runtime,共享进程仍有任务或待答问题时保留;下一轮重建 Runtime 并恢复对话历史。设置页按平台和架构显示包状态,用户显式下载、更新或导入/导出包含 Agent、固定 Node、OpenCode 和 Continue 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。Continue 需要更新后的联合包与配套 Desktop。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。历史 macOS OpenCode 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证,不代表 Continue 的同等实机覆盖或新版本已经发布。当前源码候选为 Agent `0.13.4`、Desktop `0.13.14`;详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以独立发布渠道为准。
- [x] **托管 SSH OpenCode 与 Continue**:该能力由“设置 > 平台功能”中的独立“远程项目”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 Linux x64、Linux arm64 和 macOS arm64 Host,不支持 Intel Mac;通过 SSH 按需启动 detached Agent,不要求开机服务。Ask 在 Runtime 工具边界保持只读,Execute 使用所选 SSH 账号的完整权限。Agent 持有 Prompt、模型和工具轮次、Runtime 进程及语义 transcript,Desktop 断开后重新 Attach 原操作而不重发 Prompt;模型凭据仅在当前操作内存中使用,不写入 Renderer、SSH 参数、环境或磁盘。当前 Agent 可以常驻,更新替换的旧 Agent 排空活动任务后退出。任务完成后回收空闲 Runtime,共享进程仍有任务或待答问题时保留;下一轮重建 Runtime 并恢复对话历史。设置页按平台和架构显示包状态,用户显式下载、更新或导入/导出包含 Agent、固定 Node、OpenCode 和 Continue 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。Continue 需要更新后的联合包与配套 Desktop。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。历史 macOS OpenCode 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证,不代表 Continue 的同等实机覆盖。当前 Agent 源码锁为 `0.15.3`,配套 Desktop 候选为 `0.15.10`;详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以独立发布渠道为准。
- [x] **SSH Host 手动环境准备源码链路**:Host Key、认证和系统探针成功后先保存 Host,并只读探测共享 Agent/Runtime;保存 Host、打开项目都不自动安装。Host 卡片只有一个按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮,次级 SegmentedControl 选择默认且不持久化的“自动”、Host 下载或 GoodBuddy 传输;“版本匹配”badge 不等同环境健康。自动模式只在 operation/prepare 前探测并择一,显式选择保持有效,任何 prepare、commit 或 adoption 失败都不跨 acquisition 自动 fallback。两种方式把同一签名 compound `.gbagent` 交付到固定 staging 后,共用 control-plane prepare、commit、Agent activate/health、Runtime activate、finalize 与显式 cleanup。GoodBuddy 路径可在同一次操作下载并验证缺失候选、缓存并取得 lease,再有界流式 SFTP 上传一个归档和其中已验证的 bootstrap Node,不把约 294 MiB 整包读入 Main `Buffer`;Host 在解包时完成一次完整 payload 校验。未完成操作只记录暂存 cleanup 所需的 operation ID;下次更新尽力清理旧暂存后重新 prepare,不保存远端 metadata 副本,也不让 cleanup 失败阻塞新更新或回滚健康环境。已有项目在实际使用 Workspace/Runtime 时按需解析 Host current identity 并执行固定 `attach-or-bootstrap`,注册后的 health、capabilities 和 prompt 启动不扫描完整 payload。详见 [设计说明](./docs/features/remote-host/environment-provisioning-technical-design.md)。
- **SSH Host 环境准备验证记录**:当前源码已在隔离 Linux x64 环境通过包安装、Ask/Execute、原生子代理工作区外写入、重连及 stop/bootstrap。现有记录未覆盖 Host 卡片的完整 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输矩阵,候选 CI/原生打包与系统休眠唤醒也尚未验证;开发记录不代表版本已经发布。
- [x] **远程工作区与长任务更新**:Agent `0.11.23` 新增远程文件/Git 管理和可选模型限额,随包 Runtime 不再施加固定十分钟 Prompt 时限;无限请求时长仍保留独立连接超时。请先升级 Desktop 至 `0.12.11`,再下载并更新 Host 上的 Agent。
- [x] **远程原生问答与取消修复**:Agent `0.11.24` 修复远程原生问答转交和取消待答后同一会话续发,需要先升级 Desktop 至 `0.13.0`。
- [x] **DeepSeek Harness(预览)**:使用 GoodBuddy 固定 Host 和 OpenAI 兼容模型连接;优先使用管理员提供的连接,否则跟随兼容的默认模型或首个兼容连接,无需单独重复选择,设置页显示实际管理员或回退模型来源。Ask 只允许调用 Host 中真实注册的 `read`、`skill` 以及 Main 管理的 Web Search/Fetch 代理,拒绝插件同名冒充,Execute 放行全部已启用内置及插件工具,并以当前用户权限运行。图像输入跟随所选模型连接的能力声明,文本模型在 Host 或模型调用前拒绝图片,图片模型通过有界内联内容和临时 Attachment Store 接收 JPEG/PNG。Windows Host 启动和 ACP 会话共用规范工作区路径,同目录不同写法可复用正常 Runtime,不再因路径不一致而创建会话失败。
- [x] **DeepSeek Harness(预览)**:使用 GoodBuddy 固定 Host 和 OpenAI 兼容模型连接;优先使用管理员提供的连接,否则跟随兼容的默认模型或首个兼容连接,无需单独重复选择,设置页显示实际管理员或回退模型来源。Ask 只允许调用 Host 中真实注册的 `read`、`skill` 以及 Main 管理的 Web Search/Fetch 和已启用的 Story Graph 代理,拒绝插件同名冒充,Execute 放行全部已启用内置及插件工具,并以当前用户权限运行。图像输入跟随所选模型连接的能力声明,文本模型在 Host 或模型调用前拒绝图片,图片模型通过有界内联内容和临时 Attachment Store 接收 JPEG/PNG。Windows Host 启动和 ACP 会话共用规范工作区路径,同目录不同写法可复用正常 Runtime,不再因路径不一致而创建会话失败。
- [x] **DSH npm 插件市场**:市场默认关闭,由用户显式开启后搜索公共 npm 的 `dsh-plugin` 包,使用捆绑 npm 执行精确版本安装和普通 lifecycle scripts,并支持启停、JSON 配置、移除、失败启动自动停用和离线管理已安装插件;关闭市场只隐藏目录与管理界面,不改变已有插件的启停状态,第三方代码不受 Ask 初始化隔离。
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权,包括工作区外路径及原生子代理工作。
- **本地 Runtime 原生客户端(预览)**:输入区可按当前本地项目与模型,在工作栏终端打开 Continue/OpenCode,或在系统浏览器打开官方 DS Web。客户端使用独立会话,不导入 GoodBuddy 对话历史;关闭 DS 网页不停止服务,可在 GoodBuddy 中停止。Continue/OpenCode Ask 已通过原生权限映射 Main 绑定的只读工具。远程快捷入口未接通,托管标准 Node 交付和完整跨平台安装包验收仍有缺口;Windows 真实模型证据不代表全部会话能力等价,详见[实现与剩余工作](./docs/features/assistant-workbar/progress.md#2026-09-27-local-native-client-validation)。
- **Runtime 遥测关闭配置**:本地 DS Web、远程 Runtime 和模型桥启动共用遥测关闭环境,不限制模型、MCP 或用户主动联网。Continue 在自动更新关闭时跳过检查;远程改动随配套 Agent 新启动的 Runtime 生效,当前源码 Linux x64 Agent/model 验证见 [0.15.0 发布准备](./docs/development/release-preparation-0.15.0.md)。
- [x] **Runtime 原生交互转交**:OpenCode 与 Continue 的选择、yes/no、自由文本回答和跳过使用现有问答卡片,本机与托管 SSH OpenCode 同时转交属于当前请求的子会话提问。托管 SSH OpenCode 问答需要 Desktop `0.13.0`、Agent `0.11.24` 和新启动的托管 Runtime,不扩展到任意 ACP 服务。回答或跳过成功后在原位置保留问题与答案,支持多轮记录和本地会话重载;旧版本已丢弃的答案不能恢复。并行问题按顺序等待回答,重复事件保留草稿,提交失败可重试;取消远程待答后可在同一会话继续发送。Execute 权限确认自动处理,不等待第二次审批;具体支持范围见[交互边界](./docs/features/assistant-workbar/runtime-interactions.md)。
- [x] **原生执行清单**:OpenCode 和 Continue 在对话顶部更新只读执行清单,进度随会话保存。显式清空和远程重放保留请求归属,取消不把未完成条目标成完成;远程交付需要配套 Agent,详见[清单契约](./docs/features/assistant-workbar/runtime-checklist-technical-design.md)。
- [x] **原生执行清单**:OpenCode 和 Continue 在对话顶部更新只读执行清单,进度随会话保存。详情在可滚动的锚定浮层中展开,不挤动聊天内容;点击外部关闭,Escape 关闭后焦点返回入口。显式清空和远程重放保留请求归属,取消不把未完成条目标成完成;远程交付需要配套 Agent,详见[清单契约](./docs/features/assistant-workbar/runtime-checklist-technical-design.md)。
- **取消后的历史**:模型可见历史保留已中断的部分回复或占位标记,避免旧问题合入下一轮;本地 OpenCode 复用会话时附带一次中断说明。
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个专家并行分析。专家继承父请求 Ask/Execute 模式,可使用已启用的本机直连模型工具,Ask 仍只读;这些工具不是远程 OpenCode 子会话。聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
- [x] **OpenCode 子代理过程与最终结果**:子代理卡片按顺序显示文字、推理和工具过程,最终结果独立展示。远程实时过程需要 Agent `0.11.20`,旧包仍显示最终结果但不补造缺失过程。
- [x] **OpenCode 事件连接回收**:聊天与原生上下文压缩在结束事件迭代前关闭各自订阅,覆盖完成、失败、取消和消费方提前结束,不取消其他并行会话。
- [x] **准确的消息底部状态**:区分请求准备、等待重试、开始重试、工具活动、待回答和终态;本机 OpenCode 使用原生重试次数和计划时间,直连模型显示自身退避阶段。消息底部状态点保持静态,未接入的 Runtime 重试不编造提示,会话列表闪点及发送、停止行为不变。
- [x] **直连模型命令工作目录**:Execute 命令可通过绝对路径、相对路径或符号链接使用工作区外目录;工作区仍为默认目录和相对路径基准,Ask 保持只读。
- [x] **简化 Runtime 选择**:会话与项目菜单不再枚举每个 Runtime 与模型的组合,Runtime 模型在系统设置中配置;已保存的项目固定模型仍可使用,并明确显示其固定模型。
- [x] **Runtime 与模型分层选择**:按会话、项目、全局逐字段解析 Runtime 和模型。会话双栏选择器显示来源,可覆盖或恢复项目默认;项目与通道设置共用规则,Main 在每次请求及排队派发时重新解析。已删除或不兼容的连接会回退,缺少凭据仍明确报错。schema 49 迁移已保存选择,用途模型仍为规划,详见[选择设计](./docs/features/model-connections/runtime-selection-design.md)。
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Responses、OpenAI Chat Completions、OpenAI Images 和无认证本机模型;新用户默认连接为本机 Ollama 兼容地址,不预置第三方云模型服务。升级时只替换从未配置凭据且仍完全等于旧内置值的历史默认连接,用户显式保存或加密凭据的连接保持不变;已有部署仅使用兼容环境变量提供凭据时继续沿用对应的历史连接参数,通用模型环境变量仍优先。“保存并测试模型”会发送有界的真实文本或图片生成请求并校验生成结果,而不是只检查 HTTP 连通性,因此可能产生少量服务商用量费用。
- [x] **模型请求定制**:每个 LLM 连接可填写有界 JSON 自定义请求头和顶层请求体。直连与 Continue 支持两者;本地 OpenCode 和 DeepSeek Harness 只使用各自原生支持的请求头,受管 SSH OpenCode 模型网关支持两者。Runtime、协议和认证字段始终优先,这些普通连接设置不能用于保存 API Key 或其他密钥。详见[模型连接请求定制](./docs/features/model-connections/README.md)。
- [x] **本地直连手动压缩**:文本模型会话可在自动压缩关闭或未到阈值时手动生成摘要,保留最近轮次和完整历史,保存的摘要用于后续对话;图像连接和远程直连入口不适用。摘要模型调用可能产生费用。
- [x] **上下文用量与自动压缩**:直连模型按每次成功调用更新供应商用量,图片与工具轮次使用同一口径,供应商缺失 usage 时才回退估算;界面明确区分“本次模型调用”和“压缩后对话估算”,压缩线始终根据当前设置与所选模型窗口即时计算,不在每个对话中保存旧配置;压缩标识的前后值使用同一估算口径,运行记录仍保留各次模型调用的供应商 usage。对话与多轮工具 Agent 可在已完成调用越过阈值后自动重复压缩,规划时先为固定提示、工具定义和摘要预留预算;同一回复会分别保留 Agent 工具上下文与对话历史的压缩标识,并在应用重启或较早消息滚出本地历史窗口后继续复用摘要。
- [x] **持久凭据保护**:API Key 由 Main 使用系统安全存储加密且不暴露给 Renderer。托管 SSH accepted Prompt 只把当前 profile 与密钥放入 Agent 内存,不写入 SSH 参数、远端环境或磁盘;其他路径仍在 Main 内使用。密钥随对应模型连接保存,修改服务地址或临时切换为无需认证不会要求重新输入;只有用户显式清除凭据或删除连接时才移除。
- [x] **有界故障诊断**:Desktop 在用户数据目录中轮转保存启动、Runtime 和远程连接的固定阶段失败,最多 4 个 256 KiB 文件;GoodBuddy Agent 在各 installation 的私有 state 目录中轮转保存 daemon、连接、恢复和 Runtime 生命周期,最多 3 个 64 KiB 文件,并可通过固定 `diagnostics --installation-id` 命令读取。两端都只记录白名单阶段、稳定错误码/类型和固定短消息,不保存 Prompt、凭据、文件内容、路径、环境、SSH 参数或 Provider 原始响应;诊断写入失败不改变正常运行。
@@ -83,10 +90,12 @@
- [x] **Skills 按需接入**:可分配给直连模型、OpenCode、Continue 和 DeepSeek Harness,并使用有界资源和受控 Runtime 边界。
- [x] **本机工具执行环境源码链路**:在“能力与工具 > 工具执行环境”中为本机 Skills 与 stdio MCP 选择 GoodBuddy 托管 Node.js、按需安装的托管 Python,或经过真实验证的自定义解释器;提供独立的原生地址/OSS 镜像选择、诊断、安装进度、取消和删除。新的本机 Runtime 与 stdio MCP 获得不可变 PATH 快照,不修改普通终端、系统环境或远程 Host。Windows x64 托管 Node 与原生地址 Python 已通过真实安装验证;托管 Python 归档按目标文件系统验证,Linux 保留大小写不同的合法路径,所有目标仍拒绝完全重复路径和不安全条目。
- **本机工具执行环境验证记录**:六个平台/架构的 OSS 镜像对象已完成字节、大小和 SHA-256 公开回读验证。托管 Python 保持按需下载,不向 Desktop 发行包额外带入许可证文件。每个标准打包任务使用目标架构原生 Runner,并在打包前真实安装托管 Python,验证 SSL、pip 与 venv。不可变的 `v0.12.0` 尝试在发布前通过 Windows 与 macOS,但暴露了 Linux TAR 大小写处理问题;`v0.12.1` 在同步 Agent 发布时于原生打包前取消。已发布的 `v0.12.2` 随后通过全部六个原生打包任务及其托管 Python 安装探针。现有记录未覆盖真实 Skill/MCP、自定义解释器和运行中进程协调。
- [x] **内置 MCP 按需接入**:知识库、魔法笔记、GoodBuddy 配置与内置浏览器 MCP 可分别启停,并可分配给直连模型、GoodBuddy 管理的 OpenCode 和 Continue;DeepSeek Harness 在设置中明确显示为暂不支持。内置 MCP 仅通过当前请求的短期本机权限提供,Ask / Execute 读写边界不受用户配置放宽。
- [x] **内置 MCP 按需接入**:知识库、魔法笔记、GoodBuddy 配置与内置浏览器 MCP 可分别启停,并可分配给直连模型、GoodBuddy 管理的 OpenCode 和 Continue;这些 Server 在 DeepSeek Harness 中仍不支持。Story Graph 另通过 Main 代理支持 Harness。内置 MCP 仅通过当前请求的短期本机权限提供,Ask / Execute 读写边界不受用户配置放宽。
- **Obsidian 接入(源码已实现,验收中)**:默认关闭,默认发现全部本机注册仓库,也可指定文件夹并在保存或启用前测试。随包提供 MCPVault 0.16.0,19 个共享工具保留全部上游能力;生产组件自动化、Electron 托管 Node、Windows 设置与聊天 UI,以及受限网络下的 Windows x64 解包应用已验证。物理断网、安装器及其他系统验收待完成。DeepSeek Harness 与远程执行保持不可用,详见[功能文档与验证记录](./docs/features/obsidian/README.md);此项不表示已发布。
- [x] **MCP Tools**:显式启用的自定义 MCP 可按 Runtime 分配给直连模型、GoodBuddy 管理的 OpenCode、Continue Agent Execute 和 DeepSeek Harness,并仅在 Execute 加载;Agent 子进程只获得按请求签发的本机回环权限,MCP 地址、命令和凭据保留在 Main,动态工具仍经过发现、执行记录与权限边界。直连模型与 Harness 的健康调用复用当前请求已发现的工具清单,不在每次调用前重复发现。
- [x] **MCP Prompts 与 Resources 元数据**:MCP 测试仅在 Server 声明对应能力时发现有界的 Prompt、参数与 Resource 元数据,不读取 Resource 内容;Runtime 支持的 Prompt 可填入聊天草稿后继续编辑。OpenCode 可报告实验性 Resource 清单,Continue 当前版本明确不支持 Resources。
- [x] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。知识范围按对话保存,新对话默认不选择知识库;创建与检索高级参数默认折叠。文档区分可用、处理中和失败,支持打开来源、失败重试及移除来源前确认。
内容来源与文档索引共用可搜索列表,单文档来源不重复占行,目录保留分组文档,失败来源保留恢复操作;任务历史和次级操作从统一入口访问。
- [x] **外部知识库接入**:在知识库页面管理 Dify、FastGPT 和 RAGFlow 实例,通过目录或手工 ID 验证并绑定远端知识库,保留 Provider 专属检索配置和引用;对话支持按需检索及每次回答前检索。外部系统只负责知识检索,不接入其 App、Chat、Workflow 或 Agent,也不批量同步、本地索引或修改远端内容。被回答引用的有界检索片段随会话保存在本机。详见[外部知识库接入 PRD](./docs/features/knowledge-base/external-knowledge-prd.md)。
- **外部知识库验证记录**:三家 Provider 已通过真实服务、本机 HTTP MCP 和生产 IPC 短答案检查,Dify 另通过完整 App 输入区到回答及引用的链路。现有记录未覆盖 FastGPT/RAGFlow 桌面专属参数、复杂多库问答和远程回答前检索,详见[验证记录](./docs/features/knowledge-base/progress.md)。
- [x] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
@@ -98,28 +107,33 @@
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论。笔记从自适应卡片总览打开;独立待办页提供搜索、状态筛选、分组列表和直接勾选,宽屏列表稳定在左侧,任务说明、来源记录及 AI 评论在右侧同一区域显示,窄屏按需显示详情并提供返回列表。紧凑页签放在页头新建按钮左侧,筛选放在搜索右侧。打开原笔记再返回可恢复任务上下文;切换任务不会混入上一项的来源内容,读取失败使用应用通知并提供就地重试,来源缺失有明确提示。左侧入口可按设置显示未完成待办数量,创建、保存和评论结果使用统一应用通知。Agent / MCP 写入后,已打开的工作台自动更新,并保留当前选择与未保存草稿。
- **会话采集与快速笔记**(已实现,真实 Electron 验收通过):单条助手回答或完整会话正文可先预览、编辑,再追加到已有笔记或连同首条记录创建新笔记。右侧笔记 Tab 支持标题与正文搜索、快速追加及打开完整工作区,记录来源可返回会话和消息;切换会话保留草稿。真实 App/preload/IPC/SQLite 验收覆盖完整 502 条历史及 214px 面板原生键盘操作,模型调用为 0。[功能验证进度](./docs/features/magic-notes/progress.md)保留聚焦通过、fixture 修复与当时的测试和类型检查失败;[0.15.0 候选验证](./docs/development/release-preparation-0.15.0.md)已通过全量测试、类型检查和 lint。
- 待办宽屏列表与详情之间可拖动分隔条并记住宽度;来源笔记分组可独立折叠,不清除当前选中任务的详情。
- [x] **连续记录工作区**:笔记详情连续展示全部记录,左侧索引默认 168px,可拖动右边界或用键盘在 140–320px 内调宽,并记住偏好;索引与右侧 AI 评论独立收起。每条画布记录显示真实首页缩略图、页数和时间,文字记录显示摘要;点击索引只滚动定位,保留当前编辑器和草稿。打开笔记就在标题下方显示空白输入区,保留文字/画布选择和保存操作;新增保存后清空,供下一条记录使用,旧记录通过编辑入口修改。空白输入区和保存后未再修改的记录返回时不提示;保存失败和真实修改仍受保护。窄屏索引使用固定宽度折叠抽屉,不显示调宽分隔条,AI 评论移至底部。
- [x] **分页画布笔记**:已集成 PeopleLib Fabric + Quill 编辑器,支持跨页正文、笔迹、高亮、对象选择及变换、浮动文字、图片和纸张模板。PDF 可作为分页底版导入并提取原生文字;正文和批注可导出为栅格 PDF,导出文件不保留可搜索文字层。用户手动保存后,记录正文与二进制资源存入本地笔记文件,SQLite 保留元数据、索引、修订、待办和评论。备份须协调保存 SQLite 与笔记目录,产品无一键备份入口。撤销/重做限当前批注页或 Quill 模式,不提供全局文档撤销、无限画布或 PNG 下载按钮。草稿、已保存记录和来源为画布的待办按默认模型图像输入能力使用页面图像加文字,或明确标注仅文字降级;“发送画布页数”可选 1~8,默认 1,按当前顺序发送前 N 页,文字和图片范围一致,保存与导出仍支持 50 页。纯视觉记录/草稿需要支持图像输入的模型。即时评论模式提供手动画布分析,保存后自动分析失败不阻断或回滚保存。提取文字不变时保留文字评论;布局变化使视觉评论失效,并在保存后自动评论模式下重新分析。MCP 禁止以纯文字覆写画布记录。实现已完成,最终验证状态见[功能进展](./docs/features/magic-notes/progress.md)。
编辑与只读画布均支持 25%–300% 视图缩放、100% 还原及随视口调整的“适应宽度”,不改变保存内容或导出尺寸。
首次升级将历史正文迁移到文件,并随附件变更把数据库升级至 schema 39;旧客户端不能再打开,回退需使用升级前备份。
历史正文迁移已改用文件存储,当前 schema 48 新增会话来源元数据;旧客户端不能再打开,回退需使用升级前备份。
- [ ] **MCP Server Control Plane**(规划中):统一 MCP 生命周期、健康检查、重连、Schema 缓存、隔离、审批和审计。
- [ ] **可追溯笔记摘录与 AI 编辑**(规划中):从对话、知识和网页收集带来源的摘录,并提供需确认的总结、改写和整理操作。
- [ ] **知识/网页摘录与 AI 编辑**(规划中):在已实现的会话正文采集之外,扩展知识和网页摘录,并提供需确认的总结、改写和整理操作。
### 工作管理、长期协作与工作流
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和独立成果文件;普通聊天回复只保留在会话中,不再自动复制到成果栏,已有重复聊天 Markdown 从成果列表隐藏但不物理删除。运行记录改由 Main SQLite 保存,不再受旧 Renderer 的 500 条、单详情 4,000 字符或约 2 MB 上限影响,页面按批继续显示;不再常驻旧缓存截断警告,但不会恢复此前已丢失的历史。Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;用量按项目或会话分组时默认收起并显示汇总,展开后显示 Runtime / 模型子行,按模型分组时保持平铺;活动按会话分组并默认收起,避免长历史占满页面。
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和独立成果文件;普通聊天回复只保留在会话中,不再自动复制到成果栏,已有重复聊天 Markdown 从成果列表隐藏但不物理删除。运行记录改由 Main SQLite 保存,不再受旧 Renderer 的 500 条、单详情 4,000 字符或约 2 MB 上限影响,页面按批继续显示;不再常驻旧缓存截断警告,但不会恢复此前已丢失的历史。Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;用量按项目或会话分组时默认收起并显示汇总,展开后显示 Runtime / 模型子行,按模型分组时保持平铺。独立“系统任务”页签按来源汇总后台用量,支持手动刷新和记录更新后刷新;监督回顾、图片重新生成、图谱提取和远程嵌入补充用量记录,不补算缺失的历史或供应商数据。活动按会话分组并默认收起,避免长历史占满页面。
- [x] **紧凑会话工具记录**:逐项展开工具记录即可查看和复制结果、错误及输入参数;会话与子任务进度使用简洁行,减少重复 Runtime 摘要。OpenCode、Continue、DeepSeek Harness 和直连模型可显示已有文件路径、命令或搜索摘要,并在工具完成和会话重载后保留。
- [x] **Task 与定制任务体验**:每个产品级 Task 只关联一条 Conversation,一条 Conversation 可承载多个 Task;左侧会话列表通过行首展开按钮显示带共享状态点的 Task 子项,父会话行不重复任务标签,UI 只展示到 Task,不暴露 Job/Run 层级。新建定制任务可关联当前或新 Conversation,默认 Execute 并沿用 Runtime、工具和审批边界;重复触发复用同一 Task,文本结果回写 Conversation,独立文件和图片保留为成果。普通消息与到期 Scheduled Task 共用 Conversation 级持久发送队列,同一会话一次只执行一项;当前回复期间仍可继续发送,队列按顺序续跑,并允许删除或“立即中断并插入”。Task Center 继续作为完整索引,不建设独立 Automation Center。当前计划触发支持单次、每日和每周;高级时区、Cron、事件触发与重试治理仍按 PRD 逐步实现。详见 [Task Center PRD](./docs/features/task-and-job/task-center-prd.md) 和 [Scheduled Task PRD](./docs/features/task-and-job/scheduled-task-prd.md)。
- [x] **记忆与智能心跳**:当前提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
- [x] **记忆与智能心跳**:计划触发共享增量回顾,不再单独生成心跳报告;可只更新记忆图谱,或从未决事项、分歧、修订和候选约定生成有依据的建议。接受未决事项创建暂停任务,确认约定写入长期背景;建议失败可单独重试,不撤销已完成回顾。
- [x] **定时消息沿用会话**:定时消息使用会话当前历史、Runtime、工作模式和已保存的知识检索设置;任务详情显示实际运行模式,当前 occurrence 未结束时禁止重复“立即运行”。
任务中心通过状态数量和筛选定位运行中及待处理任务,审批操作保留在对应任务卡片中。
- [x] **自动监督设置与范围**:“监督者 > 自动监督”统一管理计划、自动监督报告和建议,支持创建和编辑 Global 或指定一个、多个 Project 的每日、每周计划;工作回顾阅读带日期的统一监督结果及历史。旧单项目配置无损迁移,项目级记忆与行动输出必须显式指定范围内的 Project。Task Center 和设置中心不再复制心跳表单;按分区回顾、候选生成和唤起条件仍待设计。详见 [智能心跳 PRD](./docs/features/smart-heartbeat/prd.md)。
- [x] **监督者回顾、故事线图谱与活动**:按范围和时间回顾工作,查看保存结果的图谱及来源,确认或修订实体,预览写入本地知识库实体。侧栏可跟随或固定 Conversation、Task,并打开匹配结果。活动合并心跳报告与下游监督的阶段状态,保留失败和旧结果入口。自动阶段只处理新增、修改及未处理的来源片段,无变化时跳过模型;手动回顾重新整理有界历史。回顾只读且禁止工具,模型调用可能产生费用,详见[实施与验证记录](./docs/features/conversation-supervision/progress.md)。
- 手动回顾分页处理所选历史,并按批保存事实与来源;支持暂停和从保存进度继续。设置提供并发、超时、批次大小、执行预算和响应容量,活动记录显示实际阶段与已保存批次详情;工作回顾和活动记录使用面板可用全宽。
- [x] **智能心跳设置与范围**:“监督者 > 智能心跳”统一管理 Global 或指定 Project 的每日、每周计划及建议,Task Center 和设置中心不复制表单。待处理建议不重复,忽略后有新依据才再次提出;项目与全局共用按来源版本维护的时间线进度,会话级实时主动介入仍为规划。详见[实现规则](./docs/features/conversation-supervision/logic-design.md)。
- [x] **故事、经验与时间螺旋**:事件可组织为项目功能、子线索及可选跨项目故事,支持重命名、合并、调整归属、移除和撤销。经验保留形成及应用依据与人工调整标记,心跳可提示暂无进展故事或可能适用的经验。平铺和懒加载 WebGL 时间螺旋共用选择与来源,保留平铺回退及图形资源释放;整理与建议可能增加模型费用,自动结果须核对来源。详见[实施证据](./docs/features/conversation-supervision/progress.md)。
- [x] **监督者回顾、故事线图谱与活动**:按范围和时间回顾工作,查看保存结果的图谱及来源,确认或修订实体,并带入相关回顾上下文继续讨论。侧栏可跟随或固定 Conversation、Task,并打开匹配结果。活动合并心跳触发与下游监督的阶段状态,保留失败和旧结果入口。自动与默认手动回顾只处理新增、修改及未处理来源,无变化时跳过模型;显式“重新整理”重读区间,但不推进或重置共享进度,自动回顾不续跑用户暂停的运行。回顾只读且禁止工具,模型调用可能产生费用,详见[实施与验证记录](./docs/features/conversation-supervision/progress.md)。
手动开始、自动触发和继续共用一个活动回顾;活动记录可取消并保留已保存批次,清理结束前不接纳新回顾,已取消回顾不可继续。故事线事件、实体和关系按独立页签展示数量,并与画布选择同步。
可从监督反馈或图谱来源直接继续讨论,编辑问题并明确打开目标会话;界面不再提供知识实体写回控件或重复上下文说明,智能心跳用可处理的监督建议代替报告指标和趋势。
- 手动回顾分页处理所选历史,并按批保存事实与来源;不再按固定时长自动暂停,仍支持主动暂停和从保存进度继续。设置提供并发、单次请求超时、批次大小和响应容量;活动记录突出相连阶段、已保存批次与剩余来源,次要统计可展开查看。工作回顾采用全宽纵向布局,运行状态单独显示,空态不再挤在控件旁。
- [ ] **监督后续能力**(规划中):逐事件完整回放、Experiment 目标、人工修改时间序列审计、事件触发及更完整的执行观察仍未完成。当前自动回顾受输入预算和滚动窗口限制,删除报告不会重置处理进度或重建图谱。详见 [会话监督 PRD](./docs/features/conversation-supervision/prd.md)。
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
- **Story Graph 只读工具(Desktop 0.15.9 / Agent 0.15.3)**:Agent 可搜索已保存工作、故事和经验,读取上下文并分页核对来源。监督者开关及 Runtime 分配同时控制发现和调用,旧会话也受限制;本地和远程路径共用桌面读取入口。共享来源版本事实按允许范围复用,关键词匹配和工具说明引导模型按需查询,不自动注入全图;经验是参考而非指令,查询本身不调用模型,`as_of` 仍未实现。详见[设计与验证](./docs/features/conversation-supervision/story-graph-mcp-design.md)。
- [ ] **可视化受控工作流**(规划中):提供版本化 DAG、条件分支、审批、取消和恢复,执行节点继续经过 Main Runtime 边界。
- [ ] **统一 Run Graph 与回放**(规划中):关联任务、Subagent、模型、知识、工具审批、用量和成果,支持失败定位、重试和脱敏导出。
@@ -133,7 +147,7 @@
- [x] **微信 ClawBot 扫码与媒体**:通过独立 Sidecar 完成本机扫码、验证码、加密凭据和文字收发;支持个人微信私聊图片与文件,单条消息最多 4 个附件、解密后合计不超过 12MB。
- [x] **微信安全回传**:支持返回当前任务生成的图片,或在用户明确要求时将本次最终文本生成为 Markdown 附件;不自动读取或发送已有工作区文件。
- [x] **企业微信与钉钉连接**:支持 Main-only 加密设置、环境变量只读覆盖、连接测试、动态启停、发送者范围和状态诊断。
- [x] **受管本地模型下载源**:在“平台功能 / 通用设置”中为后续语音输入与 OCR 模型下载全局选择 ModelScope(默认)或 Hugging Face;所选来源缺少完整已验证文件时明确不可用,不静默换源或混合文件。
- [x] **受管本地模型下载源**:“模型连接”页头在模型类型页签同行右侧、保存操作下方显示当前来源与次按钮。弹窗为后续受管本地模型下载全局选择 ModelScope(默认)或 Hugging Face,确认才保存,取消保持已保存来源不变,错误可重试;向量、语音和 OCR 的更换下载源入口直接打开该弹窗。所选来源缺少完整已验证文件时明确不可用,不静默换源或混合文件。
- [x] **可选本地语音模型管理**:应用不内置模型权重;提供校验下载、进度与取消、来源链接、ZIP 或本地目录导入、切换和删除。
- [x] **本地录音与离线转写**:采集麦克风音频并使用已选择的本地模型离线转写,支持停止、取消、状态反馈和资源释放。
- [x] **版本检查与镜像节点**:在“关于与更新”中选择 GitHub(默认)或镜像节点;手动检查、启动时检查和下载页使用同一选择,并只读取固定可信的发布索引,不自动下载或安装。
@@ -143,11 +157,22 @@
### 开源、构建与发布
- 当前源码候选为 Desktop `0.13.14`、Agent `0.13.4`,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
- Desktop `0.13.14` 改善监督者分页回顾与续跑、ripgrep 原生搜索、待办布局和远程结果未知后的显式续聊。数据库升级至 schema 47,回退旧客户端需使用升级前完整备份。
- Desktop `0.15.3` 修复本地直连手动压缩和请求有效期间的工具授权,区分取消与失败并去除重复提示,优化侧栏搜索、注册成功后的快捷键提示及笔记操作布局,减少启动和存储中的重复处理,并精简重复依赖及开发文件。不宣称整体渲染性能已显著提升;Office 实时编辑仍为设计。证据见[0.15.3 发布准备](./docs/development/release-preparation-0.15.3.md)。
- 当前 Desktop 候选为 `0.15.10`,已发布 Agent 为 `0.15.3`、最低要求 Desktop `0.15.9`,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
- Desktop `0.15.10` 统一项目/会话菜单并显示十个最近会话,增加独立监督模型选择及默认开启的会话 Story Graph 开关。活动索引分页与完整汇总、文档 Worker、延迟启动、窄渲染订阅、列表窗口化、终端批次和旧 Runtime 缓存清理减少重复工作,不承诺通用提速比例。schema 58 要求升级前备份数据库及笔记;高频本地写入改用 NORMAL,断电/系统崩溃可能丢失最近部分写入,远程确认、通道投递和任务生命周期仍用 FULL。Agent 保持 0.15.3;四次真实 Host 模型请求及 1000 行 Unicode SSH 终端检查通过,精确候选 CI 与六平台打包仍为发布门槛,不宣称已修复尚未定位的远程恢复错误。见[发布准备](./docs/development/release-preparation-0.15.10.md)。
- Desktop `0.15.9` 增加共享回顾进度、故事经验与时间螺旋,schema 56 要求升级前备份,相关模型整理可能增加费用。会话与消息窗口化、Markdown 缓存、只读 Worker、活动差量保存及远程 checkpoint 批次落库减少重复工作,不承诺所有机器相同比例提速;页内查找不覆盖未挂载行,快速滚动可能显示占位,Worker 不可用时同步回退。Execute 配置修改取消重复原生确认,Electron 为 44.5.1。当前源码四次真实 Host 模型请求通过,仍需精确候选 CI 与打包验证,见[发布准备](./docs/development/release-preparation-0.15.9.md)。
- Desktop `0.15.8` 与 Agent `0.15.2` 支持 Execute 模式保存会话图片到本机或 Host 绝对路径,覆盖需明确指定,不支持转换为 WebP。MCP 设置列出图像工具,不代表所有模式均可用。笔记增加新记录取消、编辑焦点返回及可用内容全宽;缺少浏览器和磨砂设置时默认开启,已保存选择不变。schema 仍为 52。真实 Linux x64 Host 工具层证据与待完成的精确候选 CI、打包验证分别记录在[发布准备](./docs/development/release-preparation-0.15.8.md)。
- Desktop `0.15.7` 复用已完成的流式 Markdown 块与会话派生值,不改默认流式节奏。运行记录进入时读取、可见时每 15 秒刷新,也可手动刷新;本地自定义 MCP 发现故障按服务隔离。受管模型下载源移至模型连接中的确认弹窗,设备共享改为显式启用的测试功能。schema 仍为 52,仍需精确候选 CI 与打包验证,组件测量不等同完整 App 结果。详见[发布准备](./docs/development/release-preparation-0.15.7.md)。
- Desktop `0.15.6` 增加默认关闭、可保存的顶栏与清单磨砂外观,不透出系统桌面;减少聊天重复渲染,草稿与队列图片支持直接预览,设置分组及开关右对齐,笔记标题和编辑间距更紧凑。本地 OpenCode 在清理后对明确暂时性 MCP 传输错误最多重试一次,作废结果不确定的名称,不重放 Prompt 或工具调用;反馈包含脱敏连接诊断。schema 52 增加执行统计索引,升级前备份数据库和笔记。详见[发布准备](./docs/development/release-preparation-0.15.6.md)。
- Desktop `0.15.5` 更新智能心跳与增量回顾,保留中断轮次历史,执行清单改为浮层,笔记保存成功后退出编辑;自动评论不重新打开编辑器,失败保留草稿。新增手动笔记标签、AND 筛选及重命名/合并/删除,双栏笔记与全宽概览,固定工作区文件操作栏,缓存 OpenCode 终端依赖并缩短 MCP 名称。schema 51 增加标签存储,同时迁移旧待确认记忆为待处理约定;升级前备份数据库与相邻笔记。证据见[发布准备](./docs/development/release-preparation-0.15.5.md)。
- Desktop `0.15.4` 增加 Runtime/模型分层选择及独立系统任务用量,减少 Runtime 指令和历史重复。schema 49 迁移项目与会话选择,回退须恢复完整升级前数据库和相邻笔记备份。Agent `0.15.1` 要求 Desktop `0.15.4`,修复远程 Continue 历史重复封装,Runtime 锁版本不变。证据见[0.15.4 发布准备](./docs/development/release-preparation-0.15.4.md)。
- Desktop `0.15.1` 已完成六平台原生打包及 GitHub/OSS 发布。Desktop `0.15.2` 新增 Story Graph 只读工具和完整可编辑的监督讨论消息,简化笔记采集并补充退出诊断;远程图谱读取需配套 Agent `0.15.0`。主机与云环境管理应用仍为待实现规划,当前候选证据见[0.15.2 发布准备](./docs/development/release-preparation-0.15.2.md)。
- Desktop `0.15.1` 承接未发布的 `0.15.0` Obsidian 接入、会话采集到魔法笔记和预览中的本地原生客户端,并修复知识库、监督者与界面操作。schema 48 新增会话来源元数据,回退旧客户端需使用升级前完整备份。Agent `0.15.0` 要求 Desktop `0.15.2`,保留 Node `24.19.0`,补齐远程遥测关闭配置及 Continue 跳过已关闭的更新检查,不新增远程客户端快捷入口。
- Desktop `0.13.15` 承接未发布的 `0.13.14`,改善监督者分页回顾与续跑、ripgrep 原生搜索、待办布局和远程结果未知后的显式续聊。数据库升级至 schema 47,回退旧客户端需使用升级前完整备份。
- Desktop `0.13.16` 分离结构升级与旧历史回收,移除回顾固定时长自动暂停,并改善监督者布局。相较 `0.13.15` 不新增 schema 变更,也不要求更新 Agent。
- Agent `0.13.3` 回收空闲 Runtime,并使被替换的 Agent 等已有工作结束后退出;复合包要求 Desktop `0.13.12`,请先升级 Desktop,再更新 Host 环境。Node 保持 `24.19.0`。
- Agent `0.13.4` 在完成前等待模型交付确认,空闲 Runtime 回收失败不改变已完成结果,并在环境更新后清理已确认停用的旧程序及所属孤儿进程;保留历史、活动及状态不明的安装。独立包要求 Desktop `0.13.14`,请先升级 Desktop,再更新 Host。
- 发布准备要求本地全量测试、发布说明校验、类型检查及 lint;候选主分支 CI、生产构建、原生打包及公开发布核验为独立验收步骤。本轮不做本地生产构建、打包或龙芯预览,当前验证状态见[发布准备记录](./docs/development/release-preparation-0.13.14.md)。
- Agent `0.13.4` 在完成前等待模型交付确认,空闲 Runtime 回收失败不改变已完成结果,并在环境更新后清理已确认停用的旧程序及所属孤儿进程;保留历史、活动及状态不明的安装。独立包要求 Desktop `0.13.14` 或更新版本;因 `0.13.14` 未发布,请先升级至 Desktop `0.13.15`,再更新 Host。
- 发布准备要求全量测试、发布说明校验、类型检查及 lint;候选主分支 CI、生产构建、原生打包及公开发布核验为独立验收步骤。本轮不做本地生产构建、打包或龙芯预览,当前验证状态见[发布准备记录](./docs/development/release-preparation-0.15.0.md)。
- Desktop `0.13.12` 和 Agent `0.13.3` 已完成原生发布流程,但不替代开发期真实 Host 场景覆盖。
- 验证记录与功能状态分开。存储已有本地及 Linux x64 Host 真实工具任务、多项目多会话、取消和无损迁移验证;进程复用有 Windows 完整 App 与 Linux x64 Host 证据,PPTX OCR 与空闲内存释放有 Windows 真实文件证据。图片工具已有真实服务商生成/编辑及 Host 传输证据,这些记录未覆盖完整界面自然语言调用和本地/远程切换。候选仍需主分支 CI 及原生发布打包,详见[Runtime 验证记录](./docs/features/assistant-workbar/progress.md)与[图片工具验证记录](./docs/features/conversation-media-generation/progress.md)。
- 发布准备前,`9836a4c` 已通过本地 4,858 项测试(67 项跳过)、完整类型检查和 lint;真实服务商画布分析仍未验证。该提交的三个原生 Agent CI 构建通过;桌面 CI `35485936807` 为 4,875 项通过、8 项 Electron 测试失败,均因 Linux 沙箱辅助程序未配置,在页面断言前退出。候选配置该程序并在 Xvfb 下运行测试,不关闭沙箱;候选本地验证、主分支 CI 与原生标签打包分别记录,不在发布准备中运行本地生产构建或打包。
@@ -159,13 +184,13 @@
### 开放接口、团队协作与远程执行
- [x] **远程 Continue 交付**:已接入 CN 1.5.47 受管包、Agent HTTP-to-ACP 适配、模型路由、项目选择和原生清单。默认包同时携带 OC/CN;Windows 桌面到 Linux x64 已通过安装、真实模型清单更新、长清单、取消及重启恢复验证。Execute 会话 MCP 已通过真实文本模型 Host 往返,图片服务使用替代接收端,不代表真实生图验证。候选 Agent `0.13.4` 需要 Desktop `0.13.14`;正式发布及原生平台验收与 Linux 开发证据分开,详见[验证记录](./docs/features/remote-host/runtime-checklist-validation.md)。
- [x] **远程 Continue 交付**:已接入 CN 1.5.47 受管包、Agent HTTP-to-ACP 适配、模型路由、项目选择和原生清单。默认包同时携带 OC/CN;Windows 桌面到 Linux x64 已通过安装、真实模型清单更新、长清单、取消及重启恢复验证。Execute 会话 MCP 已通过真实文本模型 Host 往返,图片服务使用替代接收端,不代表真实生图验证。已发布 Agent `0.13.4` 需要 Desktop `0.13.14` 或更新版本;正式发布及原生平台验收与 Linux 开发证据分开,详见[验证记录](./docs/features/remote-host/runtime-checklist-validation.md)。
- [x] **远程任务委派**:仅在用户显式配置端点和令牌后启用,按全局内网兼容模式使用 HTTP(S),结果进入持久化发件箱。
- [ ] **Headless Runtime API**(规划中):提供本机优先的任务、事件、状态和成果 API,以及有范围、有效期、限流和撤销能力的令牌。
- [ ] **GoodBuddy Team Hub**(规划中):以可选服务提供组织、RBAC、项目共享、远程 Agent、策略下发和租户审计。
- **SSH 主机与远程执行空间验证记录**:主机 CRUD、Host Key、加密凭据、Project UI、Workspace、OpenCode ACP v5、Agent-owned Prompt/gateway/transcript、Ask 只读、Execute 完整账号权限、取消、detached Agent 精确重连和 release-only 双架构资源校验已经接线。Linux x64 当前源码已用真实模型与工具通过 detach、进程结束、relay 丢失、并发、取消、Provider 异常、Agent 重启和 Desktop SQLite 恢复矩阵;当前源码测试签名 Agent `0.11.17` 又通过两个并发 Ask 读取、一次 Execute 写入并读回,以及 6 次带自定义 Header 和 Body 的真实受管网关模型请求,未发现 Provider 请求重放,隔离 Agent state 中也没有 API Key 或自定义请求值。公开 signing key registry 已供应。控制面直连源码不等待新 installer-bearing 包;现有记录未覆盖当前 Linux x64/arm64 正式签名工件的完整公开核对及 GitHub/北京镜像、双架构和离线 GoodBuddy 传输矩阵,发布结果另行记录。
- [ ] **多云远程沙盒 Agent**(规划中):通过云厂商 API 和 SSH Agent 管理专用 Linux 沙盒;凭据留在 Main 进程,高风险控制面操作单独确认。
- [ ] **主机管理与云环境管理**(规划中):主机管理迁入应用中心,提供列表与详情工作页面;云环境管理作为同级应用,复用主机连接和执行能力。首版适配计划覆盖 AWS、Azure、阿里云、腾讯云、VMware vCenter / vSphere、ZStack,分批完成已有实例发现、接入及真实执行验收;实例创建、启停、销毁和网络编排后续交付。平台 API 尚未实现,现有云主机可按 SSH 条件手工接入。详见[产品范围](./docs/features/remote-host/prd.md)。
## 规划原则
+66 -29
View File
@@ -43,6 +43,10 @@
两种主题必须保持相同的信息层级,但不要求机械地反转明暗。浅色主题以白色主内容画布、冰蓝灰侧栏和轻微着色的顶栏建立空间关系;深色主题使用深海军蓝与蓝灰表面逐层提亮,避免纯黑。蓝色承担主要选择和交互,青绿色主要承担成功与可用状态,二者不得混用语义。
外观设置提供“透明磨砂特效”开关,默认开启,文字在左、共享 Switch 在右。新用户及缺少该字段的旧配置使用开启状态;已保存的开关选择保持不变。选择保存在应用设置中,保存成功后立即生效,重启后恢复。开启时,右侧内容框架顶栏的背景不透明度在浅色主题为 65%、深色主题为 70%,保留 18px 背景模糊与淡分隔线。聊天滚动视口向上延伸 58px,消息上滚时实际经过顶栏背后;不使用人工渐变充当背景。初始留白放在滚动内容中,包含顶栏高度和当前任务/待办条的实测高度,首条消息仍从这些控件下方开始。任务/待办条固定在顶栏下方并保持实色;展开的 Runtime 待办卡片使用两主题各自的 75% 不透明背景、18px 背景模糊和清晰的文字颜色。
已保存的开关状态同步到文档根节点,卸载时清理,使 body/top layer 中的待办卡片也能应用样式;其他菜单保持原有表面。输入区、右侧工作栏和非聊天页面保持原有位置,聊天加载态及无活动消息面板时不扩展视口。窗口拖拽和控制按钮保持原有行为;该效果不透出操作系统桌面。关闭后恢复顶栏下方的聊天布局,顶栏与待办卡片均恢复实色。`tests/frosted-glass.electron.test.ts` 使用真实 `ChatHistoryPane` 检查消息与顶栏的可见矩形交叠、初始留白、关闭恢复、浅深主题、跳底、历史加载锚点和会话切换。
## 3. 设计令牌
令牌以 CSS 自定义属性实现。`:root` 提供浅色值,`[data-theme="dark"]` 覆盖深色值。组件样式不得新增只服务于单个页面的颜色、阴影、圆角或间距常量。
@@ -68,7 +72,7 @@
| `--success`、`--success-subtle` | 成功状态 |
| `--warning` | 警告状态 |
| `--danger`、`--danger-strong` | 危险文本和状态 |
| `--danger-solid` | 最终确认按钮等需要反白文字的危险背景 |
| `--danger-solid`、`--danger-solid-hover` | 最终确认按钮等需要反白文字的危险背景及悬停背景 |
| `--danger-border`、`--danger-subtle` | 危险入口和失败状态背景 |
浅色与深色具体值只在 `styles.css` 的主题根节点维护。状态组件必须同时显示文字或图标,不能仅靠颜色区分。
@@ -116,6 +120,7 @@
- 业务组件通过 `--font-family-ui` 与字体尺寸令牌继承字体,不创建页面专属字体栈。
- 表单按钮、输入框、选择框和文本域必须继承界面字体,避免回退为原生控件字体。
- 文本表单复用 `.field` 下的共享输入规则;不适合增加字段包装层的独立输入或文本域使用 `.field-control`。文本输入规则排除 Checkbox 和 Radio,保留全局 `:focus-visible` 轮廓,不清除 Switch 焦点环。
- 连续阅读内容使用 `14px`,持久辅助信息不得小于 `11px`。
- 本地兜底字体资源必须随生产包交付,并同时包含 Inter 与 Noto Sans SC 的 OFL 许可证。
- 页面内不得通过同时放大字号、加粗和使用强调色制造多个同级主标题。
@@ -269,6 +274,11 @@
- `danger-solid`:确认对话框中的最终危险操作。
- `danger-zone`:设置页中集中展示的高影响操作区域。
`danger-solid` 必须是全局共享类,不依赖页面祖先;通过 Portal 挂到 `body` 后仍须生效。
尺寸、圆角、字体、间距和禁用状态与 `primary-button`、`secondary-button` 共用规则,
键盘焦点沿用全局 `button:focus-visible`。背景及可用态悬停使用危险实心色令牌,文字使用
`--text-on-accent`;禁用时不触发悬停变色。
普通页面不得用实心红色按钮与主操作并列。危险入口必须使用具体动词和对象,例如“删除知识条目”,避免只写“确定”。
### 6.7 数据表格与活动列表
@@ -287,11 +297,17 @@
- 浅色侧栏使用冰蓝灰表面,与白色主内容画布形成明确边界;深色侧栏使用比主画布略亮的蓝灰表面。
- 一级导航与最近会话之间、最近会话与底部账户区之间必须有可见结构分隔线。浅色主题使用 `--border-default`,深色主题可在可辨前提下使用 `--border-subtle`。
- 当前导航项和当前会话使用一致的淡蓝选中背景、强调色文字和中等字重,不额外描边或添加侧边标记;导航图标随选中状态使用强调色,悬停时保留选中状态。浅深主题遵循相同规则,强制颜色模式保留系统选中边框。
- 固定导航行高至少 `36px`、行间距 `4px`,图标统一为 `16px`;导航与最近会话分隔线上下各留 `16px`。普通待办和待处理建议数量使用中性色徽标,不使用危险红色;徽标高度不得改变导航行高。搜索框获得内部焦点时显示完整焦点环。
- 固定导航行高至少 `36px`、行间距 `4px`,图标统一为 `16px`;导航与会话区域之间、会话区域与底部入口之间的分隔线上下均各留 `8px`(`--space-2`)。普通待办和待处理建议数量使用中性色徽标,不使用危险红色;徽标高度不得改变导航行高。搜索框获得内部焦点时显示完整焦点环。
- 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。
- 最近会话列表在精细指针设备上使用细原生滚动条,滑块使用 `--border-control`、轨道透明;强制颜色与粗指针设备保留系统默认。
- 会话区域使用 `--surface-raised` 与主导航区分,浅色为白底,深色为相邻的深蓝灰底。不显示“最近会话”标题,保留区域的无障碍名称,列表独立滚动。搜索与新对话共用主导航上方的一行,不在会话区域另设搜索头部;展开、关闭和键盘规则见[会话搜索](./docs/features/application-tool-navigation/ui-design.md#会话搜索)。搜索框使用透明背景、`--border-control` 边框与完整焦点环。
- 窗口高度不超过 `600px` 时,侧栏允许整体纵向滚动,会话区域至少保留 `200px` 高度,避免搜索框、列表与底部入口相互覆盖。
- 最近会话的顺序不随流式输出增量反复变化;置顶与新消息排序遵守
[会话列表排序规则](./docs/features/application-tool-navigation/ui-design.md#会话置顶与操作菜单)。
- 项目选择器顶行保留项目切换与新建按钮;项目设置统一从下拉列表对应行的管理按钮进入,关闭后焦点返回项目选择器。
- 项目选择器与下方默认的新建对话按钮等宽,两行右侧图标按钮均宽 `36px`,与左侧控件间距均为 `8px`;搜索展开时项目选择器宽度保持不变。
- 项目下拉列表通过 `FloatingPortal` 浮在侧栏与会话区上方,默认宽度 `380px`,不受侧栏裁剪;距窗口边缘至少 `16px`,窄窗口自动收窄,长列表内部滚动。浮层锚定项目选择按钮,窗口、滚动和尺寸变化后重新定位。
- 项目选择器下方仅在有运行中、待处理或已完成未查看会话时显示跨项目活动汇总,无活动时隐藏;项目计数与项目、会话级联菜单遵守
- 项目选择器使用固定双行单按钮,第一行显示项目名,第二行显示跨项目活动汇总或“暂无活动”;点击任一行打开统一菜单并默认预览当前项目,全部项目范围在菜单内切换。尺寸、焦点、项目计数与菜单遵守
[项目活动汇总 UI 规则](./docs/features/application-tool-navigation/ui-design.md#10-项目活动汇总)。
活动数量不改变项目菜单的稳定分组和顺序。
- 桌面停靠状态下,主侧栏宽度允许在 `220px` 至 `420px` 范围内通过指针或键盘调整,
@@ -320,10 +336,10 @@
接受用户本机选择的 PNG、JPEG 或 WebP 内联数据,宽高不超过 `4096 × 4096`、总像素不
超过 1600 万且大小不超过 `512 KB`;不得读取远程 Logo URL。名称或副标题溢出品牌区时
单行截断,不能撑宽或挤压侧栏。
- 侧栏底部提供应用中心入口,设置按钮独立靠右。点击应用中心向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻状态筛选;点击行关闭菜单,知识库、智能心跳和魔法笔记在主内容区打开页面。本机推理监控从菜单、常驻入口或管理 Modal 打开时均使用独立 Modal,保留底层工作区。菜单底部“管理应用”打开既有整窗管理 Modal,共同应用设置沿用设置 Modal。
四个内置应用的管理列表、菜单和侧栏共用保存顺序,菜单按启用过滤,侧栏按启用且常驻过滤。知识库、智能心跳始终启用并常驻,可调整位置;管理 Modal 提供“打开”和排序,不提供启用、常驻开关或通用空设置,标签为“始终显示”(英文 `Always shown`)。原工作区配置及心跳单条计划 enabled 保持不变。只有魔法笔记、本机推理监控可修改启用和常驻,取消常驻不关闭功能。当前对话、运行记录和设置保持固定系统位置,不参与应用排序。
- 侧栏底部提供“应用 / Apps”和“设置 / Settings”两个横向等高 `nav-item` 按钮,均显示图标和文字,中间使用弱竖分隔线。“应用”带向下箭头,点击后向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻状态筛选;点击行关闭菜单,知识库、智能心跳和魔法笔记在主内容区打开页面。本机推理监控从菜单、常驻入口或管理 Modal 打开时均使用独立 Modal,保留底层工作区。菜单底部“管理应用”使用 `LayoutGrid` 图标,打开既有整窗管理 Modal,标题仍为“应用中心 / Application Center”,共同应用设置沿用设置 Modal。
五个内置应用的管理列表、菜单和侧栏共用保存顺序,菜单按启用过滤,侧栏按启用且常驻过滤。知识库始终启用并常驻,可调整位置;管理 Modal 提供“打开”和排序,不提供启用、常驻开关或通用空设置,标签为“始终显示”(英文 `Always shown`)。监督者沿用智能心跳身份,可修改启用和常驻,执行边界见[监督者逻辑](./docs/features/conversation-supervision/logic-design.md#应用启停与执行)。魔法笔记、本机推理监控同样可修改启用和常驻。设备共享在测试阶段默认关闭,标签为“可选”及启用状态,提供启用设置和排序,不提供常驻开关;启用后可从应用菜单及管理 Modal 在主内容区打开元数据页面,关闭时退出该页,规则见[设备共享 UI](./docs/features/device-sharing/ui-design.md#当前首期页面)。当前对话、运行记录和设置保持固定系统位置,不参与应用排序。
设置持久化后的事件同步各入口及工具配置写入;重开中心刷新期间锁定修改,新快照不被旧读取覆盖。文件、终端、浏览器和资源监控只通过右侧工作栏访问,不在左侧复制入口。
管理 Modal 始终使用紧凑单列横向列表,纵向对应保存顺序;左侧为图标、名称及简短用途,右侧为打开、可用的设置及上下移动操作,行间使用弱分隔线,不加卡片外框。窄窗口操作换行且不溢出,保留共享键盘焦点环。可选应用关闭时禁用“打开”。启用、常驻位于紧凑设置详情;四行均提供上下移动及拖动排序,不受启用或常驻影响,箭头按完整顺序判断边界。搜索时禁用拖动,箭头仍按完整顺序移动,不丢失未命中项。尺寸、返回与滚动重置规则见[应用中心 UI 设计](./docs/features/application-tool-navigation/ui-design.md)。已有稳定设置入口时,不在顶栏重复提供同一入口。
管理 Modal 始终使用紧凑单列横向列表,纵向对应保存顺序;左侧为图标、名称及简短用途,右侧为打开、可用的设置及上下移动操作,行间使用弱分隔线,不加卡片外框。窄窗口操作换行且不溢出,保留共享键盘焦点环。可选应用关闭时禁用“打开”。启用、常驻位于紧凑设置详情;五行均提供上下移动及拖动排序,不受启用或常驻影响,箭头按完整顺序判断边界。搜索时禁用拖动,箭头仍按完整顺序移动,不丢失未命中项。尺寸、返回与滚动重置规则见[应用中心 UI 设计](./docs/features/application-tool-navigation/ui-design.md)。已有稳定设置入口时,不在顶栏重复提供同一入口。
- 本机推理监控 Modal 复用整窗 Portal、背景隔离和焦点循环;停止、重启及释放使用其上方的紧凑影响确认。Escape 只关闭最上层,操作提交期间禁止关闭及重复提交;焦点恢复与尺寸见[本机推理监控 UI 规则](./docs/features/application-tool-navigation/ui-design.md#91-本机推理管理)。
### 6.9 助手工作栏
@@ -348,13 +364,14 @@
“当前项目”和“所有项目”;无项目任务在“所有项目”中标注“未绑定项目”。范围变化不得创建第二个 Task Center。每个 Task 只
关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,也
不得把 Job/Run 提升为可导航 UI 对象。
- 各栏目读取当前会话或项目的对应内容;当前内容不可用时仍保留栏目入口并说明原因。
- 能力、连接和内容可以动态变化,栏目入口不能随之自动隐藏。不可用状态必须说明原因、影响和可执行入口。
- 各栏目按声明的范围读取内容;笔记使用应用级数据,选择和草稿不跟随项目或会话变化。当前内容不可用时仍保留栏目入口并说明原因。
- 能力、连接和内容可以动态变化,栏目入口不能随之自动隐藏。不可用状态必须说明原因、影响和可执行入口。笔记遵循应用中心的业务启用设置,禁用前处理草稿,禁用后隐藏目录入口并移除笔记 Tab。
- 用户可以主动排序栏目,并可关闭可关闭实例或恢复默认布局;任务中心和工作区不能隐藏或
关闭。应用不能用用户偏好机制实现自动能力裁剪。
- 已打开实例保留 `tablist`、`tab`、`tabpanel`、方向键、Home、End 和焦点恢复语义。
实例过多时 Tab 区单行横向滚动,“+”保持可见,不换行或改成下拉框。不可关闭实例不渲染
关闭按钮,也不响应关闭快捷命令。
“+”位于滚动容器外并占用固定宽度;溢出时左右滚动箭头在滚动视口两侧各占独立宽度,不能覆盖页签、关闭按钮或添加入口。箭头隐藏或禁用时保留占位,避免悬停或滚动引起布局跳动。
- 当前工作栏 Tab 必须同时使用连续内容表面、可见边框、顶部指示、图标或较高字重中的至少
两种信号;悬停反馈不得强于当前选中态。Tab 关闭按钮保留固定布局空间,默认仅在对应 Tab 悬停或
`focus-within` 时显示;无悬停或粗指针设备始终显示,键盘与辅助技术始终可访问。
@@ -366,7 +383,7 @@
记忆功能实现前不展示占位内容,未来的记忆与历史执行上下文归入关联 Task 的任务中心详情。
- 徽标可以提示未解决意见、等待审批、失败或连接状态,但不能成为唯一状态信号,也不能无条件抢占当前栏目。
- 浏览器 Tab 的内容顶部固定使用一条紧凑工具栏,按“返回、刷新/停止加载、地址输入、
前往、交互、关闭浏览器”排列。“刷新”在当前页面加载时原位切换为“停止加载”;
前往、交互、关闭浏览器”排列。“前往”使用纯右箭头图标按钮,保留悬停提示和可访问名称。“刷新”在当前页面加载时原位切换为“停止加载”;
“关闭浏览器”必须使用明确文字和与页面导航控件清楚区分的危险操作视觉,不能继续标为
含义模糊的“停止浏览器”。地址输入显示浏览器权威提交 URL,但用户编辑期间保留本地草稿,
不被画面或状态刷新覆盖;提交裸域名时自动添加 `http://`,提交、失焦后的同步规则不得让
@@ -411,13 +428,13 @@
绘制,避免被 Modal 或滚动容器遮挡。菜单和帮助挂在触发按钮所属 Modal 内,通知跟随当前活动
Modal,无 Modal 时挂在 `body`;不能只提高 `z-index` 或解除背景的 `inert`。
- 原生浏览器 `WebContentsView` 不受网页 `z-index` 控制。应用级 Modal 打开时统一释放
浏览器 viewport;非模态对话框、菜单、局部确认或应用通知与浏览器区域相交时也释放。
浏览器 viewport;非模态对话框、菜单、同行帮助 tooltip、局部确认或应用通知与浏览器区域相交时也释放。
浮层关闭后只恢复当前活动实例的 viewport,不关闭、重建或刷新网页。接线与测试见
[浏览器多实例技术设计](./docs/features/assistant-workbar/browser-tabs-technical-design.md#4-renderer-与-viewport)。
- 遮罩必须 `inset: 0` 铺满整窗,并由 `styles.css` 中统一的遮罩选择器声明
`-webkit-app-region: no-drag`,否则 `.brand` 与 `.topbar` 的原生拖动区域会截获指针,
使遮罩上的控件无法悬停和点击。新增整窗遮罩时扩展该共享选择器,不要在各自规则里复制
`no-drag`,也不要用 `inset: 38px 0 0` 之类的偏移绕开标题栏。
`no-drag`,也不要用 `inset: 38px 0 0` 之类的偏移绕开标题栏。遮罩回归按 fixed/inset 声明发现,不能只按 `backdrop` 或 `modal` 类名后缀筛选;知识引用的 `-dialog` 同样适用。
- 当前会话存在 Task 时,Task 上下文栏位于标题栏下方并承载任务摘要与详情;没有 Task 时
整行隐藏。折叠状态保持为 `46px` 单行表面,与右侧工作栏标签栏等高,左右分隔线必须水平
对齐。该栏不重复放置新建入口。
@@ -475,6 +492,7 @@ Switch 用于在两个持久状态之间立即切换,例如启用能力、开
- Checkbox 保留原生 Checkbox 语义和方形勾选视觉,不得添加 `role="switch"`。多项分配、列表选择、确认声明和“保存时清除密钥”等一次性选择均属于 Checkbox。
- 不创建页面专属 Switch 样式。需要紧凑布局时仍复用同一轨道、滑块、焦点环、禁用状态和动效,只调整共享组件支持的布局变体。
- Switch 支持 Tab 聚焦和 Space 切换,键盘焦点至少显示 `2px` 高对比焦点环。可见标签应描述被控制的能力,不能只显示“开 / 关”。
- 共享开关行统一为标签在左、Switch 靠右,说明放在标签下方;不因 JSX 中输入控件与文字的先后顺序改变视觉位置。窄窗口允许文字换行,开关保持固定尺寸。Checkbox 和 Radio 保留各自的选择布局。
- 异步切换期间禁用重复操作并保留原状态。失败时恢复或保留最后确认状态,通过应用通知或就地可恢复错误说明原因。
- 涉及联网、上传、电脑控制或其他外部影响的 Switch,附近必须持续说明数据去向、权限范围或风险,不能只靠设置名称表达影响。
- 自动化测试应按 `switch` 角色查询二元开关,按 `checkbox` 角色查询多选或确认项,防止视觉迁移后语义回退。
@@ -500,6 +518,7 @@ Switch 用于在两个持久状态之间立即切换,例如启用能力、开
- `footer` 使用共享操作区,按钮整体右对齐,次要取消在前、主要保存在后。放弃修改确认沿用同一操作区,继续编辑在前、危险操作在后。
- header、滚动 body、footer 是 surface 的三个直接子区域。沿用共享 Grid 的 `auto minmax(0, 1fr) auto`,只有 body 滚动,短窗口仍可见标题、关闭和提交操作;窄窗口沿用共享外边距、内边距和主题令牌。
- X、取消、Escape 和遮罩关闭共用业务关闭检查,不绕过未保存确认或提交锁定。确认中的 Escape 或 X 返回编辑;继续复用 `activateModalFocus`、`trapTabFocus`,保留背景隔离、初始焦点和关闭后的焦点恢复。保存失败保留草稿及可恢复错误。
- 从已有 Modal 打开的子 Modal 同样复用 `--z-dialog` 和 `activateModalFocus`,以较后挂载的 body Portal 绘制在父层上方,并隔离父层;不能只手动聚焦或使用旧的页面层级常量。子层关闭后恢复父层的隔离状态与触发按钮焦点,Escape 不继续关闭父层。
## 7. 交互状态
@@ -516,6 +535,8 @@ Switch 用于在两个持久状态之间立即切换,例如启用能力、开
异步提交成功后更新内容并通过统一应用通知提供明确反馈。失败时保留用户输入和筛选上下文。
共享控件的状态规则须在容器组合中生效:菜单通用按钮样式不能覆盖危险按钮,普通图标悬停不能覆盖危险图标或作用于禁用按钮,页签悬停保留当前选中态。深色主题通过令牌提供控件颜色,避免用页面祖先下的通用输入规则替换控件边框。验证时读取最终 computed style;共享令牌对比度与源码规则存在性检查不能代替容器内、body Portal 和伪状态回归。修复与验证记录见[共享控件审查](docs/quality/shared-controls-review-2026-09-27.md)。
## 8. 范围与数据语义
### 8.1 页面范围
@@ -654,6 +675,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
全屏预览遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape 关闭和焦点恢复规则,
并沿用同一套清理与 Sandbox/CSP 边界。关闭平台功能后必须显示 HTML 源码,不能隐藏内容
或保留旧预览。
Tab 和 Shift+Tab 必须能进入及离开 iframe;焦点在预览内部时 Escape 仍关闭当前全屏层并返回入口。空 sandbox 与禁脚本 CSP 保持不变,跨文档键盘接线见[HTML 渲染技术设计](docs/features/conversation-html-rendering/technical-design.md#ui-与无障碍)。
- Mermaid 大图支持缩放和拖动;图形超过画布时,四个边缘都必须能通过滚动到达,不能因居中而裁掉顶部或左侧。
弹窗工具栏提供“导出 PNG”,保存完整图形,不受当前缩放和滚动位置影响;背景使用当前主题的画布底色。
导出按原图四周留出 `16px`,以 2 倍分辨率绘制,任一边最多 `4096px`。导出期间禁用重复点击,失败在按钮旁提示重试。
@@ -682,7 +704,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 已选择的工作模式在触发按钮中只显示 `Ask` 或 `Execute`;完整中文含义和说明保留在菜单选项、可访问名称及输入区下方的模式说明中。
- 宽度大于 `700px` 时,主工具栏的添加内容、Runtime、模式、会话选项与发送保持同一行。窄输入区按容器宽度换行,保留发送及停止入口;选项浮层不得超出输入区宽度,不再常驻独立的 Runtime 专属功能行。
- 输入框原生支持 `Ctrl+V`:文本直接进入草稿,系统截图和从文件管理器复制的一个或多个支持文件加入本次消息附件。文件粘贴与上传按钮共用读取、解析进度和数量限制;读取期间阻止重复导入及发送,错误就地关联输入框和附件按钮。具体类型、大小及批次失败规则见[聊天附件要求](./docs/features/document-processing/prd.md#321-聊天附件入口)。默认工具栏不提供独立“读取剪贴板”“截取当前屏幕”和“选择应用窗口”入口。
- “Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴文件、图片或文本”等输入操作提示放在空输入框内部,作为主占位文案的次级行;不得在输入框下方单独占用第二行。输入框下方只保留一行当前模式、安全边界或全局快捷键说明。
- 空输入框使用三行原生占位文案:第一行为消息提示(生图模式保留图片描述提示),第二行为“Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴”,第三行为“Ctrl+N 新建对话 · 实际已注册快捷键 快捷唤起”。新建与粘贴快捷键按平台显示,macOS 使用 Command;全局快捷键不可用时省略该项。输入文字后占位提示自然隐藏,输入框下方不再重复全局快捷键,保留模式、安全边界和其他元信息。
- 输入操作提示不能替代表单的可访问名称,输入框始终保留持久的程序化标签。
- 草稿与消息中的附件操作使用紧凑图标按钮,并提供悬停和键盘焦点提示;文件名、解析状态与操作行的布局遵循[附件操作规则](./docs/features/document-processing/ui-design.md#12-附件操作菜单)。
- 空对话展示可执行的起始建议,发送失败保留输入并提供重试。
@@ -714,15 +736,15 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
### 13.4 智能心跳
- 监督者入口使用 `supervisor` 壳层;智能心跳设置位于同一壳层内。
- 顶层页签为“工作回顾 / 故事线图谱 / 自动监督 / 活动记录 / 设置”,使用一套共享 PageTabs,窄窗口单行横向滚动。页签下不重复同名标题,也不保留空标题容器;所选回顾使用“监督结果”标题,历史结果不得标为最新。
- 工作回顾只展示统一监督结果、历史选择和紧凑的新回顾控件;结果保留自身范围、覆盖区间和生成时间,图谱读取同一结果 ID。自动监督集中展示当前计划、运行概览、指标、趋势、自动监督报告、记忆与行动建议及心跳运行审计;活动记录只展示统一执行活动、进度和详情。每页保留所属数据的一处刷新入口,范围与时间选择仅作用于新回顾请求。详细布局与阅读规则见[监督者 UI 设计](docs/features/conversation-supervision/ui-design.md#应用入口)。
- 监督者保留统一页面壳层;阅读与活动子面板分别复用 `--content-reading` 和 `--content-dashboard`,在壳层内左对齐。各面板的工具栏、历史导航、状态和结果共享宽度,不在全宽卡片内部再次居中窄正文。活动筛选按内容紧邻排列,阶段以文字状态行呈现。
- 顶层页签为“工作回顾 / 故事线图谱 / 智能心跳 / 活动记录 / 设置”,使用一套共享 PageTabs,窄窗口单行横向滚动。页签下不重复同名标题,也不保留空标题容器;所选回顾使用“监督结果”标题,历史结果不得标为最新。
- 工作回顾只展示统一监督结果、历史选择和紧凑的新回顾控件;结果保留自身范围、覆盖区间和生成时间,图谱读取同一结果 ID。智能心跳集中展示心跳计划、监督建议和只读的历史心跳报告;活动记录只展示统一执行活动、进度和详情。每页保留所属数据的一处刷新入口,范围与时间选择仅作用于新回顾请求。详细布局与阅读规则见[监督者 UI 设计](docs/features/conversation-supervision/ui-design.md#应用入口)。
- 监督者保留统一页面壳层;工作回顾与活动子面板铺满壳层可用内容宽度,与页签左右对齐。工具栏、历史导航、状态和结果共享宽度,不再增加子面板宽度上限。工作回顾按纵向排列,空态独占工具栏下方整行并水平居中。活动筛选按内容紧邻排列,阶段使用编号圆点和连接线,次要统计收进详情;具体布局见[监督者 UI 设计](docs/features/conversation-supervision/ui-design.md)。
- 范围明确区分 Global 与指定的一个或多个 Project;Global 与指定项目互斥,多项目选择使用
Checkbox,不能依靠进入页面时的当前项目推断。
- 状态卡片使用统一状态令牌,不只依赖颜色。
- 活动记录先显示运行状态、时间、范围及实际阶段,恢复操作紧邻进度。原始错误默认折叠,展开后在有界面板内滚动并可选择复制;配置归入次要详情。任务层级只展示可查询的记录,缺失阶段明确标注,不能用剩余来源为零推断发布成功。具体阶段与分页规则见[监督者活动设计](docs/features/conversation-supervision/ui-design.md#活动)。
- 自动监督的当前状态下显示计划列表,仅保留列表的一个创建按钮;无计划时使用列表的未配置说明,不叠加第二个空态或创建入口。创建与编辑复用同一份表单及共享 Modal 焦点、背景隔离和遮罩样式;未保存关闭须确认,保存失败保留草稿,编辑暂停计划不恢复运行。“执行记录”进入按该计划 ID 筛选的活动页,活动页不重复显示心跳审计列表。
- 设置只展示模型超时与并发、分页、分批和执行预算,不含计划、报告、审计或嵌套页签,不显示整页全局徽标或独立刷新行。
- 智能心跳的“心跳计划”卡片内直接排列状态说明、创建按钮和计划列表,不再嵌套第二层边框;仅保留一个创建按钮,说明文字按无计划、全部暂停、正常三种状态只显示一条。创建与编辑复用同一份表单及共享 Modal 焦点、背景隔离和遮罩样式;未保存关闭须确认,保存失败保留草稿,编辑暂停计划不恢复运行。“执行记录”进入按该计划 ID 筛选的活动页,活动页不重复显示心跳审计列表。
- 设置只展示模型超时与并发、分页、分批和单次响应容量,不含计划、报告、审计或嵌套页签,不显示整页全局徽标或独立刷新行。
- 监督者的设置、帮助、空态及错误文案不得向终端用户展示内部路线图、交付进度或设计与实现缺项;只说明当前行为、适用范围和可执行的下一步。不得因此夸大为提供商全局限流或完整语义识别;真实的运行失败、超时、暂停、来源失效及恢复操作必须保留。
- 智能心跳菜单入口是完整配置的权威位置;任务中心和设置中心不得复制同一 CRUD 表单。
- 分区记忆协同与唤起条件尚未实现,不得显示占位入口或平行的记忆类型。
@@ -759,7 +781,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
### 13.7 魔法笔记
- 笔记总览与待办使用一个普通按钮切换,放在页头“新建笔记”按钮左边。按钮显示目标“切换到待办”或“切换到笔记”及对应图标,按内容宽度显示,不拉伸;窄窗口操作组可换行到标题下方,保留控件高度。
会话联动已接入:右侧提供可关闭的单实例笔记 Tab,通过工作栏“+ → 笔记”打开;左下角和聊天页头不再提供重复的“快速笔记”入口。助手消息和会话标题菜单可打开文字采集预览;保存记录在面板和完整工作区显示独立来源条。草稿由 App 保留,关闭、替换及禁用前确认。具体交互由[魔法笔记会话联动界面设计](./docs/features/magic-notes/ui-design.md)维护。真实 Electron 验收已通过,全仓验证未全绿,见[验证进度](./docs/features/magic-notes/progress.md)。下列规则描述独立工作区。
- 未打开笔记详情时,笔记列表与待办使用页头普通按钮切换。按钮显示目标“切换到待办”或“切换到笔记”及对应图标,按内容宽度显示,不拉伸;笔记的新建入口位于左侧列表标题行,待办页的新建入口位于页头。窄窗口操作组可换行到标题下方,保留控件高度。
- 切换按钮不使用页签语义,切换后焦点回到同一个按钮;内容 section 持续提供当前列表的 `aria-label`。切换沿用草稿保护入口,待办状态仍使用独立的 `SegmentedControl`。
- 文字记录编辑器保留 Quill 原生列表快捷输入:输入 `-`、`*`、`1.`、`[ ]` 或 `[x]` 后按空格,分别转换为项目符号、编号或待办清单。快捷输入只生成现有 Delta 富文本,不引入 Markdown 源码模式、第二套文字记录持久化格式或第三方 Markdown 插件;新建文字记录的操作提示收进类型选择旁的 `InlineHelp`。
- 新记录可选择“文字 / 画布”;已保存画布提供分页阅读和“展开编辑画布”。画布使用 PeopleLib 的 Fabric 分页批注与 Quill 跨页正文,支持纸张模板、增删页、笔迹、高亮、整对象橡皮、选择及移动缩放旋转、浮动文字、图片和 PDF 底版。画布有固定页面边界;已保存记录、待办来源和编辑态默认容纳完整当前页及工具栏,高度随当前页实际显示尺寸变化,纵向由外层笔记流滚动。多页仍属于一条记录,通过页码翻页;窄窗口工具栏换行,纸张仅在自身视口内横向滚动,不撑宽外层页面。
@@ -770,26 +794,34 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 魔法笔记设置提供“发送画布页数”单选框,复用设置表单 `.field` 控件样式,范围 1~8,默认 1。标题同行的问号使用 `InlineHelp`,下拉框通过稳定的帮助 ID 关联展开的说明,交互遵循 6.14。说明包含当前页序前 N 页、不足时发送全部页、数量指单个画布页数而非记录数,以及完整保存与导出不受影响。修改后沿用设置保存反馈,下次分析生效。
- 草稿、已保存画布记录及来源为画布的待办,按默认模型的 `supportsImageInput` 使用所选前 N 页图像与文字,或只发送同一范围文字并标注“仅文字分析(未使用画布图像)”。所选页只有视觉内容的记录/草稿遇到文字模型时提示切换支持图像输入的默认模型,不声称已理解笔迹或布局。待办分析同时保留任务标题、说明和所选来源画布页的上下文。
- 画布文字评论在提取的 `plainText` 不变时保留;图像评论随画布内容或布局变化失效,在保存后自动评论模式下重新分析。草稿内容变化清除旧草稿分析,评论区显示实际使用的输入模式。存储和判定细节见[画布技术设计](./docs/features/magic-notes/technical-design.md#canvas-analysis-and-comments)。
- 默认进入笔记总览,不自动打开第一篇笔记。笔记以自适应卡片网格平铺,卡片显示标题、最多两行内容预览、记录数和本地化更新时间。点击笔记进入详情,详情不保留内部左列表;“返回总览”恢复搜索、滚动位置和入口焦点。新建入口在笔记总览与待办页可见,创建成功后进入笔记详情。
- 总览是全宽的“全部笔记”页,以自适应卡片网格(最小 `260px`)显示全部笔记,不显示右侧空白详情,也不自动打开第一篇;卡片显示标题、短日期、两行预览和记录数。打开笔记后切为左侧紧凑列表与右侧详情,页头右上角显示带文字的“返回总览”,点击回到全部笔记页并恢复搜索、滚动位置与卡片焦点;“切换到待办”只在总览显示。列表按紧凑两行行项显示:第一行为标题与短日期(今天显示时间,其余显示月/日,完整时间在工具提示中),第二行为单行预览;记录数只提供给读屏。行项无边框、透明底色,悬停为次级底色,选中为淡蓝底色加左侧 `3px` 强调标记;“更多”按钮悬停或聚焦时覆盖在日期位置。保留搜索、置顶菜单和当前选中状态。打开笔记只更新右侧详情;列表独立滚动,切换保留搜索和列表位置,并在工作区挂载期间记住各篇笔记的阅读位置。明确定位某条记录时优先跳到该记录。
- 笔记列表默认展开、宽 `280px`,支持拖动分隔线在 `240–420px` 内调宽,方向键每次调整 `16px`,Home/End 跳到边界;桌面端详情页提供列表显隐开关,记住宽度和展开偏好。工作区宽度不超过 `900px` 时切换显示列表与详情,返回恢复列表位置和入口焦点,不覆盖桌面偏好。新建成功后打开新笔记;切换、新建和删除当前笔记都保护未保存的正文与标题。
- 待办页是独立任务视图:搜索任务标题、说明或来源笔记,搜索框右侧同一工具栏放置“全部 / 未完成 / 已完成”筛选,显示匹配数与总数;任务按来源笔记分组。列表独立滚动,页头、搜索和筛选保持可见,窄屏工具栏空间不足时换行。直接勾选完成或恢复未完成,不自动选择任务;状态变化后按当前筛选更新列表。
- 待办内容容器宽于 `700px` 时,任务列表稳定在左侧,右侧显示当前任务说明、来源记录和 AI 评论;未选择时右侧提示选择任务。点击任务只更新右侧,不改变列表宽度、行位置或滚动位置,再次点击取消选择。AI 不增加第三栏。容器不超过 `700px` 时,按需详情替换同一内容区域,并提供“返回待办列表”;列表保持挂载,返回恢复滚动和任务行焦点,不将列表与详情上下堆叠。任务按钮提供 `aria-expanded` 和 `aria-controls`。
- 待办左右栏分割线支持拖拽,并记住列表宽度;键盘左右方向键每次调整 `16px`,Home/End 调整至边界。列表默认 `320px`、最小 `240px`,详情至少保留 `300px`;窄屏隐藏分割线,临时缩窄窗口不覆盖已保存宽度。每个来源笔记标题可独立收起或展开待办列表,保留标题与数量,通过 `aria-expanded` 和 `aria-controls` 表明状态;收起不清除右侧当前详情。
- 待办 AI 评论保留方向选择、流式输出、重新分析和已有结果;来源加载失败通过应用通知提示,并提供就地重试。从待办打开原笔记后,“返回待办”恢复任务选择、搜索、筛选和列表滚动;宽屏焦点返回任务行,窄屏返回待办详情中的返回按钮。
- 笔记详情主区按新到旧连续展示全部记录。打开笔记即在标题下、历史记录上显示空白输入区,保留文字/画布类型选择和保存操作,标题栏与索引均不提供“新增记录”按钮。新增保存成功后重建空白输入区,保留所选类型;自动分析结束也不转入旧记录编辑。旧记录通过显式编辑入口修改,保存使用同一 ID 和返回的修订号。点击索引或当前笔记内的 AI 来源只按 entry ID 滚动定位,不卸载编辑器、不清草稿、不询问离开。空白输入区离开不提示,返回总览、打开其他笔记或改为编辑另一条记录时仍保护未保存修改。
- 详情页的一级标题显示当前笔记已保存的标题,不显示模块说明;全局范围、返回导航和索引开关保持可见。原标题输入框使用正文大小,保留原有编辑、失焦保存与键盘操作,不形成第二个醒目标题。新建文字记录的输入区初始约四行,随内容自然增高,保留原有调整大小与滚动方式;此高度不作用于旧记录编辑器或画布。笔记内部列表、记录流、AI 区域和文字编辑区在精细指针设备上使用细原生滚动条,轨道透明;强制颜色与粗指针设备保留系统默认。说明、摘要和输入提示使用 `--font-body` 与 `--text-secondary`,时间和短标签保留 `--font-caption`。
- 左侧记录索引默认 `168px`,可拖动右边界在 `140–320px` 内调整栏宽,缩略图随列宽等比适配。开关独立位于详情标题左侧,提供 `aria-controls`、展开状态和动作提示;收起后完全移除索引列、分隔条、间隙、边框及轨道,不占宽度,隐藏内容不可聚焦。右侧 AI 评论独立显示、滚动和收起,不与索引共享页签。每条记录一个导航项:画布显示真实首页缩略图及总页数,文字显示摘要,两者均显示类型和时间。两侧分别记住桌面展开状态和宽度,共用原有布局偏好;宽布局分隔条共用样式及 `separator` 语义,方向键每次调整 `16px`,Home、End 跳到当前上下限。两侧宽度按可用空间协调限制,为主区保留至少 `300px`,窗口缩小时不覆盖保存的偏好。内容容器不超过 `800px` 时索引默认隐藏,由同一标题栏按钮开闭标题栏下方固定 `168px` 的抽屉,不显示调宽分隔条;标题栏自身保留控件高度,不保留轨道。选择记录或按 Escape 后关闭,Escape 将焦点返回开关。AI 移至底部,高度不超过可用布局的 40% 且最多 `280px`,不显示分隔条。
AI 评论的唯一显隐入口位于页面右上角,使用紧凑面板图标按钮;展开时应用共享选中样式,图标随显示或隐藏动作切换,并提供对应的工具提示、可访问名称、`aria-expanded` 和 `aria-controls`。评论面板内部不再显示重复的“AI 评论”标题或隐藏按钮行,保留区域的可访问名称、评论方向选择及各记录的分析操作。
- 打开笔记后页面一级标题仍为“魔法笔记”;笔记标题只在记录流顶部出现一次,使用 `22px` 的可编辑大标题,悬停显示下划线,聚焦显示强调色下划线,保留失焦保存、Enter 与 Escape。标题上方为吸顶的细工具栏,依次放置收起笔记列表、记录索引、条数与更新时间、AI 评论开关,均为带工具提示和可访问名称的图标按钮。记录流左右留白 `24px`(窄于 `600px` 时 `12px`),正文最大宽度 `880px` 并居中;吸顶工具栏不受此限制,始终铺满记录流宽度,AI 开关贴在右边缘,底部分隔线横贯整栏。
- 桌面笔记详情的列表标题行、正文工具栏与 AI 评论方向标签行顶部对齐,首行统一使用 `40px` 控件高度并垂直居中;AI 区域不叠加面板和表单的顶部留白。
- 文字记录的新建与编辑区域铺满卡片内沿,格式工具栏紧接头部,输入区域紧接工具栏;编辑器外层不再增加内边距或圆角。正文保留编辑器内部的文字留白,底部操作栏独立保留内边距与顶部分隔线;画布仍保留 `12px` 内容间距。
- 新建记录输入区空闲时收成一行虚线提示框,隐藏类型切换、格式工具栏和保存栏;聚焦、已有草稿、选择画布或出现校验错误时展开,初始约四行并随内容增高。保存成功或切换笔记后恢复为一行。此规则不作用于旧记录编辑器。
- 已保存记录的 AI 分析、编辑、删除为 `28px` 图标按钮,精细指针设备上悬停或键盘聚焦记录时才显示;删除悬停时使用危险色,确认条保持原样。粗指针设备始终显示。笔记内部列表、记录流、AI 区域和文字编辑区在精细指针设备上使用细原生滚动条,轨道透明;强制颜色与粗指针设备保留系统默认。说明、摘要和输入提示使用 `--font-body` 与 `--text-secondary`,时间和短标签保留 `--font-caption`。
- 左侧记录索引默认收起,只在用户展开后记住展开状态;展开宽度默认 `168px`,可拖动右边界在 `140–320px` 内调整栏宽,缩略图随列宽等比适配。开关位于笔记工具栏,提供 `aria-controls`、展开状态和动作提示;收起后完全移除索引列、分隔条、间隙、边框及轨道,不占宽度,隐藏内容不可聚焦。右侧 AI 评论独立显示、滚动和收起,不与索引共享页签。每条记录一个导航项:画布显示真实首页缩略图及总页数,文字显示摘要,两者均显示类型和时间。两侧分别记住桌面展开状态和宽度,共用原有布局偏好;宽布局分隔条共用样式及 `separator` 语义,方向键每次调整 `16px`,Home、End 跳到当前上下限。两侧宽度按可用空间协调限制,为主区保留至少 `300px`,窗口缩小时不覆盖保存的偏好。内容容器不超过 `800px` 时索引默认隐藏,由同一标题栏按钮开闭标题栏下方固定 `168px` 的抽屉,不显示调宽分隔条;标题栏自身保留控件高度,不保留轨道。选择记录或按 Escape 后关闭,Escape 将焦点返回开关。AI 移至底部,高度不超过可用布局的 40% 且最多 `280px`,不显示分隔条。
AI 评论默认不占列:当前笔记已有评论、正在分析或存在草稿评论时自动展开;用户对该笔记手动收起后保持收起,直到发起新的分析。无评论时手动展开即视为固定,固定状态跨笔记和重启保留,再次收起即取消固定。AI 评论的唯一显隐入口位于笔记工具栏右端,使用紧凑面板图标按钮;展开时应用共享选中样式,图标随显示或隐藏动作切换,并提供对应的工具提示、可访问名称、`aria-expanded` 和 `aria-controls`。评论面板内部不再显示重复的“AI 评论”标题或隐藏按钮行,保留区域的可访问名称、评论方向选择及各记录的分析操作。
- 笔记详情的内部记录索引使用透明底色,与主内容表面连续,仅保留细调宽分隔线。每条索引记录保留 `1px` 的 `--border-subtle` 淡边框,不加卡片底色或阴影,悬停使用轻微次级底色;选中记录使用淡蓝背景和左侧 `3px` 蓝色内侧标记,悬停时保留选中态。缩略图保留纸张边界,键盘焦点环与强制颜色模式选中边框保持可见。窄屏抽屉使用不透明的主内容表面,避免下层正文透出;深色主题沿用语义令牌。
- 笔记详情的标题、正文和记录流不再包裹额外的大外框,记录流容器不加内边距;缩略图列表顶部与正文区顶部对齐,左边界与索引栏左边界对齐,仅在右侧和底部保留 `8px` 留白,键盘焦点环向内绘制以避免滚动容器裁切。每条已保存记录使用一层 `1px` 的 `--border-subtle` 淡边框和 `--radius-control` 圆角,保留透明底色与记录间留白,不加阴影或头部下划线。记录编辑时保留输入区边框、页面安全边距及 AI 区域分隔。此规则仅作用于笔记详情,不改变卡片总览与待办独立布局。
- 笔记详情的标题、正文和记录流不再包裹额外的大外框,记录流容器只保留上述左右留白;缩略图列表顶部与正文区顶部对齐,左边界与索引栏左边界对齐,仅在右侧和底部保留 `8px` 留白,键盘焦点环向内绘制以避免滚动容器裁切。每条已保存记录使用一层 `1px` 的 `--border-subtle` 淡边框和 `--radius-control` 圆角,保留透明底色与记录间留白,不加阴影或头部下划线。记录编辑时保留输入区边框、页面安全边距及 AI 区域分隔。此规则仅作用于笔记详情,笔记列表与待办保持独立布局。
- 画布新建、编辑和只读态统一保留 `12px` 内容间距;详情中已保存画布去掉预览外框和工具栏底色、下划线,保留灰色画布衬底与白色纸张,编辑态保留编辑器边框。记录头部操作按内容宽度排列,空间不足时自然换行。文字新建的保存区与编辑器属于同一卡片,空白编辑区使用最小高度,不随容器百分比高度拉长。文字工具栏图标、菜单和原生滚动条须跟随主题。
- 整篇笔记的置顶、取消置顶和删除放在卡片的“更多”菜单,标题编辑区不重复提供入口。按钮在悬停、键盘聚焦或菜单展开时显示,触屏设备常显;菜单沿用会话列表样式,通过 Portal 避免滚动区域裁切,支持方向键、Home、End 和 Escape。删除在菜单内二次确认并说明笔记标题及全部记录;打开菜单不进入详情。
- 标签使用中性胶囊样式(`--surface-subtle` 底色、`--border-subtle` 边框、`--font-caption`),不使用颜色区分。总览搜索框下方是标签筛选行:“全部”加各标签和对应篇数,均为 `aria-pressed` 切换按钮;可以多选,多选时只显示同时带有全部所选标签的笔记。选中时使用 `--accent-subtle` 底色和强调色边框。卡片最多显示 3 个标签,其余合并为“+N”。打开笔记后,左侧列表不显示筛选行,只显示“已筛选:A · B”提示条和清除按钮。
- 笔记标题下方是标签编辑行:已有标签后带 × 移除按钮,末尾是虚线“+ 标签”按钮。点击后展开组合框,按输入内容过滤已有标签,也可以新建。方向键切换选项,Enter、逗号或中文逗号确认,Escape 关闭并把焦点还给“+ 标签”。达到 10 个时,“+ 标签”禁用并说明上限。
- 列表标题行右侧只放“管理标签”,不再放 `+` 新建按钮。“新建笔记”统一放在页头右上角,是主按钮,始终位于“切换到待办 / 切换到笔记”或“返回总览”的右侧。打开笔记时点击,桌面端展开左侧列表并显示标题输入,窄屏先回到列表。“管理标签”在总览显示图标加文字,打开笔记后只显示图标。点击后打开共享 `custom-task-dialog` 对话框,按使用篇数列出标签,每行提供重命名和删除图标按钮。重命名在行内编辑;新名称与已有标签相同时,提示保存后会合并。删除在行内二次确认,并说明涉及的篇数。Escape 依次退出行内编辑或确认,最后关闭对话框。
- 整篇笔记的置顶、取消置顶和删除放在列表行的“更多”菜单,打开详情时仍可操作,标题编辑区不重复提供入口。按钮在悬停、键盘聚焦或菜单展开时显示,触屏设备常显;菜单沿用会话列表样式,通过 Portal 避免滚动区域裁切,支持方向键、Home、End 和 Escape。删除在菜单内二次确认并说明笔记标题及全部记录;删除当前笔记还须处理未保存草稿,删除其他笔记保留当前内容与草稿。打开菜单不切换笔记。
- 左侧“魔法笔记”入口可显示未完成待办数量。数量为 0 时隐藏徽标,超过 99 时显示 `99+`,可访问名称仍说明精确数量;勾选、取消勾选以及 Agent / MCP 对笔记清单的写入都必须刷新数量。
- 未完成待办徽标的高度不得超过主导航图标;数量从 0 变为可见或再次隐藏时,主导航项行高和相邻菜单位置必须保持不变。
- 在待办面板勾选或取消勾选后,当前缓存中的源笔记正文必须立即同步对应清单状态,不得依赖离开工作区后重新加载。
- Agent / MCP 或其他入口成功写入笔记后,已打开的工作台自动更新笔记列表、当前详情、待办及来源记录,不增加刷新按钮。保留仍存在的笔记与待办选择、搜索和筛选,以及未保存的标题、输入区和记录编辑草稿;普通后台刷新不重建编辑器。
- 外部删除当前笔记时,无草稿则返回总览;有草稿则保留编辑区并提示先复制草稿。外部删除正在编辑的记录时,同样保留该编辑器并明确提示记录已删除;保存失败不得清空草稿。
- 当前记录草稿非空或标题尚未保存时,返回总览必须先就地确认;编辑另一条记录等会丢弃正文草稿的操作沿用同一保护。继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。
- 保存成功后,以该修订的编辑器初始化并完成 flush 后的内容作为修改比较基线;没有新编辑时返回不再提示。编辑器归一化不算用户修改,后续输入、画布变更和保存失败仍保留离开保护;同修订的后台刷新不替换已建立的基线。
- 文字和画布记录保存修改成功后退出编辑状态,显示已保存正文;后续自动评论成功或失败均不重新打开编辑器。再次修改须点击编辑入口,以该修订的编辑器初始化内容作为修改比较基线。编辑器归一化不算用户修改,后续输入、画布变更和保存失败仍保留离开保护;同修订的后台刷新不替换已建立的基线。
- 创建、保存、更新、删除和 AI 评论完成等短期结果进入应用级通知,不在编辑区或列表上方堆放页内通知。
- 标题或正文校验、删除确认、同步进度和可就地恢复的错误仍靠近对应编辑器或操作呈现。
@@ -798,22 +830,27 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 设置中心以整窗遮罩上的大 Modal 打开,保留底层工作区、会话草稿和助手工作栏,不切换一级页面。固定标题区和右上角关闭按钮,左侧分类与右侧内容各自滚动。关闭后返回原工作区并恢复入口焦点。
- 新建项目与项目设置弹窗固定标题、关闭按钮和底部操作区,仅表单内容滚动;滚动条不进入标题区,关闭按钮保留至少 40 × 40px 的点击区域。
- 项目弹窗通过 Portal 挂载到 `document.body`,避免侧栏内的弹窗与主窗口标题栏拖动区域重叠时,Electron 截获关闭按钮的鼠标输入。
- Modal 最大尺寸为 `1200px × 900px`,常规外边距为 `24px`;窗口宽度不超过 `1020px` 或高度不超过 `720px` 时接近全屏,保留 `16px` 外边距。左侧分类宽度依次为 `220px`、`180px`(宽度不超过 `1020px`)、`120px`(不超过 `720px`);极窄窗口隐藏分类辅助说明,仍保留左右布局。内容区在可用空间内伸缩,最大宽度为 `960px`。
- Modal 最大尺寸为 `1200px × 900px`,常规外边距为 `24px`;窗口宽度不超过 `1020px` 或高度不超过 `720px` 时接近全屏,保留 `16px` 外边距。左侧分类宽度依次为 `220px`、`180px`(宽度不超过 `1020px`)、`120px`(不超过 `720px`);窄窗口仍保留左右布局,长分类名称可换行。内容区在可用空间内伸缩,最大宽度为 `960px`。
- 设置 Modal 通过 Portal 挂载到 `document.body`,复用共享背景隔离与焦点恢复机制。打开后聚焦关闭按钮,Tab 在弹窗内循环;Escape 先交由嵌套弹窗处理,设置自身的 Escape 与关闭按钮共用未保存确认,确认出现后再按 Escape 返回编辑。点击遮罩不关闭;保存、测试和清理数据期间禁止关闭。每次打开重新挂载设置,模型和消息通道等直达入口使用本次指定分类,普通入口默认打开 Agent Runtime。
- 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。
- 所有分类使用共享的 `SettingsCategoryHeader` 呈现分类标题、说明、错误与操作,不得在内容卡片内复制分类标题或创建页面专属操作栏。左侧分类名称与说明来自同一份分类定义,新增分类时不得分别维护导航和内容标题。
- 所有分类使用共享的 `SettingsCategoryHeader` 呈现分类标题、说明、错误与操作,不得在内容卡片内复制分类标题或创建页面专属操作栏。左侧分类名称与右侧标题来自同一份分类定义,新增分类时不得分别维护导航和内容标题。
- 左侧按“常规 / 模型与执行 / 功能与扩展 / 系统”分组,顺序与键盘导航一致。分类按钮最小高度 `40px`,只显示名称;分类说明集中在右侧页头,外层标题不再重复功能简介。组名使用辅助字号,普通项使用正文和中等字重,选中项使用强调底色与加粗文字。
- 设置区块使用共享圆角、间距和表面令牌;标题为 `--font-section-title`,持久说明至少为 `--font-caption`。嵌套设置区块以顶部细分隔线和留白组织,不叠加卡片背景与四周边框。“能力与工具”的上级页签使用默认 `PageTabs` 外观,与 MCP 内部的分段页签区分。
- 当前分类存在“保存”或“测试”等未提交配置操作时,统一放在分类页头右侧;主保存操作在最右侧,测试等次操作排列在其左侧。
- 自动生效、仅执行即时命令或自行管理编辑流程的分类不显示全局保存操作。窄窗口下操作区可以换行,但保存入口必须保持清晰可见。
- 智能心跳的单条配置不在设置中心重复管理。设置中心如需呈现平台级说明,只提供
“打开智能心跳”导航,不复制创建、暂停、恢复或删除表单。
- 保存或测试成功统一进入应用通知视口,并按全局规则自动消失,不在分类页头或内容卡片中保留持久成功文案。加载、保存和测试错误显示在分类页头下方,并保留可处理的上下文。
- “模型连接”的认证字段按“认证方式 → API Key → 凭据状态”排列;只有选择 API Key 时显示输入框。已保存的 API Key 属于该连接,修改模型服务地址或临时选择“无需认证”不得自动清除,也不得要求重新输入;仅由明确的“清除凭据”或“删除连接”操作移除。
- “模型连接”页头在保存操作下方,将模型类型页签与全局模型下载源入口放在同一行;当前来源文字与“模型下载源”次按钮整体右对齐,窄窗口保持同行,空间不足时导航行可横向滚动。按钮打开挂载到 `document.body` 的 Portal 弹窗,复用共享 Modal 焦点管理;弹窗内使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片,选中状态同时依靠 Radio、边框和背景表达。Radio 只修改草稿,点击“确认”才保存;取消或关闭不改变已保存来源。读取失败提供重试并禁用确认,不以默认值伪装已保存选择;保存失败保留草稿并允许确认重试,保存期间禁止重复提交和关闭。向量、语音和 OCR 的更换下载源入口直接打开模型连接页的同一弹窗,跨分类时沿用未保存修改保护。详细规则见[模型下载源设计](docs/architecture/model-download-source-design.md#7-模型下载源界面)。
- 设置中的连接列表与详情共用 `.model-connection-manager`,按容器可用宽度响应:不超过 `640px` 时上下排列,列表内部滚动高度最多 `180px`;更宽时恢复左右排列。LLM、向量连接和角色编辑复用此规则,不以整窗宽度代替表单宽度。接口协议的中英文当前值须完整可读。回归范围见[窄容器布局修复记录](docs/quality/responsive-controls-review-2026-09-27.md)。
- “保存并测试模型”必须在保存后发送一次有界的真实模型请求。文本连接校验本次随机测试文本,图像连接校验实际返回的内联图片;仅收到成功 HTTP 状态、模型列表或健康检查响应不算测试成功。界面持续说明该操作可能产生少量服务商用量费用,成功反馈明确写为“真实生成测试通过”。
- 所有显式保存的设置草稿都参与离开保护:关闭、切换分类、主侧栏或工作区导航及托盘导航不得静默丢弃,统一通过设置中心的就地确认提供继续编辑与明确放弃入口,保存失败后保留输入。“平台功能 / 通用设置”承载全局快捷唤起的共享 Switch、可访问 accelerator 录制输入、恢复默认、保存及注册、停用或冲突状态,不新增分类或页签;注册或持久化失败时保留上一组可用快捷键和当前草稿,保存或停用成功后同步更新输入区的快捷键提示。
- “平台功能 / 通用设置”同时提供默认开启的“在会话中渲染 HTML”持久 Switch。说明必须
明确该能力只生成禁用脚本、联网、表单提交和窗口操作的静态预览;保存成功后当前会话立即
更新,保存失败时保留最后确认状态。
- “平台功能”使用共享 `PageTabs` 区分“通用设置”和“远程项目”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64、Linux arm64、macOS arm64 Agent 包清单分成独立卡片;每张卡片与下载进度按平台和架构区分,不能把两个 arm64 目标合并。清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
- 通用设置按快捷唤起、会话与通知、工作目录分组,复用现有卡片。“桌面通知”为默认开启的持久 Switch,保存后立即控制窗口未聚焦时的任务完成、失败和取消系统通知,关闭不影响应用内通知。旧配置缺少该字段时默认开启,明确关闭后重启仍保持关闭;保存失败保留最后确认状态并显示应用内错误通知。
- “平台功能”使用共享 `PageTabs` 区分“通用设置”和“远程项目”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64、Linux arm64、macOS arm64 Agent 包清单分成独立卡片;每张卡片与下载进度按平台和架构区分,不能把两个 arm64 目标合并。清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、同行帮助和原生单选下拉框放在同一行,用途说明收进 `InlineHelp`,并复用设置表单的统一控件样式;关闭启动检查后仍可选择更新源,手动检查继续使用当前选择。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查、下载页和 Agent 包在线下载,不显示底层服务商名称。
- “关于与更新”在版本信息之后提供应用内反馈卡片。反馈对话框使用原生字段、明确隐私说明和一张可选截图;另提供默认关闭的原生 Checkbox,让用户主动附加最近桌面诊断摘要。Checkbox 附近必须持续说明摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息,不含对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。勾选时为诊断摘要预留描述字符预算,超长草稿必须保留并显示字段错误,不得静默截断。失败时保留草稿、勾选状态和请求编号并提供就地重试,成功时在对话框内保留可复制编号而不重复发送全局通知;对话框遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape、焦点恢复和窄窗口布局。
- Agent Runtime 分类页头的“保存设置”同时保存 Runtime 基础配置与 Runtime 原生定制,不在原生定制卡片内提供第二个保存入口。原生定制存在未保存更改时持续显示状态和撤销入口;切换设置分类或 Runtime 不丢弃草稿,关闭设置中心前必须先保存或撤销。
@@ -896,7 +933,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- [ ] 将输入快捷键与附件提示置于空输入框内部,输入区下方保持单行说明。
- [ ] 最近对话迁移到 `standard`,统一搜索、范围、时间和删除行为。
- [ ] 知识库迁移到 `master-detail`,清除内联浅色样式并补齐窄窗口单面板流程。
- [x] 监督者使用 `supervisor`,工作回顾读取统一监督结果;自动监督包含 Global / 多 Project 计划、概览、自动监督报告、建议与审计;活动记录显示统一执行,设置只保留算法参数。
- [x] 监督者使用 `supervisor`,工作回顾读取统一监督结果;智能心跳包含 Global / 多 Project 心跳计划、监督建议与历史心跳报告;活动记录显示统一执行,设置只保留算法参数。
- [x] 在现有工作栏中完善任务中心,统一范围、状态、最近进展和筛选,不新建平行任务平台。
- [ ] 活动记录迁移到 `dashboard`,统一导航、筛选和表格行为。
- [ ] 设置中心使用共享分类定义与 `SettingsCategoryHeader`,将保存与测试操作统一放到分类页头右侧,并把成功反馈接入应用通知。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"formatVersion": 1,
"agentVersion": "0.13.4",
"agentVersion": "0.15.3",
"protocol": {
"major": 2,
"minor": 0
+4 -1
View File
@@ -159,7 +159,10 @@ function portableRequiredPaths(directory) {
}
function assertPortableOutput(directory) {
const requiredPaths = portableRequiredPaths(directory)
const requiredPaths = [
...portableRequiredPaths(directory),
join(directory, 'resources', 'runtimes', 'dsh', 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js')
]
const missing = requiredPaths.filter(
(filePath) => !statSync(filePath, { throwIfNoEntry: false })?.isFile()
)
+99
View File
@@ -0,0 +1,99 @@
// PERF-17 regression gate for the PERF-11 full-App benchmark report.
// Usage: node build/check-perf-thresholds.cjs <result.json | report directory> [thresholds.json]
// Exit codes: 0 all limits met, 1 a limit, scenario or status failed,
// 2 the report cannot be judged (throttled run, unreadable input).
const { existsSync, readFileSync, statSync } = require('node:fs')
const { join, resolve } = require('node:path')
const defaultThresholdsPath = join(__dirname, 'perf-thresholds.json')
function metricValue(scenario, path) {
let value = scenario
for (const key of path.split('.')) {
if (value === null || value === undefined) return undefined
value = value[key]
}
return value
}
// Pure: judges a parsed report against parsed thresholds.
// Returns { ok, refused, reason?, rows: [{ scenario, metric, value, limit, pass, note? }] }.
function evaluate(report, thresholds) {
const rows = []
const throttle = Number(report?.environment?.rendererCpuThrottle ?? 1)
const maxThrottle = Number(thresholds.maxRendererCpuThrottle ?? 1)
if (!(throttle <= maxThrottle)) {
return { ok: false, refused: true, reason: `renderer CPU throttle ${throttle}x exceeds ${maxThrottle}x; thresholds apply to non-throttled runs only`, rows }
}
const requiredStatus = thresholds.requiredStatus ?? 'passed'
rows.push({ scenario: '(report)', metric: 'status', value: report?.status ?? null, limit: `= ${requiredStatus}`, pass: report?.status === requiredStatus })
const scenarios = Array.isArray(report?.scenarios) ? report.scenarios : []
for (const [id, limits] of Object.entries(thresholds.scenarios ?? {})) {
const scenario = scenarios.find(entry => entry?.id === id)
if (!scenario) {
const optional = limits.optional === true
rows.push({ scenario: id, metric: '(present)', value: 'missing', limit: optional ? 'optional' : 'required', pass: optional })
continue
}
for (const [metric, bound] of Object.entries(limits)) {
if (metric === 'optional') continue
const raw = metricValue(scenario, metric)
const value = typeof raw === 'number' && Number.isFinite(raw) ? raw : null
let pass = value !== null
const limitParts = []
if (typeof bound.max === 'number') { limitParts.push(`<= ${bound.max}`); pass = pass && value <= bound.max }
if (typeof bound.min === 'number') { limitParts.push(`>= ${bound.min}`); pass = pass && value >= bound.min }
rows.push({ scenario: id, metric, value, limit: limitParts.join(', '), pass, ...(value === null ? { note: 'metric missing' } : {}) })
}
}
return { ok: rows.every(row => row.pass), refused: false, rows }
}
function resolveReportPath(input) {
const path = resolve(input)
return existsSync(path) && statSync(path).isDirectory() ? join(path, 'result.json') : path
}
function formatValue(value) {
if (typeof value !== 'number') return String(value)
return Number.isInteger(value) ? String(value) : value.toFixed(value < 1 ? 3 : 1)
}
function run(argv) {
const [input, thresholdsArg] = argv
if (!input) {
console.error('usage: node build/check-perf-thresholds.cjs <result.json | report directory> [thresholds.json]')
return 2
}
const reportPath = resolveReportPath(input)
const thresholdsPath = resolve(thresholdsArg || defaultThresholdsPath)
let report
let thresholds
try {
report = JSON.parse(readFileSync(reportPath, 'utf8'))
thresholds = JSON.parse(readFileSync(thresholdsPath, 'utf8'))
} catch (error) {
console.error(`cannot read report or thresholds: ${error instanceof Error ? error.message : error}`)
return 2
}
const result = evaluate(report, thresholds)
console.log(`\nperf thresholds: ${reportPath}`)
if (result.refused) {
console.error(`refused: ${result.reason}`)
return 2
}
console.table(result.rows.map(row => ({
scenario: row.scenario,
metric: row.metric,
value: formatValue(row.value),
limit: row.limit,
pass: row.pass ? 'ok' : `FAIL${row.note ? ` (${row.note})` : ''}`
})))
const failed = result.rows.filter(row => !row.pass).length
console.log(failed ? `${failed} of ${result.rows.length} checks failed` : `all ${result.rows.length} checks passed`)
return result.ok ? 0 : 1
}
module.exports = { evaluate, run, defaultThresholdsPath }
if (require.main === module) process.exitCode = run(process.argv.slice(2))
+1 -1
View File
@@ -184,7 +184,7 @@ async function run() {
model: 'qwen-plus',
supportsImageInput: false,
requestHeaders: {},
harnessVersion: '0.1.2-rc.1',
harnessVersion: '0.1.7-rc.2',
credentialRefs: ['GOODBUDDY_HARNESS_MODEL_API_KEY'],
skillPackages: [],
extensionPackages: [],
+65
View File
@@ -0,0 +1,65 @@
const { cp, mkdir, readFile, rm } = require('node:fs/promises');
const { existsSync } = require('node:fs');
const { dirname, join, relative } = require('node:path');
const tar = require('tar');
// Preserve npm's installed layout (including nested versions) and every package
// asset. The official loader discovers plugins and browser assets at runtime.
async function prepareNativeDshResources(projectDir, platform, architecture, downloadPackage, lockedIntegrity) {
const target = join(projectDir, '.runtime-resources', `dsh-${architecture}`);
await rm(target, { recursive: true, force: true });
await mkdir(target, { recursive: true });
const visited = new Set();
function locate(name, from) {
for (let directory = from; ; directory = dirname(directory)) {
const candidate = join(directory, 'node_modules', name);
if (existsSync(join(candidate, 'package.json'))) return candidate;
if (dirname(directory) === directory) return undefined;
}
}
async function copyPackage(directory) {
if (visited.has(directory)) return;
visited.add(directory);
const manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
await cp(directory, join(target, relative(projectDir, directory)), { recursive: true, verbatimSymlinks: true });
const optional = manifest.optionalDependencies ?? {};
for (const name of Object.keys({ ...manifest.dependencies, ...manifest.peerDependencies, ...optional })) {
const dependency = locate(name, directory);
if (dependency) await copyPackage(dependency);
else if (!(name in optional) && !manifest.peerDependenciesMeta?.[name]?.optional) {
throw new Error(`DS Web bundle is missing ${name} required by ${manifest.name}`);
}
}
}
await copyPackage(join(projectDir, 'node_modules', '@deepseek-ai', 'dsh'));
// Cross-architecture packaging cannot rely on the build host's npm optional
// selection. Fetch the exact target Sharp packages from the existing lock.
const sharp = JSON.parse(await readFile(join(projectDir, 'node_modules', 'sharp', 'package.json'), 'utf8'));
const lock = JSON.parse(await readFile(join(projectDir, 'package-lock.json'), 'utf8'));
const nativePackages = [`@img/sharp-${platform}-${architecture}`];
if (platform !== 'win32') nativePackages.push(`@img/sharp-libvips-${platform}-${architecture}`);
for (const name of nativePackages) {
const version = sharp.optionalDependencies[name];
if (!version) throw new Error(`DS Web Sharp does not support ${platform}/${architecture}`);
const destination = join(target, 'node_modules', 'sharp', 'node_modules', name);
if (existsSync(join(destination, 'package.json'))) continue;
const locked = lock.packages[`node_modules/sharp/node_modules/${name}`] ?? lock.packages[`node_modules/${name}`];
if (locked?.version !== version || !locked.integrity) throw new Error(`Missing locked ${name}@${version}`);
const integrity = locked.integrity;
const archive = await downloadPackage(projectDir, name, version, integrity);
await mkdir(destination, { recursive: true });
await tar.x({ file: archive, cwd: destination, strip: 1 });
}
const koffi = JSON.parse(await readFile(join(projectDir, 'node_modules', 'koffi', 'package.json'), 'utf8'));
const koffiName = `@koromix/koffi-${platform}-${architecture}`;
const koffiDestination = join(target, 'node_modules', koffiName);
if (!existsSync(join(koffiDestination, 'package.json'))) {
const integrity = await lockedIntegrity(projectDir, koffiName, koffi.version);
const archive = await downloadPackage(projectDir, koffiName, koffi.version, integrity);
await mkdir(koffiDestination, { recursive: true });
await tar.x({ file: archive, cwd: koffiDestination, strip: 1 });
}
return target;
}
module.exports = { prepareNativeDshResources };
+89
View File
@@ -0,0 +1,89 @@
{
"note": "PERF-17 regression ceilings for the PERF-11 full-App benchmark (npm run perf:app). They apply ONLY to non-throttled runs (environment.rendererCpuThrottle = 1, the default) with the default GB_PERF_* scale, on the reference machine of the 2026-10-02 baseline (Intel Core Ultra X7 358H x16, 32 GB, DPR 1, 120 Hz). Limits are about 2-3x the 2026-10-02 values so that run-to-run noise does not fail; they catch regressions of the size seen before PERF-12 (e.g. ~130 long tasks / 9 s while streaming). Tighten them as PERF-12..PERF-14 land; do not loosen them to make a regression pass. Every listed scenario must be present unless marked optional, and report.status must equal requiredStatus.",
"requiredStatus": "passed",
"maxRendererCpuThrottle": 1,
"scenarios": {
"idle": {
"renderer.longTasks.count": { "max": 2 },
"renderer.longTasks.totalMs": { "max": 200 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"typing-small": {
"renderer.longTasks.count": { "max": 3 },
"renderer.longTasks.totalMs": { "max": 300 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"latency.p95": { "max": 25 },
"latency.max": { "max": 100 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"idle-seeded": {
"renderer.longTasks.count": { "max": 2 },
"renderer.longTasks.totalMs": { "max": 200 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"typing-seeded": {
"renderer.longTasks.count": { "max": 3 },
"renderer.longTasks.totalMs": { "max": 300 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"latency.p95": { "max": 25 },
"latency.max": { "max": 100 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"switch-conversations": {
"renderer.longTasks.count": { "max": 5 },
"renderer.longTasks.totalMs": { "max": 500 },
"renderer.frames.jankRatio": { "max": 0.15 },
"latency.p95": { "max": 350 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"switch-conversations-warm": {
"renderer.longTasks.count": { "max": 3 },
"renderer.longTasks.totalMs": { "max": 300 },
"renderer.frames.jankRatio": { "max": 0.15 },
"latency.p95": { "max": 200 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"switch-conversations-hot": {
"optional": true,
"renderer.longTasks.count": { "max": 3 },
"renderer.longTasks.totalMs": { "max": 200 },
"renderer.frames.jankRatio": { "max": 0.1 },
"latency.p95": { "max": 100 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"open-long-conversation": {
"renderer.longTasks.count": { "max": 4 },
"renderer.longTasks.totalMs": { "max": 400 },
"latency.p95": { "max": 400 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"scroll-long-conversation": {
"renderer.longTasks.count": { "max": 2 },
"renderer.longTasks.totalMs": { "max": 200 },
"renderer.frames.p95": { "max": 20 },
"renderer.frames.jankRatio": { "max": 0.05 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"stream-seeded": {
"renderer.longTasks.count": { "max": 10 },
"renderer.longTasks.totalMs": { "max": 1500 },
"renderer.frames.p95": { "max": 34 },
"renderer.frames.jankRatio": { "max": 0.15 },
"main.eventLoopDelay.p99": { "max": 100 }
},
"stream-and-type": {
"renderer.longTasks.count": { "max": 10 },
"renderer.longTasks.totalMs": { "max": 1500 },
"renderer.frames.p95": { "max": 34 },
"renderer.frames.jankRatio": { "max": 0.2 },
"latency.p95": { "max": 50 },
"main.eventLoopDelay.p99": { "max": 100 }
}
}
}
+219
View File
@@ -0,0 +1,219 @@
// Full-App interaction benchmark supervisor (PERF-11).
// Starts a loopback fake OpenAI-compatible model on 127.0.0.1:11434 (the
// fresh-profile default model endpoint), launches the current production
// build in an isolated profile through tests/support/app-perf-driver.mjs,
// enforces a total deadline and prints a summary of the driver report.
const { spawn } = require('node:child_process')
const { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } = require('node:fs')
const { createServer } = require('node:http')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
const root = resolve(__dirname, '..')
// 11434 is the fresh-profile default endpoint. Another GoodBuddy (or Ollama)
// may already use it; GB_PERF_MODEL_PORT moves the fake model and points the
// isolated profile at it through the GOODBUDDY_MODEL_* environment variables.
const defaultModelPort = 11434
const modelPort = Number(process.env.GB_PERF_MODEL_PORT || defaultModelPort)
const totalTimeoutMs = Number(process.env.GB_PERF_TIMEOUT_MS || 600_000)
const keepProfile = process.env.GB_PERF_KEEP_PROFILE === '1'
const streamBytes = Number(process.env.GB_PERF_STREAM_BYTES || 40_000)
const chunkChars = Number(process.env.GB_PERF_CHUNK_CHARS || 12)
const streamCharsPerSecond = Number(process.env.GB_PERF_STREAM_CPS || 2_000)
const tickIntervalMs = 10
function buildAnswer(targetBytes) {
const blocks = [
'## Section {n}\n\nThis paragraph explains the {n}th step of the benchmark answer with **bold**, `inline code`, and a [link](https://example.com/{n}).\n\n',
'- First item for block {n}\n- Second item with *emphasis*\n - Nested item {n}\n- Third item\n\n',
'```ts\nexport function step{n}(input: number): number {\n const value = input * {n}\n return value + 1\n}\n```\n\n',
'| Column | Value {n} | Notes |\n| --- | --- | --- |\n| alpha | {n} | first |\n| beta | {n}1 | second |\n\n',
// Single-line display math is common in model output. GB_PERF_CORPUS=multiline-math
// puts the delimiters on their own lines instead.
process.env.GB_PERF_CORPUS === 'multiline-math'
? 'Inline math $a_{n} = b^2 + c$ and display math:\n\n$$\n\\sum_{i=1}^{{n}} i = \\frac{{n}({n}+1)}{2}\n$$\n\n'
: 'Inline math $a_{n} = b^2 + c$ and display math:\n\n$$\\sum_{i=1}^{{n}} i = \\frac{{n}({n}+1)}{2}$$\n\n',
'> Quoted note {n}: keep the streaming renderer honest with mixed blocks.\n\n'
]
let text = ''
let n = 1
while (Buffer.byteLength(text) < targetBytes) {
text += blocks[n % blocks.length].replaceAll('{n}', String(n))
n += 1
}
return text
}
function startModelServer(ledger) {
const answer = buildAnswer(streamBytes)
const server = createServer((request, response) => {
const started = Date.now()
const entry = { method: request.method, path: request.url, startedAt: started }
ledger.push(entry)
if (request.method === 'GET' && request.url?.endsWith('/models')) {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({ object: 'list', data: [{ id: 'qwen3', object: 'model' }] }))
return
}
if (request.method !== 'POST' || !request.url?.endsWith('/chat/completions')) {
response.writeHead(404).end()
return
}
let body = ''
request.on('data', chunk => { body += chunk })
request.on('end', () => {
let parsed = {}
try { parsed = JSON.parse(body) } catch { /* counted below */ }
const messages = Array.isArray(parsed.messages) ? parsed.messages : []
const last = messages.at(-1)
const lastText = typeof last?.content === 'string' ? last.content : JSON.stringify(last?.content ?? '')
const benchmark = lastText.includes('perf-stream')
entry.kind = benchmark ? 'benchmark-stream' : 'auxiliary'
entry.stream = parsed.stream === true
const content = benchmark ? answer : 'Benchmark title'
if (!entry.stream) {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({
id: 'perf', object: 'chat.completion', model: 'qwen3',
choices: [{ index: 0, finish_reason: 'stop', message: { role: 'assistant', content } }],
usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 }
}))
entry.finishedAt = Date.now()
return
}
response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' })
const send = payload => response.write(`data: ${JSON.stringify(payload)}\n\n`)
const delta = text => send({ id: 'perf', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: { content: text }, finish_reason: null }] })
send({ id: 'perf', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }] })
let offset = 0
let chunks = 0
const finish = () => {
send({ id: 'perf', object: 'chat.completion.chunk', model: 'qwen3', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })
send({ id: 'perf', object: 'chat.completion.chunk', model: 'qwen3', choices: [], usage: { prompt_tokens: 10, completion_tokens: Math.ceil(content.length / 4), total_tokens: 10 + Math.ceil(content.length / 4) } })
response.write('data: [DONE]\n\n')
response.end()
entry.finishedAt = Date.now()
entry.chunks = chunks
entry.bytes = Buffer.byteLength(content)
}
if (!benchmark) { delta(content); finish(); return }
// Emit at a fixed character rate, split into small SSE events like a
// fast token stream. Pacing is time-based so coarse timers do not
// change throughput.
const streamStart = Date.now()
const tick = () => {
if (response.destroyed) { entry.aborted = true; return }
const due = Math.min(content.length, Math.ceil(((Date.now() - streamStart) / 1000) * streamCharsPerSecond))
while (offset < due) {
delta(content.slice(offset, offset + chunkChars))
offset += chunkChars
chunks += 1
}
if (offset >= content.length) { finish(); return }
setTimeout(tick, tickIntervalMs)
}
tick()
})
})
return new Promise((resolvePromise, reject) => {
server.once('error', reject)
server.listen(modelPort, '127.0.0.1', () => resolvePromise(server))
})
}
function format(value, digits = 1) {
return value === null || value === undefined || Number.isNaN(value) ? '-' : Number(value).toFixed(digits)
}
function printSummary(report) {
console.log(`\nGoodBuddy full-App benchmark (${report.status})`)
console.log(`source ${report.source?.commit ?? '?'}${report.source?.dirty ? ' (dirty)' : ''}, Electron ${report.environment?.electron}, DPR ${report.environment?.devicePixelRatio}, ${report.environment?.cpu}`)
if (report.startup) {
console.log(`startup: window ${format(report.startup.windowCreatedMs, 0)} ms, ready-to-show ${format(report.startup.readyToShowMs, 0)} ms, composer ready ${format(report.startup.composerReadyMs, 0)} ms`)
}
const rows = (report.scenarios ?? []).map(s => ({
scenario: s.id,
ms: format(s.durationMs, 0),
longTasks: `${s.renderer?.longTasks?.count ?? '-'} / ${format(s.renderer?.longTasks?.totalMs, 0)} / ${format(s.renderer?.longTasks?.maxMs, 0)}`,
'frame p50/p95/max': `${format(s.renderer?.frames?.p50)} / ${format(s.renderer?.frames?.p95)} / ${format(s.renderer?.frames?.max, 0)}`,
'jank%': format(s.renderer?.frames?.jankRatio * 100),
'latency p50/p95/max': s.latency ? `${format(s.latency.p50)} / ${format(s.latency.p95)} / ${format(s.latency.max)}` : '-',
'mainLag p99/max': `${format(s.main?.eventLoopDelay?.p99)} / ${format(s.main?.eventLoopDelay?.max)}`,
'cpu% main/renderer/gpu': `${format(s.processes?.cpu?.Browser, 0)} / ${format(s.processes?.cpu?.Tab, 0)} / ${format(s.processes?.cpu?.GPU, 0)}`,
domNodes: s.renderer?.domNodes ?? '-',
heapMB: format(s.renderer?.heapUsedMB)
}))
if (rows.length) console.table(rows)
if (report.memoryGrowth) {
console.log(`memory growth (${report.memoryGrowth.description}):`)
console.table(report.memoryGrowth.samples)
}
if (report.environment?.rendererCpuThrottle > 1) console.log(`renderer CPU throttled ${report.environment.rendererCpuThrottle}x`)
if (report.error) console.log(`error: ${report.error}`)
}
async function main() {
const required = ['out/main/index.js', 'out/preload/index.cjs', 'out/renderer/index.html']
const missing = required.filter(file => !existsSync(join(root, file)))
if (missing.length) throw new Error(`Missing production build (${missing.join(', ')}); run npm run build:bundle first`)
const runDirectory = mkdtempSync(join(tmpdir(), 'goodbuddy-app-perf-'))
const outputDirectory = resolve(process.env.GB_PERF_OUTPUT || join(runDirectory, 'artifacts'))
mkdirSync(outputDirectory, { recursive: true })
const ledger = []
let server
try {
server = await startModelServer(ledger)
} catch (error) {
throw new Error(`Cannot listen on 127.0.0.1:${modelPort} for the fake model (${error.code ?? error.message}); stop the local service using that port, or set GB_PERF_MODEL_PORT to a free port`, { cause: error })
}
const environment = { ...process.env, GB_PERF_DIRECTORY: runDirectory, GB_PERF_ARTIFACTS: outputDirectory, GB_PERF_ROOT: root }
delete environment.ELECTRON_RUN_AS_NODE
delete environment.ELECTRON_RENDERER_URL
for (const key of ['GOODBUDDY_MODEL_API_KEY', 'GOODBUDDY_MODEL_BASE_URL', 'GOODBUDDY_MODEL_NAME', 'GOODBUDDY_BIGTOKEN_API_KEY', 'GOODBUDDY_BIGTOKEN_BASE_URL', 'GOODBUDDY_BIGTOKEN_MODEL']) delete environment[key]
if (modelPort !== defaultModelPort) {
// The fake model ignores the key; it only makes the default profile use the URL.
environment.GOODBUDDY_MODEL_API_KEY = 'perf-loopback'
environment.GOODBUDDY_MODEL_BASE_URL = `http://127.0.0.1:${modelPort}/v1`
environment.GOODBUDDY_MODEL_NAME = 'qwen3'
}
environment.GOODBUDDY_WORKSPACE = join(runDirectory, 'workspace')
mkdirSync(environment.GOODBUDDY_WORKSPACE, { recursive: true })
const started = Date.now()
const child = spawn(require('electron'), [join(root, 'tests', 'support', 'app-perf-driver.mjs')], { cwd: root, env: environment, stdio: 'inherit', windowsHide: false })
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
if (process.platform === 'win32') spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
else child.kill('SIGKILL')
}, totalTimeoutMs)
const exit = await new Promise(resolvePromise => child.once('exit', (code, signal) => resolvePromise({ code, signal })))
clearTimeout(timer)
await new Promise(resolvePromise => server.close(() => resolvePromise()))
const resultPath = join(outputDirectory, 'result.json')
let report
if (existsSync(resultPath)) report = JSON.parse(readFileSync(resultPath, 'utf8'))
else report = { status: 'failed', error: timedOut ? 'Supervisor timeout before report' : 'Driver exited without a report' }
report.supervisor = { elapsedMs: Date.now() - started, timeoutMs: totalTimeoutMs, timedOut, exitCode: exit.code, signal: exit.signal }
report.corpus = { kind: process.env.GB_PERF_CORPUS || 'single-line-math', bytes: streamBytes, charsPerSecond: streamCharsPerSecond, charsPerEvent: chunkChars }
report.modelRequests = {
boundary: 'loopback fake model only; no external requests',
total: ledger.length,
benchmarkStreams: ledger.filter(entry => entry.kind === 'benchmark-stream').length,
auxiliary: ledger.filter(entry => entry.kind === 'auxiliary').length,
ledger: ledger.map(({ method, path, kind, stream, chunks, bytes, aborted, startedAt, finishedAt }) => ({ method, path, kind, stream, chunks, bytes, aborted, durationMs: finishedAt ? finishedAt - startedAt : null }))
}
if (timedOut || exit.code !== 0) report.status = 'failed'
writeFileSync(resultPath, JSON.stringify(report, null, 2))
printSummary(report)
console.log(`\nreport: ${resultPath}`)
if (!keepProfile) rmSync(join(runDirectory, 'profile'), { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
process.exitCode = String(report.status).startsWith('passed') ? 0 : 1
// PERF-17: GB_PERF_CHECK=1 gates the run on build/perf-thresholds.json
// (exit 1 on a regression, 2 when the report cannot be judged, e.g. throttled).
if (process.env.GB_PERF_CHECK === '1') process.exitCode = require('./check-perf-thresholds.cjs').run([resultPath])
}
main().catch(error => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
+1 -1
View File
@@ -88,7 +88,7 @@ async function main() {
`${JSON.stringify(
{
name: '@deepseek-ai/dsh-llm',
version: '0.1.2-rc.1',
version: '0.1.7-rc.2',
private: true,
type: 'module'
},
+109
View File
@@ -0,0 +1,109 @@
// Document parse benchmark (PERF-15/16, P7). Bundles the production parser
// client and worker with esbuild, then imports synthetic large documents
// (hash -> parse -> chunk, as Knowledge import does) twice per round: inline on
// the main thread, then through the parse worker. Records the main event-loop
// max block with perf_hooks.monitorEventLoopDelay.
//
// node build/run-document-parse-perf.cjs
// GB_DOCPERF_ROUNDS=3 GB_DOCPERF_NODE=1 (skip Electron) GB_DOCPERF_OUTPUT=file.json
const { buildSync } = require('esbuild')
const { mkdirSync, rmSync, writeFileSync } = require('node:fs')
const { join, resolve } = require('node:path')
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
const root = resolve(__dirname, '..')
// Measure on the project's Electron (the production runtime), like perf:knowledge.
if (process.env.GB_DOCPERF_NODE !== '1') {
let electron
let version
try { electron = require('electron'); version = require('electron/package.json').version } catch { electron = undefined }
if (typeof electron === 'string' && process.versions.electron !== version) {
const { status } = require('node:child_process').spawnSync(electron, [__filename], {
stdio: 'inherit', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
process.exit(status ?? 1)
}
}
const rounds = Number(process.env.GB_DOCPERF_ROUNDS || 3)
// Under node_modules so the external pdfjs-dist resolves from the repository.
const directory = join(root, 'node_modules', '.cache', `goodbuddy-document-parse-perf-${process.pid}`)
mkdirSync(directory, { recursive: true })
const harness = join(directory, 'harness.cjs')
const workerBundle = join(directory, 'document-parse-worker.cjs')
const harnessSource = join(directory, 'harness.ts')
writeFileSync(harnessSource, [
`export * from ${JSON.stringify(join(root, 'src/main/document-parse-client.ts'))}`,
`export * from ${JSON.stringify(join(root, 'tests/support/document-parse-worker-fixture.ts'))}`
].join('\n'))
for (const [entry, outfile] of [[harnessSource, harness], [join(root, 'src/main/document-parse-worker.ts'), workerBundle]]) {
buildSync({
entryPoints: [entry], bundle: true, platform: 'node', format: 'cjs', target: 'node22',
outfile, logLevel: 'error', external: ['pdfjs-dist', 'electron', 'esbuild']
})
}
const {
DocumentParseWorkerClient, chunkDocumentOffMain, parseDocumentOffMain, sha256OffMain,
createDocx, createLargePdf, sentenceGenerator
} = require(harness)
const settings = {
version: 1, mode: 'structure', targetCharacters: 1600, overlapCharacters: 100,
parentCharacters: 1600, childCharacters: 400, contextualIndexingEnabled: false
}
const round = value => Math.round(value * 10) / 10
async function importOnce(name, buffer, client) {
await sha256OffMain(buffer, client)
const parsed = await parseDocumentOffMain(name, buffer, undefined, client)
return (await chunkDocumentOffMain(parsed, settings, client)).length
}
async function measure(name, buffer, client) {
await importOnce(name, buffer, client) // warm modules / worker
await new Promise(resolve => setTimeout(resolve, 50))
const histogram = monitorEventLoopDelay({ resolution: 5 })
histogram.enable()
// The histogram only measures from its first timer tick on; a block that
// starts before that tick (the inline path is all microtasks) is not seen.
await new Promise(resolve => setTimeout(resolve, 30))
const started = performance.now()
const chunks = await importOnce(name, buffer, client)
const wallMs = performance.now() - started
await new Promise(resolve => setTimeout(resolve, 20))
histogram.disable()
return { maxMs: round(histogram.max / 1e6), p99Ms: round(histogram.percentile(99) / 1e6), wallMs: round(wallMs), chunks }
}
async function main() {
console.log(`[docperf] runtime: node ${process.version}${process.versions.electron ? ` (electron ${process.versions.electron})` : ''}`)
const sentence = sentenceGenerator(11)
const inputs = [
{ name: 'large.pdf', buffer: createLargePdf(500, 60) },
{ name: 'large.docx', buffer: createDocx(Array.from({ length: 4000 }, () => sentence(25))) },
{ name: 'large.md', buffer: Buffer.from(Array.from({ length: 4000 }, (_, index) => `${index % 50 === 0 ? '## Section\n' : ''}${sentence(60)}.`).join('\n')) }
]
const client = new DocumentParseWorkerClient(workerBundle)
const results = []
try {
for (const input of inputs) {
for (let index = 0; index < rounds; index += 1) {
// Alternate inline / worker within each round (P7).
const inline = await measure(input.name, input.buffer, undefined)
const worker = await measure(input.name, input.buffer, client)
if (worker.chunks !== inline.chunks) throw new Error(`chunk count mismatch for ${input.name}`)
results.push({ input: input.name, bytes: input.buffer.byteLength, round: index + 1, inline, worker })
console.log(`[docperf] ${input.name} round ${index + 1}: Main loop max ${inline.maxMs} -> ${worker.maxMs} ms; wall ${inline.wallMs} / ${worker.wallMs} ms; ${inline.chunks} chunks`)
}
}
} finally {
client.close()
}
const output = process.env.GB_DOCPERF_OUTPUT
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-document-parse-benchmark', node: process.version, results }, null, 2))
console.table(results.map(r => ({ input: r.input, round: r.round, 'loop max inline/worker ms': `${r.inline.maxMs} / ${r.worker.maxMs}`, 'wall inline/worker ms': `${r.inline.wallMs} / ${r.worker.wallMs}` })))
}
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
})
+148
View File
@@ -0,0 +1,148 @@
// Knowledge retrieval benchmark (PERF-05). Bundles the production
// KnowledgeDatabase with esbuild, seeds synthetic libraries of increasing size
// in a temporary SQLite file and times the synchronous search calls that the
// Electron Main process runs inline during retrieval. Because these calls are
// synchronous, their duration is the time Main is blocked per query.
const { buildSync } = require('esbuild')
const { createHash } = require('node:crypto')
const { mkdtempSync, rmSync, writeFileSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
const root = resolve(__dirname, '..')
// Measure on the project's Electron (the production runtime). Older runtimes
// (Node 24.18 / Electron 43) stall worker threads for hundreds of ms on many
// small ArrayBuffer allocations (SQLite BLOB rows); GB_KPERF_NODE=1 opts out.
if (process.env.GB_KPERF_NODE !== '1') {
let electron
let version
try { electron = require('electron'); version = require('electron/package.json').version } catch { electron = undefined }
if (typeof electron === 'string' && process.versions.electron !== version) {
const { status } = require('node:child_process').spawnSync(electron, [__filename], {
stdio: 'inherit', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
process.exit(status ?? 1)
}
}
const sizes = (process.env.GB_KPERF_SIZES || '5000,20000,50000').split(',').map(Number)
const dimensions = Number(process.env.GB_KPERF_DIMENSIONS || 384)
const chunksPerDocument = 100
const queries = Number(process.env.GB_KPERF_QUERIES || 15)
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-knowledge-perf-'))
const bundle = join(directory, 'knowledge-database.cjs')
const workerBundle = join(directory, 'readonly-query-worker.cjs')
for (const [entry, outfile] of [
['src/main/knowledge/knowledge-database.ts', bundle],
['src/main/readonly-query-worker.ts', workerBundle]
]) {
buildSync({
entryPoints: [join(root, entry)],
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node22',
outfile,
logLevel: 'error',
// The worker reuses AssistantDatabase, which imports Electron-only modules lazily.
external: ['electron']
})
}
const { KnowledgeDatabase } = require(bundle)
// Deterministic pseudo-random generator so runs are comparable.
let seed = 42
const random = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296)
const words = 'lighthouse harbor policy engine renderer stream vector index budget latency schedule archive network storage contract invoice release window memory review'.split(' ')
const sentence = n => Array.from({ length: n }, () => words[Math.floor(random() * words.length)]).join(' ')
const vector = () => Array.from({ length: dimensions }, () => random() * 2 - 1)
const checksum = value => createHash('sha256').update(value).digest('hex')
function percentile(values, p) {
const sorted = [...values].sort((a, b) => a - b)
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))]
}
function time(body) {
const samples = []
body() // warm statement cache and page cache
for (let index = 0; index < queries; index += 1) {
const started = performance.now()
body()
samples.push(performance.now() - started)
}
return { p50: round(percentile(samples, 50)), p95: round(percentile(samples, 95)), max: round(Math.max(...samples)) }
}
const round = value => Math.round(value * 10) / 10
// Main event-loop delay while `queries` searches run back to back. A sync
// search blocks the loop for its whole duration; a worker search should not.
async function loopDelay(body) {
const histogram = monitorEventLoopDelay({ resolution: 5 })
await body() // warm worker / caches outside the measurement
// Let the warm-up's blocked interval drain before sampling.
await new Promise(resolve => setTimeout(resolve, 50))
histogram.enable()
const started = performance.now()
for (let index = 0; index < queries; index += 1) {
await body()
// Yield like a real IPC handler, so the histogram samples between queries.
await new Promise(resolve => setImmediate(resolve))
}
const wallMs = performance.now() - started
await new Promise(resolve => setTimeout(resolve, 20))
histogram.disable()
const ms = value => round(value / 1e6)
return { p50: ms(histogram.percentile(50)), p99: ms(histogram.percentile(99)), max: ms(histogram.max), perQueryMs: round(wallMs / queries) }
}
const results = []
async function main() {
console.log(`[kperf] runtime: node ${process.version}${process.versions.electron ? ` (electron ${process.versions.electron})` : ''}`)
for (const size of sizes) {
const path = join(directory, `knowledge-${size}.sqlite`)
const database = new KnowledgeDatabase(path)
database.initialize()
const library = database.createKnowledgeBase({ name: `Perf ${size}`, storageMode: 'reference' })
const sqlite = database.database
const seedStarted = performance.now()
for (let documentIndex = 0; documentIndex < size / chunksPerDocument; documentIndex += 1) {
const source = database.upsertSource({ knowledgeBaseId: library.id, type: 'file', location: `C:\\perf\\doc-${documentIndex}.md`, displayName: `doc-${documentIndex}.md`, status: 'ready' })
const chunks = Array.from({ length: chunksPerDocument }, (_, ordinal) => ({ id: `d${documentIndex}-c${ordinal}`, ordinal, content: sentence(80), location: `line ${ordinal + 1}` }))
const document = database.upsertDocument({ knowledgeBaseId: library.id, sourceId: source.id, externalId: `doc-${documentIndex}`, title: `doc-${documentIndex}`, sourceLocation: source.location }, chunks)
const stored = sqlite.prepare("SELECT id, index_content FROM chunks WHERE document_id = ? AND enabled = 1 AND role <> 'parent'").all(document.id)
database.replaceDocumentEmbeddings(document.id, 'perf', 'perf-model', stored.map(row => ({ chunkId: row.id, contentChecksum: checksum(row.index_content), vector: vector() })))
}
const seedMs = performance.now() - seedStarted
database.close()
// Reopen so timings start from a fresh connection, like an app restart.
const reopened = new KnowledgeDatabase(path)
reopened.initialize()
const query = vector()
const entry = {
chunks: size,
dimensions,
seedSeconds: round(seedMs / 1000),
ftsMs: time(() => reopened.search({ knowledgeBaseId: library.id, query: 'lighthouse budget', limit: 40 })),
vectorMs: time(() => reopened.vectorSearch({ knowledgeBaseId: library.id, provider: 'perf', model: 'perf-model', vector: query, limit: 40 })),
hybridMs: time(() => reopened.hybridSearchWithDiagnostics(hybridOptions(library.id, query)))
}
// Main event-loop delay: synchronous search on Main vs the readonly worker.
entry.mainLoopDelaySyncMs = await loopDelay(async () => reopened.hybridSearchWithDiagnostics(hybridOptions(library.id, query)))
reopened.enableReadonlyWorker(workerBundle)
entry.mainLoopDelayWorkerMs = await loopDelay(() => reopened.hybridSearchWithDiagnosticsAsync(hybridOptions(library.id, query)))
reopened.close()
results.push(entry)
console.log(`[kperf] ${size} chunks: fts p95 ${entry.ftsMs.p95} ms, vector p95 ${entry.vectorMs.p95} ms, hybrid p95 ${entry.hybridMs.p95} ms (seed ${entry.seedSeconds} s)`)
console.log(`[kperf] ${size} chunks: Main loop delay max sync ${entry.mainLoopDelaySyncMs.max} ms vs worker ${entry.mainLoopDelayWorkerMs.max} ms (p99 ${entry.mainLoopDelaySyncMs.p99} / ${entry.mainLoopDelayWorkerMs.p99} ms; per query ${entry.mainLoopDelaySyncMs.perQueryMs} / ${entry.mainLoopDelayWorkerMs.perQueryMs} ms)`)
}
const output = process.env.GB_KPERF_OUTPUT
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-knowledge-retrieval-benchmark', node: process.version, results }, null, 2))
console.table(results.map(r => ({ chunks: r.chunks, 'fts p50/p95': `${r.ftsMs.p50} / ${r.ftsMs.p95}`, 'vector p50/p95': `${r.vectorMs.p50} / ${r.vectorMs.p95}`, 'hybrid p50/p95': `${r.hybridMs.p50} / ${r.hybridMs.p95}`, 'loop max sync/worker': `${r.mainLoopDelaySyncMs.max} / ${r.mainLoopDelayWorkerMs.max}` })))
}
const hybridOptions = (knowledgeBaseId, vector) => ({ knowledgeBaseId, query: 'lighthouse budget', limit: 40, provider: 'perf', model: 'perf-model', vector, graphEnabled: false, candidateMultiplier: 1 })
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
})
+272
View File
@@ -0,0 +1,272 @@
// Main-process database benchmark (PERF-15). Bundles the production
// AssistantDatabase and readonly worker with esbuild, seeds a temporary
// assistant database (default 1,000 conversations / 20,000 messages and 24,000
// activity records) and measures, for the IPC paths the renderer hits often:
// - per-call time
// - Main event-loop delay while the calls run back to back
// for the previous synchronous path ("sync") and the current path ("after").
// conversations:list-summaries sync listConversationSummaries vs worker
// conversations:get sync getConversation vs worker
// activity-history:replace legacy replace (zod parse + BEGIN IMMEDIATE +
// JSON.stringify of every record) vs current
// activity-history:update one changed record via the incremental API
// activity startup load whole list (pre-paging get) vs first page (200)
// + summary on the readonly worker
// activity page / summary sync paths, for reference
// Env: GB_MPERF_CONVERSATIONS, GB_MPERF_MESSAGES, GB_MPERF_ACTIVITY,
// GB_MPERF_CALLS, GB_MPERF_OUTPUT (JSON), GB_MPERF_NODE=1 (skip Electron).
const { buildSync } = require('esbuild')
const { mkdtempSync, rmSync, writeFileSync, mkdirSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
const { randomUUID } = require('node:crypto')
const { monitorEventLoopDelay, performance } = require('node:perf_hooks')
const root = resolve(__dirname, '..')
if (process.env.GB_MPERF_NODE !== '1') {
let electron
let version
try { electron = require('electron'); version = require('electron/package.json').version } catch { electron = undefined }
if (typeof electron === 'string' && process.versions.electron !== version) {
const { status } = require('node:child_process').spawnSync(electron, [__filename], {
stdio: 'inherit', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
process.exit(status ?? 1)
}
}
const conversationCount = Number(process.env.GB_MPERF_CONVERSATIONS || 1000)
const messageCount = Number(process.env.GB_MPERF_MESSAGES || 20000)
const activityCount = Number(process.env.GB_MPERF_ACTIVITY || 24000)
const calls = Number(process.env.GB_MPERF_CALLS || 15)
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-main-perf-'))
const bundle = join(directory, 'assistant-database.cjs')
const workerBundle = join(directory, 'readonly-query-worker.cjs')
const schemaBundle = join(directory, 'assistant-contracts.cjs')
for (const [entry, outfile] of [
['src/main/assistant/assistant-database.ts', bundle],
['src/main/readonly-query-worker.ts', workerBundle],
['src/shared/assistant-contracts.ts', schemaBundle]
]) {
buildSync({
entryPoints: [join(root, entry)], bundle: true, platform: 'node', format: 'cjs',
target: 'node22', outfile, logLevel: 'error', external: ['electron']
})
}
const { AssistantDatabase } = require(bundle)
const { activityHistorySnapshotSchema } = require(schemaBundle)
let seed = 42
const random = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296)
const words = 'lighthouse harbor policy engine renderer stream vector index budget latency schedule archive network storage contract invoice release window memory review 发布 审批 流程'.split(' ')
const sentence = n => Array.from({ length: n }, () => words[Math.floor(random() * words.length)]).join(' ')
const round = value => Math.round(value * 10) / 10
const percentile = (values, p) => {
const sorted = [...values].sort((a, b) => a - b)
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))]
}
/**
* Runs `body` `calls` times (after one warm-up) and reports the per-call wall
* time and the Main event-loop delay histogram (p99/max) over the run.
*/
async function measure(body) {
await body(-1)
await new Promise(resolve => setTimeout(resolve, 50))
// monitorEventLoopDelay is bound to the ~15 ms Windows timer granularity, so
// the longest block is measured as the largest gap between setImmediate
// ticks (each tick is one event-loop turn).
const histogram = monitorEventLoopDelay({ resolution: 1 })
histogram.enable()
let probing = true
let last = performance.now()
let maxGap = 0
const tick = () => {
const now = performance.now()
maxGap = Math.max(maxGap, now - last)
last = now
if (probing) setImmediate(tick)
}
setImmediate(tick)
const samples = []
for (let index = 0; index < calls; index += 1) {
const started = performance.now()
await body(index)
samples.push(performance.now() - started)
// Yield like the IPC loop does between messages.
await new Promise(resolve => setImmediate(resolve))
}
await new Promise(resolve => setTimeout(resolve, 20))
probing = false
histogram.disable()
return {
callP50: round(percentile(samples, 50)), callP95: round(percentile(samples, 95)),
loopP99: round(histogram.percentile(99) / 1e6), loopMax: round(maxGap)
}
}
/** The pre-PERF-15 replace implementation, kept as the "before" reference. */
function legacyReplace(raw, cache, input) {
const snapshot = activityHistorySnapshotSchema.parse(input)
raw.exec('BEGIN IMMEDIATE')
try {
const { data_version: dataVersion } = raw.prepare('PRAGMA data_version').get()
let previous = cache.value?.dataVersion === dataVersion ? cache.value.records : undefined
if (!previous) {
previous = new Map(raw.prepare('SELECT record_key, record_json FROM activity_history_records').all()
.map(row => [row.record_key, row.record_json]))
}
const records = new Map()
const occurrences = new Map()
const order = []
// order_seq only satisfies the current schema; the old code kept order in the JSON list below.
const upsert = raw.prepare(`INSERT INTO activity_history_records (record_key, record_json, order_seq) VALUES (?, ?, ?)
ON CONFLICT(record_key) DO UPDATE SET record_json = excluded.record_json`)
for (const record of snapshot.records) {
const occurrence = occurrences.get(record.id) ?? 0
occurrences.set(record.id, occurrence + 1)
const key = JSON.stringify([record.id, occurrence])
const json = JSON.stringify(record)
order.push(key)
if (previous.get(key) !== json) upsert.run(key, json, snapshot.records.length - order.length + 1)
records.set(key, json)
}
const remove = raw.prepare('DELETE FROM activity_history_records WHERE record_key = ?')
for (const key of previous.keys()) if (!records.has(key)) remove.run(key)
const orderJson = JSON.stringify(order)
const incomplete = Number(snapshot.legacyHistoryMayBeIncomplete)
raw.prepare(`UPDATE activity_history SET record_order_json = ?, legacy_history_may_be_incomplete = ?
WHERE singleton = 1 AND (record_order_json != ? OR legacy_history_may_be_incomplete != ?)`)
.run(orderJson, incomplete, orderJson, incomplete)
raw.exec('COMMIT')
cache.value = { records, dataVersion }
} catch (error) {
raw.exec('ROLLBACK')
throw error
}
}
async function main() {
console.log(`[mperf] runtime: node ${process.version}${process.versions.electron ? ` (electron ${process.versions.electron})` : ''}`)
const workspace = join(directory, 'workspace')
mkdirSync(workspace)
const path = join(directory, 'assistant.sqlite')
const seeding = new AssistantDatabase(path)
seeding.initialize(workspace)
const seedStarted = performance.now()
const projectId = seeding.listProjects()[0].id
// Message counts follow a skewed distribution; the first conversation is the largest.
const weights = Array.from({ length: conversationCount }, (_, index) => 1 / (1 + index * 0.01))
const weightSum = weights.reduce((sum, value) => sum + value, 0)
const counts = weights.map(weight => Math.max(1, Math.round(messageCount * weight / weightSum)))
const ids = counts.map(() => randomUUID())
for (let start = 0; start < conversationCount; start += 50) {
seeding.saveLocalConversations(ids.slice(start, start + 50).map((id, offset) => {
const index = start + offset
return {
header: { id, projectId, title: `Conversation ${index} ${sentence(3)}`, updatedAt: 1_700_000_000_000 + index },
messages: Array.from({ length: counts[index] }, (_, at) => ({
id: randomUUID(), role: at % 2 === 0 ? 'user' : 'assistant', state: 'complete',
content: sentence(at % 2 === 0 ? 30 : 120), createdAt: 1_700_000_000_000 + index * 1000 + at
}))
}
}))
}
const records = Array.from({ length: activityCount }, (_, index) => ({
id: randomUUID(), conversationId: ids[index % conversationCount], requestId: randomUUID(),
...(index % 2 === 0 ? { callId: `call-${index}` } : {}),
scope: index % 3 === 0 ? { kind: 'project', projectId, projectName: 'Perf' } : { kind: 'global' },
kind: index % 5 === 0 ? 'request' : 'tool', title: `Tool ${index}`, detail: sentence(40),
status: 'completed', createdAt: 1_700_000_000_000 + index
}))
seeding.replaceActivityHistory({ records, legacyHistoryMayBeIncomplete: false })
const seedSeconds = round((performance.now() - seedStarted) / 1000)
seeding.close()
const database = new AssistantDatabase(path)
database.initialize(workspace)
const raw = database.database
const totalMessages = counts.reduce((sum, value) => sum + value, 0)
const largest = ids[0]
const typical = ids[Math.floor(conversationCount / 2)]
// The renderer retains the current conversation's history.
const detailIds = [ids[1]]
const results = { conversations: conversationCount, messages: totalMessages, activityRecords: activityCount,
largestConversationMessages: counts[0], seedSeconds }
results.listSummariesSync = await measure(async () => database.listConversationSummaries(detailIds))
results.getLargestSync = await measure(async () => database.getConversation(largest))
results.getTypicalSync = await measure(async () => database.getConversation(typical))
database.enableReadonlyWorker(workerBundle)
results.listSummariesWorker = await measure(() => database.listConversationSummariesAsync(detailIds))
results.getLargestWorker = await measure(() => database.getConversationAsync(largest))
results.getTypicalWorker = await measure(() => database.getConversationAsync(typical))
// The renderer sends a fresh structured clone of the whole list every save.
const unchanged = () => structuredClone({ records, legacyHistoryMayBeIncomplete: false })
const oneChanged = index => {
const snapshot = unchanged()
const record = snapshot.records[(index + 2) % 50]
record.detail = `${record.detail} ${index}`
return snapshot
}
// Inputs are cloned up front: cloning happens in the renderer / IPC layer.
const run = async (makeInput, call) => {
const inputs = Array.from({ length: calls + 1 }, (_, index) => makeInput(index - 1))
return measure(async index => call(inputs[index + 1]))
}
const legacyCache = {}
results.replaceUnchangedLegacy = await run(unchanged, input => legacyReplace(raw, legacyCache, input))
results.replaceOneChangedLegacy = await run(oneChanged, input => legacyReplace(raw, legacyCache, input))
// Restore a consistent state for the current implementation's cache.
database.replaceActivityHistory({ records, legacyHistoryMayBeIncomplete: false })
results.replaceUnchangedAfter = await run(unchanged, input => database.replaceActivityHistory(input))
results.replaceOneChangedAfter = await run(oneChanged, input => database.replaceActivityHistory(input))
results.updateOneRecord = await run(index => {
const record = { ...records[(index + 2) % 50], detail: `incremental ${index}`, status: 'running' }
return { changes: [{ type: 'upsert', position: 'front', record }] }
}, input => database.updateActivityHistory(input))
// Startup: the renderer used to load the whole list; now the first page and the summary.
const firstPageRequest = { limit: 200 }
const summaryRequest = () => ({ conversationIds: [...new Set(database.getActivityHistoryPage(firstPageRequest).records.map(record => record.conversationId))] })
results.activityFullLoad = await measure(async () => database.getActivityHistory())
results.activityFirstPageSync = await measure(async () => database.getActivityHistoryPage(firstPageRequest))
results.activitySummarySync = await measure(async () => database.getActivityHistorySummary(summaryRequest()))
results.activityStartupWorker = await measure(async () => {
const page = await database.getActivityHistoryPageAsync(firstPageRequest)
await database.getActivityHistorySummaryAsync({ conversationIds: [...new Set(page.records.map(record => record.conversationId))] })
})
results.activityFullLoadBytes = JSON.stringify(database.getActivityHistory()).length
results.activityFirstPageBytes = JSON.stringify(database.getActivityHistoryPage(firstPageRequest)).length
database.close()
console.log(`[mperf] seeded ${results.conversations} conversations / ${results.messages} messages (largest ${results.largestConversationMessages}), ${results.activityRecords} activity records in ${seedSeconds} s`)
const rows = [
['conversations:list-summaries', results.listSummariesSync, results.listSummariesWorker],
[`conversations:get (${counts[0]} msgs)`, results.getLargestSync, results.getLargestWorker],
[`conversations:get (${counts[Math.floor(conversationCount / 2)]} msgs)`, results.getTypicalSync, results.getTypicalWorker],
['activity replace, unchanged', results.replaceUnchangedLegacy, results.replaceUnchangedAfter],
['activity replace, 1 changed', results.replaceOneChangedLegacy, results.replaceOneChangedAfter],
['activity update, 1 record', results.replaceOneChangedLegacy, results.updateOneRecord],
['activity startup: full list vs page+summary (worker)', results.activityFullLoad, results.activityStartupWorker],
['activity first page (200), sync', results.activityFullLoad, results.activityFirstPageSync],
['activity summary, sync', results.activityFullLoad, results.activitySummarySync]
]
console.table(rows.map(([name, before, after]) => ({
path: name,
'before call p50/p95 ms': `${before.callP50} / ${before.callP95}`,
'before loop max ms': before.loopMax,
'after call p50/p95 ms': `${after.callP50} / ${after.callP95}`,
'after loop max ms': after.loopMax
})))
console.log(`[mperf] activity payload: full list ${results.activityFullLoadBytes} bytes, first page ${results.activityFirstPageBytes} bytes`)
const output = process.env.GB_MPERF_OUTPUT
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-database-benchmark', node: process.version, results }, null, 2))
}
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
})
+191
View File
@@ -0,0 +1,191 @@
const { dirname, join, resolve } = require('node:path')
const { copyFileSync, mkdtempSync, mkdirSync, rmSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { pathToFileURL } = require('node:url')
const assert = require('node:assert/strict')
if (!process.versions.electron || process.env.ELECTRON_RUN_AS_NODE === '1') {
const { spawnSync } = require('node:child_process')
const root = mkdtempSync(join(tmpdir(), 'goodbuddy-obsidian-ui-'))
const env = { ...process.env, GOODBUDDY_OBSIDIAN_ACCEPTANCE_ROOT: root }
if (process.argv.includes('--live') || process.argv.includes('--chat')) {
env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_LIVE = process.argv.includes('--live') ? '1' : '0'
env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CHAT = process.argv.includes('--chat') ? '1' : '0'
env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CONTINUE_ONLY = process.argv.includes('--continue-only') ? '1' : '0'
require('esbuild').buildSync({ entryPoints: ['tests/obsidian-runtime-acceptance.ts'],
outfile: join(root, 'runtime.cjs'), bundle: true, external: ['electron', 'ssh2', 'node-pty', 'koffi'], platform: 'node', format: 'cjs',
banner: { js: `require = require('node:module').createRequire(${JSON.stringify(resolve('package.json'))});` } })
}
delete env.ELECTRON_RUN_AS_NODE
try {
const result = spawnSync(require('electron'), [__filename], {
env, stdio: 'inherit', timeout: 300_000
})
if (result.error) throw result.error
process.exitCode = result.status ?? 1
} finally {
rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 })
}
} else {
const { app, BrowserWindow } = require('electron')
const root = process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_ROOT
assert(root)
const profile = join(root, 'profile')
mkdirSync(profile)
if (process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_LIVE === '1' || process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CHAT === '1') {
const settings = process.env.GOODBUDDY_ACCEPTANCE_SETTINGS ?? join(app.getPath('appData'), 'goodbuddy', 'runtime-settings.json')
copyFileSync(join(dirname(settings), 'Local State'), join(profile, 'Local State'))
}
mkdirSync(join(root, 'Temporary Vault'))
app.setPath('userData', profile)
app.setAppPath(resolve('.'))
const timer = setTimeout(() => { console.error('Acceptance timed out'); app.exit(1) }, 280_000)
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
const chat = process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CHAT === '1'
? await require(join(root, 'runtime.cjs')).prepareChat(root, profile) : undefined
await import(pathToFileURL(resolve('out/main/index.js')).href)
let window
for (let attempt = 0; attempt < 120; attempt++) {
window = BrowserWindow.getAllWindows().find(item => item.webContents.getURL().includes('index.html'))
if (window && await window.webContents.executeJavaScript('Boolean(document.querySelector("button"))')) break
await wait(250)
}
assert(window, 'Production main must open its BrowserWindow')
assert.equal(app.getPath('userData'), profile)
const preferences = window.webContents.getLastWebPreferences()
assert.equal(preferences.contextIsolation, true)
assert.equal(preferences.nodeIntegration, false)
const js = code => window.webContents.executeJavaScript(code).catch(error => { throw new Error(`${code}: ${error.message}`) })
const click = async expression => {
window.focus()
for (let i=0;i<100;i++) { if (await js(`Boolean(${expression})`)) break; await wait(100) }
const point = await js(`(() => { const element = ${expression}; if (!element) throw new Error('Missing control'); element.scrollIntoView({block:'center'}); const r=element.getBoundingClientRect(); return {x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)} })()`)
window.webContents.sendInputEvent({ type: 'mouseDown', button: 'left', clickCount: 1, ...point })
window.webContents.sendInputEvent({ type: 'mouseUp', button: 'left', clickCount: 1, ...point })
await wait(500)
}
if (chat) {
await js(`(async()=>{const p=(await window.goodbuddy.projects.list())[0];await window.goodbuddy.projects.update(p.id,{name:p.name,description:p.description,rootPath:${JSON.stringify(join(root,'chat-workspace'))},defaultWorkMode:'execute',runtimeSelection:{provider:'opencode'}})})()`)
window.webContents.reload()
await wait(1000)
}
await click(`document.querySelector('button[aria-label="\u8bbe\u7f6e"]')`)
await click(`Array.from(document.querySelectorAll('button')).find(b=>b.innerText.startsWith('\u80fd\u529b\u4e0e\u5de5\u5177'))`)
await click(`Array.from(document.querySelectorAll('[role="tab"]')).find(b=>b.innerText==='MCP' && b.getClientRects().length)`)
const card = `Array.from(document.querySelectorAll('.capability-card')).find(c=>c.querySelector('strong')?.textContent==='Obsidian')`
const poll = async (expression, attempts=100) => {
for (let i=0;i<attempts;i++) { if (await js(expression)) return; await wait(200) }
console.error('VISIBLE_TABS',await js(`JSON.stringify(Array.from(document.querySelectorAll('[role="tab"]')).map(e=>({text:e.innerText,selected:e.getAttribute('aria-selected')})))`))
throw new Error(`Timed out: ${expression}`)
}
await poll(`Boolean(${card})`)
assert.equal(await js(`(${card}).querySelector('[role="switch"]').checked`), false)
await click(`(${card}).querySelector('select')`)
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'End' })
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Return' })
window.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Return' })
await poll(`Boolean((${card}).querySelector('input:not([type="checkbox"])'))`)
const input = `(${card}).querySelector('input:not([type="checkbox"])')`
const vault = join(root, 'Temporary Vault')
await click(input)
await window.webContents.insertText(vault)
await click(`(${card}).querySelectorAll('.capability-card__actions button')[1]`)
await poll(`Boolean((${card}).querySelector('[role="status"]'))`)
assert.match(await js(`(${card}).querySelector('[role="status"]').textContent`), /18/)
assert.equal((await js('window.goodbuddy.capabilities.getSnapshot()')).obsidian.vaultPath, '')
assert.equal(await js(`(${card}).querySelector('[role="switch"]').checked`), false)
await click(`(${card}).querySelectorAll('.capability-card__actions button')[2]`)
await poll(`!(${card}).querySelector('.primary-button').disabled`)
assert.equal((await js('window.goodbuddy.capabilities.getSnapshot()')).obsidian.vaultPath, vault)
await click(input)
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A', modifiers: ['control'] })
window.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'A', modifiers: ['control'] })
await window.webContents.insertText(join(root, 'missing-vault'))
await click(`(${card}).querySelectorAll('.capability-card__actions button')[1]`)
await poll(`Boolean((${card}).querySelector('[role="alert"]'))`)
assert.equal(await js(`(${input}).value`), join(root, 'missing-vault'))
assert.equal((await js('window.goodbuddy.capabilities.getSnapshot()')).obsidian.vaultPath, vault)
console.log('PASS: native UI draft test (18 tools), no implicit save/enable, save through IPC, missing-folder error preserves draft')
if (process.platform === 'win32') {
await click(`(${card}).querySelectorAll('.capability-card__actions button')[0]`)
const user32 = require('koffi').load('user32.dll')
const getWindow = user32.func('uintptr_t __stdcall GetWindow(uintptr_t hwnd, uint32_t command)')
const getPid = user32.func('uint32_t __stdcall GetWindowThreadProcessId(uintptr_t hwnd, _Out_ uint32_t *pid)')
const postMessage = user32.func('bool __stdcall PostMessageW(uintptr_t hwnd, uint32_t message, uintptr_t wparam, intptr_t lparam)')
const handle = Number(window.getNativeWindowHandle().readBigUInt64LE())
const popup = getWindow(handle, 6)
assert(popup && Number(popup) !== handle, 'Native folder dialog must open')
const pid = [0]
getPid(popup, pid)
assert.equal(pid[0], process.pid, 'Only close this acceptance process dialog')
assert(postMessage(popup, 0x10, 0, 0))
await poll(`!(${card}).querySelector('.primary-button').disabled`)
assert.equal(await js(`(${input}).value`), join(root, 'missing-vault'))
console.log('PASS: real Windows folder dialog opened and cancelled without changing draft')
}
window.setMinimumSize(640, 480)
window.setSize(760, 720)
await wait(500)
const geometry = await js(`(() => {const c=${card};const r=c.getBoundingClientRect();return {width:innerWidth,left:r.left,right:r.right,scroll:c.scrollWidth,client:c.clientWidth}})()`)
assert(geometry.scroll <= geometry.client + 1 && geometry.right <= geometry.width && geometry.left >= 0, JSON.stringify(geometry))
console.log('PASS: narrow card has no horizontal overflow', JSON.stringify(geometry))
await click(input)
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A', modifiers: ['control'] })
window.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'A', modifiers: ['control'] })
await window.webContents.insertText(vault)
await click(`(${card}).querySelectorAll('.capability-card__actions button')[1]`)
await poll(`Boolean((${card}).querySelector('[role="status"]'))`)
await js(`(${card}).querySelector('[role="switch"]').focus()`)
window.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Space' })
window.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Space' })
await poll(`(${card}).querySelector('[role="switch"]').checked && !(${card}).querySelector('[role="switch"]').disabled`)
await click(`(${card}).querySelector('button[aria-expanded]')`)
await poll(`(${card}).querySelectorAll('.mcp-server-tools li').length === 19`)
console.log('PASS: keyboard switch enables Obsidian; expanded production UI lists 19 tools')
for (const theme of ['light', 'dark']) {
await js(`document.documentElement.dataset.theme=${JSON.stringify(theme)}`)
const bounds = await js(`(()=>{const c=${card};const r=c.getBoundingClientRect();return {right:r.right,width:innerWidth,scroll:c.scrollWidth,client:c.clientWidth,background:getComputedStyle(c).backgroundColor}})()`)
assert(bounds.right <= bounds.width && bounds.scroll <= bounds.client + 1)
console.log(`PASS: ${theme} narrow Obsidian bounds`, JSON.stringify(bounds))
}
if (process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_LIVE === '1') {
await require(join(root, 'runtime.cjs')).run(root, await js('window.goodbuddy.capabilities.getSnapshot()'))
}
if (chat) {
await click(`document.querySelector('.settings-panel__header button')`)
window.setSize(1280, 850)
try {
const providers = process.env.GOODBUDDY_OBSIDIAN_ACCEPTANCE_CONTINUE_ONLY === '1' ? ['Continue'] : ['OpenCode', 'Continue']
for (const provider of providers) {
if (provider === 'Continue') {
await poll(`!document.querySelector('button[title^="Runtime \u548c\u6a21\u578b"]').disabled`)
await js(`document.querySelector('button[title^="Runtime \u548c\u6a21\u578b"]').click()`)
await click(`Array.from(document.querySelectorAll('.runtime-picker__menu strong')).find(b=>b.innerText==='Continue Runtime')?.nextElementSibling`)
}
await poll(`!document.querySelector('button[title^="Runtime \u548c\u6a21\u578b"]').disabled`)
const mode = `document.querySelector('button[aria-label^="\u5de5\u4f5c\u6a21\u5f0f"]')`
if (!(await js(`(${mode}).innerText.includes('Execute')`))) {
await click(mode)
await click(`Array.from(document.querySelectorAll('[role="menuitemradio"]')).find(b=>b.innerText.startsWith('Execute'))`)
}
const marker = `CHAT_${provider}_${require('node:crypto').randomUUID()}`
const before = chat.observations.length
await click(`document.querySelector('textarea')`)
await window.webContents.insertText(`Use only Obsidian tools. Write ${provider}-chat.md containing ${marker} using obsidian_write_note, then read it using obsidian_read_note. Reply with the content read. Do not use other tools.`)
await click(`document.querySelector('button[aria-label="\u53d1\u9001"]')`)
await poll(`Array.from(document.querySelectorAll('.message--assistant')).some(e=>e.innerText.includes(${JSON.stringify(marker)}))`,450)
await poll(`!document.querySelector('button[title^="Runtime \u548c\u6a21\u578b"]').disabled`,450)
assert((require('node:fs').readFileSync(join(vault,`${provider}-chat.md`),'utf8')).includes(marker))
console.log(JSON.stringify({ provider, fullChatUi: 'passed', realModelHttpRequests: chat.observations.length-before }))
}
} finally {
console.log(JSON.stringify({ chatModelHttpRequests: chat.observations.length }))
await chat.close()
}
}
await wait(1000)
clearTimeout(timer)
app.exit(0)
})().catch(error => { console.error(error); clearTimeout(timer); app.exit(1) })
}
+125
View File
@@ -0,0 +1,125 @@
const assert = require('node:assert/strict')
const { spawn } = require('node:child_process')
const { mkdtemp, mkdir, readFile, readdir, writeFile, rm, stat } = require('node:fs/promises')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
async function main() {
const executable = resolve(process.argv[2])
await stat(executable)
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-obsidian-packaged-'))
const profile = join(root, 'profile')
const vault = join(root, 'Packaged Vault')
await mkdir(profile)
await mkdir(vault)
const audit = join(root, 'node-audit.jsonl')
const guard = join(root, 'offline.cjs')
await writeFile(guard, `
const fs = require('node:fs');
const net = require('node:net');
const original = net.Socket.prototype.connect;
net.Socket.prototype.connect = function(...args) {
const options = net._normalizeArgs(args)[0];
const host = options.host || 'localhost';
if (!options.path && !['localhost','127.0.0.1','::1'].includes(host)) throw new Error('Offline acceptance blocked network');
return original.apply(this,args);
};
globalThis.fetch = async () => { throw new Error('Offline acceptance blocked fetch'); };
require('node:module').syncBuiltinESMExports();
fs.appendFileSync(${JSON.stringify(audit)}, JSON.stringify({entry:process.argv[1],exe:process.execPath,electron:process.versions.electron})+'\\n');
`)
const env = { ...process.env, NODE_OPTIONS: `--require ${JSON.stringify(guard)}`,
PATH: join(process.env.SystemRoot, 'System32') }
delete env.Path
delete env.ELECTRON_RUN_AS_NODE
const child = spawn(executable, [`--user-data-dir=${profile}`, '--remote-debugging-port=0', '--inspect=0',
'--proxy-server=http://127.0.0.1:9', '--proxy-bypass-list=<-loopback>'], { env, stdio: ['ignore', 'ignore', 'pipe'] })
let diagnostic = ''
child.stderr.on('data', chunk => { diagnostic += chunk.toString() })
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
let socket
let mainSocket
try {
let port
for (let i=0;i<100;i++) {
try { port = (await readFile(join(profile, 'DevToolsActivePort'), 'utf8')).split('\n')[0]; break } catch { await wait(200) }
}
assert(port, 'Packaged app must expose debugging only in the isolated profile')
const mainEndpoint = diagnostic.match(/ws:\/\/127\.0\.0\.1:\d+\/[-a-f0-9]+/u)?.[0]
assert(mainEndpoint, 'The isolated Main inspector must be available')
mainSocket = new WebSocket(mainEndpoint)
await new Promise((resolve, reject) => { mainSocket.onopen=resolve;mainSocket.onerror=reject })
// Packaged Electron strips NODE_OPTIONS at startup. Restore it only for this
// disposable Main so its real MCPVault child receives the offline guard.
await new Promise((resolve,reject) => {
const timer=setTimeout(()=>reject(new Error('Main inspector timed out')),10_000)
mainSocket.onmessage=message=>{const value=JSON.parse(message.data);if(value.id===1){clearTimeout(timer);if(value.error || value.result?.exceptionDetails)reject(new Error('Unable to set child network guard'));else resolve()}}
mainSocket.send(JSON.stringify({id:1,method:'Runtime.evaluate',params:{expression:`process.env.NODE_OPTIONS=${JSON.stringify(env.NODE_OPTIONS)}`}}))
})
mainSocket.close()
let target
for (let i=0;i<100;i++) {
target = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()).find(item => item.type === 'page' && item.url.includes('index.html'))
if (target) break
await wait(200)
}
assert(target, 'Packaged production renderer must load')
socket = new WebSocket(target.webSocketDebuggerUrl)
await new Promise((resolve, reject) => { socket.onopen = resolve; socket.onerror = reject })
let id = 0
const pending = new Map()
socket.onmessage = message => { const value = JSON.parse(message.data); if (pending.has(value.id)) { const {resolve,reject}=pending.get(value.id); pending.delete(value.id); if (value.error) reject(new Error(value.error.message)); else resolve(value.result) } }
const send = (method, params={}) => new Promise((resolve,reject) => {
const key=++id
const timer=setTimeout(()=>{pending.delete(key);reject(new Error(`CDP timed out: ${method}`))},10_000)
pending.set(key,{
resolve: value=>{clearTimeout(timer);resolve(value)},
reject: error=>{clearTimeout(timer);reject(error)}
})
socket.send(JSON.stringify({id:key,method,params}))
})
const js = async expression => { const result=await send('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});assert(!result.exceptionDetails,JSON.stringify(result.exceptionDetails));return result.result.value }
const poll = async expression => { for(let i=0;i<100;i++){ if(await js(expression))return;await wait(200) } throw new Error(`Timeout: ${expression}`) }
const click = async expression => {
await js(`(()=>{const e=${expression};if(!e)throw new Error('Missing control');e.scrollIntoView({block:'center'});e.focus();e.click()})()`)
await wait(500)
}
await poll('Boolean(window.goodbuddy && document.querySelector("button"))')
await stat(join(profile,'assistant.sqlite'))
assert.equal(await js('typeof require'), 'undefined')
await click(`document.querySelector('button[aria-label="\u8bbe\u7f6e"]')`)
await poll(`Array.from(document.querySelectorAll('button')).some(b=>b.innerText.startsWith('\u80fd\u529b\u4e0e\u5de5\u5177'))`)
await click(`Array.from(document.querySelectorAll('button')).find(b=>b.innerText.startsWith('\u80fd\u529b\u4e0e\u5de5\u5177'))`)
await click(`Array.from(document.querySelectorAll('[role="tab"]')).find(b=>b.innerText==='MCP' && b.getClientRects().length)`)
const card=`Array.from(document.querySelectorAll('.capability-card')).find(c=>c.querySelector('strong')?.textContent==='Obsidian')`
await poll(`Boolean(${card})`)
await js(`{const e=(${card}).querySelector('select');e.value='folder';e.dispatchEvent(new Event('change',{bubbles:true}));}`)
await poll(`Boolean((${card}).querySelector('input:not([type="checkbox"])'))`)
await click(`(${card}).querySelector('input:not([type="checkbox"])')`)
await send('Input.insertText',{text:vault})
await click(`(${card}).querySelectorAll('.capability-card__actions button')[1]`)
await poll(`Boolean((${card}).querySelector('[role="status"]'))`)
assert.match(await js(`(${card}).querySelector('[role="status"]').textContent`),/18/)
await click(`(${card}).querySelectorAll('.capability-card__actions button')[2]`)
await poll(`!(${card}).querySelector('.primary-button').disabled`)
assert.equal((await js('window.goodbuddy.capabilities.getSnapshot()')).obsidian.vaultPath,vault)
const bin=join(profile,'tool-environment','bin')
const shims=(await readdir(bin,{recursive:true})).filter(name=>name.endsWith('node.cmd'))
assert(shims.length>0)
assert((await readFile(join(bin,shims[0]),'utf8')).includes(executable))
console.log('PASS: packaged app.asar UI -> real IPC -> packaged MCPVault (18 tools), save, managed packaged Electron shim, PATH=System32, Chromium dead proxy')
const records=(await readFile(audit,'utf8')).trim().split('\n').map(line=>JSON.parse(line))
assert(records.some(item=>item.entry?.includes('app.asar.unpacked') && item.entry.endsWith('server.js') && item.exe===executable && item.electron))
console.log('PASS: packaged MCPVault child loaded Node network guards; audited packaged executable and unpacked server entry')
console.log('Boundary: MCPVault Node API guards + Chromium proxy; no OS firewall, physical disconnection, or installer execution')
} finally {
socket?.close()
mainSocket?.close()
if (child.exitCode === null) {
const killer=spawn('taskkill',['/PID',String(child.pid),'/T','/F'],{stdio:'ignore'})
await new Promise(resolve=>killer.once('exit',resolve))
}
await rm(root,{recursive:true,force:true,maxRetries:10,retryDelay:200})
}
}
main().catch(error=>{console.error(error);process.exitCode=1})
@@ -148,7 +148,7 @@ async function main() {
[
'appId: live.digiman.goodbuddy.harness-smoke',
'productName: GoodBuddyHarnessSmoke',
'electronVersion: "43.2.0"',
`electronVersion: "${require('electron/package.json').version}"`,
'asar: true',
'npmRebuild: false',
'files:',
+381
View File
@@ -0,0 +1,381 @@
// Cold-start benchmark supervisor (PERF-01).
// Launches the current production build (out/, from `npm run build:bundle`)
// GB_PERF_STARTUP_RUNS times (default 5) per variant through
// tests/support/startup-perf-driver.mjs, each launch in a fresh temporary
// user-data directory, and reports median / p95 per startup phase.
//
// Variants (interleaved each round, per docs P7):
// first-run empty user-data dir (creates databases, shows release notes)
// empty copy of a prepared profile: release notes seen, startup update
// check off, 0 conversations
// seeded-300 same as empty plus GB_PERF_STARTUP_SEED (default 300)
// synthetic conversations, reusing the PERF-11 seed shape
//
// Isolation: temporary profile/session/logs, a loopback fake model on an
// ephemeral port (GB_PERF_MODEL_PORT to pin one; 11434 is left alone), all
// non-loopback requests blocked in the driver. Output has phase names, times
// and counts only.
//
// Env: GB_PERF_STARTUP_RUNS, GB_PERF_STARTUP_SEED, GB_PERF_STARTUP_VARIANTS
// (comma list), GB_PERF_STARTUP_SKIP_BUILD=1, GB_PERF_STARTUP_PROFILE=1 (one
// extra CPU-profiled launch per variant, excluded from the table),
// GB_PERF_OUTPUT (report directory), GB_PERF_KEEP_PROFILE=1,
// GB_PERF_STARTUP_APP_ROOT (checkout/worktree to build and launch; default .).
// Variant "real" (opt-in) launches on a fresh copy of GB_PERF_STARTUP_REAL_DATA.
const { spawn, spawnSync, execFileSync } = require('node:child_process')
const { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } = require('node:fs')
const { createServer } = require('node:http')
const { cpus, tmpdir, totalmem } = require('node:os')
const { join, resolve } = require('node:path')
const { npmInvocation } = require('./npm-invocation.cjs')
const root = resolve(__dirname, '..')
// The app under test. Defaults to this checkout; point it at a temporary
// worktree to measure a baseline or a clean HEAD while this tree is mid-edit.
const appRoot = resolve(process.env.GB_PERF_STARTUP_APP_ROOT || root)
const runs = Math.max(1, Number(process.env.GB_PERF_STARTUP_RUNS || 5))
const seedCount = Number(process.env.GB_PERF_STARTUP_SEED || 300)
const seededName = `seeded-${seedCount}`
const variants = (process.env.GB_PERF_STARTUP_VARIANTS || `first-run,empty,${seededName}`).split(',').map(v => v.trim()).filter(Boolean)
const requestedPort = Number(process.env.GB_PERF_MODEL_PORT || 0)
const launchTimeoutMs = Number(process.env.GB_PERF_STARTUP_LAUNCH_TIMEOUT_MS || 150_000)
const keepProfile = process.env.GB_PERF_KEEP_PROFILE === '1'
const cpuProfile = process.env.GB_PERF_STARTUP_PROFILE === '1'
// ------------------------------------------------------------------ build
function newestMtime(path) {
let newest = 0
const stack = [path]
while (stack.length) {
const current = stack.pop()
let info
try { info = statSync(current) } catch { continue }
if (info.isDirectory()) {
for (const entry of readdirSync(current)) if (entry !== 'node_modules') stack.push(join(current, entry))
} else if (!/\.test\.tsx?$/.test(current)) {
newest = Math.max(newest, info.mtimeMs)
}
}
return newest
}
function ensureBuild() {
const outputs = ['out/main/index.js', 'out/preload/index.cjs', 'out/renderer/index.html'].map(file => join(appRoot, file))
const missing = outputs.some(file => !existsSync(file))
const builtAt = missing ? 0 : Math.min(...outputs.map(file => statSync(file).mtimeMs))
const sourceAt = Math.max(...['src/main', 'src/preload', 'src/renderer', 'src/shared', 'electron.vite.config.ts', 'package.json'].map(p => newestMtime(join(appRoot, p))))
const stale = missing || sourceAt > builtAt
if (!stale) return { rebuilt: false }
if (process.env.GB_PERF_STARTUP_SKIP_BUILD === '1') {
if (missing) throw new Error('Missing production build and GB_PERF_STARTUP_SKIP_BUILD=1')
console.log('[startup] build is older than src/, measuring it anyway (GB_PERF_STARTUP_SKIP_BUILD=1)')
return { rebuilt: false, stale: true }
}
console.log(`[startup] ${missing ? 'no production build' : 'production build is older than src/'}; running npm run build:bundle`)
const npm = npmInvocation(appRoot)
const result = spawnSync(npm.command, [...npm.prefixArgs, 'run', 'build:bundle'], { cwd: appRoot, stdio: 'inherit', shell: npm.prefixArgs.length === 0 && process.platform === 'win32' })
if (result.status !== 0) throw new Error(`npm run build:bundle failed (${result.status})`)
return { rebuilt: true }
}
// -------------------------------------------------------------- fake model
function startModelServer(ledger) {
const server = createServer((request, response) => {
ledger.push({ method: request.method, path: request.url })
if (request.method === 'GET' && request.url?.endsWith('/models')) {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({ object: 'list', data: [{ id: 'qwen3', object: 'model' }] }))
return
}
if (request.method === 'POST' && request.url?.endsWith('/chat/completions')) {
request.resume()
request.on('end', () => {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({ id: 'perf', object: 'chat.completion', model: 'qwen3', choices: [{ index: 0, finish_reason: 'stop', message: { role: 'assistant', content: 'ok' } }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }))
})
return
}
response.writeHead(404).end()
})
return new Promise((resolvePromise, reject) => {
server.once('error', reject)
server.listen(requestedPort, '127.0.0.1', () => resolvePromise(server))
})
}
// ------------------------------------------------------------------ launch
function baseEnvironment(modelPort, workspace) {
const environment = { ...process.env, GB_PERF_ROOT: appRoot, GB_PERF_STARTUP_MARKS: '1' }
delete environment.ELECTRON_RUN_AS_NODE
delete environment.ELECTRON_RENDERER_URL
for (const key of ['GOODBUDDY_MODEL_API_KEY', 'GOODBUDDY_MODEL_BASE_URL', 'GOODBUDDY_MODEL_NAME', 'GOODBUDDY_BIGTOKEN_API_KEY', 'GOODBUDDY_BIGTOKEN_BASE_URL', 'GOODBUDDY_BIGTOKEN_MODEL']) delete environment[key]
environment.GOODBUDDY_MODEL_API_KEY = 'perf-loopback'
environment.GOODBUDDY_MODEL_BASE_URL = `http://127.0.0.1:${modelPort}/v1`
environment.GOODBUDDY_MODEL_NAME = 'qwen3'
environment.GOODBUDDY_WORKSPACE = workspace
return environment
}
async function launch({ directory, environment, mode = 'measure', seed = 0, expectConversations = 0, profilePath }) {
const resultPath = join(directory, 'startup-result.json')
const env = { ...environment, GB_STARTUP_DIR: directory, GB_STARTUP_RESULT: resultPath, GB_STARTUP_MODE: mode, GB_STARTUP_SEED: String(seed), GB_STARTUP_EXPECT_CONVERSATIONS: String(expectConversations) }
if (profilePath) env.GB_STARTUP_CPU_PROFILE = profilePath
const stderr = []
const spawnAt = Date.now()
const child = spawn(require('electron'), [join(root, 'tests', 'support', 'startup-perf-driver.mjs')], { cwd: root, env, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: false })
child.stderr.on('data', chunk => { if (stderr.length < 200) stderr.push(String(chunk)) })
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
if (process.platform === 'win32') spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
else child.kill('SIGKILL')
}, launchTimeoutMs)
const exit = await new Promise(resolvePromise => child.once('exit', code => resolvePromise(code)))
const exitAt = Date.now()
clearTimeout(timer)
const report = existsSync(resultPath) ? JSON.parse(readFileSync(resultPath, 'utf8')) : { status: 'failed', error: timedOut ? 'launch timeout' : 'driver exited without a report' }
if (exit !== 0 || timedOut) report.status = 'failed'
report.spawnAt = spawnAt
report.exitMs = exitAt - spawnAt
// stderr is kept only for failures, and only its first line (no user data exists in these profiles).
if (report.status !== 'passed') report.stderrHead = stderr.join('').split(/\r?\n/).filter(Boolean).slice(0, 8)
return report
}
// --------------------------------------------------------------- analysis
function phases(report) {
const t = report.times
const r = report.renderer
const s = report.spawnAt
const marks = Object.fromEntries((report.startupMarks ?? []).map(m => [m.name, m.at]))
const d = (from, to) => (from === undefined || from === null || to === undefined || to === null) ? null : to - from
const result = {
'spawn -> driver start (Electron boot)': d(s, t.driverStart),
'driver start -> Main import done': d(t.driverStart, t.mainImportEnd),
'spawn -> app ready': d(s, t.appReady),
'app ready -> BrowserWindow created': d(t.appReady, t.windowCreated),
'window created -> loadMainWindow': d(t.windowCreated, marks['main:load-main-window']),
'loadMainWindow -> ready-to-show': d(marks['main:load-main-window'], t.readyToShow),
'window created -> ready-to-show': d(t.windowCreated, t.readyToShow),
'ready-to-show -> first visible frame': d(t.readyToShow, r.firstVisibleFrame),
'ready-to-show -> composer interactive': d(t.readyToShow, r.composerInteractive),
'spawn -> ready-to-show': d(s, t.readyToShow),
'spawn -> first visible frame': d(s, r.firstVisibleFrame),
// Only reported by Chromium when the first paint happens while visible.
'spawn -> first contentful paint': d(s, r.firstContentfulPaint),
'spawn -> composer present': d(s, r.composerPresent),
'spawn -> composer interactive': d(s, r.composerInteractive),
'spawn -> sidebar populated (seeded)': d(s, r.sidebarPopulated),
'composer interactive -> sidebar populated': d(r.composerInteractive, r.sidebarPopulated)
}
// Main phases (GB_PERF_STARTUP_MARKS spans; parallel spans overlap).
const main = {
'main: module import (index.js eval)': d(t.mainImportStart, marks['main:module-evaluated']),
'main: whenReady -> window created': d(marks['main:when-ready'], marks['main:create-window:end']),
'main: createMainWindow()': d(marks['main:create-window:start'], marks['main:create-window:end']),
'main: tray': d(marks['main:create-window:end'], marks['main:tray-built'])
}
for (const m of report.startupMarks ?? []) {
if (!m.name.endsWith(':start')) continue
const base = m.name.slice(0, -':start'.length)
if (base === 'main:create-window') continue
main[`${base.replace(/^main:/, 'main: ')}`] = d(m.at, marks[`${base}:end`])
// Synchronous part of the span: from the call until the first await yields.
if (marks[`${base}:sync-end`] !== undefined) main[`${base.replace(/^main:/, 'main: ')} (sync part)`] = d(m.at, marks[`${base}:sync-end`])
}
main['main: whenReady -> loadMainWindow (total)'] = d(marks['main:when-ready'], marks['main:load-main-window'])
// Critical path in Main: consecutive top-level marks (spans that run in
// parallel inside the prerequisites are excluded), so each gap is the time
// Main spent between two points of the sequential startup function.
const nested = /^main:(knowledge-init|knowledge-gateway-start|runtime-hydrate|assistant-db-init)/
const sequence = (report.startupMarks ?? []).filter(m => !nested.test(m.name) && !m.name.endsWith(':sync-end')).sort((a, b) => a.at - b.at)
for (let index = 1; index < sequence.length; index += 1) {
main[`gap: ${sequence[index - 1].name} -> ${sequence[index].name}`] = sequence[index].at - sequence[index - 1].at
}
const renderer = {
'renderer: navigation start -> DOMContentLoaded': d(r.navigationStart, r.domContentLoaded),
'renderer: DOMContentLoaded -> composer present': d(r.domContentLoaded, r.composerPresent),
'renderer: navigation start -> composer present': d(r.navigationStart, r.composerPresent)
}
return { ...result, ...main, ...renderer }
}
function percentile(values, p) {
if (!values.length) return null
const sorted = [...values].sort((a, b) => a - b)
// Nearest rank; with 5 samples p95 is the maximum.
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))]
}
function summarize(reportsByVariant) {
const rows = new Map()
for (const [variant, reports] of Object.entries(reportsByVariant)) {
const passed = reports.filter(r => r.status === 'passed')
const perRun = passed.map(phases)
for (const name of new Set(perRun.flatMap(Object.keys))) {
const values = perRun.map(p => p[name]).filter(v => typeof v === 'number' && Number.isFinite(v))
const row = rows.get(name) ?? {}
row[variant] = values.length ? { median: percentile(values, 50), p95: percentile(values, 95), n: values.length } : null
rows.set(name, row)
}
}
return rows
}
function sourceInfo() {
try {
const commit = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: appRoot, encoding: 'utf8' }).trim()
const dirtyFiles = execFileSync('git', ['status', '--porcelain'], { cwd: appRoot, encoding: 'utf8' }).split('\n').filter(Boolean).length
return { commit, dirty: dirtyFiles > 0, dirtyFiles, appRoot: appRoot === root ? '.' : 'GB_PERF_STARTUP_APP_ROOT' }
} catch {
return { commit: null }
}
}
const fmt = v => (v === null || v === undefined ? '-' : Math.round(v).toString())
// ------------------------------------------------------------------- main
async function main() {
const build = ensureBuild()
const runDirectory = mkdtempSync(join(tmpdir(), 'goodbuddy-startup-perf-'))
const outputDirectory = resolve(process.env.GB_PERF_OUTPUT || join(runDirectory, 'artifacts'))
mkdirSync(outputDirectory, { recursive: true })
const workspace = join(runDirectory, 'workspace')
mkdirSync(workspace, { recursive: true })
const ledger = []
let server
try {
server = await startModelServer(ledger)
} catch (error) {
throw new Error(`Cannot listen on 127.0.0.1:${requestedPort} for the fake model (${error.code ?? error.message}); set GB_PERF_MODEL_PORT to a free port or leave it unset for an ephemeral port`, { cause: error })
}
const modelPort = server.address().port
const environment = baseEnvironment(modelPort, workspace)
const templates = {}
const prepareReports = {}
try {
// Templates are prepared once and copied per launch, so every measured
// launch starts from an identical, never-used-before directory.
for (const variant of variants) {
if (variant === 'first-run') continue
if (variant === 'real') {
// A copy of a real data directory (GB_PERF_STARTUP_REAL_DATA, for
// example the portable build's data folder). Only the stores startup
// reads are copied; the source is never opened by the app. The driver
// blocks every non-loopback request, so configured remote models are
// not called.
const source = process.env.GB_PERF_STARTUP_REAL_DATA
if (!source || !existsSync(join(source, 'assistant.sqlite'))) throw new Error('variant "real" needs GB_PERF_STARTUP_REAL_DATA with assistant.sqlite')
const directory = join(runDirectory, 'template-real')
const profile = join(directory, 'profile')
mkdirSync(profile, { recursive: true })
for (const entry of readdirSync(source)) {
const from = join(source, entry)
const keep = /\.(sqlite|sqlite-wal|sqlite-shm|json)$/i.test(entry) || entry === 'notes' || entry === 'Local Storage'
if (keep && entry !== 'DevToolsActivePort') cpSync(from, join(profile, entry), { recursive: true })
}
templates[variant] = directory
continue
}
const seed = variant === 'empty' ? 0 : variant === seededName ? seedCount : null
if (seed === null) throw new Error(`Unknown variant ${variant}`)
const directory = join(runDirectory, `template-${variant}`)
mkdirSync(directory, { recursive: true })
console.log(`[startup] preparing ${variant} profile${seed ? ` (${seed} conversations)` : ''}`)
const report = await launch({ directory, environment, mode: 'prepare', seed })
prepareReports[variant] = { status: report.status, error: report.error, seeded: report.seeded, exitMs: report.exitMs, stderrHead: report.stderrHead }
if (report.status !== 'passed') throw new Error(`Preparing ${variant} failed: ${report.error ?? 'unknown'}\n${(report.stderrHead ?? []).join('\n')}`)
rmSync(join(directory, 'startup-result.json'), { force: true })
templates[variant] = directory
}
const fresh = (variant, label) => {
const directory = join(runDirectory, `${label}-${variant}`)
if (templates[variant]) {
cpSync(templates[variant], directory, { recursive: true })
} else {
mkdirSync(directory, { recursive: true })
}
return directory
}
// The real profile's sidebar is ready once its first rows show.
const expectedFor = variant => (variant === seededName ? seedCount : variant === 'real' ? 10 : 0)
const results = Object.fromEntries(variants.map(v => [v, []]))
// A discarded warm-up launch per invocation so the first measured launch
// does not also pay for loading Electron binaries from a cold disk cache.
console.log('[startup] warm-up launch (discarded)')
await launch({ directory: fresh(variants.includes('empty') ? 'empty' : variants[0], 'warmup'), environment })
for (let round = 0; round < runs; round += 1) {
for (const variant of variants) {
const directory = fresh(variant, `run${round + 1}`)
const report = await launch({ directory, environment, expectConversations: expectedFor(variant) })
report.round = round + 1
results[variant].push(report)
const p = report.status === 'passed' ? phases(report) : {}
console.log(`[startup] round ${round + 1} ${variant}: ${report.status}, ready ${fmt(p['spawn -> app ready'])} ms, ready-to-show ${fmt(p['spawn -> ready-to-show'])} ms, first frame ${fmt(p['spawn -> first visible frame'])} ms, composer interactive ${fmt(p['spawn -> composer interactive'])} ms${report.error ? `, error ${report.error}` : ''}`)
if (!keepProfile) rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
}
const profiles = {}
if (cpuProfile) {
for (const variant of variants) {
const directory = fresh(variant, 'profiled')
const profilePath = join(outputDirectory, `main-startup-${variant}.cpuprofile`)
const report = await launch({ directory, environment, profilePath, expectConversations: expectedFor(variant) })
profiles[variant] = { status: report.status, cpuProfile: report.cpuProfile, phases: report.status === 'passed' ? phases(report) : null, file: profilePath }
console.log(`[startup] CPU-profiled ${variant}: ${report.status} -> ${profilePath}`)
}
}
const table = summarize(results)
const report = {
schemaVersion: 1,
kind: 'goodbuddy-startup-benchmark',
source: { ...sourceInfo(), build },
environment: { platform: process.platform, arch: process.arch, electron: require('electron/package.json').version, cpu: `${cpus()[0]?.model?.trim()} x${cpus().length}`, memoryGB: Math.round(totalmem() / 1073741824) },
config: { runs, variants, seedConversations: seedCount, percentile: 'nearest-rank' },
prepare: prepareReports,
phases: Object.fromEntries(table),
runs: Object.fromEntries(Object.entries(results).map(([variant, reports]) => [variant, reports.map(r => ({
round: r.round, status: r.status, error: r.error, stderrHead: r.stderrHead, exitMs: r.exitMs,
phases: r.status === 'passed' ? phases(r) : null,
renderer: r.renderer ? { domNodes: r.renderer.domNodes, conversationRows: r.renderer.conversationRows, scriptResources: r.renderer.scriptResources, scriptBytes: r.renderer.scriptBytes, jsHeapMB: r.renderer.jsHeapMB, startupDialog: r.renderer.startupDialog !== undefined, composerPresentBeforeProbe: r.renderer.alreadyAtInstall === true, longAnimationFrames: r.renderer.longAnimationFrames && { count: r.renderer.longAnimationFrames.count, totalMs: r.renderer.longAnimationFrames.totalMs, maxMs: r.renderer.longAnimationFrames.maxMs, scriptMs: r.renderer.longAnimationFrames.scriptMs } } : null,
mainEventLoopDelay: r.mainEventLoopDelay,
blockedRequests: r.blockedRequests && { fetch: r.blockedRequests.fetch.length, renderer: r.blockedRequests.renderer.length, hosts: [...new Set([...r.blockedRequests.fetch, ...r.blockedRequests.renderer])] }
}))])),
cpuProfiles: profiles,
modelRequests: { boundary: 'loopback fake model only; non-loopback requests blocked', total: ledger.length, paths: [...new Set(ledger.map(e => `${e.method} ${e.path}`))] }
}
const resultPath = join(outputDirectory, 'startup-result.json')
writeFileSync(resultPath, JSON.stringify(report, null, 2))
console.log(`\nGoodBuddy cold-start benchmark: ${runs} runs per variant, ms (median / p95, nearest rank)`)
console.log(`source ${report.source.commit ?? '?'}${report.source.dirty ? ` (dirty, ${report.source.dirtyFiles} files)` : ''}, Electron ${report.environment.electron}, ${report.environment.cpu}, ${report.environment.memoryGB} GB`)
console.table(Object.fromEntries([...table].map(([name, row]) => [name, Object.fromEntries(variants.map(v => [v, row[v] ? `${fmt(row[v].median)} / ${fmt(row[v].p95)}${row[v].n < runs ? ` (n=${row[v].n})` : ''}` : '-']))])))
const failed = Object.values(results).flat().filter(r => r.status !== 'passed').length
if (failed) console.log(`${failed} launch(es) failed; see report`)
const blocked = Object.values(results).flat().flatMap(r => [...(r.blockedRequests?.fetch ?? []), ...(r.blockedRequests?.renderer ?? [])])
console.log(`blocked non-loopback requests: ${blocked.length}${blocked.length ? ` (${[...new Set(blocked)].join(', ')})` : ''}; fake model requests: ${ledger.length}`)
console.log(`\nreport: ${resultPath}`)
process.exitCode = failed ? 1 : 0
} finally {
await new Promise(resolvePromise => server.close(() => resolvePromise()))
if (!keepProfile) {
for (const entry of readdirSync(runDirectory)) {
if (entry !== 'artifacts') rmSync(join(runDirectory, entry), { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
}
}
}
main().catch(error => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
+235
View File
@@ -0,0 +1,235 @@
// Main-process database WRITE benchmark on a real data directory (PERF-15).
// Copies assistant.sqlite (+ -wal/-shm) from GB_WPERF_DATA (for example the
// portable build's data folder) into a temporary directory - the source is
// never opened - then measures, with the production AssistantDatabase, the
// writes Main performs synchronously while a run streams:
// task-events:subagent replay of the largest real subagent progress stream
// (restored events, re-compacted on write, as live)
// task-events:tool replay of real tool events
// task-events:text 250 ms text batches (persisted buffer)
// conversations:save the renderer's 500 ms local save (header + the
// streaming assistant message) for a real conversation
// activity:update one incremental activity change
// For each: per-call time and the longest Main event-loop block.
// Event replays are paced at GB_WPERF_RATE events/s (default 270, the busiest
// second in real data; 0 = back to back).
// Env: GB_WPERF_DATA (required), GB_WPERF_EVENTS (default 2000),
// GB_WPERF_OUTPUT (JSON), GB_WPERF_KEEP=1 (keep the copy).
const { buildSync } = require('esbuild')
const { mkdtempSync, rmSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
const { randomUUID } = require('node:crypto')
const { performance } = require('node:perf_hooks')
const root = resolve(__dirname, '..')
if (process.env.GB_WPERF_NODE !== '1') {
let electron
let version
try { electron = require('electron'); version = require('electron/package.json').version } catch { electron = undefined }
if (typeof electron === 'string' && process.versions.electron !== version) {
const { status } = require('node:child_process').spawnSync(electron, [__filename], {
stdio: 'inherit', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
process.exit(status ?? 1)
}
}
const source = process.env.GB_WPERF_DATA
if (!source || !existsSync(join(source, 'assistant.sqlite'))) {
console.error('[wperf] set GB_WPERF_DATA to a GoodBuddy data directory containing assistant.sqlite')
process.exit(2)
}
const eventLimit = Number(process.env.GB_WPERF_EVENTS || 2000)
const directory = mkdtempSync(join(tmpdir(), 'goodbuddy-write-perf-'))
const bundle = join(directory, 'assistant-database.cjs')
const progressBundle = join(directory, 'subagent-progress.cjs')
for (const [entry, outfile] of [
['src/main/assistant/assistant-database.ts', bundle],
['src/main/assistant/subagent-progress-storage.ts', progressBundle],
['src/main/readonly-query-worker.ts', join(directory, 'readonly-query-worker.cjs')]
]) {
buildSync({
entryPoints: [join(root, entry)], bundle: true, platform: 'node', format: 'cjs',
target: 'node22', outfile, logLevel: 'error', external: ['electron']
})
}
const { AssistantDatabase } = require(bundle)
const { restoreSubagentPayload } = require(progressBundle)
const round = value => Math.round(value * 10) / 10
const percentile = (values, p) => {
const sorted = [...values].sort((a, b) => a - b)
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))]
}
/** Per-call times plus the longest gap between event-loop turns. */
async function measure(count, body, { yieldEvery = 1, rate = 0 } = {}) {
let probing = true
let last = performance.now()
let maxGap = 0
const tick = () => {
const now = performance.now()
maxGap = Math.max(maxGap, now - last)
last = now
if (probing) setImmediate(tick)
}
setImmediate(tick)
const samples = []
const slow = []
const started = performance.now()
for (let index = 0; index < count; index += 1) {
const at = performance.now()
body(index)
const elapsed = performance.now() - at
samples.push(elapsed)
if (elapsed > 50) slow.push([index, round(elapsed)])
if (rate > 0) {
// Real streams arrive at a rate; wait until this call's scheduled time.
const due = started + ((index + 1) * 1000) / rate
const wait = due - performance.now()
await new Promise(resolve => (wait > 1 ? setTimeout(resolve, wait) : setImmediate(resolve)))
} else if ((index + 1) % yieldEvery === 0) await new Promise(resolve => setImmediate(resolve))
}
const total = performance.now() - started
await new Promise(resolve => setTimeout(resolve, 20))
probing = false
return {
calls: count, totalMs: round(total), callP50: round(percentile(samples, 50)),
callP95: round(percentile(samples, 95)), callP99: round(percentile(samples, 99)),
callMax: round(Math.max(...samples)), loopMax: round(maxGap), slowCalls: slow.slice(0, 10)
}
}
async function main() {
console.log(`[wperf] runtime: node ${process.version}${process.versions.electron ? ` (electron ${process.versions.electron})` : ''}`)
const copyStarted = performance.now()
for (const name of ['assistant.sqlite', 'assistant.sqlite-wal', 'assistant.sqlite-shm']) {
if (existsSync(join(source, name))) copyFileSync(join(source, name), join(directory, name))
}
// Initialization checks note files against the database.
if (existsSync(join(source, 'notes'))) cpSync(join(source, 'notes'), join(directory, 'notes'), { recursive: true })
console.log(`[wperf] copied data in ${round((performance.now() - copyStarted) / 1000)} s`)
const workspace = join(directory, 'workspace')
mkdirSync(workspace)
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'))
const openStarted = performance.now()
database.initialize(workspace)
const raw = database.database
const results = { openMs: round(performance.now() - openStarted) }
// Experiment switch: override the durability level after initialization.
if (process.env.GB_WPERF_SYNCHRONOUS) raw.exec(`PRAGMA synchronous = ${process.env.GB_WPERF_SYNCHRONOUS}`)
results.synchronous = raw.prepare('PRAGMA synchronous').get().synchronous
if (process.env.GB_WPERF_AUTOCHECKPOINT) raw.exec(`PRAGMA wal_autocheckpoint = ${process.env.GB_WPERF_AUTOCHECKPOINT}`)
// As in production: checkpoints run on a worker thread (GB_WPERF_CHECKPOINT_WORKER=0 disables).
if (process.env.GB_WPERF_CHECKPOINT_WORKER !== '0' && typeof database.enableWalCheckpointWorker === 'function') {
database.enableWalCheckpointWorker(join(directory, 'readonly-query-worker.cjs'))
await new Promise(resolve => setTimeout(resolve, 500))
}
results.walAutocheckpoint = raw.prepare('PRAGMA wal_autocheckpoint').get().wal_autocheckpoint
const count = (sql, ...args) => Number(raw.prepare(sql).get(...args).n)
results.taskEvents = count('SELECT COUNT(*) AS n FROM task_events')
results.messages = count('SELECT COUNT(*) AS n FROM messages')
results.activityRecords = count('SELECT COUNT(*) AS n FROM activity_history_records')
// The real task with the most subagent events, restored in order.
const sourceTask = raw.prepare(
`SELECT task_id FROM task_events WHERE kind = 'subagent' GROUP BY task_id ORDER BY COUNT(*) DESC LIMIT 1`
).get()?.task_id
const blocks = new Map()
const subagentEvents = sourceTask ? raw.prepare(
`SELECT payload_json FROM task_events WHERE task_id = ? AND kind = 'subagent' ORDER BY id LIMIT ?`
).all(sourceTask, eventLimit).map(row => restoreSubagentPayload(JSON.parse(row.payload_json), blocks)) : []
const toolEvents = raw.prepare(
`SELECT payload_json FROM task_events WHERE kind = 'tool' ORDER BY id DESC LIMIT ?`
).all(eventLimit).map(row => JSON.parse(row.payload_json))
const conversation = raw.prepare(
`SELECT conversation_id AS id FROM messages GROUP BY conversation_id ORDER BY COUNT(*) DESC LIMIT 1`
).get()?.id
const taskId = randomUUID()
database.createTask({ id: taskId, title: 'write perf', instructions: 'replay', workMode: 'ask', conversationId: conversation })
const replay = (events, kind) => index => {
const event = { ...events[index % events.length], requestId: taskId }
database.appendTaskEvent(taskId, kind, event)
}
if (process.env.GB_WPERF_INSPECT) {
const at = Number(process.env.GB_WPERF_INSPECT)
const { compactSubagentPayload } = require(progressBundle)
const event = subagentEvents[at]
const t0 = performance.now()
const json = JSON.stringify(event)
const t1 = performance.now()
const restoredBlocks = new Map()
for (let i = 0; i < at; i += 1) restoreSubagentPayload(JSON.parse(JSON.stringify(compactSubagentPayload(subagentEvents[i], restoredBlocks))), restoredBlocks)
const t2 = performance.now()
const compact = JSON.stringify(compactSubagentPayload(event, restoredBlocks))
const t3 = performance.now()
console.log(`[wperf] event ${at}: full ${json.length} B (stringify ${round(t1 - t0)} ms), compact ${compact.length} B (compact ${round(t3 - t2)} ms)`)
console.log(`[wperf] event ${at} progress blocks: ${(event.progress ?? []).length}, updates: ${(event.progressUpdates ?? []).length}`)
}
// Paced at the busiest second seen in real data (269 subagent events/s); 0 = back to back.
const rate = Number(process.env.GB_WPERF_RATE ?? 270)
if (subagentEvents.length) {
results.subagentEvents = await measure(subagentEvents.length, replay(subagentEvents, 'subagent'), { rate })
}
if (toolEvents.length) results.toolEvents = await measure(toolEvents.length, replay(toolEvents, 'tool'), { rate })
const delta = 'streamed text '.repeat(40)
results.textBatches = await measure(400, index => database.appendTaskEvent(taskId, 'text',
{ type: 'text', requestId: taskId, delta: `${delta}${index}` }))
// Renderer local save: the conversation header and the streaming assistant message growing.
if (conversation) {
const snapshot = database.getConversation(conversation)
const header = {
id: snapshot.id, projectId: snapshot.projectId, title: snapshot.title,
runtimeSelection: snapshot.runtimeSelection, updatedAt: Date.now()
}
const messageId = randomUUID()
let content = ''
results.conversationSave = await measure(200, index => {
content += 'Lorem ipsum dolor sit amet, streaming reply chunk. '.repeat(8)
database.saveLocalConversations([{
header: { ...header, updatedAt: Date.now() + index },
messages: [{ id: messageId, role: 'assistant', state: 'streaming', content, createdAt: Date.now() }]
}])
})
results.conversationGet = await measure(20, () => database.getConversation(conversation))
}
const sample = database.getActivityHistoryPage({ limit: 50 }).records
if (sample.length) {
results.activityUpdate = await measure(200, index => database.updateActivityHistory({
changes: [{ type: 'upsert', position: 'in-place', record: { ...sample[index % sample.length], detail: `perf ${index}` } }]
}))
}
results.walBytes = existsSync(join(directory, 'assistant.sqlite-wal'))
? require('node:fs').statSync(join(directory, 'assistant.sqlite-wal')).size : 0
database.close()
console.log(`[wperf] ${results.taskEvents} task events, ${results.messages} messages, ${results.activityRecords} activity records; open ${results.openMs} ms`)
const rows = [
['task event: subagent (real stream)', results.subagentEvents],
['task event: tool (real)', results.toolEvents],
['task event: text batch', results.textBatches],
['conversation save (500 ms tick)', results.conversationSave],
['conversation get (largest)', results.conversationGet],
['activity update, 1 record', results.activityUpdate]
].filter(([, value]) => value)
console.table(rows.map(([name, r]) => ({
path: name, calls: r.calls, 'p50 ms': r.callP50, 'p95 ms': r.callP95, 'p99 ms': r.callP99,
'max ms': r.callMax, 'loop max ms': r.loopMax, 'calls/s': Math.round(r.calls / (r.totalMs / 1000))
})))
for (const [name, r] of rows) if (r.slowCalls.length) console.log(`[wperf] slow calls (index, ms) ${name}: ${JSON.stringify(r.slowCalls)}`)
console.log(`[wperf] synchronous=${results.synchronous} wal_autocheckpoint=${results.walAutocheckpoint} wal=${results.walBytes} B`)
const output = process.env.GB_WPERF_OUTPUT
if (output) writeFileSync(output, JSON.stringify({ kind: 'goodbuddy-main-write-benchmark', node: process.version, results }, null, 2))
}
main().catch(error => { process.exitCode = 1; console.error(error) }).finally(() => {
if (process.env.GB_WPERF_KEEP === '1') console.log(`[wperf] kept ${directory}`)
else rmSync(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
})
+2
View File
@@ -18,6 +18,7 @@ const {
prepareBundledOpenCodeConfig,
} = require("./opencode-config.cjs");
const { npmInvocation } = require("./npm-invocation.cjs");
const { prepareNativeDshResources } = require("./native-dsh-resources.cjs");
const architectureNames = {
1: "x64",
@@ -184,6 +185,7 @@ module.exports = async function prepareBundledRuntimes(context) {
architecture === "x64" ? `${architecture}-baseline` : architecture;
const packageName = `opencode-${packagePlatform}-${suffix}`;
const projectDir = context.packager.projectDir;
await prepareNativeDshResources(projectDir, platform, architecture, downloadPackage, lockedIntegrity);
await prepareBundledOpenCodeConfig(projectDir);
const projectPackage = JSON.parse(
await readFile(join(projectDir, "package.json"), "utf8"),
@@ -12,8 +12,8 @@
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
| 相关基线 | [统一界面设计系统](../../UI-DESIGN.md)、[文档解析与本地 OCR](../features/document-processing/prd.md)、[本地文本向量模型与连接设计](../features/knowledge-base/local-embedding-technical-design.md)、[全双工实时语音交互设计](../features/full-duplex-voice/technical-design.md) |
本文定义“设置 → 平台功能”的二级页签结构,以及所有 GoodBuddy 托管本地模型共同使用的
“模型下载源”设置。首期下载源为 ModelScope 和 Hugging Face,默认使用 ModelScope。
本文定义“设置 → 平台功能”的二级页签结构,以及位于“设置 → 模型连接”、所有 GoodBuddy
托管本地模型共同使用的“模型下载源”设置。首期下载源为 ModelScope 和 Hugging Face,默认使用 ModelScope。
模型下载源是用户明确选择的供应链路径。当前来源不可用或没有所选模型时,GoodBuddy
必须明确失败并提供设置入口,**不能静默切换到另一个下载源。**
@@ -25,7 +25,8 @@
1. “平台功能”保留现有一级设置分类,在分类内部增加共享 `PageTabs`:
- **通用设置**
- **远程项目**
2. “通用设置”首个功能为“模型下载源”。
2. “模型下载源”位于“模型连接”页头,与模型类型页签同一行右对齐,在保存操作下方;
显示当前来源和次按钮,点击后在弹窗中选择并确认保存。
3. 首期提供两个互斥选项:
- `modelscope`,用户文案为 **ModelScope**,默认值。
- `hugging-face`,用户文案为 **Hugging Face**。
@@ -47,8 +48,8 @@
同一模型包的部分文件从 ModelScope 下载、部分文件从 Hugging Face 下载。
8. 下载任务启动时冻结来源。任务运行期间切换全局设置,不改变、重启或迁移该任务;新的
下载使用新来源。
9. 当前来源失败时,不自动重试另一个来源。错误可以提供“前往通用设置更换下载源”,由
用户明确选择。
9. 当前来源失败时,不自动重试另一个来源。向量、语音和 OCR 的更换下载源入口直接打开
“模型连接”页的下载源弹窗,由用户明确选择并确认。
10. 魔法笔记设置由应用中心与笔记页共享,不在全局设置中重复提供入口。
---
@@ -59,10 +60,11 @@
“平台功能”现已使用两个二级页签:
- “通用设置”承载全局模型下载源。
- “通用设置”承载快捷唤起、会话与通知、工作目录设置。
- “远程项目”承载远程项目开关与 Agent 包管理。
页签复用共享 `PageTabs`,默认打开“通用设置”,不会把当前页签写入应用设置。
全局模型下载源由“模型连接”页头提供入口,布局和弹窗行为见第 7 节。
### 2.2 受管模型下载
@@ -99,7 +101,7 @@
- 切换来源时不影响已安装、已选择或正在使用的模型。
- 下载失败时知道实际使用了哪个来源。
- 所选来源没有模型时得到明确说明,并可以主动前往设置切换。
- 在“平台功能”的“通用设置”中选择模型下载源。
- 在“模型连接”页头打开模型下载源弹窗,选择后确认保存。
### 3.2 产品目标
@@ -163,9 +165,12 @@
```text
设置
├─ 模型连接
│ └─ 页头:模型类型页签同行右侧的当前来源与“模型下载源”按钮
│ └─ 模型下载源弹窗
└─ 平台功能
├─ 通用设置
│ └─ 模型下载源
│ └─ 快捷唤起、会话与通知、工作目录
└─ 远程项目
├─ 远程项目开关
└─ Agent 包管理
@@ -221,17 +226,20 @@
---
## 7. 通用设置界面
## 7. 模型下载源界面
### 7.1 布局
“模型连接”页头在保存操作下方,将模型类型页签与下载源控件放在同一行:页签居左,
“当前来源:ModelScope”和“模型下载源”次按钮整体右对齐。窄窗口保持同行,空间不足时
导航行可横向滚动。点击按钮打开弹窗;通用设置的工作目录卡片不再提供下载源控件。共享布局规则见
[UI 设计规范](../../UI-DESIGN.md#138-设置中心)。
```text
平台功能
管理通用平台行为与可选工作区能力
模型连接 [保存]
[文本] [向量] [重排] [语音] 当前来源:ModelScope [模型下载源]
[ 通用设置 ] [ 远程项目 ]
┌ 模型下载源 ────────────────────────────────────────────┐
┌ 模型下载源(弹窗)───────────────────────────────── [×]┐
│ 选择 GoodBuddy 托管本地模型后续下载使用的平台。 │
│ 已安装模型和 ZIP 导入不受影响。 │
│ │
@@ -240,19 +248,19 @@
│ │
│ ○ Hugging Face │
│ 适合可以稳定访问 Hugging Face 的网络环境 │
│ │
│ [取消] [确认] │
└────────────────────────────────────────────────────────┘
```
页面只保留一个视觉上最突出的任务。模型下载源保存属于即时设置,不增加与来源选项竞争的
大型“保存全部”按钮。
弹窗通过 Portal 挂载到 `document.body`,复用共享 Modal 样式、背景隔离和焦点管理。
模型连接页头的保存操作与下载源弹窗的“确认”各自保存对应设置。
### 7.2 控件
来源选项包含说明和未来可能的不可用原因,因此使用语义化 Radio Group,而不是原生
`select` 或 `SegmentedControl`:
- `fieldset` + `legend` 表达“模型下载源”。
- 弹窗内使用 `fieldset` + 持久的屏幕阅读器 `legend` 表达“模型下载源”。
- 每个选项使用 Radio 和整行可点击卡片。
- 当前项同时显示选中 Radio、强调边框和选中背景。
- 不能只用站点 Logo 或颜色表达选择。
@@ -281,12 +289,16 @@
### 7.4 保存交互
- 用户选择另一个 Radio 后立即调用应用设置更新。
- 保存期间禁用两个选项,保留最后确认值。
- 成功后更新当前选择,并通过应用通知提示:
- 打开弹窗时以已保存来源初始化草稿;选择 Radio 只修改草稿,不调用应用设置更新。
- 点击“确认”才保存;草稿与已保存来源相同时直接关闭,不重复写入。
- 取消、关闭按钮、Escape 或点击遮罩均丢弃草稿,已保存来源不变。
- 保存期间禁用选项、确认、取消和关闭,阻止重复提交及遮罩、Escape 关闭。
- 成功后更新页头当前来源并关闭弹窗,通过应用通知提示:
`模型下载源已切换为 Hugging Face。`
- 失败时恢复最后确认值,在 Radio Group 附近显示可重试错误。
- 保存失败时保留弹窗和草稿,已保存来源不变;就地显示错误,允许再次点击“确认”重试。
- 读取失败时显示错误和“重试”,禁用确认,不以默认值伪装为已保存来源。
- 同一个失败不再同时显示页面横幅和全局错误通知。
- 弹窗打开后聚焦关闭按钮,Tab 在弹窗内循环;关闭后焦点回到页头“模型下载源”按钮。
来源变化不触发:
@@ -627,7 +639,7 @@ resolveModelDownloadPackage(files, source)
### 12.1 来源状态
语音和 OCR 的当前模型详情各保留一个全局下载源标签。向量内置连接不重复显示平台功能中
语音和 OCR 的当前模型详情各保留一个全局下载源标签。向量内置连接不重复显示模型连接页头中
已经明确的全局来源,也不显示固定本地安装路径;下载仍使用已保存的全局来源。
所有模型管理界面都显示当前模型是否可下载、活动操作的进度与取消入口,以及已安装模型
@@ -645,10 +657,12 @@ resolveModelDownloadPackage(files, source)
```text
Hugging Face 暂不提供此模型的完整已验证文件。
[前往通用设置]
[模型下载源]
```
不能显示可点击“下载”后才告诉用户缺少某个文件。
向量、语音和 OCR 的“模型下载源”入口均导航到“模型连接”并直接打开同一弹窗。
跨分类导航沿用设置中心的未保存修改确认,用户取消离开时保留原表单。
### 12.2 已安装模型
@@ -693,7 +707,7 @@ ZIP 是来源无关的离线迁移方式:
- `正在从 {{source}} 下载`
- `打开 {{source}} 模型仓库`
- `当前下载源暂不提供此模型`
- `前往通用设置更换下载源`
- `模型下载源`(打开下载源弹窗)
不再写死:
@@ -739,7 +753,7 @@ ZIP 是来源无关的离线迁移方式:
失败状态提供:
1. 重试当前来源。
2. 前往“平台功能 → 通用设置”。
2. 打开“模型连接”的模型下载源弹窗,选择并确认保存。
3. 取消。
用户切换来源后需要重新点击下载。不能在设置保存后自动恢复上一失败任务。
@@ -806,7 +820,7 @@ ModelScope 和 Hugging Face 都可能跳转到文件存储或 CDN。每个来源
| 设置 | 位置 | 作用 |
| --- | --- | --- |
| 模型下载源 | 平台功能 → 通用设置 | GoodBuddy 托管本地模型 |
| 模型下载源 | 模型连接页头 → 模型下载源弹窗 | GoodBuddy 托管本地模型 |
| 检查更新源 | 关于与更新 | GoodBuddy 版本检查和下载页 |
切换任一设置不得修改另一个设置。用户选择 ModelScope 不表示应用更新使用镜像节点;选择
@@ -846,9 +860,9 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
| 场景 | 行为 |
| --- | --- |
| 读取设置失败 | 显示平台功能阻塞错误,不猜测或写入来源 |
| 保存来源失败 | 保留上一个确认值,显示就地错误 |
| 当前来源无完整模型包 | 下载按钮禁用,显示前往通用设置 |
| 读取设置失败 | 页头显示读取失败,弹窗提供就地重试并禁用确认,不猜测或写入来源 |
| 保存来源失败 | 已保存来源不变,保留弹窗和草稿,显示就地错误并允许确认重试 |
| 当前来源无完整模型包 | 下载按钮禁用,提供直接打开模型连接页下载源弹窗的入口 |
| 当前来源网络不可用 | 当前下载失败或允许同来源重试,不请求另一来源 |
| 当前来源返回 404 | 显示来源缺少固定文件,不请求另一来源 |
| 重定向 Host 不受信任 | 立即失败并删除本次暂存 |
@@ -876,15 +890,19 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
- 更高未知版本拒绝,不覆盖文件。
- 并发设置更新保持完整 JSON。
### 17.2 平台功能界面
### 17.2 设置界面
- 默认选中“通用设置”Tab。
- Tab 使用 `tablist`、`tab`、`tabpanel` 和 `aria-selected`。
- 方向键、Home、End 和焦点恢复正确。
- 切换平台功能页签不改变任何设置,全局设置不显示魔法笔记入口。
- 模型连接页头的当前来源与次按钮位于模型类型页签同一行右侧、保存操作下方;通用设置不再显示下载源控件。
- Radio Group 具有持久 Label。
- 选择来源时保存一次,保存期间防止重复提交。
- 保存失败恢复原值并保留可重试错误。
- 选择 Radio 只更新草稿,确认后保存一次;未改变来源时确认不写入。
- 取消、关闭、Escape 和遮罩关闭不改变已保存来源;保存期间禁止关闭及重复提交。
- 保存失败保留草稿并允许确认重试;读取失败提供重试且禁用确认。
- 弹窗使用 body Portal、焦点循环和关闭后焦点恢复。
- 向量、语音和 OCR 的更换下载源入口直接打开模型连接页弹窗,跨分类时保留离开保护。
- 窄窗口不隐藏页签或来源名称。
### 17.3 目录
@@ -935,7 +953,7 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
- 新增 `ModelDownloadSource` Schema 和默认值。
- 将应用设置版本升级并迁移旧版本。
- 在“平台功能”中增加 `PageTabs`。
- 增加“通用设置”与模型下载源 Radio Group。
- “通用设置”保留平台行为;模型下载源已移至模型连接页头,通过弹窗 Radio 草稿和确认保存。
- 魔法笔记卡已迁入应用共同设置,全局页签已移除,见第 8 节。
### 阶段 2:共享目录和来源解析(已完成)
@@ -967,7 +985,9 @@ Ollama 的模型下载由用户和 Ollama 管理。GoodBuddy 的模型下载源
- “平台功能”显示“通用设置”和“远程项目”两个可访问页签。
- 默认打开“通用设置”。
- “通用设置”提供 ModelScope 和 Hugging Face,默认 ModelScope。
- “模型连接”页头在模型类型页签同行右侧、保存操作下方显示当前来源及“模型下载源”次按钮。
- 下载源弹窗提供 ModelScope 和 Hugging Face,默认 ModelScope;Radio 只编辑草稿,确认才保存,取消不改变已保存来源。
- 读取与保存失败均可就地重试;向量、语音和 OCR 的更换下载源入口直接打开该弹窗。
- 来源选择说明影响范围,并明确已安装模型、ZIP、Ollama 和应用更新不受影响。
- 切换来源不会下载、删除、切换或重新验证任何已安装模型。
- 当前来源没有模型时,在点击下载前即可看到不可用原因。
+141
View File
@@ -0,0 +1,141 @@
# GoodBuddy 性能原则
## 文档信息
- **适用范围:** Desktop(Electron Main、Preload、Renderer)及其后台进程和线程。
- **用途:** 新功能设计和代码评审的约束。多个 agent 并行修改代码时,以本文为统一标准。
- **关联文档:** `docs/roadmap/product-performance-experience-improvement-plan.md`(测量记录与实施进度)。
- **参照:** VS Code 的进程分工和渲染做法。我们没有在同一台机器上实测过 VS Code,下面的预算按常见的流畅标准设定。
## 1. 性能预算
“流畅”用下面的数字判断,不靠感觉。“标准机”指不降速运行 `npm run perf:app`,“低端机”指加上 `GB_PERF_CPU_THROTTLE=4`。
| 指标 | 标准机 | 低端机 |
| --- | --- | --- |
| 按键到下一帧,p95 | ≤ 16 ms | ≤ 50 ms |
| 点击到界面响应(切换会话、打开面板),p95 | ≤ 100 ms | ≤ 300 ms |
| 稳定交互期间(打字、滚动、流式输出)超过 50 ms 的长任务 | 0 | 尽量为 0,总时长不超过场景时长的 5% |
| 流式输出、滚动时的帧间隔,p95 | ≤ 17 ms | ≤ 33 ms |
| Main 事件循环单次阻塞 | ≤ 16 ms | — |
| Main 事件循环延迟,p99 | ≤ 30 ms | — |
| DOM 节点数 | 不随会话数、消息数增长 | 同左 |
标准机的阈值由 `npm run perf:check` 检查。低端机的数值目前只做记录,等结果稳定后再加进阈值检查。
## 2. 原则
### P1 Main 只做调度
Main 负责窗口、生命周期、权限和消息转发。数据库查询、文档解析、哈希、向量计算、批量文件读写,都放到 worker 线程或 `utilityProcess` 里执行。
- 新代码不能在 Main 里使用 `DatabaseSync` 和同步文件 API。lint 已有白名单,名单只能减少。
- 暂时必须留在 Main 的同步操作,单次耗时要能证明在 16 ms 以内,并在代码里注明理由。
- 耗时可能超过 16 ms 的操作,必须支持取消和进度上报,不能一直占着 Main。
### P2 界面状态局部订阅
状态按领域放进外部 store,组件通过 selector 只订阅自己用到的那一部分。
- 不再往 `App.tsx` 里加 `useState`、`useEffect` 或领域逻辑。`App.tsx` 有行数上限,只能往下调。
- IPC 订阅放在 store 模块里,组件不直接调用 `window.goodbuddy.*.on*`。
- 一个按键、一个流式片段,只能让显示它的那一小块界面重新渲染。
- 传给 memo 组件的回调和 context 值,引用必须稳定。
**模块约定(便于维护,所有拆分都按这个结构做):**
| 层 | 文件命名 | 内容 | 不允许 |
| --- | --- | --- | --- |
| Store | `<domain>-store.ts` | `create<Domain>Store()` 返回 `getState / subscribe / 领域动作`;纯 TS,不依赖 React;可单测 | 访问 DOM、调用 React hook |
| 选择器 / Hook | `<domain>-selectors.ts` 或 `use-<domain>.ts` | 基于 `useSyncExternalStore` 的 selector,返回值引用稳定 | 在 selector 里新建对象却不做相等比较 |
| 同步层 | `<domain>-sync.ts` | 与 Main 的 IPC:首次加载、订阅推送、增量保存队列、失败重试、退出前刷盘 | 放在组件里;整份数据回写 |
| 视图 | `<Domain>*.tsx` | `memo` 组件,只从 selector 和稳定的动作读数据 | 接收 App 的大对象或内联回调 |
- `App.tsx` 只负责布局外壳、路由和把 store 实例通过 context 提供给子树。新增领域状态一律走上表四层。
- 每个 store 的单元测试和 store 文件放在一起(`<domain>-store.test.ts`),覆盖顺序、失败重试和边界情况。
- Main 侧对应:`<domain>` 的数据库访问集中在一个 repository 类里,IPC handler 只做校验和转发。
### P3 高频数据走专门通道,按帧合并
流式文本、终端输出、进度、滚动这类高频数据,不进全局状态。
- 多条更新合并后,每帧最多提交一次;同步到全局状态的频率要更低,目前是 250 ms。
- 高频路径上不能读取布局(`getBoundingClientRect`、`scrollHeight` 等),布局读取要集中到每帧一次。
- 合并更新不能丢失、重复或打乱顺序。每条数据带序号,并有测试覆盖。
### P4 数据只有一份来源,只传变化部分
- 每类数据只有一个权威来源,由数据所在进程负责持久化。界面不把整份数据写回。
- 跨进程的通知直接带上变化的内容,接收方不再“收到通知后重新查全量”。
- 写入合并成批次和事务,不按每个事件单独开一个事务。
### P5 工作量和可见内容成正比
- 长列表只渲染可见部分,包括消息、会话、笔记、文件树和知识库列表。
- 解析和渲染的结果要缓存,比如 Markdown 语法树。内容没变,就不重新计算。
- 增量处理:只解析变化的部分。没有把握时,退回到慢但正确的完整处理,并记录在测试里。
### P6 启动只做首屏需要的工作
- 首屏之前只做显示应用壳和恢复上次界面所需的工作。
- 其他初始化(Provider、Runtime、知识库、通知渠道等)按需执行,或等首屏显示后再空闲执行。
- 后台模块在第一次使用时才加载。
### P7 先测量,再修改
- 先跑测量,拿到数据再改代码;每项改动都要有前后对比,基线用临时 worktree 构建。
- 每个版本至少跑 3 轮,交替运行;结果差异小于同一版本多次运行之间的波动时,不能说成“有提升”。
- 测量用隔离的用户目录和本地假模型,不发出外部模型请求,结果里不含用户内容。
### P8 正确性优先于速度
- 事件顺序、持久化、取消、崩溃恢复和显示结果都不能因为性能而改变。
- 异步改造要先确认“先写入、后读取”的顺序,没有测试覆盖就不合并。
- 风险高、收益小,或者会改变现有功能的优化先不做,在计划文档里记录理由。
### P10 体验不倒退
性能优化只能让体验变好,不能用体验换速度。“行为不变”不仅指数据和结果,也包括用户能感知到的交互:
- **动画和过渡保持原样。** 平滑滚动(如“到底部”按钮)、展开收起、悬停、淡入淡出都不能因为优化而消失。任何瞬间写入 `scrollTop`/`scrollTo` 的代码都会打断正在进行的平滑滚动,必须在平滑滚动期间暂停。
- **只渲染可见部分(P5)时,用户状态不能丢。** 行被卸载后,它的局部状态也会消失。以下行必须保持挂载,或者把状态存到行外:有焦点的行、用户展开过 `<details>` 或输入过内容的行、选中文本的两端、待回答的问题和待审批项、打开菜单或正在重命名的行。
- **只渲染可见部分时,基本操作要和完整列表一致:** 跨行选择和复制、滚动位置不因上方插入或删除而跳动(关闭原生 `overflow-anchor` 时要自己做锚定)、快速拖动滚动条时没有空白帧、渲染结果缓存让重新挂载的行不出现“加载中”。
- **合并和批处理不能增加交互延迟。** 第一条数据立即处理(leading edge),只合并之后同一帧内的数据。打字回显、终端回显不能多等一帧。
- **低频同步不能让用户看到旧数据。** 复制、导出、保存这类读取全局状态的操作,要先把待同步的数据刷进去。
- **体验要有测试。** 涉及动画、滚动、焦点的改动,需要真实 Electron 测试(逐帧采样)或单元测试,并确认测试在旧代码上会失败。
### P9 用工具守住规则
- lint 规则和性能阈值是合并前的硬性条件,不靠人工记得。
- 规则只能收紧,白名单和上限只能往下调。
- 每个 agent 只提交自己改的文件和片段,不把别人未提交的改动带进自己的提交。
## 3. 评审清单
提交性能相关改动,或者会影响热点路径的功能改动时,逐项确认:
1. Main 里有没有新增同步数据库调用、同步文件 IO,或者耗时可能超过 16 ms 的计算?
2. 有没有往 `App.tsx` 里加状态?一次按键或一个流式片段,会让多大范围的界面重新渲染?
3. 高频路径上有没有读取布局,有没有每次事件都提交状态?
4. 有没有整份数据回写,或者“收到通知后重新查全量”?
5. 列表的 DOM 数量会不会随数据量增长?
6. 有没有在启动阶段新增同步初始化?
7. 是否附上了 `perf:app`、`perf:main` 或 `perf:knowledge` 的前后对比?
8. 顺序、取消、持久化和恢复,有没有测试覆盖?
9. 动画、平滑滚动、过渡还在吗?有没有新增会打断平滑滚动的瞬间滚动写入?
10. 只渲染可见部分的列表,滚走再滚回来,展开状态、输入内容、选区、焦点还在吗?
11. 合并或延迟处理后,第一次响应(打字回显、点击反馈)有没有变慢?复制、导出读到的是不是屏幕上的最新内容?
## 4. 当前差距
截至 2026-10-04(第一期结项,见 `docs/review/performance-phase-1-report-2026-10-04.md`),按原则对照的主要差距:
| 原则 | 差距 |
| --- | --- |
| P1 | 同步数据库调用仍在 Main,没有独立数据进程;但热点读已在 worker,高频写单次 0.1–0.3 ms,WAL checkpoint 在后台线程,启动恢复按索引读取。文档解析、分块和大文件哈希已移到 worker。消息附加数据冗余(子任务工具输出、`tools` 与 `blocks` 重复),大会话读取约 170 ms |
| P2 | `App.tsx` 约 7,900 行。输入区、隐藏路由、任务和结果事件、切换会话已不再整体重渲染 App;当前会话的流式吸收(每 250 ms)、队列和活动记录仍经过 App |
| P3 | 已基本满足:流式输出和终端输出都已按帧合并 |
| P4 | 活动记录已改为增量保存、按页读取(界面只保留已加载的页,计数和分组来自 Main 的汇总);部分 `*Changed` 通知之后还会重新查全量 |
| P5 | 已基本满足:消息、会话、笔记、文件树、知识库文档表格都只渲染可见部分 |
| P6 | 空配置启动到输入框可用约 0.96 秒;真实数据(2.4 GB)约 1.36 秒。本地工具环境已后台初始化,低频模块按需加载 |
| P9 | 性能阈值还没有接入 CI,也没有真实低端机的基线 |
+9 -1
View File
@@ -18,6 +18,11 @@
- 先修新增数据路径,再修历史数据。历史转换与新写入尽量使用同一套转换函数。
- 普通 schema 变更使用既有 `PRAGMA user_version` 和 SQLite 事务;
已升级 schema 不允许旧客户端强行打开,不通过回写版本号支持降级。
- 将结构升级、旧内容转换和空间回收分别判断。结构版本落后不代表需要整理历史;
普通更新产生的空闲页也不是旧数据证据。只有明确的旧内容转换才允许检查空闲页并
执行 `VACUUM`,后续新增结构迁移不自动继承全库回收。启动页按实际操作显示状态。
- 同一次启动的重试可在内存中保留已确认的转换/回收决定;退出后重新根据数据库
schema 和未转换内容判断,不为仅剩空闲页增加持久化标记或重复启动回收。
- 大数据转换不得同步阻塞 Electron Main 或 Renderer。放到工作线程,在业务连接
打开前处理,并使用应用的共享字体、主题与页面组件显示进度和可执行的失败提示。
- 根据实测设置有界读取批次与缓存,批次提交后再报告进度。保持 SQLite 事务与
@@ -40,6 +45,9 @@
对象比较必须覆盖真实生产形状和 JSON 往返,尤其是可选字段值为 `undefined` 时与字段缺省
的差异。再次修复已发布迁移产生的数据时,提高目标 schema,并覆盖已升级用户的入口;
不能只处理尚未迁移的旧库。
仅修改升级调度、不改变存储格式时不增加 schema。回归必须覆盖已完成历史转换的库
在后续结构升级时仍有空闲页,确认不重新转换或 `VACUUM`,并验证真实 Worker、
同次启动重试和下一次启动的区别。
### 真实旧库
@@ -71,5 +79,5 @@
表示,并回收空间。具体规则见
[执行记录存储与升级回收](../features/assistant-workbar/execution-history-storage.md),
数据保留与真实旧库验证见[工作栏进度](../features/assistant-workbar/progress.md)。
- 当前未发布源码:schema 35 同时整理旧快照和 schema 34 的重复工具块,工具字段按
- Desktop 0.13.2:schema 35 同时整理旧快照和 schema 34 的重复工具块,工具字段按
JSON 可保存的值比较,不因 `undefined` 被省略而误判变化。
+1 -1
View File
@@ -10,7 +10,7 @@ Debian `loong64` 桌面,不代表 GoodBuddy 已将龙芯纳入正式发布支
均为 LoongArch ELF。
预览版使用社区维护的 Electron 42.3.0 LoongArch 构建。GoodBuddy 正式版本
当前使用 Electron 43.2.0,因此该预览包只用于移植和真机验证,不能作为正式
当前使用 Electron 44.5.1,因此该预览包只用于移植和真机验证,不能作为正式
发布基线。Electron 官方目前也不提供 Linux LoongArch 预编译包。
已完成的实际构建结果:
@@ -1,5 +1,10 @@
# Desktop 0.13.14 / Agent 0.13.4 发布准备
后续状态:Desktop 标签流程 `36165805814` 在测试阶段失败,未进入构建或发布;
Agent 流程 `36165805884` 成功且 0.13.4 已公开。两个历史标签均保留,
桌面恢复版见 [0.13.15 准备记录](./release-preparation-0.13.15.md)。
以下为原候选准备时的范围与证据,不代表其桌面发布成功。
## 范围与审批
- 用户确认从 0.13.13 升至 0.13.14,现有七个测试修改及三个验证辅助文件一起提交。
@@ -0,0 +1,40 @@
# Desktop 0.13.15 恢复版准备
## 发布范围
- 以已发布的 `v0.13.13` 为用户更新基线,完整承接未发布 `v0.13.14` 的产品变更。
历史准备及 Host 验证见 [0.13.14 记录](./release-preparation-0.13.14.md)。
- 0.13.14 标签流程 `36165805814` 在测试阶段失败,生产构建、打包与发布均未执行;
GitHub Release API 返回 404。不可变标签保留,包内条目改为 0.13.15,
不保留重复的未发布说明。
- Agent 0.13.4 流程 `36165805884` 成功,三个包已公开,最低 Desktop 要求仍为
0.13.14。恢复版满足该最低要求;不重发 Agent、不更改其签名目录或标签。
- 用户批准 0.13.15 标题及升级提示调整,其余双语正文沿用已批准内容;
要求先推送候选,等 CI 全量验证与生产构建成功后再打标签。
- 不构建 LoongArch,不运行本地生产构建、打包或安装包启动探针。
- 中英文功能矩阵已复核,同步恢复版本和 Agent 已发布状态,保留其他未验收边界。
## CI 修复
`cd27163` 仅改测试和质量规范,不改变产品、Agent、数据库或外部契约:
- 原生能力清单测试控制第二次响应,等待重试已开始再展开控件,并分别校验响应前后状态。
- 浏览器关闭测试在同一个 `waitFor` 中检查两次调用、页签移除和错误清除。
- 笔记布局测试显式扫描 fixture 依赖,使用可见的隔离 Electron 窗口执行原生输入,
保留 sandbox/contextIsolation,明确检查超时、信号和退出码后才读取结果。
对应 CI 证据:标签流程有 1 项失败(原生能力按钮未找到);
main 流程 `36164698647` 有 2 项失败(关闭错误残留、布局测试约 90 秒后缺少结果文件)。
两次失败点不同,不能仅凭本地通过认定 CI 已修复。
## 验证
- 本地三个定向用例通过,Electron 布局约 3.46 秒,全部几何和交互断言保留。
- 修复提交完整 typecheck、lint 通过。
- 本轮没有重复本地全量测试,以新候选 Linux CI 的完整测试结果为准;
先前 5,127 项本地通过仅为历史记录。
- 无新真实模型调用,未改变 Agent 或 Runtime 生产源码,无需重复 Host 开发探针。
- 恢复候选的说明校验、9 项发布说明测试和 `git diff --check` 通过;
三处桌面版本一致。逐项比较确认只替换版本及已批准升级提示,
0.13.14 无重复条目,Agent lock 仍为 0.13.4。
- 主分支 CI、生产构建和标签发布结果待后续核对。
@@ -0,0 +1,43 @@
# Desktop 0.13.16 发布准备
日期:2026-09-26。当前为候选,不代表已发布。
## 范围与批准
- 用户确认 Desktop 0.13.16、Agent 保持 0.13.4、不构建 LoongArch 预览。
- 基线为 `325b702934ba0f0c076f27e926a897267262ab55`,用户明确批准纳入新提交的
Supervisor 界面修改。相对已发布 `v0.13.15` 包含:
- `e6ac073`:监督回顾持续分批处理,保留主动暂停、单次模型超时和并发。
- `4c7193e`:结构升级不自动回收正常空闲页,准确显示操作状态及保留同启动重试。
- `325b702`:监督回顾空态、运行状态与活动阶段布局修复。
- 用户已批准中英文三项问题修复及两项升级提示,写入 `resources/release-notes.json`;
已发布版本说明保留。最终候选 SHA 需另行确认,主分支 CI 通过后才创建新标签。
- `FEATURES.md` 和 `FEATURES.zh-CN.md` 已核对并更新当前版本、持续回顾、
结构升级及界面行为,删除过时的执行预算暂停描述。
## 数据与 Agent 边界
相对 0.13.15 不提高 schema,仍为 47。不新增持久化迁移标记,不改写用户数据库。
旧格式仍执行必要转换;普通结构升级不因空闲页执行全库 VACUUM。
监督回顾仍只读、无工具,模型调用可能收费;取消固定时长暂停后可连续处理更多批次。
本轮没有 Agent、Runtime、模型桥、部署锁或远程协议修改;本机与远程历史共用桌面升级
实现。监督通过桌面 Ask 编排,不改变 Host 运行路径,不要求 Agent 更新或新 Host 部署。
本轮实际模型请求数为 0。
## 验证
在独立工作树准备,避免其他工作修改影响候选,复用相同锁定依赖,未复制本机私有环境文件。
候选第一轮全量发现 `HeartbeatCenter.test.tsx` 的两种语言仍要求已移除的重复说明存在;
改为断言说明不存在,保留范围、操作、标题及页面内容校验,专项 **42 项通过**。
最终候选完整测试:**5,128 通过、84 跳过、1 失败**。唯一失败为未修改的
`mermaid-viewer.electron.test.ts` 子进程退出码 null;首次单独复跑被工具的 150 秒
上限中断,随后按用例原有时间窗口复跑 **1 项通过**,覆盖九种显示场景和十次实际 PNG
导出。未修改该用例、提高其自身超时或弱化断言,独立复跑不替代全量结果。
`npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 均通过。
先前迁移定向 47 项通过;最终开发期全量为 5,137 通过、74 跳过、两项 Electron
失败,失败用例独立复跑两项通过。该记录不是本候选的全量或 CI 通过证明。
未运行本地生产构建、打包或安装包启动。主分支 CI、六目标原生打包和公开发布核验待完成。
@@ -0,0 +1,238 @@
# Desktop 0.15.1 与 Agent 0.15.0 发布准备及恢复
日期:2026-09-27 至 2026-09-28。原 Desktop 0.15.0 标签构建失败,
用户已选择 Desktop 0.15.1 与 Agent 0.15.0 恢复发布;验证通过不代表已发布。
## 范围与批准
- 用户要求 Desktop 与 GoodBuddy Agent 同步升级至 `0.15.0`,明确不构建 LoongArch。
- 源码基线:`0f8bf405db7ca21a85cc70009ad083eb837e1e03`。
- Desktop 差异基线为已发布的 `v0.13.16`,Agent 为 `agent-v0.13.4`。
- `v0.15.0` 已创建并推送,保持不可变;恢复使用 `v0.15.1`,
`agent-v0.15.0` 尚未创建或推送。
- 用户已批准两份完整双语说明,正式正文仅维护在
[Desktop 发布资源](../../resources/release-notes.json) 的 `0.15.1` 条目与
[Agent 发布资源](../../resources/agent-release-notes/0.15.0.md)。
已逐条核对写入文本与批准草案一致,本记录不再复制正文。
- `FEATURES.md` 与 `FEATURES.zh-CN.md` 已核对本轮能力、限制和候选版本。
- 用户要求两张未跟踪宣传截图保留原位、不提交,打标签前另行处理;
不移动、删除、暂存或更改忽略规则。
- 2026-09-28 用户更新授权:当前工作区的宣传文档与素材可以一起提交、推送,
后续候选由代理自行审核确认,不再逐次等待用户批准,继续完成双产品正式发布。
三张新增候选图已读取检查,保留原始草稿属性,不冒充正式发布截图或对外媒体发文。
发布说明正文保持不变,`releasedAt` 更新为实际准备发布日期 2026-09-28。
## 本地验证
- `npm run typecheck`:通过。
- `npm run lint`:通过。
- `npm run release:notes:verify`:通过。
- 发布说明相关三个文件定向测试:16 项通过。
- 首轮 `npm test`:5,313 通过、79 跳过、4 失败,耗时 697.61 秒。
三项失败来自当时尚未批准写入的 0.15.0 Desktop/Agent 发布说明;
另一项为 `magic-notes-layout.electron.test.ts` 的布尔断言。
该布局用例单独复验 1 项通过,未修改源码或放宽断言。
单独通过不替代全量结果。
- 正式说明写入后的最终 `npm test`:**5,317 通过、79 跳过、0 失败**,
453 个测试文件通过、11 个跳过,耗时 645.49 秒。随后再次执行完整
`npm run typecheck` 与 `npm run lint`,均通过。
- 最终验证期间出现并行宣传文档编辑及新增 `storygraph.png`,均不属于本次
发布准备改动,不暂存或覆盖。打标签前仍须处理工作区洁净条件。
- 修改文档的相对文件链接、版本一致性和 `git diff --check` 通过。
- 不运行本地生产构建、打包或安装包启动。准确候选提交须经用户批准,
然后双远端推送、主分支 CI、生产构建、原生发布与公开核验分别进行。
## Agent 当前源码实机验证
共享 Linux x64 Host 的 LAN/VPN SSH 端口均可达,实际通过 LAN 与既有固定
Host Key、加密凭据连接。重新捆绑当前源码的 Agent CLI、daemon、model bridge
与 Desktop 适配器,仅上传独立 `/root/tmp/gb-lifecycle-*` 测试目录,复用已安装且
版本锁未变的 Node/OpenCode/Continue,不替换原安装,不构建正式包。
最终 OpenCode、Continue 各通过一次 Ask 和一次 Execute,无工具请求,
验证回答、后续历史、空闲 Runtime 回收及测试停止后所属进程为 0。
模型请求时读取所属进程的固定遥测字段,确认 `DO_NOT_TRACK=1`、
`OTEL_SDK_DISABLED=true` 及三个 exporter 均为 `none`,不输出整个环境。
这些结果补齐 `eb53c82` 的 Agent/model bridge 真实链路验证,
但不证明后台没有其他网络流量,也不替代三目标正式包验收。
首轮 Continue 探针沿用了 OpenCode 原生历史持久化假设,未像 Desktop 一样传入
`request.history`,第二轮历史断言失败;当前 Continue 冷启动源码明确要求传入
保存历史。修正测试输入及会话 ID 断言后通过,未修改产品实现。
有完整计数的实机运行共 **8 次**真实模型 HTTP 请求:首次 OpenCode 2 次、
首次 Continue 2 次、最终 Continue 2 次、最终 OpenCode 2 次。
此前一次直接启动 Electron 的探针未捕获子进程输出,不能确认是否实际发出请求,
该次上限 2 次,不纳入通过证据;因此不能把 8 次写成本轮精确总数,总量为 8–10 次。
后续均使用等待退出并记录脱敏输出的启动器;本轮探针 Electron 进程已退出。
本地诊断证据在临时 `opencode/release-015-host/`,不提交脚本、日志或凭据。
## 首次候选 CI 与测试修正
用户批准的首次候选 `5a6ba6c14f5c58f3da5e9afe281b18e0cbdb1ae8`
已推送至 `origin/main` 和 `github/main`,未创建两个发布标签。
- [Agent CI 36329271991](https://github.com/mesalogo/goodbuddy/actions/runs/36329271991)
源码验证及 Linux x64、Linux arm64、Darwin arm64 原生构建全部通过。
- [Desktop CI 36329271967](https://github.com/mesalogo/goodbuddy/actions/runs/36329271967)
为 5,342 通过、51 跳过、3 失败。两个 App 长历史场景超过默认 5 秒单用例时限;
会话采集 Electron 场景等待 Escape 返回笔记列表超时。生产构建未运行。
- 测试修正仅让原生键盘辅助函数显示窗口、获取焦点并等待 `document.hasFocus()`,
然后发送真实输入;两个长历史场景采用仓库既有的 15 秒单用例时限。
不修改产品实现、不跳过 Linux、不关闭沙箱、不删除或放宽数据和键盘断言。
- Windows 聚焦复验三个失败场景通过,其中真实 Electron 覆盖完整 502 条历史、
214px 窄栏键盘操作、浅深主题确认按钮、真实 IPC/SQLite 和重开保留。
修正后首次全量复验被中断,不计为通过;随后完整重跑为 **5,317 通过、
79 跳过、0 失败**,453 个文件通过、11 个跳过,耗时 646.48 秒。
类型检查、lint 和发布说明校验均通过,新候选 Linux CI 仍需独立通过。
- 本次只改测试和验证文档,不改变 Agent、远程 Runtime 或模型桥;
沿用上方当前源码 Host 验证,不新增真实模型请求。
- 新提交必须重新取得准确 SHA 批准,原发布说明正文保持不变。
## Linux 实机复现与最终同步修正
2026-09-28:第二次批准候选 `12594961fe1e814e89f55de1fbfe245310505115`
的 [Desktop CI 36331853137](https://github.com/mesalogo/goodbuddy/actions/runs/36331853137)
为 5,343 通过、51 跳过、2 失败。两个 App 长历史场景已通过;
Escape 返回列表仍失败,活动历史迁移这次超过默认 5 秒时限,生产构建仍未执行。
按用户要求直接在共享 Linux x64 Host 测试准确候选源码。使用独立磁盘测试目录、
锁定 Node 24.19.0 和 `npm ci`,补齐 Xvfb 与匹配的 X server 系统依赖;
没有创建新账号,使用既有非 root 账号运行测试,保留 Electron 沙箱及其标准 helper 权限。
不替换既有 Agent,不修改其安装或工作区,不执行生产构建或打包。
原测试在 Host 上稳定复现:缩窄窗口会按产品既有行为收起工作栏,隐藏面板仍有非零宽度。
诊断显示 Escape 实际发给 `aria-expanded=false` 的工作栏开关,而不是笔记输入框。
因此仅获取窗口焦点不够;测试改为等待窄视口及响应式渲染完成,按真实按钮打开工作栏,
确认展开状态和目标输入框焦点后再发送键盘输入。临时诊断代码已移除。
迁移测试在 Host 上以原默认时限通过,保留 10,000 条记录、完整迁移、WAL 字节比、
扫描次数与无变化写入断言;仅为这一个大数据场景采用已有的 15 秒单用例时限,
不改数据库实现或全局时限。
- 最终 Linux Host 完整 `npm test`:**5,345 通过、51 跳过、0 失败**,
449 个文件通过、15 个跳过,耗时 751.09 秒。
- Windows 聚焦回归:3 项通过,包含真实 Electron 和迁移。
- Windows 完整类型检查、lint、发布说明校验:全部通过。
- Linux Electron 实测覆盖 502 条历史、218px 窄栏、Escape/Tab/Enter、
浅深主题确认按钮、真实 IPC/SQLite 与重开保留;Windows 对应窄栏为约 214px。
- 本阶段不新增真实模型请求,不改变产品代码或双语说明。
新候选仍须重新批准 SHA 并通过自己的主分支 CI/生产构建,两个标签仍未创建。
## 后续 CI 焦点前提与确定性取消验证
`b8b6007` 的 [CI 36335076841](https://github.com/mesalogo/goodbuddy/actions/runs/36335076841)
两次执行均为 5,344 通过、51 跳过、1 失败,失败点改为 `new title focus`,
生产构建没有运行。该次失败未在 Host 的 10 次带诊断复验中重现,其中 5 次限制单 CPU;
移除临时诊断后再测也通过。这些独立结果不替代 CI。
测试原先直到键盘阶段才显示窗口,前面的自动聚焦检查仍在隐藏窗口执行。
现让测试窗口从开始即显示并确认焦点,保留标题自动聚焦原断言;
超时增加实际焦点、标题禁用/可见/隔离状态和保存状态,避免只报告阶段名称。
可见窗口版本在 Host 连续五次聚焦复验通过。
随后 Host 全量出现一个独立的 Continue 取消测试竞争:
`setTimeout(0)` 没有保证取消发生在子进程启动前,空 mock 已被调用。
用例改在既有启动环境回调处精确取消,仍验证回调执行、子进程未启动和临时配置删除,
不更改 Continue 产品实现或取消机制。
- 最终 Host 完整测试:**5,345 通过、51 跳过、0 失败**,449 个文件通过、
15 个跳过,耗时 743.34 秒。
- Windows 可见窗口 Electron 回归及确定性取消回归通过。
- 完整类型检查、lint 通过;双语说明正文不变,日期调整后另行验证发布资源。
- 上述均为测试或验证文档调整,不新增真实模型调用,不改变 Agent 发布输入。
正式发布仍要求当前候选自己的 CI/生产构建通过。
## 完整分支快照重新核对
`d9e1e42` 的 [Desktop CI 36340682594](https://github.com/mesalogo/goodbuddy/actions/runs/36340682594)
已通过原生会话采集焦点测试,但为 5,367 通过、51 跳过、4 失败。
核对祖先后确认准备期间另有四个并行提交进入分支:
`ee1bf8a` 输入区控件样式、`8e60736` 流式会话稳定排序、
`f35198d` 桌面通知开关、`d47f967` 监督结果继续讨论。
它们已随用户授权的当前分支推送;前述 Host 的 5,345 项通过使用较早源码快照,
不能作为包含这四项的新候选验证结果。
本轮已重新核对四个提交的实现和文档,补充两份功能矩阵与双语发布说明:
通知开关默认开启且不影响应用内提示;流式更新不改变排序;
监督结果可直接编辑问题并继续讨论,界面不再提供知识实体写回控件。
Agent 产品源码及 Runtime 锁未因这四项改变,Agent
[CI 36340682607](https://github.com/mesalogo/goodbuddy/actions/runs/36340682607)
源码校验和三个原生构建全部通过。
补齐通知默认值的契约 fixture;Obsidian 多次真实子进程调用使用单用例 15 秒时限,
避免前项超时后的在途调用污染下一项取消断言;笔记来源用例按面板查询控件,
并等前次来源操作释放禁用状态后再发起下一次。
这些修正不改变产品实现。Windows 聚焦回归 19 项通过,类型、lint 和说明校验通过。
从 Git tree `1758ff58a6ff33fcf7ddaa3d35dd2fcc57cd1265` 导出完整快照,
覆盖 Host 测试源码,而非只同步早期快照中的少数文件。
该完整快照 `npm test` 为 **5,371 通过、51 跳过、0 失败**,
451 个文件通过、15 个跳过,耗时 751.42 秒,包含全部四个并行提交。
最终提交前再次核对源码、测试、依赖和发布资源与该已验证 tree 一致,
仅追加本段验证结果;完整候选仍需自己的主分支 CI 与生产构建通过。
### 完整候选后的焦点同步修正
`5cdffee` 的 Agent [CI 36343184917](https://github.com/mesalogo/goodbuddy/actions/runs/36343184917)
三个原生构建和源码校验通过;桌面
[CI 36343184916](https://github.com/mesalogo/goodbuddy/actions/runs/36343184916)
为 5,369 通过、51 跳过、2 失败,生产构建未执行。
会话采集的焦点等待可被 `body` 后代文字误满足,菜单随后被真正的消息聚焦关闭;
另一个原生 Runtime 控件用例在启动完成前打开失焦关闭的菜单。
本轮收紧消息焦点的元素断言,并等待项目能力请求及启动渲染后再打开 Runtime 选项。
不重试菜单、不跳过场景、不改产品实现。
完整冻结 tree `2ddc0da69b6bcb42d8272c44dd11e3e6904d0438` 在 Host 全量
**5,371 通过、51 跳过、0 失败**,耗时 748.21 秒。
Windows 原生 Runtime 聚焦用例、类型检查和 lint 通过。
提交仅追加本段记录;仍须等待修正候选的主分支 CI 和生产构建。
`706726b` 的桌面 [CI 36345672365](https://github.com/mesalogo/goodbuddy/actions/runs/36345672365)
已通过上述采集与 Runtime 用例,但另一个外部知识库测试失败:
5,370 通过、51 跳过、1 失败,生产构建未执行。
该 fixture 只在查询新库 ID 时返回包含已创建库的快照,普通轮询会返回创建前数据;
同时错误地要求最后一次请求必须属于新库,忽略后台轮询。
改为模拟已持久化的库列表,等待新库请求发生,保留原库与新增库选中、被排除库未选中的
全部界面断言,不修改产品代码。Windows 完整 App 300 项通过,类型、lint 和说明校验通过。
冻结 tree `42a92450ad7d9ebc50167c7946bff53cff2889e6` 的 Linux 全量为
**5,371 通过、51 跳过、0 失败**,耗时 748.67 秒。
## Desktop 0.15.1 恢复
`65a21390debe0dd04871417a363a9d87a29339e6` 的主分支
[CI 36347785076](https://github.com/mesalogo/goodbuddy/actions/runs/36347785076)
验证与生产构建全部通过后,两个远端创建了不可变 `v0.15.0`。
其正式 [CI 36348938269](https://github.com/mesalogo/goodbuddy/actions/runs/36348938269)
两个 macOS 目标在 `codesign --verify --deep --strict` 失败,
报 `invalid destination for symbolic link in bundle`;GitHub Release 未发布。
Host 隔离 fixture 经实际 DS 资源准备和 electron-builder 文件复制,
复现相对 npm CLI 链接变成原构建目录的绝对链接。不是签名凭据或测试超时问题。
用户明确选择 **Desktop 0.15.1 + Agent 0.15.0** 恢复,不移动或复用旧标签。
修正仅让复制保留相对链接;新增回归移除原安装与暂存目录后验证最终链接仍可读。
该路径不用于 Agent,远程 Runtime 与 Agent 源码未改,不增加真实模型调用。
未发布 Desktop 0.15.0 的双语条目迁移到 0.15.1,保留全部功能说明;
Agent 版本不变,其包元数据从新版 Desktop manifest 派生最低版本 0.15.1,
两份说明和功能矩阵同步这个升级顺序。
原标签最终 Windows/Linux 四个目标成功,两个 macOS 失败,发布任务跳过,
公开 Latest 仍为 `v0.13.16`。
Windows 打包相关测试 45 通过、1 个 POSIX 链接用例跳过,类型、lint、
发布说明校验通过。冻结 tree `99d3271174a68922fbf0a1a4cfbadbb85a6067c8`
在 Linux Host 全量 **5,372 通过、51 跳过、0 失败**,耗时 745.88 秒;
新增符号链接回归实际执行通过。提交仅追加验证记录,恢复候选 CI 与 macOS 签名待验证。
## 数据、兼容与验收边界
Desktop schema 48 新增魔法笔记会话来源元数据。升级前应备份完整用户数据,
包括数据库及相邻 `notes/` 目录;旧客户端不能打开升级后的数据库。
本次发布准备不修改数据库迁移实现,也不直接改写用户数据库。
Agent 发布工作流以候选 `package.json` 设置最低 Desktop 版本,因此
Agent 0.15.0 包要求 Desktop 0.15.1;Node、OpenCode、Continue 锁不变。
Agent 与 Desktop 独立发布,Agent 不替代 Desktop Latest。
原生客户端的远程 coordinator、托管标准 Node 交付整合、Continue/OpenCode
Ask 内置 MCP 映射及完整跨平台安装包验收仍有缺口,见
[原生客户端进度](../features/assistant-workbar/progress.md#2026-09-27-local-native-client-validation)。
本次说明明确标为本地预览,不将这些未完成部分宣称为已提供能力。
@@ -0,0 +1,83 @@
# Desktop 0.15.10 发布准备
日期:2026-10-04。基线 `v0.15.9`,冻结已提交源码
`74efbcdc1703e6893764dbb2c48e3d1c47b21fce`,35 个提交。
仅纳入已提交实现和发布准备,保留并排除工作区中的导航、文档处理等并行修改。
标准六平台 Desktop,不含 LoongArch;Agent 保持已发布的 0.15.3。
## 范围与兼容性
- 统一项目与会话菜单、当前/全部范围、活动及十个最近会话、悬停预览。
- 监督者独立文本模型选择及默认开启的会话 Story Graph 开关。
- 活动差量保存、索引分页与完整汇总,schema 57 保留历史并迁移排序;
schema 58 增加启动恢复候选消息索引。升级前关闭应用并备份完整数据库
和相邻 notes 目录,旧客户端回退须恢复升级前备份。
- 文档解析 Worker、依赖延迟加载、本地工具环境后台准备、Composer 和
路由等渲染边界、笔记/文件树/知识文档窗口化、滚动和焦点保留。
- 终端 Main 合并与 xterm 帧批次,远程事件 16ms 定时 flush;
checkpoint 前落库、事务顺序和回放去重保留。
- 旧 Runtime 缓存按明确版本和布局命名清理,跳过符号链接;
保留当前布局、运行记录、会话和配置。工作栏间距及操作修复。
- 高频本地写入临时切换 NORMAL 并恢复原 PRAGMA;嵌套事务由外层决定。
保留应用崩溃原子性,但断电/系统崩溃可能丢失最近部分本地写入。
远程确认、通道投递和任务生命周期继续 FULL,后台 PASSIVE checkpoint
不代表已经验证破坏性断电恢复。
- Agent 守护进程只有测试变更,Continue host-v7 仅提取常量,协议和部署
行为未变,因此不发布新 Agent。Electron/Node/OpenCode/Continue 锁不变。
不承诺通用提速比例。窗口化未挂载行不支持页内查找,快速滚动可显示占位;
只读 Worker 失效可同步回退。开发报告中的 stable controller generation resume
及 Runtime 未到终态错误仍是调查项,本轮不声称修复。
## 本轮真实 Host 验证
中断前已针对 `e89db96` 源码使用固定 Host 身份与既有凭据存储,
在隔离测试目录经生产 Agent、模型桥、远程 ACP、RemoteEventBatcher
和 SQLite 持久化入口执行。后续 `74efbcd` 仅修改工作栏 CSS 间距,
不改变已验证的远程实现。OpenCode 探针显式使用已有 Continue 模型配置,
不是产品静默回退;未构建生产安装包。
**4 次真实模型请求,4 次 HTTP 200**:
| Runtime / 模式 | 批次 | 事件 | checkpoint |
| --- | ---: | ---: | ---: |
| OpenCode Ask | 3 | 4 | 2 |
| OpenCode Execute | 22 | 42 | 21 |
| Continue Ask | 3 | 4 | 2 |
| Continue Execute | 21 | 40 | 20 |
checkpoint 前落库、完整已保存流再次写入返回已存在、Runtime 重建后历史恢复,
本轮拥有进程清零。生产 SSH 连接池与终端会话另输出 1000 行 Unicode,
13 个输出事件;CRLF 归一化后内容完全一致、序号递增、输出先于退出。
最初终端探针选错 Host 路由,发生在模型请求前,真实调用数为零;
随后复用成功固定的路由完成。临时 Electron 缓存警告非致命,不算产品修复。
这些证据不是完整 Renderer、已安装 Agent 升级或全部断线恢复矩阵,
也没有验证所有消息投影恢复分支。开发阶段其他模型调用不计入本轮四次。
## 发布门槛
按用户持续授权,直接提交、推送并跟进 CI 和发布,不逐步询问。
不运行本地测试套件、类型检查、lint 或生产构建;完整验证和构建由精确候选
main CI 执行,通过后才创建新的不可变 Desktop 标签。双语说明与 FEATURES
同时更新;Agent 不打新标签。开发文档记载的未通过测试不能视为已解决,
由此次精确候选 CI 决定,失败须定位修复。
最终需核验六平台打包、macOS 签名与公证、GitHub Latest 和 20 项资产、
北京 OSS 索引一致性与网站 12 种下载选择。不下载或启动安装包,
不将候选或推送成功称为发布完成。
### 首轮候选 CI
`273a7fa` 的 Desktop CI `37174433765`:5953 通过、59 跳过、3 失败,
生产构建未执行;Agent CI `37174433777` 通过。
监督设置测试需要先进入“回顾整理”页签再操作其可见保存按钮,
Electron 布局测试更新为真实标题“模型与运行限制”,截图锚点限定可见设置面板。
保留保存值、再次打开恢复、布局及溢出断言。
1200 条活动历史测试会实际显示至 1000 行,专用超时从 30 秒改为 60 秒;
不减少数据、不移除分页、清空总数或离页裁剪断言,等待下一次 CI 验证。
`362fe50` 的 CI `37175597089`:上述三个测试均通过,5955 通过、59 跳过;
另一个工作区 KeepAlive 测试在默认一秒内未找到魔法笔记标题。
仅将该轮换测试的异步标题等待设为五秒,保留聊天 DOM 身份、旧工作区淘汰
及缓存数量断言。CI 观察命令曾出现 GitHub API EOF,最终失败以工作流结果为准。
@@ -0,0 +1,48 @@
# Desktop 0.15.2 与 Agent 0.15.0 发布准备
日期:2026-09-28。用户要求包含当前本地工作、推送双远端,等待候选 CI 和生产构建通过后打标签;
确认同时发布 Agent 0.15.0,不构建 LoongArch。沿用此前自行审核候选和双语说明的授权。
## 发布范围
- Desktop 基线为已发布的 `v0.15.1`(`4e5239256d42c856bb1420d87c4a338f17867b80`)。
[发布 CI 36368878429](https://github.com/mesalogo/goodbuddy/actions/runs/36368878429)
六个原生目标和 GitHub/OSS 发布全部成功,0.15.1 的历史说明保留。
- 新源码截止 `66f77b83c2b6ebe0018a6c5c2bc72e1b4c1e43b1`:
renderer 退出诊断、笔记采集界面、双语网站及 Story Graph 只读工具与完整讨论草稿。
- Agent 0.15.0 尚未发布,基线仍为 `agent-v0.13.4`。
既有遥测和 Continue 更新检查修复保留,补充远程 Story Graph 工具;
最低 Desktop 版本由当前 manifest 派生为 0.15.2。三个原生目标和 Runtime 锁不变。
- 当前未提交的功能矩阵、主机/云环境设计和宣传文稿一并审核纳入。
云平台适配与主机入口迁移明确为待实现规划,不进入发布功能说明。
宣传稿保持草稿和原核查截止点,不代表已对外发布。
- 正式双语正文仅维护在 `resources/release-notes.json` 的 0.15.2 条目和
`resources/agent-release-notes/0.15.0.md`。两份 FEATURES 已对照实现与规划核对。
## 验证边界
Story Graph 已有开发期证据见
[监督者验证进度](../features/conversation-supervision/progress.md#2026-09-28-story-graph-只读工具):
真实 Linux Host 上的 OpenCode/Continue Ask、Execute 调用与禁用检查通过;
推理为本机确定性响应,10 次请求含 2 次标题生成,外部付费请求 0 次。
不将这些证据称为真实模型建议质量验证,也不宣称历史重建、跨范围事实合并或大库性能已验收。
本轮类型、lint、双语说明及网站静态校验通过。按当前源码重建隔离 Agent 探针,
在共享 Linux x64 Host 经固定身份完成 OpenCode/Continue Ask、Execute、空闲进程回收、
冷启动历史恢复和遥测关闭环境检查;每个 Runtime 2 次真实模型请求,共 **4 次**,
没有重试或假模型调用,结束时归属进程均为 0。该补充检查不等于真实模型图谱建议质量验收。
准备期间另有 `0cdf042` 宣传稿提交,已核对只有三份文档变化,产品源码仍截止 `66f77b8`,
一并纳入完整候选。用户随后中断本地全量步骤,明确要求直接推送等待 CI;
本地全量不记为通过,由精确候选的主分支 CI 执行完整测试和生产构建,通过后才打标签。
不运行本地生产构建、安装包打包或安装包启动;使用既有隔离 Host 目录,不更改生产 Agent。
schema 保持 48;从旧版升级应备份完整数据库和相邻 `notes/`,不能用旧客户端打开升级后的库。
## 首次候选 CI
`76e781a` 已同步双远端。Agent [CI 36379713606](https://github.com/mesalogo/goodbuddy/actions/runs/36379713606)
通过;Desktop [CI 36379713607](https://github.com/mesalogo/goodbuddy/actions/runs/36379713607)
5,389 通过、51 跳过、1 失败,生产构建未执行,未打标签。
失败的设置测试用固定五个服务器名称过滤按钮,却用新增 Story Graph 后的六项目录断言数量。
同步把旧模拟列表与按钮身份断言改为复用正式目录,保留数量和原交互断言,并逐项验证目录按钮存在。
最终完整 SettingsPanel 文件、类型、lint 和发布说明检查通过;全量继续交由新提交 CI。
@@ -0,0 +1,36 @@
# Desktop 0.15.3 发布准备
日期:2026-09-28。用户确认沿用双远端推送、精确提交 CI 和生产构建通过后自动打标签的流程;
本次仅发布 Desktop 0.15.3,已发布 Agent 0.15.0 保持不变,不构建 LoongArch。
## 范围
- 已发布基线:`v0.15.2`,提交 `47ba54f47db4f22f69375310925eedab020b3cf0`。
- 本轮已核对 15 个提交,截止 `86347af0b1cc8726c1166b3f9fa3d7f12a0cc5e3`。
包含手动压缩、请求级工具授权、取消/失败状态、聊天/笔记重复工作、
侧栏与工作栏布局、打包依赖精简。起始工作区干净。
- Office 同会话实时编辑仅更新需求与验收设计,不计入交付能力。
- `package.json`、lockfile 两处 Desktop 版本统一为 0.15.3。
已发布 0.15.2 与 Agent 0.15.0 的正文保持不变;新双语说明单独追加。
- 两份 FEATURES 对照实现更新手动压缩、状态反馈、搜索、箭头方向及发布状态。
不用局部 Markdown 基准宣称整体渲染性能提升,不提前承诺跨平台安装包缩小比例。
## Agent 与验证边界
`src/agent-daemon`、`src/main/remote-agent`、Agent 和 Runtime 锁在本轮未变。
授权修复属于 Desktop 的 `KnowledgeMcpGateway`:请求有效期间不再按固定十分钟撤销,
取消和清理继续撤销;不修改远程工具通道或部署包,不需要重发 Agent 0.15.0。
手动直连压缩仅用于本地文本会话,UI 与 IPC 均排除 SSH/远程直连入口。
开发期证据分别见[直连模型进度](../features/direct-model-agent/progress.md)、
[导航进度](../features/application-tool-navigation/progress.md)和
[性能基准](../roadmap/product-performance-experience-improvement-plan.md)。
开发期全量曾报告侧栏和笔记 Electron 用例失败,不把这些历史结果计为本候选通过。
本轮 `npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 通过。
网关、Obsidian 授权、桌面资源与 DS 打包、手动压缩真实 Electron 生产链路共 6 文件:
**85 通过、1 跳过**。跳过的是 Windows 不适用的 POSIX 符号链接用例。
压缩测试使用本机 HTTP fixture,不冒充外部模型调用;本轮没有新增付费模型请求。
沿用用户确认的直接推送等待 CI 流程,完整测试及生产构建以精确候选 CI 为发布门槛。
不进行本地生产构建、打包或安装包启动。
schema 保持 48;从更早版本升级前备份完整数据库及相邻 `notes/` 目录。
@@ -0,0 +1,55 @@
# Desktop 0.15.4 与 Agent 0.15.1 发布准备
日期:2026-09-29。用户确认两个补丁版本,提交并推送两个远端;
候选 CI 和生产构建成功后自动打标签发布,不构建 LoongArch。
## 范围与说明
- Desktop 已发布基线为 `v0.15.3`(`3c9229ac7a57071fc955adb1095c499ffdfd006a`)。
本轮源码截止 `9c88ca20f3c62ca29b9aca179dc191b460db37d8`,开始时工作区干净。
- 三个提交包含系统任务 Token 统计、Runtime/模型分层和宣传稿归档。
正式功能说明以实现为准;用途模型和旧全局 provider 移除仍为规划。
- Agent 基线为已发布 `agent-v0.15.0`。本轮 Continue helper 识别首次历史封装、
去除保存消息中的工作模式前缀,因此发布新 Agent 0.15.1,不覆盖旧产物。
Node 24.19.0、OpenCode 1.18.29、Continue 1.5.47 和协议锁保持不变。
- 正文仅维护在 Desktop 0.15.4 JSON 条目及 Agent 0.15.1 Markdown;
已发布版本正文保留。FEATURES 中修正旧的简化选择描述,并同步用量、版本和限制。
## 数据与兼容
schema 49 在既有 SQLite 事务中迁移项目与会话选择,不引入额外恢复系统。
已读迁移指南并核对合成 schema 48 数据迁移回归;本轮不改写用户原库。
关闭应用后备份完整数据库及相邻 `notes/`,旧客户端不能打开升级后的库,
降级需要恢复完整升级前备份。远程先升级 Desktop 0.15.4,再更新 Agent 0.15.1。
统计无法补算历史未记录的调用或缺失的供应商 usage。
## 验证
- `npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 通过。
- 10 个聚焦文件、221 项通过:包含数据库迁移/用量分类、选择解析、选择器、
Activity UI、后台用量记录和 Continue helper。
- 当前源码隔离 Agent 在共享 Linux x64 Host 验证 OpenCode 与 Continue Ask、Execute、
空闲进程回收及冷启动历史恢复。成功场景每个 Runtime 两次真实模型请求。
OpenCode 原模型连接前三次尝试未完成,第三次脱敏诊断明确为上游 `ECONNRESET`;
随后测试显式使用 Continue 已配置且成功的模型连接,未修改用户设置或产品回退规则。
本轮共 7 次上游请求分发尝试,4 次成功、3 次未完成;不推断失败尝试的计费情况。
所有探针结束时归属进程为 0,用户设置只在临时加密副本中读取,测试副本已清理。
- 既有开发记录缺少完整 UI 目测及全仓通过结果,保留边界,不将局部通过冒充全量。
完整测试与生产构建交由精确提交 CI;不执行本地生产构建、打包或安装包启动。
## 首轮候选 CI
`ed47e2c` 已同步两个远端。Agent `36580699297` 三平台验证通过。
Desktop `36580699255` 为 5,472 通过、52 跳过、1 失败,生产构建未执行,
尚未创建任何本轮发布标签。问答测试在 Runtime 就绪前点击仍禁用的发送按钮,
没有产生首个请求;按现有异步同步机制等待按钮可用,保留全部问答与重载断言。
该修正仅影响测试和发布记录,运行时源码、锁文件及双语发布说明不变。
修正后完整 `App.test.tsx` 314 项通过,类型检查与 lint 再次通过。
## 发布前界面修正
用户要求移除会话 Runtime 按钮图标与名称间的圆点,并确认继续提交、推送和发布。
删除专用覆盖标记 class 及其伪元素样式,保留菜单内来源提示和恢复默认操作,
同步双语说明及选择设计。完整 App 与样式回归共 351 项通过,类型检查和 lint 通过。
该改动仅在 Renderer,不影响 Agent 或桌面至 Agent 协议,无需新增真实模型请求。
此前中断的测试不计为通过;本条结果来自重新执行。最终新提交仍须等待独立 CI。
@@ -0,0 +1,74 @@
# Desktop 0.15.5 发布准备
日期:2026-09-30。用户授权按新增改动确定版本、提交推送两个远端,
CI 与生产构建通过后自动打标签发布;本轮不构建 LoongArch。
## 范围
已发布基线 `v0.15.4`:`a662b16f5e076f67788457653d29c0d2fbacdab0`。
初始工作区干净,产品源码截止 `2d8e58e995c4ac2e3d885b028e588d49d06f05cf`。
四个提交分别为取消轮次历史、执行清单浮层、智能心跳增量回顾与监督建议、
笔记保存后退出编辑。双语正文维护在 `resources/release-notes.json`;
已发布正文保留,两个 FEATURES 同步当前能力和剩余边界。
Agent 保持 0.15.1,不发布新标签或修改其历史说明。Agent daemon、协议实现和
Runtime 锁没有变化;共享合同新增的是桌面监督配置/API,历史构建在 Renderer/Main
进行,远程继续通过现有 history 字段传输。因桌面至 Agent 历史输入改变,
仍对当前源码完成真实 Host 取消后续聊验证。
## 数据与行为边界
schema 50 在既有事务中增加心跳介入方式、建议状态和建议表;旧报告中仍 proposed
的记忆迁为 pending 约定,不确认或删除原记录。已读迁移指南并核对 49→50 回归;
没有读取或升级用户原库,也不声明大规模真实旧库性能已验证。
升级前关闭应用,备份完整数据库及相邻 `notes/`;降级需恢复完整升级前备份。
手动回顾默认增量,显式重新整理不推进或重置共享进度。建议以本次结果中的依据为准,
有候选才进行额外模型调用;项目与全局分别维护进度,会话级实时介入仍未实现。
本地 OpenCode 的取消会话标记不等同于 Agent 实现;远程使用共享桌面历史输入与
原有取消/会话恢复路径,不声称复用了本地 OpenCode 类。
## 验证记录
- 类型检查和 lint 通过。
- 首轮定向 34 文件:1092 通过、1 跳过、1 失败。会话合并用例只等待 Runtime
mock 调用便触发新会话,单独复跑通过;补充等待已发送消息可见,保留两次同批
新建请求和最终会话数量断言。最终 34 文件全部通过,1093 项通过、1 项既有条件跳过;
类型检查、lint、双语发布说明及差异检查再次通过。
- 当前源码在共享 Linux x64 Host:OpenCode、Continue 各在 Ask 输出后主动取消,
用生产 `buildRuntimeHistory` 生成中断历史,再以 Execute 发送无工具的新问题。
两条路径均只回答新问题,未续写原列表;归属进程最终为 0。
本轮恰好 4 次真实上游请求,全部 HTTP 200,无重试或假模型;用户设置只读取
临时加密副本,未修改正式 Agent 安装和工作目录。
- 智能心跳开发记录中的 4 次 DeepSeek 请求是该功能开发证据,不计入本轮新增调用。
参见[实施记录](../features/conversation-supervision/progress.md)。
- 不执行本地生产构建、打包或安装包启动。全仓验证和生产构建以最终提交的 CI
为准,标签只能在该门槛通过后创建。
## 中断后扩展候选
用户新增四个提交并授权再次推送继续发布;源码范围扩至
`6edfc0046b83150f2027167352000f161fb0dd0a`,恢复时工作区干净。
新增 OpenCode 终端依赖缓存、短 MCP 名称与并行占用、工作区固定工具栏、
笔记双栏布局及手动标签。说明和功能矩阵已同步,目标 schema 改为 51。
标签支持多选 AND 筛选、重命名/合并/删除及桌面笔记工具字段,非自动分类;
Agent daemon 和协议不变,Agent 仍为 0.15.1。
上一候选 `925d410` 的 Agent CI 通过,Desktop `36664224641` 为 5481 通过、
55 跳过、3 失败,生产构建未执行,未创建标签。两个笔记分析用例的旧编辑按钮
假设已由新提交修正;新源码真实 Electron 复验已通过。监督布局仍查找旧记忆建议
标题及已移除的心跳运行审计,修正为监督建议区域和两条建议断言、审计不重复展示,
保留页签、宽度、历史报告和导航检查。
扩展候选复验:15 文件、502 项通过,包含上述三个失败场景、笔记标签与布局、
工作区滚动布局、schema 51 数据库、MCP 网关/名称及原生终端缓存测试;
类型检查、lint、双语说明校验和差异检查通过。该轮测试使用隔离 fixture,
没有新增付费模型调用;先前 4 次真实 Host 请求仅对应取消后续聊范围,
不作为新增标签工具的完整远程 UI 实测声明。
`89fc100` 的 Agent CI 通过;Desktop `36707706849` 为 5507 通过、55 跳过、
2 失败,前述三个失败场景均已通过。笔记滚动在请求完成回调中早于 DOM 提交,
以立即执行动画帧的回归稳定复现后,改在布局 Effect 中定位已挂载记录。
手动压缩独立复跑通过,夹具同步当前分层选择格式并等待目标会话历史显示;
尚不将独立通过视为 Linux 全仓通过。修正后完整 App/笔记组件及真实 Electron
导航/压缩共 450 项通过,类型检查和 lint 通过,无新增外部模型调用。
@@ -0,0 +1,70 @@
# Desktop 0.15.6 发布准备
日期:2026-10-01。用户授权提交当前未提交改动并继续双远端推送与发布,
如 Agent 实际受影响可升补丁版本;另明确本轮仅标准六平台,不含 LoongArch。
## 候选范围
基线 `v0.15.5` 为 `3be55348a215f4343326b1950a69529247cbae6a`,
六个新增提交截止 `9cc5dc124464a67ed93014c53cbc93f96764caa2`。
初始未提交改动仅 `application-center.css` 一行选择器:
侧栏底部仅对第二个直接子元素文字 span 禁止伸缩,避免影响其他 span。
已核对并纳入本轮,未包含凭据、日志或临时产物。
新增内容包括磨砂外观、输入/缓存会话切换渲染优化、直接图片预览、设置与笔记布局、
本地 MCP 恢复及不确定名称退役、诊断和执行统计索引。
双语说明和功能矩阵按实现更新,不将已有开发记录中的全量失败记为成功。
Agent 保持 0.15.1:相对其发布标签,daemon 与锁文件无变化。
本地 OpenCode 的 HTTP 注册恢复属于 `OpenCodeRuntime`,远端使用
`RuntimeAcpBackend`/`AgentImageToolMcp`,不经过这条 HTTP 注册路径。
没有据此声称远程恢复得到同样改进,也没有新发布 Agent 包。
## 数据与验证
已读数据库迁移指南。schema 52 在事务内增加任务事件部分索引,不转换消息正文,
不执行 VACUUM;建立索引的耗时受历史规模影响,本轮未测真实大库升级。
升级前关闭应用并备份数据库和相邻 `notes/`,降级需要完整升级前备份。
- 当前源码 17 个定向文件、886 项通过,包含真实 OpenCode + HTTP 网关合成服务的
MCP 故障恢复和晚到初始化回归,数据库/统计、诊断、设置、App、图片附件和真实
Electron 磨砂、笔记布局、工作栏设置布局。
- `npm run typecheck`、`npm run lint` 通过;本轮新增付费模型调用 0 次。
- 完整测试和生产构建等待精确候选 CI,不运行本地生产构建、打包或安装包探针。
## 首轮 CI
候选 `cc451f6` 已含用户未提交样式修正并同步两个远端。Agent `36743915140`
通过,Desktop `36743915207` 为 5573 通过、57 跳过、1 失败,生产构建未执行。
磨砂测试切换主题后立即读取样式,窗口按钮 color 正在过渡,导致深色关闭态与
先前基线不同。测试改为等待当前 CSS Transition 完成,保留完整样式、几何、
拖拽、对比度及原生交互断言,不修改产品外观或全局超时。
修正后完整磨砂 Electron 回归、类型检查及 lint 通过;双语说明与产品源码不变。
`8201c9c` 的 CI `36746918608` 中磨砂回归通过,唯一失败为 161 条历史的多次
热切换 App 用例超过默认 5 秒(5573 通过、57 跳过)。只将该用例预算设为 15 秒,
保留消息数量、DOM 复用、零额外渲染、草稿、附件、展开状态和滚动位置的全部断言;
不修改全局超时或将运行时性能要求替换为等待通过。
调整后完整 App 319 项、类型检查及 lint 通过。
## 再次扩展候选
用户要求将当前未提交改动一起提交。新增 `0a02a17`(新建笔记入口)和
`a3b41bf`(设备共享元数据),并纳入五份应用导航文档修改。
设备共享需另行运行 ShareServer,首期面向本地未认证服务,仅注册设备及管理元数据;
不执行远程能力,不传输或检索知识正文,设备 ID 不构成所有权认证。
Agent 实现和锁不变,仍不升版。
扩展源码类型检查和 lint 已通过。用户随后明确要求停止本地测试、直接推送等 CI,
进行中的定向测试已中断,不计为通过,也不沿用旧候选的全量结果。
最终发布取决于包含新增提交和文档的精确候选 CI 与生产构建。
## 扩展候选焦点回归
`2c5300d` 的 Desktop CI `36753498640`:5580 通过、58 跳过、1 失败,
生产构建未执行;Agent CI `36753498643` 通过。
笔记导航用例在打开并取消带 `autoFocus` 标题输入框的新建表单后,
才断言此前打开详情时的返回按钮焦点,混合了两个不同交互阶段。
将原焦点断言移至打开新建表单之前,并断言新建标题输入框取得焦点;
保留返回列表、滚动位置、搜索内容和原卡片焦点断言,不改产品代码或延长超时。
按用户要求不运行本地测试,由新候选 CI 验证。
@@ -0,0 +1,53 @@
# Desktop 0.15.7 发布准备
日期:2026-10-01。基线为 `v0.15.6`,源码候选 `c8f327f`。
检查时工作区干净,用户要求纳入的先前改动已由该提交包含,共 75 文件。
本轮 Desktop 升至 0.15.7,Agent 保持 0.15.1,仅标准六平台,不含 LoongArch。
## 范围
- 流式 Markdown 分段复用、会话行与派生值缓存。默认 16ms 节奏不变。
- 运行记录进入时读取、可见时 15 秒刷新及页头手动刷新,不是逐事件实时视图。
- 本地自定义 MCP 发现与动态目录故障按服务隔离,取消和撤销仍生效。
- 模型连接中的受管模型下载源弹窗,确认保存、取消不变及相关入口统一。
- 设备共享默认关闭的测试入口,显式启用,关闭不撤销服务端记录。
网关修改只涉及本地自定义 MCP 发现方法;远程 Agent 的独立 MCP 实现与
Story Graph 绑定不经过这些方法,Agent 源码、锁及包未变。边界与开发验证见
[工作栏进度](../features/assistant-workbar/progress.md)。
数据库仍为 schema 52,无新增迁移。
## 证据与发布门槛
本轮遵照用户要求,不运行本地测试、类型检查、lint 或生产构建,由新候选 CI 验证。
已有开发会话记录不等于本候选全仓通过;网关会话的历史全仓失败也不掩盖。
流式输入测量见 [组件测量记录](../../tests/support/streaming-input-results.md),
不据此声称完整 App 或所有机器的相同比例提速。本轮外部模型调用为 0。
中英文 FEATURES 已核对设备共享默认值、下载源位置及实现边界,并补充本轮性能和活动行为。
发布说明保留 schema 52 备份提醒,不修改历史已发布条目。
双远端推送精确候选后,必须通过主分支全量验证与生产构建,并确认候选及双语说明后,
才能创建不可变 `v0.15.7` 标签。
## 首轮 CI
`227507d` 的 Desktop CI `36872224138`:5613 通过、59 跳过、2 失败,
生产构建未执行;Agent CI `36872224140` 通过。
英文设置布局 fixture 在 640×420 窗口将控件居中到包含固定页头的整个滚动区,
点击检测被较高的英文页头遮挡。测试共用滚动辅助函数改为扣除实际页头高度的
可见区域,仍检查命中、溢出、文字宽度和原生键盘交互,不改产品布局。
旧 outbox fixture 先建当前数据库,再降为 schema 23 并执行完整迁移,
超过默认 5 秒;仅该用例设 15 秒,保留终态、尝试次数及附件清理断言。
修复不修改发布说明或运行时代码,按用户要求继续交新候选 CI 验证。
## 第二轮 CI
`7cb74cc` 的 CI `36874700588`:首轮两项失败已通过,但仍有 5613 通过、
59 跳过、2 失败,生产构建未执行。检查提交链确认先前提交期间并行加入了
`5e77118`,移除文字输入框占位提示的问候语,App 单元测试已同步为两行,
Electron 侧栏探针却仍要求三行,导致断言失败。探针同步为两行并检查首行键盘提示,
保留平台快捷键、输入、清空、焦点、布局和搜索检查。
Markdown 用例逐一比较超过 1500 个流式前缀,超过默认 5 秒;
仅该用例改为 15 秒,保留全部 DOM 等价、前缀数及多段覆盖断言。
用户明确授权连续修复、双远端推送和 CI 通过后的标签发布,不再中途确认。
本轮仍不运行本地测试,以上修复须由下一次精确候选 CI 验证。
@@ -0,0 +1,57 @@
# Desktop 0.15.8 / Agent 0.15.2 发布准备
日期:2026-10-02。Desktop 基线 `v0.15.7`,Agent 基线 `agent-v0.15.1`;
已提交源码固定为 `b1129cedec5e4890d82fcddc186b13467f466a6f`。
仅纳入四个已提交功能修正及本次必要发布准备,不提交已有 Story Graph、
会话监督设计或宣传文章等未提交文件。
## 范围
- `dd29f30`:保存会话图片到本机或 Host、设置中列出图像工具。
- `f4d8fe7`:新记录取消、编辑焦点返回、笔记正文使用可用宽度。
- `2e5c348`:缺失内置浏览器设置时默认启用,保留已保存配置。
- `b1129ce`:缺失磨砂外观字段时默认开启,保留已保存配置。
- 数据库仍为 schema 52;Desktop 标准六平台,无 LoongArch。
- Agent 保存路径涉及守护进程、工具声明与桌面图像二进制回复,升至 0.15.2,
最低 Desktop 0.15.8;协议主版本及 Runtime 锁版本不变。
## 当前源码共享 Host 验证
共享 Linux x64 Host 的现有 SSH 固定身份可达。在独立 `/root/tmp` 目录运行
当前源码 AgentImageToolMcp,通过真实 SSH 传递工具消息,与 Desktop 的
MainImageToolSession、ImageGenerationService、真实 SQLite 会话协作。
图像为本轮生成的合法随机 PNG,196992 字节,超过单个 128 KiB 分块。
保存后的 Host 文件 SHA-256 与 Desktop 原始内容相同;默认覆盖拒绝、明确覆盖成功、
其他会话图片 ID 拒绝、Ask 写入拒绝且无文件落盘。共 8 个回复帧,
最大 174892 字节,低于 256 KiB 上限。外部模型调用 0 次。
这是生产保存服务与 Agent MCP 的真实 Host 工具层验证,不冒充完整 App、
模型自动选工具、安装包或三个 Host 架构验证。使用 SSH 流作为该探针传输;
未改变用户工作区、产品配置或 Host 已安装 Agent。
临时探针最初因打包 Zod 初始化顺序、混入仅 Desktop 依赖而无法启动,
修正探针依赖边界后验证通过;这些失败不计为产品模型调用或产品修复。
探针和包不进入仓库。用户要求不跑本地测试套件,精确候选由 CI
执行全量验证和生产构建后,才允许打 Desktop 和 Agent 不可变标签。
## 发布核对
中英文 FEATURES、Desktop 与 Agent 双语说明必须与上述实现边界一致。
只暂存必要发布文件,保留工作区用户文件;双远端核验候选和标签一致。
公开验证包括六平台 Desktop、三平台 Agent 签名目录、GitHub/OSS 同步及 Latest 分离。
## 首轮 CI 修正
`20317c2`:Agent CI `36960575058` 成功;Desktop `36960575157` 为
5620 通过、59 跳过、4 失败,构建未执行。
Obsidian 专项 IPC fixture 未安装浏览器服务,显式关闭无关浏览器能力;
MCP 列表断言纳入新增图像卡片的数量和名称;覆盖层原生点击先将控件居中,
避免磨砂顶栏覆盖 `nearest` 保留在顶部的控件,并补充命中遮挡诊断。
保留所有业务、覆盖层和原生交互断言,不改变产品默认值、关闭沙箱或跳过测试。
这些修正由新候选 CI 验证。
`5368e0e` 的 CI `36962139369` 中上述四项通过,余下新记录取消焦点用例失败,
5623 通过、59 跳过、1 失败。测试用 `fireEvent.click` 点击取消不会像原生点击
那样移动焦点,确认框记住的仍是先前的返回总览按钮。用例先等打开详情的焦点完成,
明确聚焦取消按钮后再点击,并等继续编辑实际聚焦编辑器后进行下一步;
保留放弃草稿后回到记录与未保存新记录的断言,不改产品焦点规则或延长超时。
@@ -0,0 +1,56 @@
# Desktop 0.15.9 / Agent 0.15.3 发布准备
日期:2026-10-03。Desktop 基线 `v0.15.8`,源码
`50481deaa1f7d17184500bb6f5abb3e1a0c8bbd5`,37 个提交、163 个文件。
检查时工作区干净,范围仅为已提交实现与必要发布准备。
标准六平台 Desktop、三平台 Agent,不含 LoongArch。
## 范围与边界
- 监督共享时间线、故事归属与人工调整、经验抽取、故事回顾与心跳建议;
平铺与时间螺旋、紧凑图谱控件及 WebGL 生命周期修复。
- schema 53~56:合并既有进度,增加故事、经验及建议类型,保留历史结果。
旧数据库副本验证和模型质量抽查见[监督进度](../features/conversation-supervision/progress.md),
不将 schema 53 的单库耗时当作本轮完整迁移保证。
- 会话外部存储、侧栏和消息窗口化、Markdown 缓存、只读 Worker、活动差量写入、
远程事件 checkpoint 批次持久化;保留丢失焦点、滚动、去重与提交顺序检查。
- Execute 配置工具不再重复弹原生确认,Ask 只读边界不变。
- Electron 44.5.1;Node、OpenCode、Continue 锁不变。
- Agent 虽无守护进程文件变更,但打包的 Story Graph 工具 schema 增加 story/experience,
旧 Agent 不支持这些输入,因此升级 0.15.3,最低 Desktop 0.15.9。
性能范围及暂缓项见[性能计划](../roadmap/product-performance-experience-improvement-plan.md)。
不把组件或特定机器的指标作为普遍提速保证;向量仍可全表扫描,Worker 失效会同步回退,
窗口化的未挂载行不支持页内查找,快速滚动可能短暂显示占位。
故事、经验、建议与输出重试有模型费用,自动结果须人工核对。
## 本轮真实 Host 验证
当前源码临时打包到隔离测试目录,不构建生产安装包。使用现有固定 Host 身份及
凭据存储,通过生产 Agent Attach、模型桥及远程 ACP 路径执行 OpenCode 与 Continue
各一次 Ask、一次 Execute。OpenCode 探针显式选择既有 Continue 模型配置,
不修改产品设置,也不是产品静默降级。
共 **4 次真实提供方请求,4 次 HTTP 200**,无模拟推理、无自动请求重试。
验证 Runtime 释放后重建、历史恢复和本轮进程清零。
每次消费真实远程语义流,通过生产 `RemoteEventBatcher` 与
`AssistantDatabase.appendRemoteTaskEventsOnce` 在 checkpoint 前落库;
对完整已提交事件再次写入均返回已存在。四次请求分别记录:
| Runtime / 模式 | 批次 | 事件 | checkpoint |
| --- | ---: | ---: | ---: |
| OpenCode Ask | 3 | 4 | 2 |
| OpenCode Execute | 21 | 40 | 20 |
| Continue Ask | 3 | 4 | 2 |
| Continue Execute | 23 | 44 | 22 |
验证没有启动完整 Renderer,也不声称覆盖会话消息投影的所有恢复分支。
Story Graph 新对象与来源语义由本轮源码测试及开发记录覆盖,以上四次请求不调用图谱工具。
开发文档中的模型调用不计入本轮 4 次。临时探针的两次编译钩子定位失败均发生在请求之前。
## 发布门槛
按用户要求,不执行本地测试套件、lint、类型检查或生产打包;
精确候选推送双远端后由 CI 做全量验证和生产构建,再打不可变双版本标签。
中英文 FEATURES 需同步共享进度、已实现故事经验、成本和窗口化限制。
公开核验 Desktop Latest、标准产物、Agent 签名目录、两个源及网站,保留任何并行未提交工作。
+4 -2
View File
@@ -21,8 +21,10 @@ diagnosis of their cause.
- Native crashes and main-frame load failures use the existing bounded desktop
diagnostics store (`desktop.renderer.gone`, `desktop.renderer.load-failed`,
stage `renderer`). Persistent records retain fixed summaries rather than raw
errors. Crash reason/exit code and React errors/component stacks go to their
respective process consoles. There is no new diagnostic IPC.
errors. Renderer exit records also retain Electron's enumerated `reason` and
numeric `exitCode` when written, read back and exported; older records without
these fields remain readable. The exit details still go to the Main console.
React errors/component stacks remain console-only. There is no new diagnostic IPC.
The boundary does not catch module evaluation/startup failures before React
mounts, event-handler exceptions, asynchronous rejections, a hung renderer, or
@@ -1,9 +1,13 @@
# 应用导航与系统工具入口
本功能整理 GoodBuddy 主侧栏的入口层级:系统入口保持固定,四个内置应用共用可调整的保存顺序,知识库始终启用并常驻;
智能心跳、魔法笔记和本机推理监控可通过应用管理控制启用和常驻。底部应用中心向上展开轻量锚定菜单,列出已启用应用;底部“管理应用”打开既有管理 Modal。系统工具由右侧工作栏 Tab 承载。应用商店是后续功能,首期不提供
本功能整理 GoodBuddy 主侧栏的入口层级:系统入口保持固定,五个内置应用共用可调整的保存顺序,知识库始终启用并常驻;
智能心跳、魔法笔记和本机推理监控可通过应用管理控制启用和常驻。底部“应用”向上展开轻量锚定菜单,列出已启用应用;底部“管理应用”打开既有“应用中心”管理 Modal。系统工具由右侧工作栏 Tab 承载。应用商店是后续功能,首期不提供
商店入口、推荐位或安装流程。
设备共享作为第五项追加到原有应用顺序,测试阶段默认关闭,启用后可从菜单和管理 Modal 打开,不提供常驻
开关;服务地址与设备名称在其页面保存。首期仅支持本地服务的设备注册与元数据目录,详见
[设备共享](../device-sharing/README.md)。
当前分期见 [PRD 第 8 节](./prd.md#8-分期),实施步骤见[技术设计第 11 节](./technical-design.md#11-实施顺序)。
本轮 A–D 已接入生产源码:应用菜单、应用管理 Modal、共享应用设置、持久化偏好及本机推理监控。菜单不按打开历史或常驻状态筛选,无模态遮罩;点击应用行关闭菜单,知识库、智能心跳和魔法笔记打开主内容区页面。本机推理监控从菜单、常驻入口或管理 Modal 打开时均使用独立 Modal,保留底层工作区。管理列表、菜单和侧栏按同一保存顺序排列,侧栏按启用和常驻过滤。知识库在管理 Modal 中提供“打开”和排序,不提供启用、常驻开关或通用空设置;原工作区配置及心跳单条计划 enabled 保持原义。验证范围及集成缺口见[实施进度](./progress.md)。
@@ -16,13 +20,18 @@
相关验收见 [User Stories](./user-stories.md#7-后续需求场景)。
项目活动汇总独立于上述应用中心计划,范围见 [FR-14](./prd.md#fr-14-项目活动汇总)。
计数规则由[活动逻辑](./logic-design.md#9-项目活动汇总)定义,入口与级联菜单见
生产项目入口是一个固定双行按钮:第一行显示项目名,第二行显示全局活动或“暂无活动”,点击任一行都打开统一工作空间菜单并默认预览当前项目。项目分类依次为“全部项目、本地、远程、消息通道”,点击“全部项目”切换全局范围,悬停预览会话,点击项目直接进入。生产最近会话上限为 10 条,验证状态见实施进度。
计数规则由[活动逻辑](./logic-design.md#9-项目活动汇总)定义,统一菜单见
[活动界面](./ui-design.md#10-项目活动汇总),数据加载与会话跳转见
[技术设计](./technical-design.md#12-项目活动汇总)。此处“活动”包括当前运行、待处理及本次客户端
会话内已完成但未查看的会话;完成提示在实际进入聊天后清除,不累计历史完成总数。
## 文档导航
待实现的应用扩展见 [FR-17](./prd.md#fr-17-主机管理与云环境管理待实现):
主机管理从设置中心迁入独立工作页面,云环境管理作为同级应用;业务定义由
[远程主机功能文档](../remote-host/README.md)维护。该计划不包含已接入的设备共享元数据页面。
现有 `heartbeat` 入口承载监督者工作回顾与故事线图谱,可管理启用、常驻和排序。应用默认关闭;完整执行边界见[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)。未来市场复用同一应用身份,不增加平级重复入口,目标定义见[监督者产品设计](../conversation-supervision/supervisor-prd.md)。
| 文档 | 权威职责 |
@@ -32,8 +41,24 @@
| [功能逻辑设计](./logic-design.md) | 分类规则、入口状态、优先级与失败结果 |
| [UI 设计](./ui-design.md) | 侧栏信息架构、控件、文案、键盘和响应式行为 |
| [技术设计](./technical-design.md) | 现有模块映射、状态契约、持久化和实施顺序 |
| [实施进度](./progress.md) | 应用中心、设置及项目活动级联的实现与验证记录 |
| [实施进度](./progress.md) | 应用中心、设置及统一工作空间菜单的实现与验证记录 |
| [应用中心交互 Demo](./navigation-demo.html) | 对比六个候选方案;配套 CSS 和 JS 仅服务原型 |
| [工作空间交互 Demo](./workspace-demo.html) | 左上角开始菜单式项目与并发会话候选方案;配套 `workspace-demo.css` 与 `workspace-demo.js` 仅服务原型 |
以下保留工作空间 Demo 的历史原型说明,包括最近 5 条的旧上限。生产实现已接入独立的 `WorkspaceMenu`,以功能设计和实施进度为准;Demo 的分类布局、排序、快捷键及模拟设置不作为生产契约。
工作空间 Demo 可直接打开,展示项目工作空间、会话窗口、全局活动、状态筛选、待办处理与完成待查看。
左上角项目和活动摘要打开同一个锚定下拉菜单,没有独立总览页面或全屏遮罩。左栏筛选项目,右栏按状态分组显示紧凑会话列表,列表在菜单内滚动。
菜单桌面宽度扩至 920px、高度至 640px,并受视口约束;左栏按本地项目、远程项目、消息通道分组。远程项目按主机归属展示,主机连接状态与会话活动计数分开;分组、主机和通道均使用演示数据。
左侧通过本地、远程、消息通道 Tab 点击切换分类,项目悬停预览;演示包含 20 个本地项目、20 个远程项目和 3 个消息通道。搜索跨分类匹配项目名、主机及类型,远程主机分组支持折叠。“全部项目”、搜索和分类固定,项目列表独立滚动。
项目行最小高度 52px,无活动时显示名称、工作目录两行;有活动时显示名称、活动状态、工作目录三行,高度随内容增长。当前项目以勾选区分于悬停预览。设置按钮独立,悬停或聚焦显示,触屏常显。设置弹窗可修改演示名称、说明、目录、默认模式及 Runtime;通道名称只读,执行空间不可修改。所有目录均为示例路径,不访问文件系统;原型未模拟真实目录选择、归档删除或远程恢复流程。
点击项目入口默认筛选当前项目,点击活动摘要查看全部项目;悬停左侧项目只预览右侧会话。点击项目直接进入并恢复上次打开的会话,尚无会话则创建新会话;点击右侧会话直接进入指定会话。设置按钮独立,不触发项目切换,会话草稿保留。
支持浅深主题、窄屏、点击外部关闭、Escape 关闭及 `Ctrl/Command+Shift+K` 切换菜单。
菜单底部“新建项目”展开名称表单,创建后进入新项目的首个会话;“新建会话”与状态筛选同排,在所选项目创建会话,全部项目视图下使用当前项目。窄屏状态筛选横向滚动,创建按钮保持可见。均只修改演示数据,重置后清除。
页面顶部可模拟后台完成,进入会话后清除该会话的完成提醒;已查看会话仍保留在最近会话中,不额外标记状态。
状态沿用正式活动入口:待处理、运行中、已完成;待处理会话区分“等待你的回答”和“等待审批”。不提供“可继续”状态或筛选。“已完成”仍表示完成未查看提醒,不是全部历史完成会话。
默认列表将待处理、运行中置顶,其余会话归入“最近会话”,按最近进入、发送或状态更新排序,最多显示 5 条;完成未查看在会话行标注。待处理和运行中不受此上限限制,也不重复列入最近会话。“已完成”筛选展示全部未查看结果,不受最近 5 条限制。没有活动时仍显示最近会话。
所有执行、审批和消息均为内存中的模拟,不访问真实项目、不调用 AI;该候选方案尚未接入正式产品。
Demo 可直接在支持原生 dialog、popover 的现代浏览器中打开,无需依赖或构建。
六个方案共用本次页面会话内的配置,刷新后全部恢复默认,不持久化或修改真实设置。
@@ -50,7 +75,7 @@ Demo 的模型管理和 F 方案均使用模拟数据;正式本机推理监控
列表,悬停或点击项目向右展开会话子菜单,点击会话才切换项目与对话。子菜单与项目列表
相接,项目行保留子菜单箭头,会话行只显示名称与状态。支持上下方向键选择、右键进入、左键或 Esc 返回,顶层 Esc 关闭;窄屏在同一浮层内
进入会话层,并提供返回按钮。页面上方可切换“有待处理、仅运行中、仅当前项目运行、全部
空闲”四种场景。项目与会话均为演示数据;该级联交互已接入正式产品活动入口,范围与
空闲”四种场景。项目与会话均为演示数据;该旧级联交互已由生产统一工作空间菜单替代,范围与
验证记录见[实施进度](./progress.md)。
Demo 已移除左侧系统工具快捷区:文件、终端、浏览器和资源监控只在右侧
工作栏 Tab 中演示,不列入应用中心。旧原型仍直接打开整窗 Modal,其中使用可搜索应用列表。
@@ -16,7 +16,7 @@
2. 运行记录不进入工具注册表、应用显隐设置或插件市场。
3. 文件浏览器、终端、浏览器和资源监控始终是系统工具,不能卸载或被插件替换。
4. 左下角提供应用中心和独立固定设置入口;文件、终端、浏览器、资源监控只通过右侧工作栏 Tab 和“+”访问,不进入应用中心。
5. 四个内置应用共享保存顺序。知识库始终启用并常驻,位置可调整;智能心跳、魔法笔记、本机推理监控可独立设置启用和常驻。取消常驻不关闭功能,关闭不删除数据或导航偏好。
5. 五个内置应用共享保存顺序。知识库始终启用并常驻,位置可调整;智能心跳、魔法笔记、本机推理监控可独立设置启用和常驻。设备共享在测试阶段默认关闭,提供启用开关,启用后可从应用菜单打开,不常驻侧栏。关闭设备共享时退出其页面,不清除目录或撤销已发布记录。取消常驻不关闭功能,关闭不删除数据或导航偏好。
6. 入口不能扩大执行空间、文件、PTY、网络或浏览器权限。
7. 应用中心始终可达,点击向上展开无模态遮罩的锚定菜单,只列已启用应用,不使用打开历史或常驻过滤。菜单底部“管理应用”打开既有 Modal,关闭的可选应用在其中仍可搜索、配置和重新开启。
8. 所有入口和新请求的功能判断读取同一份保存成功的权威设置;保存中的目标值不提前生效。
@@ -25,6 +25,7 @@
```ts
type NavigationContribution =
| { type: 'application'; id: 'device-sharing'; source: 'builtin'; enabled: true; pinned: false }
| {
type: 'system-entry'
id: 'chat' | 'activity' | 'application-center' | 'settings'
@@ -201,7 +202,7 @@ User Stories 已同步上述入口规则,工具实例行为沿用右侧工作
首期覆盖应用中心访问、启用与常驻组合、顺序恢复、默认值与旧值迁移、共同设置、保存失败、
知识库固定入口、心跳应用启停、可选应用已打开页面恢复,以及右侧实例选择、本机 Home、托管 SSH、无会话、
目录失效、资源部分可用与采样停止。工具内部取消、超时、连接恢复及业务后台生命周期由
相应功能负责,本功能不复制状态机。底部入口打开轻量菜单,“管理应用”才进入与设置相同的整窗 Modal。实现与验证的剩余范围见[实施进度](./progress.md)。
相应功能负责,本功能不复制状态机。底部入口打开轻量菜单,“管理应用”才进入与设置相同的整窗 Modal。FR-14 覆盖统一菜单的入口与范围、预览与进入、最近条数、状态筛选、通道创建限制及会话期恢复,规则见第 9 节。实现与验证的剩余范围见[实施进度](./progress.md)。
## 8. 后续产品决策
@@ -256,14 +257,18 @@ ASR 按请求加载与释放,支持独立取消;TTS 尚无执行服务。具
- 通知只保存在 Renderer 内存,不跨重载或重启恢复,也不从普通未读状态推导。
- 仅当目标会话实际成为当前聊天、设置未打开且文档未隐藏时清除通知;普通侧栏导航和活动菜单
导航采用同一查看条件。已在查看的会话完成后不保留通知,恢复可见聊天时也清除通知。
- 展开活动项目、查看菜单、设置覆盖下保留挂载的聊天、隐藏文档以及取消设置离开都不算查看。
- 预览项目、查看菜单、设置覆盖下保留挂载的聊天、隐藏文档以及取消设置离开都不算查看。
没有实时状态且没有已完成未查看通知时才移除活动行。
级联菜单先按项目归属聚合活动行,沿用本地、SSH Host、通道的稳定项目顺序;缺少项目
元数据的归属组及“未归属项目”组放在已知项目之后。无归属组只用于访问这些会话,不增加
项目计数。每个项目内依次列待处理、运行中、已完成会话,同类别保留来源顺序。
统一菜单的分类、预览范围、状态筛选和项目搜索相互独立。项目名与活动摘要属于同一个按钮,点击任一行打开时分类为“全部项目”,预览范围为当前项目;点击菜单内“全部项目”分类将范围重置为全部。悬停或聚焦项目只更新预览范围,不导航、不读取会话详情。分类按全部项目、本地、远程、消息通道排列;搜索跨分类匹配名称、目录、主机名/地址及通道,搜索期间展开命中的主机组。远程能力关闭时隐藏 SSH 用户项目及其活动,消息通道仍可见。
展开项目只改变菜单层级,不导航。选中项目的活动行全部移除后返回项目列表;全局无活动行后
关闭菜单并隐藏摘要入口。后台更新保留仍存在的焦点项,删除焦点项时将焦点
移至当前层可用项。关闭菜单后,新的活动只恢复摘要,不会自动重新打开菜单。
左栏沿用本地、SSH Host、消息通道的项目顺序,主机支持折叠,不按活动数量排序。右栏合并内存会话摘要与活动行,按 Conversation ID 去重;摘要尚未到达的活动仍可显示。无项目归属的会话只在全部项目范围显示,不增加项目计数;已不存在或不可见项目的会话排除,通道项目只显示通道会话。
默认筛选依次列待处理、运行中、最近会话。前两组不限条数且不重复列入最近;组内排序时间取最新消息创建时间与本次应用会话内访问/状态变化时间的最大值,无消息时用会话更新时间参与比较,同时间按 ID 排序,不按置顶优先排序。最近组最多 10 条,已完成未查看会话归入最近组并标注状态;“已完成”筛选列出全部完成未查看会话,不受 10 条限制。
首次活动状态快照只建立基线,不更新最近时间;后续状态变化更新时间,唯独从已完成变为无活动的提醒清除不更新时间。实际打开会话仍记录访问时间;悬停预览不记录。访问与状态时间仅保留在当前 Store 内存中,不跨重载或重启恢复。
点击项目经离开检查进入聊天,优先恢复本次应用会话内为该项目记住的有效 Conversation ID,否则按最新消息时间从候选会话中选最近一条,无消息时取会话更新时间。普通项目无会话时创建;通道项目只选通道会话,无会话时显示空态。记忆不跨重载或重启保存。点击会话仍按确切 ID 导航;设置按钮独立,不触发项目切换。“新建会话”使用预览项目,全部项目范围使用当前项目;目标缺失或为消息通道时禁用。
全局无活动时第二行显示“暂无活动”,按钮仍可打开菜单并预览当前项目的最近会话或空态,也可在菜单内切换全部项目范围。活动归零不改变控件高度,也不关闭已打开的菜单。点击外部、焦点离开、Escape 或隐藏侧栏关闭菜单,后台活动不自动打开。查看预览不清除完成通知,实际进入可见聊天才按上述规则清除。
@@ -46,7 +46,7 @@
- 用户从侧栏底部进入应用中心,查找、打开和配置内置应用。
- 主导航承载应用入口,本机推理监控打开 Modal,其余内置应用打开完整页面;辅助工具由工作栏承载。
- 运行记录始终可发现,不受应用显示设置和插件状态影响。
- 四个内置应用均支持排序;智能心跳、魔法笔记和本机推理监控可独立设置启用与常驻,知识库始终显示,未常驻的已启用应用仍可从应用菜单打开。
- 五个内置应用均支持排序;智能心跳、魔法笔记和本机推理监控可独立设置启用与常驻,知识库始终显示,设备共享在测试阶段默认关闭,可启用但不常驻侧栏。未常驻的已启用应用仍可从应用菜单打开。
- 为插件应用和插件工具保留明确的入口契约,不提前建设插件安装能力。
## 4. 非目标
@@ -75,7 +75,7 @@
### FR-3 内置应用区
主导航在当前对话之后按共享保存顺序显示应用,知识库始终显示,可选应用仅显示已启用且常驻项;运行记录位于应用区之后。四个应用均可排序。本机推理监控入口打开独立 Modal,其余应用打开对应一级页面,保留
主导航在当前对话之后按共享保存顺序显示应用,知识库始终显示,可选应用仅显示已启用且常驻项;运行记录位于应用区之后。五个应用均可排序。本机推理监控入口打开独立 Modal,其余应用打开对应一级页面,保留
现有徽标、范围和页面状态。管理 Modal 列出全部内置应用,知识库只提供“打开”和排序。
### FR-4 隐藏应用
@@ -86,7 +86,7 @@
### FR-5 应用中心入口
侧栏底部提供应用中心和独立设置入口。点击应用中心向上展开锚定按钮的轻量菜单,无模态遮罩。菜单列出已启用应用,不按是否打开过、历史记录或常驻状态筛选;点击应用行关闭菜单,本机推理监控打开 Modal,其余应用在主内容区打开页面。
侧栏底部提供“应用”(英文 `Apps`)和独立“设置”入口,两个按钮均显示文字。点击“应用”向上展开锚定按钮的轻量菜单,无模态遮罩。菜单列出已启用应用,不按是否打开过、历史记录或常驻状态筛选;点击应用行关闭菜单,本机推理监控打开 Modal,其余应用在主内容区打开页面。
菜单底部“管理应用”关闭菜单并打开既有管理 Modal,在其中浏览、搜索全部内置应用,为已关闭的可选应用提供设置与重新开启入口。无搜索结果时显示空状态并可清除搜索。
首期不展示商店、安装、推荐或系统工具清单。入口具有可访问名称,Escape 关闭后恢复触发位置的焦点。
@@ -127,13 +127,13 @@ Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止
### FR-11 应用导航设置
管理 Modal 的四个应用行均提供上下移动和拖动排序,不受启用或常驻状态限制;管理列表、菜单和侧栏共用一份顺序。箭头边界按完整顺序计算,搜索过滤不丢失隐藏项。智能心跳、魔法笔记和本机推理监控另提供应用设置、常驻管理;知识库提供“打开”和排序,不添加通用空设置页或开关。原工作区配置入口及心跳单条计划 enabled 保持不变。设置中“启用应用”与“常驻左侧菜单”分别保存,
管理 Modal 的五个应用行均提供上下移动和拖动排序,不受启用或常驻状态限制;管理列表、菜单和侧栏共用一份顺序。箭头边界按完整顺序计算,搜索过滤不丢失隐藏项。智能心跳、魔法笔记和本机推理监控另提供应用设置、常驻管理;知识库提供“打开”和排序,不添加通用空设置页或开关。设备共享提供启用设置、打开和排序,不提供常驻。原工作区配置入口及心跳单条计划 enabled 保持不变。设置中“启用应用”与“常驻左侧菜单”分别保存,
修改即时提交;请求期间串行处理并禁用会修改同一配置的控件,失败就地反馈。
魔法笔记设置统一从应用中心对应行进入,笔记页与全局设置不再提供重复入口。持久化成功后的设置变更同步覆盖工具配置写入;每次重开中心先刷新并锁定修改,较新的确认快照优先于迟到的读取结果。
### FR-12 偏好持久化
四个应用的顺序及可选应用启用、常驻按设备持久化,重启后恢复。默认顺序为知识库、智能心跳、魔法笔记、本机推理监控。心跳应用默认关闭,其余应用默认启用;心跳和魔法笔记默认常驻,本机推理监控默认不常驻;
五个应用的顺序及可选应用启用、常驻按设备持久化,重启后恢复。默认顺序为知识库、智能心跳、魔法笔记、本机推理监控、设备共享。心跳和测试阶段的设备共享默认关闭,其余应用默认启用;心跳和魔法笔记默认常驻,本机推理监控默认不常驻,设备共享不提供常驻开关;
魔法笔记待办数量默认显示,评论方式与形式沿用现有默认值。迁移保留已保存的启用、常驻和排序选择,
旧魔法笔记入口开关绑定业务启用值,迁为“启用应用”并保留原值;常驻使用独立配置,
只有已发布的独立显隐偏好才映射到常驻,不能由常驻推导业务启停。旧可选应用顺序补入知识库、智能心跳默认位置并保留已有相对顺序,具体补齐规则见[技术设计](./technical-design.md#43-持久化与-ipc)。`heartbeatEnabled` 的缺失值与保存值按[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)处理;不读取未发布草案中的 `knowledgeEnabled`。
@@ -146,10 +146,11 @@ Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止
### FR-14 项目活动汇总
用户在任一项目中都能查看跨项目的待处理、运行中与已完成未查看会话,并从活动列表直接进入所选会话。
汇总入口仅在有上述活动行时显示在项目选择器下方,无活动行时整行隐藏。点击后先显示有活动会话的项目,
悬停或点击项目展开会话子菜单,点击会话进入该条对话。项目菜单保留原有分组与顺序,并显示
各项目的活动数量;级联菜单内的项目顺序不随各类活动数量变化。
汇总不受当前项目、搜索条件或最近历史条数限制。计数与排除规则见
左上角使用一个固定两行的按钮:第一行显示项目名,第二行显示全局活动摘要,无活动时显示“暂无活动”,按钮高度不随活动变化。点击任一行打开同一个锚定工作空间菜单,默认预览当前项目,全部项目范围从菜单内切换;无活动时仍可查看最近会话。
左栏依次提供“全部项目、本地、远程、消息通道”分类,远程能力关闭时隐藏远程分类及其项目;支持跨分类搜索、远程主机折叠及独立项目设置。项目行按名称、活动数量、目录分行显示,无活动时省略数量行,项目顺序不随活动数量变化。
悬停或键盘聚焦项目只预览右栏,点击项目直接进入,优先恢复本次应用会话内上次打开的有效会话,否则选择最近会话;普通项目无会话时创建,消息通道无会话时保持空态。点击右栏会话进入确切会话。
右栏默认依次列待处理、运行中、最近会话,最近会话最多 10 条;已完成未查看在行内标注。“全部、待处理、运行中、已完成”筛选与“新建会话”同排,新建使用预览项目,全部项目范围使用当前项目,消息通道禁用手动新建。左栏底部“新建项目”进入真实项目创建流程。
活动计数、待处理、运行中及已完成筛选不受最近 10 条限制;菜单不继承侧栏会话搜索条件。计数与排除规则见
[活动逻辑](./logic-design.md#9-项目活动汇总),交互见[活动界面](./ui-design.md#10-项目活动汇总)。
验收覆盖跨项目后台运行、同会话多任务去重、提问与审批优先、较早的仍在运行或等待处理的
@@ -173,7 +174,7 @@ Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止
- 主导航能明确区分固定系统入口和可配置内置应用。
- 运行记录在所有应用显隐组合下保持可见并可打开。
- 左下角不再用“本地工作区”与系统架构占据主要空间。
- 应用菜单只列已启用应用,点击行关闭菜单;本机推理监控打开保留底层工作区的 Modal,其余应用进入主内容区。“管理应用”打开可搜索的既有 Modal。四个应用均可排序,知识库始终可用,三个可选应用可管理启用与常驻,首期无商店入口。
- 应用菜单只列已启用应用,点击行关闭菜单;本机推理监控打开保留底层工作区的 Modal,其余应用进入主内容区。“管理应用”打开可搜索的既有 Modal。五个应用均可排序,知识库始终可用,四个可选应用可管理启用,其中设备共享不提供常驻,首期无商店入口。
- 系统工具仅从右侧 Tab 与“+”访问;选择 Tab 不创建或改绑实例,“+”可显式创建额外浏览器或终端。
- 资源监控以 2 秒间隔更新,保留不超过 150 个样本;面板不活动、工作栏收起或窗口隐藏、
最小化后停止请求新样本。
@@ -188,7 +189,7 @@ Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止
- 侧栏最底部应用中心打开向上锚定的轻量菜单,无模态遮罩;底部“管理应用”进入与设置相同的整窗 Modal 和可搜索应用列表,设置按钮保持独立。
- 应用中心提供魔法笔记、知识库、智能心跳及本轮追加的本机推理监控入口,不要求安装。本机推理监控 Modal 与服务单独实现和验证。
- 四个应用共享排序;支持智能心跳、魔法笔记、本机推理监控常驻主导航及取消常驻,知识库始终显示。已启用未常驻应用仍可从菜单打开,取消常驻不删除数据或停止后台任务。
- 五个应用共享排序;支持智能心跳、魔法笔记、本机推理监控常驻主导航及取消常驻,知识库始终显示。设备共享仅从应用菜单和管理 Modal 进入,业务定义见[设备共享](../device-sharing/README.md)。已启用未常驻应用仍可从菜单打开,取消常驻不删除数据或停止后台任务。
- 通用应用设置统一从应用中心对应行进入;应用页面保留实际业务配置,本机推理监控保留“模型与连接设置”。
魔法笔记优先接入待办数量、AI 评论方式和评论形式,入口显示与常驻控制统一,避免重复开关。
- 文件、终端、浏览器保留在右侧工作栏 Tab,不在左下角复制入口,也不纳入应用中心。
@@ -264,3 +265,14 @@ Modal 只有服务列表,显示名称、简短状态、引擎、模型、资
资源监控仍是只读系统工具,本机推理监控聚焦本地服务状态。该项独立于商店接入生产源码,支持 OCR 空闲加载与释放、内置向量进程启动/停止/重启;TTS 显示不可用。后端保留请求登记及原有取消能力,供执行路径和服务影响校验使用。各引擎边界见
[技术清单](./technical-design.md#111-本机推理任务与执行服务),验证范围见[实施进度](./progress.md)。
原首期资源监控的 GoodBuddy 桌面进程统计范围不因此扩大。
### FR-17 主机管理与云环境管理(待实现)
2026-09-28 确认新增两个同级应用:“主机管理”和“云环境管理”。主机管理从设置中心迁入,
两个应用均从应用菜单、管理应用和常驻入口打开主内容区独立页面,不嵌入管理 Modal。
应用中心负责通用应用配置,业务操作在各自页面完成。
导航扩展须保留已有应用排序、常驻偏好和远程能力启用值;取消主机常驻不影响云环境使用
主机能力。两个应用的能力依赖、迁移范围及产品验收由
[主机与云环境 PRD](../remote-host/prd.md)和[功能逻辑](../remote-host/logic-design.md)定义,
业务页面布局见[界面设计](../remote-host/ui-design.md)。该项尚未实现,不改变上文五应用的当前状态描述。
@@ -1,5 +1,147 @@
# 应用导航实施进度
## 2026-10-04:双行入口合并为单个按钮
对应 FR-14、US-E1 至 US-E3。项目名和活动摘要现在属于同一个 `60px` 按钮,共用键盘焦点、展开状态和菜单关联。点击任一行默认预览当前项目,全部项目范围在菜单内切换;活动描述和按钮外的恢复反馈通过 `aria-describedby` 关联。移除了独立摘要触发引用与入口范围状态,保留最近 10 条及现有活动计数,没有新增轮询。
本次验证:`ProjectSwitcher`、`WorkspaceMenu`、`WorkspacePrimitives` 共 80 项通过;App 活动、审批、问题队列和流式侧栏相关测试 37 项通过;`npm run typecheck` 与 `npm run lint` 通过。真实 Electron overlay harness 在 `1280×800`、`1280×480`、`560×640` 的浅深主题下通过,验证单按钮、空闲与活动状态的 `60px` 高度、第二行点击、菜单布局和 Escape 焦点恢复,模型调用 0 次。
## 2026-10-04:双行入口与菜单布局调整
本节保留此次单按钮更正前的实现与验证记录;现行入口规则见上节。
对应 FR-14、US-E1 至 US-E3。已核对 `ProjectSwitcher.tsx`、`WorkspaceMenu.tsx` 和 `workspace-menu.css`:左上角控件固定高 `60px`,项目名和全局活动为同级按钮,第二行无活动时显示“暂无活动”;恢复反馈位于控件下方并关联可访问描述。左栏搜索框使用 `border-box` 限制边界,两栏之间使用 `--border-control` 分隔线,全部项目及搜索结果补充分组标题,右栏状态工具栏收紧间距。菜单项目行继续按名称、活动、目录分行显示,高度随内容增长。具体规则见[活动界面](./ui-design.md#10-项目活动汇总)。
以下结果由实现方提供,本次文档更新未重跑产品测试:
- 79 项定向组件测试、68 项项目相关 App 测试通过。
- 完整类型检查与修改范围 lint 通过。
- 真实 Electron 在 `1280×800`、`1280×480`、`560×640` 的浅深主题下验证搜索框边界、分隔线、紧凑工具栏、项目三行信息,以及左上角控件在空闲和活动状态下的固定高度与焦点。
- 模型调用 0 次。
最近会话上限已由 5 条调整为 10 条,生产设计与验收条目已同步。最终运行 `npx vitest run --project unit src/renderer/src/workspace-menu-selectors.test.ts src/renderer/src/WorkspaceMenu.test.tsx src/renderer/src/ProjectSwitcher.test.tsx`,3 个文件共 44 项通过,覆盖最近 10 条、活动不占最近名额、已完成筛选超过 10 条,以及完成后保留历史。Demo 文件未修改,仍保留原型的最近 5 条行为。
## 2026-10-04:生产统一工作空间菜单
本节保留首次接入时的历史实现与验证范围,其中最近 5 条为当时上限,现行调整见上节。
对应 FR-14、US-E1 至 US-E3。已核对生产 `WorkspaceMenu.tsx`、`workspace-menu-selectors.ts`、`ProjectSwitcher.tsx` 与 `App.tsx` 差异:项目入口与活动摘要共用菜单,替代独立 `ProjectActivity` 级联。已接入全部项目优先的分类 PageTabs、悬停预览与点击进入、最近 5 条、同排状态筛选与新建会话、真实项目设置和创建、项目三行信息,以及会话期内上次打开记录。预览只用内存数据,项目和会话列表复用共享窗口化;规则见[活动逻辑](./logic-design.md#9-项目活动汇总)与[技术设计](./technical-design.md#12-项目活动汇总)。
最终源码补充:菜单最近时间取最新消息时间与 Store 会话期访问/状态时间的最大值;`recordActivityStatuses` 首次只建立基线,清除完成提醒不更新时间。项目上次打开 ID 仍只保留在内存,获准后的项目进入及新建回调使用 `commitView`,避免重复离开检查。
以下为实现代理提供的验证结果,本次仅更新文档,未重跑产品测试:
- 82 项定向测试通过。
- 31 项选定 App 测试通过。
- 1 项真实 Electron overlay 测试通过,最终场景也在全量测试运行中通过。
- 最终完整 `npm run typecheck`、`npm run lint` 通过。
- 全量 `npm test` 在 600 秒超时前未完成。已结束的 App 文件共 347 项,346 项通过、1 项监督者设置测试失败;`tests/supervisor-layout.electron.test.ts` 也失败。尚无证据将这些失败归为既有问题,全量测试未通过。
- 首次 200 秒尝试报告 `activity-history-repository` worker 失败;第二次截至超时未报告该处失败。同期有并行性能修改,未确定失败原因,也不据此认定已修复。
本次没有性能基准结论。README 保留未提交的工作空间 Demo 描述,并区分生产菜单与旧演示;下方级联菜单记录保留当时实现和验证范围,不再定义当前生产交互。
## 2026-10-01:设备共享入口
新增 `device-sharing`,启用后应用菜单及管理 Modal 均进入主内容区页面;管理行可排序。
首期初始实现没有启用开关,同日后续改为测试阶段默认关闭、可手动启用,不提供常驻开关。
旧顺序通过现有存储归一化追加新项,保留原设置;权威写入要求五项完整顺序。
ApplicationCenter、ApplicationMenu、设置迁移以及 App 入口/排序回归已通过,真实 Electron
从应用中心进入页面并完成后端流程。精确命令、完整范围及全仓测试记录见
[设备共享实施进度](../device-sharing/progress.md)。下方历史四应用记录保留原验证时的范围。
## 2026-09-28: Composer shortcut initialization follow-up
The composer now retains successful shortcut-settings snapshots independently of
the startup app-info request. A failed or pending app-info read no longer drops a
successful shortcut save, and a late startup response cannot restore a disabled
hint. Registration remains authoritative; no default shortcut is advertised when
registration is unavailable. This preserves the existing [composer rule](../../../UI-DESIGN.md#131-聊天).
The failed/pending read regressions reproduced the missing hint before the fix.
The reported screenshot's actual registration status and IPC result were not
captured, so its specific runtime cause remains unconfirmed.
Validation: App, sidebar layout, main IPC, and shortcut-service suites passed
(500 tests, one skipped); `npm run typecheck`, ESLint on the four changed source
and test files, and `git diff --check` passed. The first broader run hit the
60-second command limit; the rerun completed in 95 seconds. No full-repository
suite, native Electron screenshot reproduction, packaging, or OS registration
probe was run for this follow-up.
## 2026-09-28:新建对话与搜索共用一行
已按[会话搜索](./ui-design.md#会话搜索)接入收起与展开状态,移除列表上方的常驻搜索头部。
沿用会话创建、延迟搜索和项目过滤接口。后续按用户反馈恢复默认按钮的聊天加号图标、原有
字号字重和整体左对齐;搜索展开后保留居中的同一图标。快捷键说明移至聊天输入框原生占位文案,
具体规则见[聊天 UI 规范](../../../UI-DESIGN.md#131-聊天)。
本次调整验证:`App.test.tsx` 与 `AppSidebarLayout.test.ts` 合计 316 项通过;最终长标签换行
调整后,布局 5 项与 `magic-notes-capture.electron.test.ts` 1 项复验通过。Windows Electron
在上述 6 种侧栏组合检查原字号字重、左对齐、图标居中、文字无溢出及搜索交互,并验证
原生输入后占位提示隐藏、清空后恢复。App 回归覆盖中英文、生图提示、macOS 快捷键格式、
全局快捷键缺省以及设置保存后的更新和停用。类型检查、修改文件 ESLint 与差异空白检查通过。
未运行全仓测试、打包或 macOS/Linux 原生窗口验证。模型调用 0 次。
此前搜索展开改动的验证记录:
```text
npm test -- src/renderer/src/App.test.tsx src/renderer/src/AppSidebarLayout.test.ts tests/magic-notes-capture.electron.test.ts
npm run typecheck
```
三文件合计 311 项通过。最终动画边界与文字宽度调整后,布局及 Electron 共 5 项、App 搜索
交互 1 项复跑通过,typecheck 通过。Electron 复用现有 App、生产 Preload、IPC 和临时 SQLite
测试环境,以英文界面检查浅深主题下 220px、420px 侧栏和短窗口抽屉,共 6 种组合;检查
展开过程及结束后的右边缘、列表位置、完整按钮文字、原生鼠标与 Escape、自动聚焦、关闭
焦点恢复、点击列表保留关键词及减少动态效果。中文 App 回归同时覆盖标题、正文、项目
过滤和加号创建。模型调用 0 次。
修改文件的 ESLint 与 `git diff --check` 通过。全仓 ESLint 有 10 个错误,均来自本任务未修改
的未跟踪 `sidebar-final-probe.ts` 和 `sidebar-final-run.mjs`。未跑全仓测试、打包程序或
macOS/Linux 窗口验证。中文新增规则经 deai-writing 扫描无阻断项,并已人工核对。
## 2026-10-02:补齐五秒排序检查
会话列表已接入 `useConversationListOrder`,保留实时行数据,定时调整展示顺序。
交互暂停与即时操作规则见[会话置顶与操作菜单](./ui-design.md#会话置顶与操作菜单)。
本地和消息通道共用此 Renderer 路径,未修改 Agent 执行、同步或持久化。
验证命令:
```text
npx vitest run src/renderer/src/use-conversation-list-order.test.tsx src/renderer/src/chat-conversation.test.ts src/renderer/src/App.test.tsx -t "useConversationListOrder|conversation display order|parallel streaming conversations|summary and detail order|sorts pinned conversations|merges persisted pin metadata|dedicated pin update"
```
3 个文件、23 项通过,覆盖五秒边界、连续增量、交互暂停与恢复、立即增删与置顶、
项目/搜索范围切换、计时器清理,以及 App 中本地/通道摘要更新的实际列表接线。
新增 hook 与相关测试的定向 ESLint、`git diff --check` 通过。未运行 Electron 窗口验证。
全仓检查未通过:typecheck 报并行修改中的 `App.tsx:5436` 缺少 `startTransition`;
ESLint 报打包文件及现有会话视图缓存的 refs 错误;`npm test` 在 120 秒后超时。
额外运行的草稿置顶失败测试未通过,实际显示持久化错误而非预期的置顶错误。
## 2026-09-28:并行会话输出时保持列表顺序
修复多个会话交替输出时,左侧列表随每个增量反复换位的问题。展示顺序统一使用消息创建
时间,摘要查询补充只读 `latestMessageAt`。交互规则见[会话置顶与操作菜单](./ui-design.md#会话置顶与操作菜单),
时间字段与合并边界见[会话列表读取与前端保留](../assistant-workbar/execution-history-storage.md#会话列表读取与前端保留)。
本地及远程事件的更新时间写入、快照合并判定和增量保存保持原实现。
聚焦验证命令:
```text
npx vitest run src/renderer/src/chat-conversation.test.ts src/renderer/src/App.test.tsx src/main/assistant/assistant-database.test.ts -t "conversation display order|parallel streaming conversations|summary and detail order|sorts pinned conversations|merges persisted pin metadata|lists lightweight history|persists only the changed conversation|preserves live .* across snapshot|loads only the opened history"
```
结果:3 个文件、13 项通过。覆盖并行文字增量与完成后顺序、保存时间继续推进、本地和消息
通道的摘要/详情刷新、新消息前移、置顶合并、同时间固定顺序、空会话回退、摘要附带实时
消息及真实 SQLite 摘要时间。历史加载回归继续验证刷新不重复保存未变化的消息。
修改过的 7 个 TypeScript 文件定向 ESLint 通过。中文文档按 deai-writing 自查清单复核。
本轮验证为 App 组件事件回归与真实 SQLite 测试,未进行 Electron 窗口或真实模型调用。
改动仅涉及桌面展示及桌面 Main 的只读摘要,未改变 Agent 协议或桌面到 Agent 的执行路径,
无需部署 Host。全仓 typecheck、ESLint 和 `git diff --check` 通过;全量测试运行十分钟后
超时,期间报告配置摘要和 Agent 离线安装测试失败,未取得全仓测试通过结果。
2026-09-23 文档校正:当前 `heartbeat` 已作为可选应用管理,默认值与执行边界见[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行),设置与导航契约见[技术设计](./technical-design.md#4-应用导航偏好)。下方日期记录保留当时实现与验证结果;其中“心跳始终显示”“无应用级开关”和旧字段删除说明不再代表当前行为。本次仅核对源码并更新文档,未重新执行功能测试。
## 2026-09-18:窄幅单列应用列表
@@ -326,7 +468,7 @@ Windows Electron 43.2.0 / Chromium 150 的当前组件 fixture 验证浅深主
## 2026-09-13:生产活动入口
对应 [FR-14](./prd.md#fr-14-项目活动汇总)、[US-E1 / US-E2](./user-stories.md#6-epic-e项目活动级联)。
对应 [FR-14](./prd.md#fr-14-项目活动汇总)、[US-E1 / US-E2](./user-stories.md#6-epic-e统一工作空间菜单)。
本页只记录项目活动入口,不表示应用中心计划已交付。
`App` 已接入 `ProjectActivity` 常驻摘要和锚定级联菜单。会话和项目计数继续使用现有
@@ -22,6 +22,7 @@ type WorkspaceView =
| 'knowledge'
| 'heartbeat'
| 'local-inference'
| 'device-sharing'
| 'activity'
| 'settings'
```
@@ -31,7 +32,7 @@ type WorkspaceView =
实现以[浏览器多实例技术设计](../assistant-workbar/browser-tabs-technical-design.md)为准。代码中尚无资源指标采集和资源监控
面板。侧栏底部已替换为应用中心与独立设置按钮。
应用中心、四应用共享排序、可选应用常驻及启用联动、共同设置页和本机推理监控已接入生产源码;本机资源监控的
应用中心、五应用共享排序、可选应用常驻及启用联动、共同设置页和本机推理监控已接入生产源码;本机资源监控的
Main/Preload/Renderer 实现仍待交付。系统工具只使用右侧工作栏已有 Tab 和“+”链路。
行为由[功能逻辑](./logic-design.md)定义,验证范围见[实施进度](./progress.md)。
@@ -61,7 +62,7 @@ Main/Preload/Renderer 实现仍待交付。系统工具只使用右侧工作栏
### 3.1 内置应用清单
```ts
type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat' | 'local-inference'
type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat' | 'local-inference' | 'device-sharing'
type BuiltInApplicationDefinition = {
id: BuiltInApplicationId
@@ -81,7 +82,7 @@ type BuiltInApplicationDefinition = {
应用中心调用既有 `navigateFromSidebar` 或同一页面导航函数,沿用未保存离开检查和焦点
策略。打开前检查确认的启用值;未常驻但已启用的应用不附带常驻写入。本机推理监控从常驻入口、菜单及管理 Modal 均经 `setView('local-inference')` 打开独立 Modal,保留底层工作区与设置草稿;其他应用继续页面导航。
`ApplicationMenu` 按四个应用共用的持久化顺序派生清单,仅按启用过滤,不读取常驻或打开历史。应用行先关闭菜单再导航;底部“管理应用”关闭菜单、恢复锚点焦点并打开既有 `ApplicationCenter` Modal。
`ApplicationMenu` 按五个应用共用的持久化顺序派生清单,仅按启用过滤,不读取常驻或打开历史。应用行先关闭菜单再导航;底部“管理应用”关闭菜单、恢复锚点焦点并打开既有 `ApplicationCenter` Modal。设备共享使用默认 `false` 的 `deviceSharingEnabled`,启用后进入主内容区页面,无常驻字段;迁移与关闭规则见[设备共享技术设计](../device-sharing/technical-design.md#当前首期实现)。
应用中心对应行的设置操作仅接受 `EditableApplicationId`;应用页和全局设置不提供重复入口。知识库的管理行提供“打开”和排序,原工作区实际配置保留,不添加通用空设置页。
共同页复用设置 Modal 容器、配置读取及更新方法,不建立独立配置存储或重复表单。
@@ -124,9 +125,9 @@ type ApplicationNavigationSettings = {
}
```
`applicationNavigation` 保存四个应用的完整顺序和三个可选应用的常驻布尔值。默认顺序为
`knowledge`、`heartbeat`、`magic-notes`、`local-inference`;心跳和魔法笔记常驻默认 `true`,本机推理监控默认 `false`。
权威返回 schema 和更新 schema 均要求四个 ID 各出现一次,拒绝重复、未知和缺失 ID;`pinned` 包含三个可选应用,缺失 `heartbeat` 时补 `true`,显式布尔值保留。Renderer 直接读取该顺序,再按各入口的可见性过滤。知识库始终启用、常驻,无可写开关键。
`applicationNavigation` 保存五个应用的完整顺序和三个可选应用的常驻布尔值。默认顺序为
`knowledge`、`heartbeat`、`magic-notes`、`local-inference`、`device-sharing`;心跳和魔法笔记常驻默认 `true`,本机推理监控默认 `false`,设备共享无常驻开关。
权威返回 schema 和更新 schema 均要求五个 ID 各出现一次,拒绝重复、未知和缺失 ID;`pinned` 包含三个可选应用,缺失 `heartbeat` 时补 `true`,显式布尔值保留。Renderer 直接读取该顺序,再按各入口的可见性过滤。知识库始终启用、常驻,无可写开关键。
按[排序规则](./logic-design.md#3-主导航组合规则)提交完整顺序;不持久化可推导的可见 ID 列表。
首期不预建插件字段,也不为未发布的旧隐藏 ID 草案增加迁移读取器。
@@ -166,7 +167,7 @@ Main 继续校验可信 sender、Zod 输入,复用 `ApplicationSettingsStore`
`true`、`false`、单选值和无关设置,不迁移或清理笔记、知识及会话业务数据。
新安装和升级时缺失整个 `applicationNavigation` 均使用共享默认值,本机推理监控启用但不常驻;本次不增加存储版本。已保存的常驻值均保留,无法区分旧默认 `true` 与用户主动固定,不强制改为 `false`。
仅存储读取允许补齐旧数据:缺失的 `knowledge`、`heartbeat` 按此顺序前置,再保留已有 ID 的相对顺序,最后按默认顺序追加缺失的可选应用。旧 `[local-inference, magic-notes]` 因此变为 `[knowledge, heartbeat, local-inference, magic-notes]`;完整四项顺序原样保留。缺失或空顺序采用默认顺序,部分 `pinned` 映射仅补缺失键,显式 `false` 保留。重复或未知 ID 仍按现有损坏文件处理规则处理,不静默去重或丢弃。版本 11 迁移时写入归一化结果;版本 12 读取时归一化,下一次设置保存时写入完整结果。IPC 和配置工具写入不接受这些部分数据。
仅存储读取允许补齐旧数据:缺失的 `knowledge`、`heartbeat` 按此顺序前置,再保留已有 ID 的相对顺序,最后按默认顺序追加缺失项。旧 `[local-inference, magic-notes]` 因此变为 `[knowledge, heartbeat, local-inference, magic-notes, device-sharing]`;旧完整四项顺序保留并在末尾追加 `device-sharing`。缺失或空顺序采用默认顺序,部分 `pinned` 映射仅补缺失键,显式 `false` 保留。重复或未知 ID 仍按现有损坏文件处理规则处理,不静默去重或丢弃。版本 11 迁移时写入归一化结果;版本 12 读取时归一化,下一次设置保存时写入完整结果。IPC 和配置工具写入不接受这些部分数据。
版本 12 属于未发布的分支格式,原草案 `knowledgeEnabled` 已移除,不增加兼容读取器。`heartbeatEnabled` 是当前应用启用字段,由 Store 归一化缺失值并保留显式保存值。
@@ -175,17 +176,21 @@ Main 继续校验可信 sender、Zod 输入,复用 `ApplicationSettingsStore`
原 `user-card` 整行设置按钮已替换为容器和相邻独立按钮:
```tsx
<div className="sidebar-footer">
<button onClick={toggleApplicationMenu} aria-haspopup="menu" aria-expanded={applicationMenuOpen} ...>
{t('navigation.applicationCenter')}
<div className="sidebar-footer sidebar-footer--applications">
<button className="nav-item" onClick={toggleApplicationMenu} aria-haspopup="menu" aria-expanded={applicationMenuOpen} ...>
<Grid2X2 aria-hidden="true" />
<span>{t('applications.menuLabel')}</span>
<ChevronUp aria-hidden="true" />
</button>
<button aria-label={t('navigation.settings')} ...>
<div className="sidebar-footer__divider" aria-hidden="true" />
<button className="nav-item" aria-label={t('navigation.settings')} ...>
<Settings aria-hidden="true" />
<span>{t('navigation.settings')}</span>
</button>
</div>
```
应用中心和设置为相邻独立按钮。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
“应用”(`applications.menuLabel`:中文“应用”、英文 `Apps`)和设置为横向等高独立按钮,使用 `nav-item` 样式和弱竖分隔线;“管理应用”菜单项使用 `LayoutGrid` 图标,管理 Modal 标题保持“应用中心” / `Application Center`。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
保留设置预加载、焦点恢复和未保存离开确认。该调用只打开设置 Modal,不更新当前工作区或
KeepAlive 缓存;`settingsOpen` 独立管理打开状态。关闭后卸载设置内容,下次按入口指定的分类
和消息通道重新挂载,避免缓存上次分类覆盖本次直达目标。底层工作区继续挂载,普通关闭不跳回聊天。
@@ -348,7 +353,7 @@ resourceMonitor.getLocalSnapshot(input: {
### 10.2 Main 与契约测试
- 顺序包含四个应用 ID 各一次;公开写入拒绝重复、缺失、未知 ID 及无效常驻字段;旧存储顺序按迁移规则补齐,不为未发布的 `knowledgeEnabled` 添加迁移。
- 顺序包含五个应用 ID 各一次;公开写入拒绝重复、缺失、未知 ID 及无效常驻字段;旧存储顺序按迁移规则补齐,不为未发布的 `knowledgeEnabled` 添加迁移。
- 未配置的心跳应用默认关闭,其余应用启用及笔记布尔项默认开启,本机推理监控默认不常驻;已保存的启用、常驻值和评论单选值经迁移及重启保留。
- 更新常驻或顺序不修改启用、计划或业务数据;笔记既有配置键保持语义。
- 串行 patch 在 Main 最新快照上合并,原子写入失败不发布目标状态;响应丢失后重读实际结果。
@@ -463,7 +468,7 @@ OCR 模型可用性由 Main 校验,Worker 运行状态由实际持有它的 Re
- `conversation-activity.ts` 从 `App.tsx` 的会话、`activeConversationIds`、全部可见
`assistantTasks`、`completedConversationIds` 和项目元数据派生会话行、全局及项目计数,不新增持久化活动副本。
`ProjectActivity` 在有活动时渲染汇总与项目、会话级联菜单,`ProjectSwitcher` 复用项目计数。
`ProjectSwitcher` 使用单个固定双行按钮显示项目名与全局活动,活动为空时第二行显示空闲文案,统一打开 `WorkspaceMenu` 并默认预览当前项目;生产 `App` 不再挂载独立 `ProjectActivity` 级联菜单。按钮通过 `aria-describedby` 关联活动描述及控件外的恢复反馈,尺寸与样式见活动界面。
- `use-unviewed-completions.ts` 在 Renderer 内存维护完成集合和上一轮 Task 状态;已观察到的
顶层 Task 转为 `completed` 时加入集合,初始历史不生成通知。`App` 的本地 `done` 事件和
活动运行持久化快照的成功终态调用 `markConversationCompleted`,失败、取消不调用。
@@ -472,16 +477,15 @@ OCR 模型可用性由 Main 校验,Worker 运行状态由实际持有它的 Re
`visibilitychange`,文档不为 `hidden` 时才按当前会话清除通知。这使普通侧栏、活动菜单
导航和可见性恢复共用查看规则,KeepAlive 挂载本身不代表查看。集合不写入 SQLite 或设置。
- 聚合状态新增 `completed`,全局和项目计数包含 `attention`、`running`、`completed`,
同一 Conversation ID 只按最高优先级计数;活动行为空才隐藏摘要。
- `App` 向 `ProjectActivity` 传入项目元数据、活动行、侧栏可见状态与既有精确跳转回调。
菜单按项目 ID 分组并记忆化派生集合;排序仅影响展示,不改聚合状态或持久化数据。
- 两层菜单在同一 body Portal 内相接,桌面会话层使用独立 fixed 定位,不参与项目层高度计算。
项目层锚定摘要,会话层锚定选中项目行并独立限制窗口边界。布局在打开、层级变化、实时数据更新、窗口缩放和
滚动时按锚点重算;窄窗口隐藏项目面板并保留返回操作。菜单不调用 Modal 焦点隔离,
捕获阶段消费 Escape,防止触发窄侧栏的文档级监听;Tab 关闭后交还正常焦点顺序。
菜单关闭与焦点恢复在导航回调之前同步完成。
- `role="menu"` 使浮层直接进入共享 `browser-viewport-occlusion.ts` 相交检测。未增加
浏览器实例的特殊隐藏条件,也不关闭或重建浏览器会话。
同一 Conversation ID 只按最高优先级计数;活动行为空时以“暂无活动”替换计数,保留全局入口。
- `App` 向 `ProjectSwitcher` 传入 `conversationStore`、活动行、侧栏可见状态和确切会话跳转、新建回调。`WorkspaceMenu` 持有分类、预览范围、搜索及状态筛选;分类复用 `PageTabs`,状态筛选复用 `SegmentedControl`,项目行与真实设置/创建表单继续由 `ProjectSwitcher` 提供。
- `workspace-menu-selectors.ts` 订阅会话 Store,投影 ID、项目、标题、排序时间及通道标记,以字段相等比较保留未变化结果;排序时间为 `Math.max(conversationActivityTime(conversation), activityTimes.get(conversation.id) ?? 0)`。与活动行按 ID 合并后派生分组和最近 10 条;仅有活动、尚无摘要的行使用 Store 时间,缺失时取 0。悬停只改变 Renderer 预览范围,不发 IPC、不加载详情或请求模型。打开菜单所需的既有主机快照读取与悬停预览分开。
- Store 的 `activityTimes` 保存本次应用会话内的访问/状态变化时间,更新时间为 `Math.max(Date.now(), lastActivityTime + 1)`。`ProjectSwitcher` 调用 `recordActivityStatuses`:首次快照只建立基线,后续状态变化触发更新,但 `completed` 变为无状态的完成提醒清除不触发更新。`WorkspaceMenu` 经 `useSyncExternalStore` 订阅该时间映射;时间不持久化,悬停不更新,普通侧栏原有消息排序规则不变。
- 项目、会话列表均使用共享 `useListWindow`,分别估算 `64px`、`56px` 行高并测量实际高度,保留键盘目标行;范围改变时重置滚动。窗口化仅限制挂载行数,不截断活动集合,不代表已有性能基准结果。
- 菜单通过 `FloatingPortal` 显示同一双栏表面,定位和焦点恢复共用项目按钮的引用;尺寸、窗口和滚动变化时重算位置。非模态 `dialog` 不调用焦点隔离,Escape 关闭整个菜单,Tab 可访问内部控件,焦点离开后关闭。窄窗口仍为双栏,侧栏隐藏时卸载菜单。
- 浮层沿用共享 `browser-viewport-occlusion.ts` 相交检测,不增加浏览器实例的特殊隐藏条件,也不关闭或重建浏览器会话。
- `conversationStore.rememberOpened` 在 `App.setActiveId` 时为存在的会话记录访问时间,并记住其项目最后打开的 ID。`projectConversation` 优先返回仍在候选集合中的记忆 ID,否则按 `conversationActivityTime` 选择最近会话,不使用菜单的访问/状态时间;通道项目只选带 `remote` 的会话。记忆仅存在当前 Store 的 Map 中,不持久化、不跨重载恢复。普通项目无会话时创建,通道无会话时清空活动 ID。
- 项目进入、新建会话及创建项目后的导航均经过 `requestWorkspaceLeave`。获准后的 `commitProjectSelection` 与新建会话 `ready` 回调直接调用 `commitView('chat')`,避免经 `setView` 再次触发离开检查;新建会话就绪后才提交项目、会话及输入框焦点。
- 持久化快照刷新时,同一条仍在流式输出的消息保留本地待审批、待回答信息,持久化终态
则清除这些信息。响应成功后对应 Task 立即退出等待状态;响应期间到达的新问题、审批或
终态不能被旧响应覆盖。
@@ -498,5 +502,5 @@ OCR 模型可用性由 Main 校验,Worker 运行状态由实际持有它的 Re
- 远程恢复成功提示沿用独立的自动消失计时器;活动汇总更新不重置或替代该计时器。
完成通知的定向验证应覆盖本地成功、持久化活动运行成功、后台 Task 状态转换、初始历史、
失败和取消、实时状态优先、三类计数、普通侧栏查看、设置覆盖、隐藏文档与恢复可见、无活动行隐藏。
验证记录见[项目活动级联实施进度](./progress.md);源码核对不能替代真实 Electron 路径验证。
失败和取消、实时状态优先、三类计数、普通侧栏查看、设置覆盖、隐藏文档与恢复可见、无活动时的空闲占位。
统一菜单还应覆盖固定双行单按钮及焦点、当前项目默认范围和菜单内全部项目切换、分类与跨分类搜索、搜索框边界与两栏分隔、紧凑工具栏、预览无 IPC、最近 10 条与不限量活动筛选、真实设置/创建入口、会话期恢复及窗口化键盘导航。验证记录见[实施进度](./progress.md);源码核对不能替代真实 Electron 路径验证。
@@ -19,10 +19,9 @@
```text
品牌
项目选择
项目活动汇总(仅有活动时显示)
新对话
搜索
项目名称(固定双行控件的第一行)
全局活动汇总/暂无活动(第二行)
[新对话] [搜索图标]
当前对话
应用(按保存顺序;知识库始终显示,可选应用仅显示启用且常驻项)
@@ -32,24 +31,37 @@
...
────────────────────────
[应用中心] [设置]
[应用 v] | [设置]
```
“当前对话”和“运行记录”属于固定系统入口,常驻应用位于二者之间。首期应用清单为魔法笔记、
知识库、智能心跳及本轮追加的本机推理监控,应用中心与设置始终可见。侧栏底部不再显示“本地工作区”和 `platform · arch`;
排障信息移至关于或诊断页面。左侧不再提供文件、终端、浏览器、资源监控的重复入口。
### 会话搜索
新建对话与搜索共用原新建对话位置的一行。默认显示 `MessageSquarePlus` 图标与完整“新建对话”文字,整体左对齐,沿用原有 13px、600 字重和 12px 左内边距,右侧为搜索图标;
点击搜索后,输入框从右向左展开,新建对话只显示居中的同一图标,保留“新建对话”可访问名称与悬停提示。
最窄侧栏中的长标签允许在固定行高内换行,保留字号和图标。搜索右边缘和整行高度保持不变,列表不因展开或关闭而上下移动。宽度动画使用
`--motion-normal`,减少动态效果时立即切换;沿用桌面侧栏调宽和窄窗口抽屉尺寸。
通道项目继续不提供新对话按钮,搜索展开后使用整行宽度。
控件行与新建对话按钮高 `44px`;搜索框本体高 `36px`,上下各留 `4px`,垂直居中。保留 `2px` 焦点外框与 `2px` 外间距,聚焦后外沿与新建对话按钮上下对齐。
展开时聚焦输入框。X 始终可用,X 或控件行内的 Escape 清空关键词并收起输入框,
焦点返回搜索图标。失焦、点击会话列表保持搜索状态和关键词;新建对话图标继续调用原新对话操作。
无结果区域的“清除搜索”只清空关键词,保留展开状态。项目活动跳转仍沿用既有清空搜索规则。
## 2. 底部应用中心入口
底部使用连续平整表面和顶部分隔线,应用中心在前,固定设置靠右,二者为独立按钮:
底部使用连续平整表面和顶部分隔线,“应用”在前,“设置”靠右,二者为横向排列的等宽、等高 `nav-item` 独立按钮,内容各自居中,竖分隔线位于底部入口区域的正中间:
| 位置 | 图标 | 可见工具提示 | 点击结果 |
| 位置 | 图标 | 可见文字 | 点击结果 |
| --- | --- | --- | --- |
| 1 | `Grid2X2` | 应用中心 | 向上展开已启用应用菜单,底部“管理应用”进入管理 Modal |
| 末位 | `Settings` | 设置 | 在当前工作区上打开设置 Modal |
| 1 | `Grid2X2` + `ChevronUp` | 应用 / Apps | 向上展开已启用应用菜单,底部“管理应用”进入管理 Modal |
| 末位 | `Settings` | 设置 / Settings | 在当前工作区上打开设置 Modal |
图标使用项目现有 Lucide 库。应用中心保留文字,设置图标采用统一的 `34px × 34px` 点击区域。
底部使用共享间距和焦点样式,设置通过弹性空白靠右;不得把整个容器做成设置按钮。
图标使用项目现有 Lucide 库。两个按钮均保留可见文字,沿用主导航的高度、间距和焦点样式;
“应用”文字后显示向上箭头,与菜单展开方向一致。竖分隔线宽 1px、高 `--space-6`,使用 `--border-default`,与上方横分隔线颜色一致。不得把整个容器做成设置按钮。
### 2.1 状态
@@ -73,18 +85,22 @@
```
取消常驻或关闭可选应用后不留下空白。知识库始终启用并显示;心跳按启用和常驻显示,位置遵守共享应用顺序。运行记录始终位于应用区之后。
四个应用均在管理行上通过拖动及“上移”“下移”按钮排序;首尾无目标时禁用对应按钮。两种方式提交完整应用顺序,保存成功后更新,重启保留。当前对话、运行记录和底部系统入口不参与排序。
五个应用均在管理行上通过拖动及“上移”“下移”按钮排序;首尾无目标时禁用对应按钮。两种方式提交完整应用顺序,保存成功后更新,重启保留。当前对话、运行记录和底部系统入口不参与排序。
取消常驻不关闭当前页面或本机推理监控 Modal。重新常驻按已保存顺序恢复,关闭应用保留常驻与顺序偏好。
主侧栏始终读取最后确认配置,不展示尚未保存成功的排序或开关目标值。
## 4. 应用中心与共同设置
设备共享追加到默认顺序末尾,测试阶段默认关闭。管理行显示“可选”及启用状态,提供打开、
设置与排序按钮;设置内提供启用 Switch,不提供常驻开关。关闭时禁用“打开”,应用菜单只在
启用时显示该项,点击后在主内容区打开[设备共享页面](../device-sharing/ui-design.md#当前首期页面)。
底部点击打开轻量菜单,锚定按钮向上展开,无模态遮罩、背景隔离或焦点循环。菜单按共享保存顺序列出已启用应用,不按打开历史或常驻状态过滤。点击应用行关闭菜单,本机推理监控打开独立 Modal,知识库、智能心跳和魔法笔记在主内容区打开页面。分隔线下的“管理应用”关闭菜单并打开既有管理 Modal。
菜单通过共享 `AnchoredMenu` 挂载到 body,宽度最多 `280px`,距窗口边缘至少 `16px`,与按钮上缘间隔 `8px`;短窗口限制高度并内部滚动,滚动、窗口和内容尺寸变化时重算位置。原生浏览器相交遮挡复用共享机制。
管理 Modal 沿用设置 Modal 的表面、遮罩和焦点规则,提供全部内置应用的名称及用途搜索、打开,以及可选应用的常驻和设置:
“管理应用”菜单项使用 `LayoutGrid` 图标。管理 Modal 标题仍为“应用中心”(英文 `Application Center`),沿用设置 Modal 的表面、遮罩和焦点规则,提供全部内置应用的名称及用途搜索、打开,以及可选应用的常驻和设置:
应用管理始终采用紧凑单列横向列表,从上到下对应保存顺序;行间使用 `--border-subtle` 弱分隔线,不加卡片外框。概览与设置详情共用 `640px` 最大宽度,窄窗口保留两侧各 `16px` 外边距,高度随内容增长并受窗口限制。
每行左侧显示图标、名称、状态和简短用途,右侧对齐打开、可用的设置及上下移动操作;空间不足时操作组换到下一行并保持右对齐,组内也可换行,上下箭头保持成组。名称旁区分
@@ -189,7 +205,8 @@ CPU 4% · 内存 620 MB · 桌面进程 7
| 对象 | 文案 |
| --- | --- |
| 底部入口 | 应用中心 |
| 底部入口 | 应用 / Apps |
| 管理 Modal 标题 | 应用中心 / Application Center |
| 菜单底部管理入口 | 管理应用 |
| 应用设置开关 | 启用应用 |
| 文件系统工具 | 文件 |
@@ -231,10 +248,18 @@ CPU 4% · 内存 620 MB · 桌面进程 7
会话行的“更多会话操作”提供“置顶会话/取消置顶”,本地与消息通道会话均可使用。
置顶会话显示带“已置顶”可访问名称的图标;当前项目和搜索过滤仍生效,过滤后的列表先显示
置顶会话,两组内各按更新时间倒序。取消置顶恢复时间排序,不改变会话更新时间。
置顶会话,两组内各按最新消息的创建时间倒序;时间相同按会话 ID 固定顺序,无消息时使用
会话更新时间。新增消息在每 5 秒的排序检查时可以前移会话,仅顺序变化时调整位置;同一消息的文字、推理、工具进度和完成状态更新不
改变排序。有消息的会话重命名或修改设置也不前移。摘要刷新、打开详情和重新启动沿用同一
规则,行右侧仍显示实际更新时间。取消置顶恢复消息时间排序,不改变会话更新时间。
置顶状态重启后保留;保存成功后更新图标和顺序,失败保留原状态并显示应用错误通知。
存储尚未就绪或置顶请求进行中时禁用置顶操作。
消息内容、标题与运行状态实时更新。鼠标停留在列表内、焦点在列表内、滚动尚未停止
200ms 或会话操作菜单打开时,跳过自动排序;操作结束后在下一次 5 秒检查时调整。
新建、删除、置顶和取消置顶立即反映到列表中,不提前应用其他会话待处理的位置变化。
切换项目或搜索范围立即显示对应列表;排序计时器不因消息增量而重新计时。
操作菜单通过 body Portal 浮在会话列表上方,不撑开会话行。菜单锚定更多按钮,默认位于
按钮下方,随滚动、窗口及菜单尺寸变化重新定位,距窗口边缘至少 `8px`;空间不足时限制
宽高并允许菜单内部滚动。使用共享表面色、边框、圆角与浮层阴影。
@@ -282,27 +307,20 @@ OCR 忙碌或模型不可用时禁用加载与释放,并在执行模块再次
对应 [FR-14](./prd.md#fr-14-项目活动汇总),状态与计数遵守
[活动逻辑](./logic-design.md#9-项目活动汇总)。
- 全局汇总按钮仅在有活动行时显示在项目选择器下方,紧凑显示待处理、运行中与已完成数量,无活动行时整行隐藏,
右侧显示展开箭头,与下方新建会话保留 `8px` 间距。可访问名称明确数量属于“全项目活动”。
- 项目菜单在项目行显示同口径数量,保留“本地项目 / 远程项目 / 远程通道”分组和 SSH Host
层级,不因活动状态重排项目。数量为零时不显示标记,连接状态仍独立显示。
项目名独占一行,状态数量排列在名称下方,避免窄侧栏中数量挤占名称宽度。
- 点击汇总在按钮下方打开项目菜单,只列有活动会话的项目,每项显示项目名和同口径计数。
桌面鼠标悬停或点击项目展开右侧会话菜单,右侧空间不足且左侧可容纳时向左展开。两个面板
相接,各自拥有边框、圆角与阴影;会话层对齐所选项目行,底部空间不足时仅上移会话层,
不改变项目层的位置或高度。指针离开项目行进入会话层时不收起。展开项目不切换当前会话。
- 会话层顶部显示项目名,每行显示完整可换行的会话标题与明确状态文字,待处理会话排在
运行中会话之前,已完成未查看会话排在最后并显示“已完成”。项目行保留子菜单箭头,会话行不显示箭头。不展开 Task、Job 或 Run 列表。
- 点击会话先关闭菜单并恢复入口焦点,再请求进入该条会话;允许离开当前页面后,
选择所属项目、清除搜索和该会话未读标记、清除任务选择与会话操作菜单,窄窗口同时关闭侧栏。
设置页沿用未保存修改的离开检查,取消离开时保留原页面和完成通知。完成通知在目标聊天
实际可见后清除,从普通侧栏进入也相同;设置覆盖下的聊天或隐藏文档不触发清除。
- 菜单挂载到 body,使用 `menu`、`menuitem`、`aria-haspopup`、`aria-expanded` 与控制关系,
不隔离背景、不增加遮罩。原生浏览器相交遮挡沿用共享机制。
- 打开后焦点进入首个项目;上下方向键循环选择当前层项目或会话,Home、End 到首尾,
Enter、Space 激活,右方向键进入会话层。左方向键或 Escape 返回项目层并恢复对应项目
焦点;顶层 Escape 关闭并恢复汇总焦点,不继续关闭窄侧栏。Tab 关闭菜单后继续正常焦点
顺序;点击外部、焦点离开、再次点击入口或隐藏侧栏也关闭菜单。
- 单面板宽度为 `264px`,窗口小于 `620px` 或项目菜单两侧均无法容纳子菜单时,就地进入会话层并显示“返回项目列表”,
触控无需悬停。浮层距窗口边缘至少 `16px`,每层最大高度为 `480px`,短窗口进一步限制
高度并独立滚动。成功完成但尚未查看的会话继续保留;所有活动行移除后,关闭已打开菜单并隐藏摘要。
左上角是固定高 `60px` 的单个按钮,第一行显示项目名称,第二行显示全局待处理、运行中、已完成数量,无活动时显示“暂无活动”。点击任一行都打开锚定整个按钮的工作空间菜单,默认预览当前项目;菜单内点击“全部项目”切换至全局范围。按钮只有一个键盘焦点、焦点环及展开状态,`aria-controls` 关联菜单,`aria-describedby` 关联活动描述和控件下方的远程恢复反馈。活动变化不改变按钮高度,也不关闭已打开的菜单;长内容单行截断。
左栏固定搜索、分类及底部“新建项目”,项目列表独立滚动。分类使用共享 `PageTabs` 的 `segmented` 变体,依次为“全部项目、本地、远程、消息通道”,保留 `tablist`/`tab`/`tabpanel`、游标焦点及方向键语义;远程能力关闭时隐藏远程分类。搜索跨分类匹配,SSH Host 分组可折叠,连接状态独立于会话数量。
左栏使用 `--surface-subtle`,与右栏之间以 `1px` 的 `--border-control` 竖线分隔。搜索框使用 `border-box`,宽度包含边框和内边距,并扣除左右外边距,避免越过分隔线。全部项目及搜索结果按实际存在的本地、远程、消息通道显示分组标题,组间用横线和留白区分;远程组内保留 Host 标题。
项目行显示名称、活动数量、目录三行,无活动时省略数量行,高度随内容增长;恢复状态继续显示在目录下方。当前项目以勾选和 `aria-checked` 标识,预览使用单独背景。设置按钮独立,桌面悬停或聚焦显示,窄窗口及粗指针设备可直接访问;打开既有真实项目设置,保留目录选择、Runtime、模式、删除及远程恢复等适用操作,不触发项目切换。悬停或聚焦项目只预览右栏,点击项目直接进入;通道无会话时保留空态。
右栏顶部用共享 `SegmentedControl` 显示“全部、待处理、运行中、已完成”,采用辅助字号和紧凑内边距,与靠右的“新建会话”同排。新建使用预览项目,全部项目范围使用当前项目;消息通道禁用并显示自动创建说明。默认会话列表按待处理、运行中、最近会话分组,最近最多 10 条,完成未查看在行内标注;“已完成”筛选不受此上限限制。会话标题单行截断,全部项目范围补充项目名,不展开 Task、Job 或 Run。无活动仍显示最近会话,无匹配会话时显示空态。
点击会话关闭菜单并请求确切 ID 导航;通过既有未保存离开检查后,选择所属项目、清除侧栏搜索、未读标记、任务选择和会话菜单,窄窗口关闭侧栏。取消离开保留原页面和完成通知。完成通知仅在目标聊天实际可见后清除,预览、设置覆盖或隐藏文档不触发清除。
菜单使用共享 `FloatingPortal`,外层为非模态 `dialog`,入口提供 `aria-haspopup="dialog"`、展开状态及控制关系,不增加遮罩或背景隔离。项目列表使用 `menu`/`menuitemradio`,独立操作使用 `menuitem`;会话列表使用 `list`/`listitem` 和按钮。原生浏览器相交遮挡沿用共享机制。
打开后聚焦选中分类。列表内上下方向键循环移动,Home、End 到首尾;右方向键进入会话列表,左方向键返回预览项目,项目不可见时回到选中分类。Enter、Space 激活按钮;Tab 在控件间正常移动,焦点离开后关闭。Escape 关闭整个菜单并恢复仍可用的触发入口焦点,触发入口已卸载时回到项目入口,不继续关闭窄侧栏。点击外部、再次点击入口或隐藏侧栏也关闭。
菜单最大宽度 `920px`,高度 `640px` 且受视口限制,距窗口边缘至少 `16px`,锚点下方间隔 `8px`。桌面左栏为 `240px` 至 `360px`,窄窗口仍保留双栏并各自滚动,不使用旧级联返回页。状态筛选可横向滚动,新建按钮保持可见。项目和会话列表均采用共享窗口化渲染,键盘可到达完整列表首尾。
@@ -42,12 +42,12 @@
- Given 任意主页面和任意侧栏宽度
- When 用户查看侧栏底部
- Then 设置与应用中心为始终可见的独立按钮,点击区域不重叠
- Then “设置”与“应用”为始终显示文字的独立按钮,点击区域不重叠
- And 点击后继续遵守未保存内容的离开确认
## 3. Epic B:管理应用入口
本节的启用、常驻和共同设置适用于智能心跳、魔法笔记、本机推理监控;四个应用均可排序,知识库始终启用并常驻。心跳的默认关闭及执行边界按[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)验收,保留既有故事 ID。
本节的启用、常驻和共同设置适用于智能心跳、魔法笔记、本机推理监控;五个应用均可排序,知识库始终启用并常驻。设备共享在测试阶段默认关闭,提供启用开关,不提供常驻开关;启用后可从应用菜单和管理 Modal 打开,关闭后退出当前页面并保留目录及发布记录,旧设置缺失时默认关闭,明确启用值在重启后保留。业务验收见[设备共享首期需求](../device-sharing/prd.md#当前首期本地元数据)。心跳的默认关闭及执行边界按[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)验收,保留既有故事 ID。
### US-B1 识别完整应用
@@ -139,14 +139,14 @@
验收:
- Given 首次使用且未配置导航偏好
- Then 默认顺序为知识库、智能心跳、魔法笔记、本机推理监控;心跳默认关闭且保留默认常驻偏好,魔法笔记默认常驻,本机推理监控默认启用但不常驻,仍可从应用菜单打开;侧栏只按启用与常驻过滤可选项
- Then 默认顺序为知识库、智能心跳、魔法笔记、本机推理监控、设备共享;心跳默认关闭且保留默认常驻偏好,魔法笔记默认常驻,本机推理监控默认启用但不常驻,仍可从应用菜单打开;设备共享默认关闭,启用后只在应用菜单和管理 Modal 提供入口;侧栏只按启用与常驻过滤可选项
- When 用户修改常驻,或通过拖动、键盘可操作的上移和下移按钮排序并保存成功
- Then 管理列表从上到下、菜单和侧栏按同一保存顺序显示,知识库和智能心跳可移动;当前对话、运行记录及底部系统入口位置不变,重启后恢复保存结果
- And 每行均提供上下移动,以完整保存顺序中的相邻应用为目标;未常驻及关闭应用仍可排序,设置详情不提供排序控件
- And 搜索时箭头仍按完整顺序移动,保留未命中应用,拖动禁用;清除搜索后显示完整保存结果
- Given 旧设置仅保存本机推理监控、魔法笔记的顺序
- When 升级后读取设置
- Then 知识库、智能心跳补在前面,两个可选应用保留原有相对顺序和常驻值
- Then 知识库、智能心跳补在前面,两个可选应用保留原有相对顺序和常驻值,设备共享追加到末尾
- Given 应用已关闭或取消常驻
- When 用户重新开启或常驻
- Then 按保存顺序恢复入口;关闭或取消常驻不删除顺序,已关闭应用仍可管理常驻偏好
@@ -356,7 +356,7 @@
- Given 右侧工作栏有多个工具实例
- Then Tab 单行横向滚动且“+”持续可见,工作栏停靠回流,不遮罩主页面;布局和 Tab 交互仍由工作栏负责
## 6. Epic E:项目活动级联
## 6. Epic E:统一工作空间菜单
### US-E1 从摘要进入确切会话
@@ -364,8 +364,11 @@
查看跨项目活动,先定位项目,再进入需要处理的会话。
- Given 多个项目存在运行中和待处理会话,当前搜索仅匹配当前项目
- When 打开全项目活动并悬停或点击另一个项目
- Then 项目顺序保持稳定,该项目会话按待处理优先展示,当前页面和会话保持原选择
- When 打开全项目活动并悬停或键盘聚焦另一个项目
- Then 同一菜单右栏预览该项目会话,待处理、运行中优先,最近会话最多 10 条,当前页面和会话保持原选择
- And 分类依次为全部项目、本地、远程、消息通道;远程能力关闭时隐藏远程分类及项目,搜索跨分类,远程主机可折叠
- When 点击项目
- Then 进入该项目,恢复本次应用会话内上次打开的有效会话,否则选择最近会话;普通项目没有会话时新建,消息通道保持空态
- When 点击会话
- Then 进入该确切 Conversation ID,清除搜索、任务选择和会话菜单,窄窗口关闭侧栏
- Given 设置页有未保存修改
@@ -373,20 +376,30 @@
- Given 用户选择的确切 Conversation ID 未在内存中或已被删除
- When 请求进入该会话
- Then 刷新持久化会话列表后仍只查找同一 ID;刷新失败或会话不存在时显示通知并保留原页面和选择,不回退到项目最新会话或其他会话
- Given 从项目入口打开菜单
- Then 默认预览当前项目;点击“全部项目”恢复全局范围,“已完成”筛选列出全部完成未查看会话,不受最近 10 条限制
- When 实际打开会话、产生新消息或活动状态变化
- Then 菜单按最新消息与本次应用会话内访问/状态变化时间中的较晚者排序;首次状态载入和仅清除完成提醒不更新时间,悬停不改变排序
- When 点击与状态筛选同排的“新建会话”
- Then 在预览项目创建,全部项目范围使用当前项目;消息通道禁用手动新建并说明自动创建规则
- When 点击项目行独立设置按钮或左栏底部“新建项目”
- Then 打开真实设置或创建表单;设置不触发项目切换,创建成功后经离开检查进入新项目
- And 项目进入或新建会话已获准离开后,提交聊天视图不再次触发同一离开检查
### US-E2 键盘、窄窗口与实时结束
- Given 菜单已打开
- Then 上下方向键与 Home、End 选择当前层,右方向键或 Enter 进入会话层,左方向键或
Escape 返回;再次 Escape 关闭活动菜单并恢复摘要焦点,窄侧栏保持打开
- Given 窗口小于 `620px`
- Then 点击项目后仅显示会话层,可通过返回按钮回到项目列表;长列表可滚动到最后一项
- Given 鼠标从项目行移向相接的会话面板
- Then 子菜单保持打开,点击会话正常导航
- Then 初始焦点进入选中分类,上下方向键与 Home、End 移动列表焦点,右方向键进入会话列表,左方向键返回预览项目或选中分类;Enter、Space 激活按钮
- When 按 Escape
- Then 关闭整个工作空间菜单并恢复可用触发入口焦点,窄侧栏保持打开
- Given 窄窗口或长列表
- Then 两栏有清晰分隔并独立滚动,搜索框不越过左栏边界,全部项目及搜索结果按本地、远程、消息通道显示分组标题;紧凑状态筛选可横向滚动,新建按钮保持可见,触控点击项目直接进入
- Given 当前项目已无待处理、运行中或已完成未查看会话
- Then 返回项目层并恢复可用焦点;全局无活动时整行隐藏摘要并关闭菜单,新活动到来时只恢复摘要
- When 点击外部、焦点离开、按 Tab 或隐藏侧栏
- Then 显示最近会话或空态;全局无活动时第二行显示“暂无活动”,仍可打开菜单并在其中切换全部项目范围,已打开的菜单仍可使用,新活动不自动打开菜单
- And 左上角是一个固定两行高度的按钮,项目名和全局活动共用一个键盘焦点;点击任一行都默认预览当前项目,恢复提示显示在按钮下方并关联可访问描述
- When 点击外部、焦点离开或隐藏侧栏
- Then 菜单关闭,后台新活动不自动重新展开
- And Tab 可在菜单内访问搜索、分类、项目设置、创建及会话按钮,焦点离开后关闭
### US-E3 完成后保留至查看
@@ -396,7 +409,7 @@
- When 本次应用会话内的运行成功完成,或已观察到的后台任务转为完成
- Then 活动会话保留为“已完成”,全局与项目的已完成计数同步更新
- When 从活动菜单或普通侧栏实际进入该会话的可见聊天
- Then 清除其完成通知;没有其他活动行时隐藏摘要并关闭菜单
- Then 清除其完成通知;没有其他活动行时第二行恢复“暂无活动”,已查看会话仍可出现在最近会话中
- Given 聊天被设置覆盖或文档隐藏
- When 对应运行完成
- Then 完成通知保留,直到设置关闭且目标聊天在可见文档中显示;取消设置离开不清除通知
@@ -0,0 +1,69 @@
:root {
color-scheme: light;
--surface-canvas: #e8f0f5; --surface-raised: #fff; --surface-subtle: #f4f8fb;
--text-primary: #17263a; --text-secondary: #4f6278; --border-default: #cedce7;
--accent: #086fc7; --accent-subtle: #e8f4ff; --accent-selected: #a7cfea;
--success: #147566; --success-subtle: #e5f4ee; --warning: #865200;
--radius-control: 10px; --radius-card: 14px;
--shadow: 0 24px 80px #17263a26;
font-family: "Inter Variable", "PingFang SC", "Microsoft YaHei UI", system-ui, sans-serif;
}
[data-theme="dark"] { color-scheme: dark; --surface-canvas: #07131f; --surface-raised: #102335; --surface-subtle: #162c40; --text-primary: #edf7ff; --text-secondary: #b5c6d5; --border-default: #29445a; --accent: #72c5ff; --accent-subtle: #123b5b; --accent-selected: #28668f; --success: #72d8be; --success-subtle: #123d37; --warning: #f2bd66; }
* { box-sizing: border-box; }
body { margin: 0; padding: 24px 32px; background: var(--surface-subtle); color: var(--text-primary); font-size: 13px; line-height: 1.6; }
button, textarea, input { font: inherit; color: inherit; }
button { border: 1px solid transparent; border-radius: var(--radius-control); background: transparent; padding: 8px 12px; cursor: pointer; }
button:hover { background: var(--accent-subtle); }
button:focus-visible, textarea:focus-visible, input:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
button:disabled { opacity: .5; cursor: default; }
h1, h2, h3, p { margin: 0; } .muted, .eyebrow, .section-label { color: var(--text-secondary); }
.eyebrow, .section-label { font-size: 11px; letter-spacing: 1px; }
.lab, .lab-foot { max-width: 1440px; margin: auto; display: flex; justify-content: space-between; gap: 16px; align-items: center; }
.lab { margin-bottom: 24px; }.lab strong { font-size: 15px; }.lab span { margin-left: 24px; color: var(--text-secondary); }.tools { display: flex; gap: 4px; }.tools button { border-color: var(--border-default); }
.shell { max-width: 1440px; margin: auto; height: calc(100dvh - 160px); min-height: 580px; display: grid; grid-template-columns: 256px minmax(0, 1fr); border: 1px solid var(--border-default); border-radius: var(--radius-card); overflow: hidden; background: var(--surface-raised); }
.sidebar { background: var(--surface-canvas); padding: 24px 12px 12px; display: flex; flex-direction: column; border-right: 1px solid var(--border-default); }.brand { display: flex; align-items: center; gap: 8px; margin: 0 8px 32px; }.brand .muted { font-size: 11px; margin-left: auto; }.logo { background: var(--text-primary); color: var(--surface-raised); border-radius: var(--radius-control); padding: 4px 10px; }
.workspace-control { background: var(--surface-raised); border: 1px solid var(--border-default); border-radius: var(--radius-card); overflow: hidden; }#workspace-trigger { position: relative; display: grid; text-align: left; width: 100%; padding: 12px; }#project-name { font-size: 16px; }.expand { position: absolute; right: 12px; top: 20px; font-size: 22px; color: var(--accent); }#activity-trigger { border-top: 1px solid var(--border-default); border-radius: 0; width: 100%; font-size: 10px; padding: 8px; display: flex; gap: 4px; justify-content: space-between; }
.new { margin: 16px 0 24px; text-align: left; border-color: var(--border-default); }.section-label { margin: 0 12px 8px; }#sessions { overflow: auto; }#sessions button { width: 100%; text-align: left; display: flex; gap: 8px; align-items: center; }#sessions button span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }[aria-current="true"], button[aria-pressed="true"] { background: var(--accent-subtle); border-color: var(--accent-selected); color: var(--accent); }.sidebar-bottom { margin-top: auto; padding-top: 24px; }.sidebar-bottom > span { display: block; margin: 8px 12px; font-size: 11px; }.sidebar-bottom button { width: 100%; text-align: left; }kbd { font: 10px/1.5 inherit; border: 1px solid var(--border-default); border-radius: 4px; padding: 2px 4px; color: var(--text-secondary); margin-left: 8px; }
.workspace { display: flex; flex-direction: column; min-width: 0; overflow: hidden; }.titlebar { padding: 16px 24px; border-bottom: 1px solid var(--border-default); background: var(--surface-subtle); display: flex; justify-content: space-between; gap: 12px; }#conversation { flex: 1; overflow: auto; padding: 48px max(32px, calc((100% - 740px) / 2)); }#conversation h1 { font-size: 28px; font-weight: 550; letter-spacing: -1px; margin: 12px 0 32px; }.user-message { margin: 0 0 32px 48px; padding: 16px 24px; background: var(--surface-subtle); border-radius: var(--radius-card); }.assistant-label { color: var(--accent); font-size: 12px; margin-bottom: 12px; }.answer { font-size: 15px; line-height: 1.9; white-space: pre-wrap; }.execution { margin-top: 24px; padding: 16px; border: 1px solid var(--border-default); border-radius: var(--radius-control); }.execution p { margin: 8px 0 16px; color: var(--text-secondary); }.primary { background: var(--accent); color: var(--surface-raised); }.primary:hover { background: var(--text-primary); color: var(--surface-raised); }#composer { margin: 16px 32px 24px; border: 1px solid var(--border-default); border-radius: var(--radius-card); padding: 12px 16px; }textarea { display: block; width: 100%; resize: vertical; border: 0; background: transparent; padding: 8px 0; }#composer > div { display: flex; align-items: center; justify-content: space-between; }.lab-foot { margin-top: 16px; color: var(--text-secondary); font-size: 11px; }
.waiting { color: var(--warning); }.running { color: var(--accent); }.done { color: var(--success); }.idle { color: var(--text-secondary); }.empty { text-align: center; padding: 48px 16px; color: var(--text-secondary); }#toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border: 1px solid var(--accent-selected); background: var(--surface-raised); color: var(--text-primary); border-radius: var(--radius-card); box-shadow: var(--shadow); z-index: 5; }
@media (max-width: 800px) { body { padding: 12px; }.lab { flex-wrap: wrap; margin-bottom: 12px; }.lab span { display: none; }.shell { grid-template-columns: 208px minmax(0, 1fr); }.brand .muted { display: none; }#conversation { padding: 24px; }.lab-foot span:last-child { display: none; } }
@media (max-width: 560px) { .shell { display: flex; flex-direction: column; height: auto; min-height: calc(100dvh - 150px); }.sidebar { border-right: 0; border-bottom: 1px solid var(--border-default); padding: 12px; }.brand, .new, .section-label, #sessions, .sidebar-bottom { display: none; }.workspace-control { display: flex; }#workspace-trigger { width: 48%; }#activity-trigger { width: 52%; flex-wrap: wrap; align-content: center; border-top: 0; border-left: 1px solid var(--border-default); }.workspace { min-height: 540px; }.titlebar { padding: 12px; }.titlebar > .muted { display: none; }#conversation { padding: 24px 16px; }#composer { margin: 12px; }.user-message { margin-left: 16px; } }
#overview { position: fixed; inset: auto; margin: 0; padding: 0; width: min(920px, calc(100vw - 16px)); height: 640px; border: 1px solid var(--border-default); border-radius: var(--radius-card); box-shadow: var(--shadow); background: var(--surface-raised); color: var(--text-primary); overflow: hidden; }
#overview:popover-open { display: flex; flex-direction: column; }
#activity-trigger[hidden] { display: none; }
.menu-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; border-bottom: 1px solid var(--border-default); flex-shrink: 0; }.menu-header button { font-size: 11px; }
.menu-layout { display: grid; grid-template-columns: 360px minmax(0, 1fr); flex: 1; min-height: 0; }
.project-pane { display: flex; flex-direction: column; min-height: 0; min-width: 0; background: var(--surface-subtle); border-right: 1px solid var(--border-default); }
#project-search { margin: 12px 8px 8px; width: calc(100% - 16px); min-width: 0; border: 1px solid var(--border-default); background: var(--surface-raised); border-radius: 6px; padding: 8px; font-size: 12px; }
#all-projects { text-align: left; margin: 0 8px 8px; padding: 8px; }
#project-tabs { display: flex; gap: 4px; padding: 0 8px 8px; border-bottom: 1px solid var(--border-default); flex-shrink: 0; }#project-tabs[hidden] { display: none; }#project-tabs button { flex: 1; white-space: nowrap; font-size: 11px; padding: 6px 4px; }#project-tabs [aria-selected="true"] { color: var(--accent); background: var(--accent-subtle); border-color: var(--accent-selected); }
#desktops { display: block; padding: 8px; overflow-y: auto; flex: 1; min-height: 0; overscroll-behavior: contain; }
.search-result > small { display: block; padding: 0 8px 8px 32px; font-size: 10px; color: var(--text-secondary); }.search-result .desktop { margin-bottom: 0; }
.host-heading { cursor: pointer; }.host-heading > span:first-child::before { content: '▸'; margin-right: 4px; }details[open] > .host-heading > span:first-child::before { content: '▾'; }
.project-row { position: relative; margin-bottom: 4px; }
.project-row .desktop { display: grid; grid-template-columns: 16px minmax(0, 1fr) 14px; min-height: 52px; padding: 8px 44px 8px 8px; gap: 8px; margin: 0; }
.project-copy { min-width: 0; display: grid; gap: 2px; }.project-heading { display: flex; align-items: center; gap: 8px; min-width: 0; }.project-heading strong { font-size: 13px; font-weight: 650; }.project-path { display: block; font-size: 11px; line-height: 16px; color: var(--text-secondary); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }.project-row .project-counts { width: auto; padding: 0; flex-wrap: nowrap; }.current-project { color: var(--accent); font-size: 13px; }
.project-settings-button { position: absolute; top: 50%; right: 4px; transform: translateY(-50%); width: 32px; height: 32px; padding: 8px; opacity: 0; pointer-events: none; color: var(--text-secondary); }.project-row:hover .project-settings-button, .project-row:focus-within .project-settings-button { opacity: 1; pointer-events: auto; }
.project-copy > .project-counts { font-size: 11px; line-height: 16px; flex-wrap: wrap; gap: 2px 8px; }.project-copy > .project-counts > span { white-space: nowrap; }
@media (hover: none) { .project-settings-button { opacity: 1; pointer-events: auto; } }
#project-settings { width: min(480px, calc(100vw - 24px)); max-height: calc(100dvh - 32px); overflow-y: auto; border: 1px solid var(--border-default); border-radius: var(--radius-card); background: var(--surface-raised); color: var(--text-primary); padding: 24px; box-shadow: var(--shadow); }#project-settings::backdrop { background: color-mix(in srgb, var(--text-primary) 25%, transparent); }
#settings-form { display: grid; gap: 16px; }#settings-form header, #settings-form footer { display: flex; align-items: center; gap: 8px; }#settings-form header strong, #settings-form footer small { flex: 1; }#settings-form label { display: grid; gap: 4px; font-size: 12px; }#settings-form input, #settings-form select, #settings-form textarea { width: 100%; min-width: 0; border: 1px solid var(--border-default); border-radius: 6px; background: var(--surface-subtle); color: var(--text-primary); padding: 8px; font: inherit; }
.desktop { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px; margin-bottom: 4px; text-align: left; border-color: transparent; background: transparent; }.desktop strong { display: block; margin: 0; min-width: 0; flex: 1; font-size: 12px; font-weight: 500; }.project-counts { display: flex; gap: 8px; font-size: 10px; color: var(--text-secondary); flex-shrink: 0; }
#desktops svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
.project-section { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-default); }.project-section h2 { display: flex; align-items: center; gap: 8px; margin: 0 8px 8px; font-size: 11px; font-weight: 500; color: var(--text-secondary); }.host-group + .host-group { margin-top: 12px; }.host-heading { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 4px 8px; font-size: 11px; }.host-status { border: 1px solid var(--border-default); border-radius: 4px; padding: 0 4px; font-size: 10px; }.host-group .desktop { padding-left: 16px; }
@media (min-width: 561px) and (max-width: 800px) { .menu-layout { grid-template-columns: 300px minmax(0, 1fr); } }
.activity-pane { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
#menu-new-chat { color: var(--accent); font-size: 11px; padding: 4px 8px; white-space: nowrap; flex-shrink: 0; }
#project-form:not([hidden]) { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px; border-top: 1px solid var(--border-default); flex-shrink: 0; }#project-form input { min-width: 80px; width: 120px; flex: 1; background: var(--surface-raised); border: 1px solid var(--border-default); border-radius: 6px; padding: 8px; }.desktop strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.overview-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-top: 0; border-bottom: 1px solid var(--border-default); }
#filters { display: flex; gap: 4px; overflow-x: auto; min-width: 0; }#filters button { font-size: 11px; padding: 4px 8px; white-space: nowrap; flex-shrink: 0; }
#windows { overflow-y: auto; padding: 8px; flex: 1; min-height: 0; overscroll-behavior: contain; }
.window-group { margin: 0 0 16px; }.group-title { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 500; margin: 8px; }
.session-row { display: flex; align-items: center; gap: 12px; text-align: left; width: 100%; padding: 12px 8px; border-radius: 6px; }.session-copy { flex: 1; min-width: 0; }.session-copy strong { display: block; font-weight: 500; }.session-copy small { display: block; font-size: 11px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.row-action { font-size: 10px; flex-shrink: 0; }
.overview-footer { padding: 8px 16px; background: var(--surface-subtle); border-top: 1px solid var(--border-default); min-height: 48px; flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--text-secondary); font-size: 11px; }
@media (max-width: 560px) {
.project-row .desktop { grid-template-columns: minmax(0, 1fr) 12px; padding: 8px 36px 8px 4px; gap: 4px; }.project-row .desktop > svg { display: none; }.project-heading { flex-wrap: wrap; gap: 0; }.project-heading strong { flex-basis: 100%; }.project-settings-button { right: 0; }.project-row .project-counts { font-size: 9px; }.project-row .project-path { font-size: 10px; }
#project-tabs { overflow-x: auto; gap: 0; padding-inline: 4px; }#project-tabs button { flex: 0 0 auto; }#project-search { font-size: 11px; padding: 6px; }.search-result > small { padding-left: 8px; }
.desktop { flex-wrap: wrap; gap: 4px; }.desktop strong { flex-basis: calc(100% - 24px); }.project-counts { width: 100%; padding-left: 19px; }.host-heading { flex-wrap: wrap; gap: 4px; }.host-group .desktop { padding-left: 8px; }
.menu-layout { grid-template-columns: 176px minmax(0, 1fr); }#desktops { padding: 4px; }.desktop { padding: 8px; }.desktop strong { font-size: 12px; }.project-counts { gap: 4px; flex-wrap: wrap; }.overview-toolbar { padding: 8px; }#filters { gap: 0; }#filters button { padding: 4px; }.session-row { align-items: flex-start; gap: 4px; flex-direction: column; }.session-copy { width: 100%; }.overview-footer { padding: 8px; gap: 4px; }.overview-footer span { max-width: 55%; }
}
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GoodBuddy · 工作空间实验</title>
<link rel="stylesheet" href="workspace-demo.css">
<script src="workspace-demo.js" defer></script>
</head>
<body>
<header class="lab"><strong>GoodBuddy / 工作空间实验</strong><div class="tools"><button id="simulate">模拟后台完成</button><button id="theme">切换深色</button><button id="reset">重置</button></div></header>
<main class="shell">
<aside class="sidebar">
<div class="brand"><b class="logo">G</b><strong>GoodBuddy</strong><span class="muted">个人空间</span></div>
<div class="workspace-control"><button id="workspace-trigger" aria-haspopup="dialog" aria-controls="overview" aria-expanded="false"><span class="eyebrow">工作空间</span><strong id="project-name"></strong><span class="expand">▾</span></button><button id="activity-trigger" aria-haspopup="dialog" aria-controls="overview" aria-expanded="false" aria-label="查看全部项目活动"></button></div>
<button class="new" id="new-chat">+ 新会话</button>
<div class="section-label">会话</div><nav id="sessions" aria-label="会话窗口"></nav>
</aside>
<section class="workspace">
<header class="titlebar"><span id="breadcrumb"></span><span class="muted">会话窗口</span></header>
<article id="conversation" aria-live="polite"></article>
<form id="composer"><label for="message" class="eyebrow">继续这项工作</label><textarea id="message" placeholder="输入消息,继续与 GoodBuddy 协作…" rows="2" required></textarea><div><span class="muted">OpenCode · Execute</span><button class="primary" type="submit">发送 ↑</button></div></form>
</section>
</main>
<footer class="lab-foot"><span>Demo · 模拟数据</span></footer>
<section id="overview" popover="auto" role="dialog" aria-labelledby="overview-title">
<header class="menu-header"><strong id="overview-title">项目与活动</strong><button id="close-overview" aria-label="关闭项目与活动菜单">×</button></header>
<div class="menu-layout">
<aside class="project-pane">
<input id="project-search" type="search" aria-label="搜索项目" placeholder="搜索项目…" autocomplete="off">
<button id="all-projects" data-scope="all" aria-pressed="false">全部项目</button>
<div id="project-tabs" role="tablist" aria-label="项目类型"></div>
<nav id="desktops" role="tabpanel" aria-label="项目列表"></nav>
</aside>
<section class="activity-pane">
<div class="overview-toolbar"><div id="filters" role="group" aria-label="筛选会话状态"></div><button id="menu-new-chat">+ 新建会话</button></div>
<div id="windows"></div>
</section>
</div>
<form id="project-form" hidden><label for="project-input">项目名称</label><input id="project-input" maxlength="40" required autocomplete="off"><button type="submit" class="primary">创建</button><button type="button" id="cancel-project">取消</button></form>
<footer class="overview-footer"><button id="new-project" aria-expanded="false" aria-controls="project-form">+ 新建项目</button></footer>
</section>
<dialog id="project-settings" aria-labelledby="settings-title">
<form id="settings-form">
<header><strong id="settings-title">项目设置</strong><button type="button" id="close-settings" aria-label="关闭项目设置">×</button></header>
<label>名称<input id="settings-name" maxlength="120" required></label>
<label>说明<textarea id="settings-description" rows="2"></textarea></label>
<label>执行空间<input id="settings-space" readonly></label>
<label>工作目录<input id="settings-path" required></label>
<label>默认模式<select id="settings-mode"><option value="ask">Ask · 只读问答</option><option value="execute">Execute · 完全权限</option></select></label>
<label>新对话默认 Runtime<select id="settings-runtime"><option>OpenCode</option><option>Continue</option><option>直连模型</option></select></label>
<footer><small class="muted">仅修改 Demo 数据</small><button type="button" id="cancel-settings">取消</button><button type="submit" class="primary">保存</button></footer>
</form>
</dialog>
<div id="toast" role="status" hidden></div>
</body>
</html>
@@ -0,0 +1,355 @@
const projects = [
{ id: 'buddy', name: 'GoodBuddy', kind: 'local', location: '本地工作空间' },
{ id: 'web', name: '品牌官网', kind: 'local', location: '本地工作空间' },
{ id: 'data', name: '数据分析', kind: 'remote', host: 'dev-server', location: '远程 · dev-server' },
{ id: 'api', name: '后端服务', kind: 'remote', host: 'dev-server', location: '远程 · dev-server' },
{ id: 'lab', name: '模型实验', kind: 'remote', host: 'gpu-lab', location: '远程 · gpu-lab' },
{ id: 'channel', name: '产品讨论群', kind: 'channel', location: '消息通道' }
];
const hosts = [{ name: 'dev-server', status: '就绪' }, { name: 'gpu-lab', status: '未连接' }];
for (let index = 3; index <= 20; index++) {
projects.push({ id: `local-${index}`, name: `本地项目 ${String(index).padStart(2, '0')}`, kind: 'local', location: '本地工作空间' });
}
for (let index = 4; index <= 20; index++) {
const host = index % 2 ? 'gpu-lab' : 'dev-server';
projects.push({ id: `remote-${index}`, name: `远程项目 ${String(index).padStart(2, '0')}`, kind: 'remote', host, location: `远程 · ${host}` });
}
projects.push(
{ id: 'channel-2', name: '研发协作群', kind: 'channel', location: '消息通道' },
{ id: 'channel-3', name: '运维通知群', kind: 'channel', location: '消息通道' }
);
const icons = {
all: '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>',
local: '<rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8m-4-4v4"/>',
remote: '<rect x="3" y="3" width="18" height="7" rx="2"/><rect x="3" y="14" width="18" height="7" rx="2"/><path d="M7 6h.01M7 17h.01"/>',
channel: '<path d="M21 11a8 8 0 0 1-8 8H6l-4 3V11a8 8 0 0 1 8-8h3a8 8 0 0 1 8 8Z"/><path d="M7 9h9M7 13h6"/>',
folder: '<path d="M3 7V5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/>'
};
const icon = (name) => `<svg viewBox="0 0 24 24" aria-hidden="true">${icons[name]}</svg>`;
for (const project of projects) {
project.rootPath = project.kind === 'remote' ? `/srv/projects/${project.id}` : `D:/workspace/${project.id}`;
project.description = '';
project.defaultMode = 'ask';
project.runtime = 'OpenCode';
}
const initialProjects = structuredClone(projects);
const initial = [
{ id: 1, project: 'buddy', title: '工作空间交互设计', state: 'idle', detail: '设计讨论已保存,随时可以继续。', prompt: '让项目与活动会话的关系更直观,做出操作系统的感觉。', answer: '左上角就是工作入口。\n\n展开项目与活动菜单,可以查看多个项目正在进行的会话,也可以直接处理等待你的工作。选择项目只筛选列表,进入会话才切换工作现场。\n\n所有状态都在这个下拉菜单中,不需要再打开独立页面。' },
{ id: 2, project: 'buddy', title: '登录流程调整', state: 'waiting', detail: '需要你选择:使用单点登录还是邮箱登录?', prompt: '帮我完善内部用户的登录流程。', answer: '已梳理现有认证入口。需要确定首选登录方式,然后继续调整界面。', question: true },
{ id: 3, project: 'buddy', title: '导航组件测试', state: 'running', detail: '正在检查键盘导航与焦点恢复。', prompt: '检查导航组件的交互和测试覆盖。', answer: '已经完成组件检查,正在验证键盘导航与焦点恢复。' },
{ id: 4, project: 'web', title: '发布新版首页', state: 'waiting', detail: '预览构建已就绪,等待你批准部署。', prompt: '构建新版首页,发布前让我确认。', answer: '预览构建已完成,页面检查通过。请确认是否继续部署。' },
{ id: 5, project: 'web', title: '首页文案精修', state: 'done', detail: '完成 3 处文案调整,结果等待查看。', prompt: '把首页介绍改得更简洁、自然。', answer: '首页文案调整已完成。\n\n首屏标题:把想法,变成已完成的工作。\n产品介绍:查资料、写方案、改代码。在一个工作空间里,与 AI 一起推进。\n行动按钮:开始你的第一项工作。' },
{ id: 6, project: 'data', title: '本周使用情况分析', state: 'running', detail: '正在汇总各项目的使用趋势。', prompt: '整理本周各项目的使用情况。', answer: '已完成示例数据整理,正在汇总各项目的使用趋势。' },
{ id: 7, project: 'api', title: '接口变更记录', state: 'idle', detail: '昨天的会话', prompt: '整理本次接口变更。', answer: '接口变更记录已整理。' },
{ id: 8, project: 'lab', title: '推理参数对比', state: 'idle', detail: '上次会话', prompt: '对比两组推理参数。', answer: '上次对比结果已保存。' },
{ id: 9, project: 'channel', title: '版本发布讨论', state: 'idle', detail: '今天的会话', prompt: '整理群里的版本讨论。', answer: '本轮发布讨论已整理。' }
];
for (const project of projects) {
if (!initial.some((session) => session.project === project.id)) {
initial.push({ id: initial.length + 1, project: project.id, title: '最近工作记录', state: 'idle', detail: '上次会话', prompt: '整理当前工作。', answer: '工作记录已保存。' });
}
}
const labels = { waiting: '待处理', running: '运行中', done: '已完成' };
const symbols = { waiting: '!', running: '◌', done: '✓' };
const statusLabel = (session) => session.state === 'waiting'
? session.question ? '等待你的回答' : '等待审批'
: labels[session.state] || '';
const $ = (id) => document.getElementById(id);
const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
let sessions, current, scope = 'all', filter = 'all', projectTab = 'local', toastTimer, menuTrigger, activityOrder, settingsProjectId;
const remembered = {};
const drafts = {};
const overview = $('overview');
const projectOf = (session) => projects.find((p) => p.id === session.project);
const badge = (state) => `<span class="${state}">${symbols[state]} ${labels[state]}</span>`;
function notify(text) {
$('toast').textContent = text;
$('toast').hidden = false;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { $('toast').hidden = true; }, 3500);
}
function render() {
const session = sessions.find((s) => s.id === current);
const project = projectOf(session);
$('project-name').textContent = project.name;
$('breadcrumb').textContent = `${project.name} / ${session.title}`;
$('activity-trigger').innerHTML = '<span class="muted">全部</span>' + ['waiting', 'running', 'done'].filter((state) => sessions.some((s) => s.state === state)).map((state) => `<span class="${state}">${symbols[state]} ${sessions.filter((s) => s.state === state).length} ${labels[state]}</span>`).join('');
$('activity-trigger').hidden = !sessions.some((s) => labels[s.state]);
$('sessions').innerHTML = sessions.filter((s) => s.project === session.project).map((s) => `<button data-open="${s.id}" aria-current="${s.id === current}">${statusLabel(s) ? `<span class="${s.state}" aria-label="${statusLabel(s)}">${symbols[s.state]}</span>` : ''}<span>${escapeHtml(s.title)}</span></button>`).join('');
$('conversation').innerHTML = `<span class="eyebrow">${escapeHtml(project.location)}${statusLabel(session) ? ` / ${statusLabel(session)}` : ''}</span><h1>${escapeHtml(session.title)}</h1><div class="user-message">${escapeHtml(session.prompt)}</div><div class="assistant-label">G / GoodBuddy</div><div class="answer">${escapeHtml(session.answer)}</div>${session.state === 'waiting' ? `<section class="execution"><span class="waiting">! ${statusLabel(session)}</span><p>${escapeHtml(session.detail)}</p>${session.question ? '<button class="primary" data-resolve="单点登录">使用单点登录</button> <button data-resolve="邮箱登录">使用邮箱登录</button>' : '<button class="primary" data-resolve="批准部署">批准并继续</button> <button data-resolve="暂不部署">暂不部署</button>'}</section>` : session.state === 'running' ? `<section class="execution">${badge('running')}<p>${escapeHtml(session.detail)} 切换到其他工作空间后仍会继续。</p></section>` : ''}`;
$('simulate').disabled = !sessions.some((s) => s.state === 'running');
renderOverview();
}
function renderOverview() {
const focused = document.activeElement;
const focusScope = focused?.dataset.scope;
const focusFilter = focused?.dataset.filter;
const projectButton = (p) => {
const list = sessions.filter((s) => p.id === 'all' || s.project === p.id);
const counts = ['waiting', 'running', 'done'].filter((state) => list.some((s) => s.state === state)).map((state) => `<span class="${state}">${symbols[state]} ${list.filter((s) => s.state === state).length} 个${labels[state]}</span>`).join(' ');
const active = sessions.find((s) => s.id === current).project === p.id;
const detail = p.kind === 'local' ? `本地目录 · ${p.rootPath}` : p.kind === 'channel' ? `消息通道 · ${p.rootPath}` : p.rootPath;
return `<div class="project-row"><button class="desktop" data-scope="${p.id}" title="${escapeHtml(p.name)}\n${escapeHtml(detail)}">${icon(p.kind === 'channel' ? 'channel' : 'folder')}<span class="project-copy"><span class="project-heading"><strong>${escapeHtml(p.name)}</strong></span>${counts ? `<span class="project-counts">${counts}</span>` : ''}<small class="project-path">${escapeHtml(detail)}</small></span><span class="current-project" ${active ? 'aria-label="当前项目"' : 'aria-hidden="true"'}>${active ? '✓' : ''}</span></button><button class="project-settings-button" data-settings="${p.id}" aria-label="管理项目 ${escapeHtml(p.name)}" title="项目设置"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 3-.7 3-2 .9-2.7-.8-2 3.4 2 2.2v2.6l-2 2.2 2 3.4 2.7-.8 2 .9L9 23h4l.7-3 2-.9 2.7.8 2-3.4-2-2.2v-2.6l2-2.2-2-3.4-2.7.8-2-.9L13 3Z" transform="translate(1 -1) scale(.95)"/><circle cx="12" cy="12" r="3"/></svg></button></div>`;
};
const query = $('project-search').value.trim().toLocaleLowerCase();
const kinds = { local: '本地', remote: '远程', channel: '消息通道' };
if (!$('project-tabs').children.length) {
$('project-tabs').innerHTML = Object.entries(kinds).map(([kind, label]) => `<button id="tab-${kind}" role="tab" data-tab="${kind}" aria-controls="desktops">${label} <small></small></button>`).join('');
}
$('project-tabs').hidden = Boolean(query);
$('project-tabs').querySelectorAll('[data-tab]').forEach((button) => {
const selected = button.dataset.tab === projectTab;
button.setAttribute('aria-selected', String(selected));
button.tabIndex = selected ? 0 : -1;
button.querySelector('small').textContent = projects.filter((p) => p.kind === button.dataset.tab).length;
});
$('desktops').setAttribute('aria-label', query ? '搜索结果' : kinds[projectTab]);
$('all-projects').setAttribute('aria-pressed', String(scope === 'all'));
const shown = projects.filter((p) => query
? `${p.name} ${p.rootPath} ${p.host || ''} ${kinds[p.kind]}`.toLocaleLowerCase().includes(query)
: p.kind === projectTab);
const projectMarkup = query
? shown.map((p) => `<div class="search-result">${projectButton(p)}<small>${kinds[p.kind]}${p.host ? ` · ${escapeHtml(p.host)}` : ''}</small></div>`).join('')
: projectTab === 'remote'
? hosts.map((host) => `<section class="host-group" aria-label="${host.name}"><details open><summary class="host-heading"><span>${host.name}</span><span class="host-status ${host.status === '就绪' ? 'done' : 'muted'}">${host.status}</span></summary>${shown.filter((p) => p.host === host.name).map(projectButton).join('')}</details></section>`).join('')
: shown.map(projectButton).join('');
// Keep hovered project nodes stable while the right-hand list changes.
if ($('desktops').dataset.markup !== projectMarkup) {
$('desktops').innerHTML = projectMarkup || '<div class="empty">无匹配项目</div>';
$('desktops').dataset.markup = projectMarkup;
}
$('desktops').querySelectorAll('[data-scope]').forEach((button) => {
button.setAttribute('aria-pressed', String(button.dataset.scope === scope));
});
const list = sessions.filter((s) => scope === 'all' || s.project === scope);
$('filters').innerHTML = ['all', 'waiting', 'running', 'done'].map((state) => `<button data-filter="${state}" aria-pressed="${filter === state}">${state === 'all' ? '全部' : labels[state]} <small>${list.filter((s) => state === 'all' || s.state === state).length}</small></button>`).join('');
const targetProject = projects.find((p) => p.id === (scope === 'all' ? sessions.find((s) => s.id === current).project : scope));
$('menu-new-chat').title = `在 ${targetProject.name} 中新建会话`;
$('menu-new-chat').setAttribute('aria-label', $('menu-new-chat').title);
const groups = filter === 'all' ? ['waiting', 'running', 'recent'] : [filter];
$('windows').innerHTML = groups.map((state) => {
const sorted = list.filter((s) => state === 'recent' ? s.state === 'done' || s.state === 'idle' : s.state === state)
.sort((a, b) => b.lastActivity - a.lastActivity);
const group = state === 'recent' ? sorted.slice(0, 5) : sorted;
if (!group.length) return '';
return `<section class="window-group"><h2 class="group-title">${state === 'recent' ? '最近会话' : badge(state)} <span class="muted">${group.length}</span></h2>${group.map((s) => `<button class="session-row" data-open="${s.id}" title="${escapeHtml(s.detail)}"><span class="session-copy"><strong>${escapeHtml(s.title)}</strong>${scope === 'all' ? `<small>${escapeHtml(projectOf(s).name)}</small>` : ''}</span><span class="row-action ${s.state}">${s.state === 'done' ? '✓ 已完成' : s.state === 'waiting' ? statusLabel(s) : s.id === current ? '当前' : '↗'}</span></button>`).join('')}</section>`;
}).join('') || '<div class="empty">暂无会话</div>';
if (focusScope) overview.querySelector(`[data-scope="${focusScope}"]`)?.focus();
if (focusFilter) overview.querySelector(`[data-filter="${focusFilter}"]`)?.focus();
}
function openSession(id) {
const session = sessions.find((s) => s.id === id);
drafts[current] = $('message').value;
current = id;
session.lastActivity = ++activityOrder;
remembered[session.project] = id;
if (session.state === 'done') { session.state = 'idle'; session.detail = '结果已查看,可以继续这项工作。'; }
closeMenu();
$('message').value = drafts[id] || '';
render();
$('conversation').scrollTop = 0;
}
function positionMenu() {
const anchor = document.querySelector('.workspace-control').getBoundingClientRect();
const top = Math.max(8, Math.min(anchor.bottom + 8, window.innerHeight - 240));
overview.style.left = `${Math.max(8, Math.min(anchor.left, window.innerWidth - overview.offsetWidth - 8))}px`;
overview.style.top = `${top}px`;
overview.style.maxHeight = `${window.innerHeight - top - 8}px`;
}
function closeMenu() {
if (overview.matches(':popover-open')) overview.hidePopover();
}
function showOverview(project = 'all', trigger = $('workspace-trigger')) {
if (overview.matches(':popover-open')) { closeMenu(); return; }
menuTrigger = trigger;
scope = project;
projectTab = projects.find((p) => p.id === (project === 'all' ? sessions.find((s) => s.id === current).project : project)).kind;
$('project-search').value = '';
filter = 'all';
$('project-form').hidden = true;
$('new-project').setAttribute('aria-expanded', 'false');
renderOverview();
overview.showPopover();
positionMenu();
overview.querySelector('[data-scope][aria-pressed="true"]').focus();
}
document.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
if (button.dataset.settings) {
settingsProjectId = button.dataset.settings;
const project = projects.find((p) => p.id === settingsProjectId);
$('settings-name').value = project.name;
$('settings-name').readOnly = project.kind === 'channel';
$('settings-description').value = project.description || '';
$('settings-space').value = project.location;
$('settings-path').value = project.rootPath;
$('settings-mode').value = project.defaultMode || 'ask';
$('settings-runtime').value = project.runtime || 'OpenCode';
$('settings-runtime').querySelector('option:last-child').disabled = project.kind === 'remote';
closeMenu();
$('project-settings').showModal();
}
if (button.dataset.tab) {
projectTab = button.dataset.tab;
renderOverview();
$('desktops').scrollTop = 0;
}
if (button.dataset.open) openSession(Number(button.dataset.open));
if (button.dataset.scope === 'all') { scope = 'all'; renderOverview(); }
else if (button.dataset.scope) {
const project = button.dataset.scope;
const session = sessions.find((s) => s.id === remembered[project]) ||
sessions.filter((s) => s.project === project).sort((a, b) => b.lastActivity - a.lastActivity)[0];
if (session) openSession(session.id); else createSession(project);
}
if (button.dataset.filter) { filter = button.dataset.filter; renderOverview(); }
if (button.dataset.resolve) {
const session = sessions.find((s) => s.id === current);
const declined = button.dataset.resolve === '暂不部署';
session.state = declined ? 'idle' : 'running';
session.lastActivity = ++activityOrder;
session.detail = declined ? '已暂缓部署,等待后续安排。' : `已确认${button.dataset.resolve},正在继续执行。`;
session.answer = session.detail;
render();
notify(declined ? '已暂缓本次部署' : '已处理,工作继续进行');
}
});
$('desktops').addEventListener('pointerover', (event) => {
if (event.pointerType === 'touch') return;
const button = event.target.closest('[data-scope]');
if (!button || button.dataset.scope === scope) return;
scope = button.dataset.scope;
renderOverview();
});
$('project-search').addEventListener('input', () => { renderOverview(); $('desktops').scrollTop = 0; });
$('project-tabs').addEventListener('keydown', (event) => {
const tabs = [...$('project-tabs').querySelectorAll('[data-tab]')];
const index = tabs.indexOf(document.activeElement);
if (index < 0 || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length;
tabs[next].focus();
tabs[next].click();
});
$('settings-form').onsubmit = (event) => {
event.preventDefault();
const project = projects.find((p) => p.id === settingsProjectId);
const name = $('settings-name').value.trim();
const path = $('settings-path').value.trim();
if (!name || !path) return;
if (project.kind !== 'channel') project.name = name;
project.rootPath = path;
project.description = $('settings-description').value;
project.defaultMode = $('settings-mode').value;
project.runtime = $('settings-runtime').value;
render();
$('project-settings').close();
};
$('close-settings').onclick = $('cancel-settings').onclick = () => $('project-settings').close();
$('project-settings').addEventListener('close', () => {
showOverview(settingsProjectId);
overview.querySelector(`[data-settings="${settingsProjectId}"]`)?.focus();
});
$('workspace-trigger').onclick = () => showOverview(sessions.find((s) => s.id === current).project);
$('activity-trigger').onclick = () => showOverview('all', $('activity-trigger'));
$('close-overview').onclick = () => { closeMenu(); menuTrigger?.focus(); };
$('simulate').onclick = () => {
const session = sessions.find((s) => s.state === 'running' && s.id !== current) || sessions.find((s) => s.state === 'running');
if (!session) return;
const visible = session.id === current && !overview.matches(':popover-open') && !document.hidden;
session.state = visible ? 'idle' : 'done';
session.lastActivity = ++activityOrder;
session.detail = visible ? '结果已查看,可以继续这项工作。' : '工作已完成,结果等待查看。';
session.answer = '本轮工作已完成。\n\n示例结果:检查项已通过,相关说明已整理。你可以继续追问,或者切换到下一项工作。';
render();
notify(`${projectOf(session).name} / ${session.title} 已完成`);
};
overview.addEventListener('toggle', (event) => {
const open = event.newState === 'open';
$('workspace-trigger').setAttribute('aria-expanded', String(open));
$('activity-trigger').setAttribute('aria-expanded', String(open));
if (open) return;
const session = sessions.find((s) => s.id === current);
if (session.state === 'done') { session.state = 'idle'; session.detail = '结果已查看,可以继续这项工作。'; render(); }
});
$('theme').onclick = () => {
const dark = document.documentElement.dataset.theme !== 'dark';
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
$('theme').textContent = dark ? '切换浅色' : '切换深色';
};
function createSession(project) {
const id = Math.max(...sessions.map((s) => s.id)) + 1;
sessions.push({ id, project, title: '新的工作', state: 'idle', detail: '等待你的第一条消息。', prompt: '开始一项新的工作。', answer: '在下方输入你的想法。' });
openSession(id);
$('message').focus();
}
$('new-chat').onclick = () => createSession(sessions.find((s) => s.id === current).project);
$('menu-new-chat').onclick = () => createSession(scope === 'all' ? sessions.find((s) => s.id === current).project : scope);
$('new-project').onclick = () => {
$('project-form').hidden = !$('project-form').hidden;
$('new-project').setAttribute('aria-expanded', String(!$('project-form').hidden));
if (!$('project-form').hidden) $('project-input').focus();
};
$('cancel-project').onclick = () => {
$('project-form').hidden = true;
$('new-project').setAttribute('aria-expanded', 'false');
$('new-project').focus();
};
$('project-form').onsubmit = (event) => {
event.preventDefault();
const name = $('project-input').value.trim();
if (!name) { $('project-input').focus(); return; }
const id = `project-${projects.length + 1}`;
projects.push({ id, name, kind: 'local', location: '本地工作空间', rootPath: `D:/workspace/${id}`, description: '', defaultMode: 'ask', runtime: 'OpenCode' });
scope = id;
$('project-form').reset();
$('project-form').hidden = true;
$('new-project').setAttribute('aria-expanded', 'false');
createSession(id);
};
$('composer').onsubmit = (event) => {
event.preventDefault();
const text = $('message').value.trim();
if (!text) return;
const session = sessions.find((s) => s.id === current);
if (session.state === 'running' || session.state === 'waiting') { notify('请先完成当前模拟执行或处理待办,再发送消息。'); return; }
session.prompt = text;
session.state = 'running';
session.lastActivity = ++activityOrder;
session.detail = '正在处理你的消息。';
session.answer = '已开始模拟执行。你可以切换工作空间,然后点击页面上方“模拟后台完成”查看通知效果。';
$('message').value = '';
render();
};
function reset() {
projects.splice(0, projects.length, ...structuredClone(initialProjects));
scope = 'all';
projectTab = 'local';
$('project-search').value = '';
filter = 'all';
$('project-form').reset();
sessions = structuredClone(initial);
sessions.forEach((session, index) => { session.lastActivity = initial.length - index; });
activityOrder = initial.length;
current = 1;
Object.keys(remembered).forEach((key) => delete remembered[key]);
Object.keys(drafts).forEach((key) => delete drafts[key]);
remembered.buddy = 1;
closeMenu();
$('message').value = '';
render();
}
$('reset').onclick = reset;
document.addEventListener('keydown', (event) => {
if ($('project-settings').open) return;
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.code === 'KeyK') {
event.preventDefault();
if (overview.matches(':popover-open')) { closeMenu(); menuTrigger?.focus(); } else showOverview();
}
if (event.key === 'Escape' && overview.matches(':popover-open')) {
event.preventDefault(); closeMenu(); menuTrigger?.focus();
}
});
window.addEventListener('resize', () => { if (overview.matches(':popover-open')) positionMenu(); });
reset();
+7 -1
View File
@@ -1,6 +1,11 @@
# 助手工作栏
助手工作栏统一承载本机与远程执行空间、Task、成果、文件、浏览器和终端等持续工作界面。
助手工作栏统一承载本机与远程执行空间、Task、成果、文件、浏览器、终端和笔记等持续工作界面。
“笔记”为应用级、可关闭的单实例 Tab,新布局默认不打开。它提供搜索、浏览和快速追加文字,
与主页面魔法笔记共用存储;会话采集、来源跳转与草稿规则由
[魔法笔记文档](../magic-notes/README.md)维护。该应用已接入并通过真实 Electron 验收;
全仓验证未全绿,聚焦复跑与执行时失败见[笔记验证进度](../magic-notes/progress.md)。
## 文档导航
@@ -13,6 +18,7 @@
| [执行记录存储与升级回收](./execution-history-storage.md) | 子任务差量存储、远程去重、旧库转换、会话置顶、摘要与完整历史按需读取、内存引用释放 |
| [Runtime 交互边界](./runtime-interactions.md) | 原生权限、结构化问答、后台拒绝、终端交互、并行与输出、消息请求状态及资源生命周期规则 |
| [Runtime 进程复用技术设计](./runtime-process-reuse-technical-design.md) | 跨 Runtime 资源调研、锁定协议证据、进程与会话拆分、远端模型桥、实施顺序和性能验收 |
| [Runtime 原生客户端启动方案](./runtime-native-client-proposal.md) | Local terminal/DS Web shortcut requirements, startup rules, current integration, validation boundaries and remaining remote/resource work |
| [Runtime 执行清单技术设计](./runtime-checklist-technical-design.md) | OC/CN 原生清单提取、远程 Agent 传递复用、消息恢复与真实部署验收;尚未实现 |
当前尚未拆出独立 User Stories、功能逻辑和 UI 设计文档;产品场景与验收保留在现有 PRD,
@@ -81,12 +81,14 @@ Record<ConversationId, Record<BrowserTabId, BrowserLiveState>>
`BrowserViewport` 统一观察 body Portal 的插入、移除和可见属性变化,以及浮层尺寸、
窗口尺寸、滚动和过渡结束。`browser-viewport-occlusion.ts` 按共享语义识别
`aria-modal="true"`、dialog、alertdialog、menu 和应用通知:可见应用级 Modal 始终释放
`aria-modal="true"`、dialog、alertdialog、menu、tooltip 和应用通知:可见应用级 Modal 始终释放
viewport,其余浮层只在与浏览器区域相交时释放。隐藏节点和隐藏祖先不阻塞浏览器。
最后一个遮挡消失后,只有当前活动实例恢复同一 viewport 租约;网页、历史和页面状态不变。
终端关闭确认复用此机制,不再维护单独的隐藏条件。页面无需逐个登记弹窗,也不能靠提高
CSS `z-index` 覆盖原生子视图。此逻辑仅涉及 Desktop Renderer 的显示租约,不改变
Runtime、MCP 或远端 Agent 的请求、导航与生命周期。
同行帮助仍使用 `role="tooltip"`;相交判定使用其内容矩形,不使用铺满窗口的
`FloatingPortal` 宿主。真实 App 的相交与非相交验证见[浮层审查修复记录](../../quality/overlay-review-2026-09-27.md)。
浏览器应用声明 `visibleAcrossContextSwitches: true`,打开的 Tab 跨项目、会话切换继续显示。
切换活动 Conversation 不改变当前选中的浏览器或其 `targetRef.conversationId`;用户在会话 B
@@ -13,6 +13,8 @@
处理速度;处理时间取决于历史数据量与磁盘速度,不承诺固定几分钟。用户可以退出,
下次启动重新检查并继续转换未完成的数据;完成后回收空闲页并进入应用。失败时留在
该页提供重试与退出,磁盘空间不足会明确说明,不删除聊天来腾出空间。
以上转换和回收只针对仍需转换的旧格式数据;普通结构升级使用独立状态,不宣称正在
清理子任务历史,也不会仅因数据库有空闲页而执行全库回收。
## 存储规则
@@ -50,11 +52,11 @@ WAL 增长与 SQL 全历史扫描次数的定向验证见 [BUILD.md](../../../BU
本次读取优化不修改 schema、迁移或用户数据:
- Renderer 启动及焦点、会话、队列刷新使用 `conversations.listSummaries(detailIds)`。
普通未打开会话只返回标题、项目、置顶状态、上下文设置和消息数量/首条角色,不解析或跨 IPC
普通未打开会话只返回标题、项目、置顶状态、上下文设置和消息数量/首条角色/最新消息创建时间,不解析或跨 IPC
传输正文、工具、子任务及图片操作元数据。
- 当前会话、现有 KeepAlive 视图,以及 Main 仍持有请求、数据库中仍有活动 Task 或
streaming 消息的会话返回完整消息。列表包含全部 active 会话,不限制数量,
按置顶优先、组内更新时间倒序返回;仅置顶不会加载完整历史。
Main 按置顶优先、组内更新时间倒序返回;Renderer 使用下述展示排序。仅置顶不会加载完整历史。
- 打开历史通过 `conversations.get(id)` 读取全部消息,不使用会截断历史的分页上限。
加载时明确显示进度;失败保留会话与输入,提供原地重试。发送、队列派发和压缩上下文
必须先确认完整历史,发送时不按消息条数截断;复制与导出临时读取完整内容,不把未打开会话留入视图缓存。
@@ -69,6 +71,13 @@ WAL 增长与 SQL 全历史扫描次数的定向验证见 [BUILD.md](../../../BU
接口保留供其他调用方使用。
- 图片状态通过同一合并函数处理。本地更新与 Main 图片操作按原有时间优先级合并,
未变化消息返回原对象,避免每次列表刷新后把无变化的整个历史当作脏消息再次保存。
- 展示顺序由 Renderer 的 `sortConversationsForDisplay` 统一派生,规则见
[会话列表排序](../application-tool-navigation/ui-design.md#会话置顶与操作菜单)。完整消息取
`createdAt` 最大值;摘要的 `latestMessageAt` 在已有聚合查询中取 `MAX(created_at)`,
与摘要上附带的实时消息取最大值。该字段仅用于桌面只读投影,不增加数据库列或持久化副本。
`updatedAt`/`updated_at` 继续随流式事件更新,原有快照合并、终态优先与逐消息保存规则不变。
`useConversationListOrder` 在内存中保留展示 ID 顺序,以单个 5 秒计时器应用自动排序,
并按上述交互规则暂缓重排;行数据始终使用最新会话对象,不延迟保存或同步。
- 本机和托管 SSH 的 Renderer/Main 读取、持久化与恢复投影共用以上实现。Agent 的
transcript、原生 Session、权限及模型桥协议未改变;完整列表、摘要和详情都附带
Main 当前恢复请求的问题投影。
@@ -94,7 +103,10 @@ Renderer 合并时独立接收 Main 的置顶属性,即使本地正文更新
启动检查只根据旧 schema 或仍未转换的笔记正文决定是否进入升级流程;正常写入、更新
与删除产生的 SQLite 空闲页不是旧格式证据。迁移后新增数据使用当前格式,不因正常使用
再次迁移。同一次启动已进入的迁移流程可重试压缩;若正文已转换、仅压缩中断,退出后
再次迁移。进入升级流程后仍分别判断结构、笔记转换与空间回收,不把旧 schema 当作
回收许可。回收只用于实际存在旧子任务事件(schema < 35)、旧活动记录(schema < 44)
或旧笔记正文的转换;例如 schema 45/46 升到当前结构不会自动转换历史或 `VACUUM`。
同一次启动在内存中保留这一决定,只有原本需要回收的流程可重试压缩;若正文已转换、仅压缩中断,退出后
重开不单凭空闲页重新进入迁移,剩余页由 SQLite 后续写入复用。不新增持久化迁移标记。
桌面在打开业务数据库之前启动独立 Node 工作线程,使用同一套编码和还原函数转换旧库。
@@ -108,8 +120,8 @@ Renderer 合并时独立接收 Main 的置顶属性,即使本地正文更新
在转换前复制整份膨胀内容。回收仍需要额外可用磁盘空间,失败保留已提交转换并允许重试。
子任务整理对应 schema 35;schema 34 用户也会执行该整理,成功后后续启动直接跳过。
继续复用 schema 34 的子任务事件部分索引和同一差量表示。当前业务初始化随后在事务中
升级为 schema 36,新增 `pinned INTEGER NOT NULL DEFAULT 0 CHECK(pinned IN (0, 1))`,
继续复用 schema 34 的子任务事件部分索引和同一差量表示。业务初始化随后在事务中
执行后续结构迁移,其中 schema 36 新增 `pinned INTEGER NOT NULL DEFAULT 0 CHECK(pinned IN (0, 1))`,
已有会话默认未置顶,保留原消息与时间戳;失败回滚本次字段迁移。
低于该 schema 的旧版客户端不能重新打开已经升级的数据库。需要回退客户端时,必须使用
升级前备份,不要修改 `user_version` 强行绕过版本检查。
@@ -120,6 +132,8 @@ Renderer 合并时独立接收 Main 的置顶属性,即使本地正文更新
- 工具 fixture 必须含生产对象会携带的 `undefined` 可选字段,并经过实际 JSON 写入/重读。
同时验证旧完整事件和 schema 34 差量事件的整理、混合重试及无变化记录不被重写。
- 验证远程重复事件、冲突事件,以及消息更新失败时事件和过程缓存的事务回滚。
- 覆盖已完成历史转换、但正常聊天更新产生空闲页的后续结构升级:无转换进度、无
`VACUUM`,会话、笔记、任务事件与完整性不变;验证真实 Worker 和同次启动重试。
- 在真实旧库的已核验备份副本上运行 portable 的正常启动入口,核对事件逐条等价、
其他用户表内容、数据库完整性和实际文件大小,不能只统计空闲页或只测试内存 fixture。
- 同时验证真实本地 Runtime 和共享 Linux Host 的桌面持久化路径。真实场景必须让主代理
+15 -5
View File
@@ -86,6 +86,7 @@ Runtime
资源监控
成果
文档
笔记
```
- 应用不得根据当前项目、会话、Runtime、主机或探测结果无提示地增删能力目录项。
@@ -96,6 +97,7 @@ Runtime
Conversation、Job、Run 或心跳事项。
- 当前能力、连接、数据和空状态可以动态变化。
- 能力不可用时,目录项或已打开面板显示原因、影响和可执行的配置或切换入口,不能只通过隐藏表示。
- 笔记遵循魔法笔记的业务启用设置,启用时提供应用级单实例 Tab,禁用前处理草稿,禁用后移除入口和实例;取消主侧栏常驻不影响工作栏可用性。采集与来源规则见[魔法笔记逻辑设计](../magic-notes/logic-design.md)。
- 用户可以在设置中调整目录顺序;恢复默认布局恢复标准目录与默认打开面板,不强制打开全部能力。
- 浏览器、终端及其他声明为多实例的能力可以创建多个实例,每个实例拥有独立身份和范围绑定。
@@ -232,7 +234,7 @@ Runtime
从当前聊天右栏迁移时,Task Center 和工作区作为默认打开、单实例且不可关闭的基础应用保留;
浏览器保留一个默认打开实例,但注册为可关闭的多实例应用;成果保持默认打开、单实例且可
关闭;终端不默认打开,由用户通过“+”创建,并从首期开始支持多实例。Tab 必须单行横向滚动,
不能自动隐藏或缩写到不可辨认,“+”紧随最后一个 Tab 并随标签滚动,溢出时两端提供固定滚动箭头。Task Center 继续作为 Task 的现有
不能自动隐藏或缩写到不可辨认。“+”固定在滚动区外的行末,溢出时页签区两端提供固定滚动箭头,箭头不覆盖“+”。Task Center 继续作为 Task 的现有
入口;审批定位到所属 Task 或 Runtime。智能心跳不作为工作栏页签,其报告、建议、历史和
完整配置统一归属“智能心跳”菜单入口。当前阶段不新增独立自动化中心。
@@ -391,7 +393,7 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
当前项目目录的文件浏览与 Git 交互遵循以下规则:
- FR-W1:顶部同一行放置“文件 / Git 工作区”视图切换和右侧刷新;非 Git 目录仍保留刷新。
- FR-W1:顶部同一行放置“文件 / Git 工作区”视图切换和右侧刷新;非 Git 目录仍显示选中的“文件”入口和刷新,仅隐藏“Git 工作区”。
下一行只提供当前视图的工具:文件视图提供新建文件、新建目录,Git 视图提供分支、Fetch 和更改显示方式,
不在 Git 视图重复目录导航或新建文件操作。操作使用带名称和提示的紧凑图标按钮。
文件视图在根目录不显示孤立的文件夹图标或空路径行;进入子目录后提供“项目目录名 / 子目录”
@@ -399,6 +401,7 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
长目录名单行省略,完整路径通过提示查看;多级路径在导航内部横向滚动,不撑宽工作栏。
选中目录时在该目录创建,选中文件时在其父目录创建,未选择时在当前浏览目录创建。
无可用目录时隐藏无关操作。
文件列表独立纵向滚动;顶部视图切换、刷新、新建操作和子目录路径导航保持可见,不随列表滚走。
- 使用共享 `SegmentedControl` 切换“文件”和“Git 工作区”,默认显示文件;
选择只在当前面板挂载期间保存在内存中,不持久化。未提交更改包含当前项目的全部
Git 更改,不按 AI、用户或其他工具的修改来源筛选。
@@ -445,6 +448,7 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
- 文件预览提供复制项目相对路径、刷新,以及仅本机项目可用的“使用默认应用打开”。
刷新失败保留此前内容并显示错误;Markdown 默认渲染预览,可切换源码。
- Diff 提供刷新、复制项目相对路径和“查看当前文件”;删除项不显示查看当前文件入口。
文件路径与操作按钮位于分隔线上方,空间不足时换行;只有未暂存差异时省略“工作树更改”标题,同时存在已暂存差异时保留分区标题。
- 手动刷新或任务结束更新 Git 状态时,同步重读根目录、当前浏览目录和已展开目录并去重,保留目录展开状态,
刷新失败保留已有目录列表;重新展开目录时重读。目录读取错误在对应目录旁显示,
可就地重试。正在查看的单文件 Diff 同时重读,旧请求不得覆盖新结果。
@@ -662,6 +666,10 @@ SSH/远程项目操作;已有 Host、项目和凭据保持不变。若当前
启用后,设置中心显示“主机与远程执行”分类,管理:
> 上述入口为当前实现。2026-09-28 已确认后续迁入应用中心的“主机管理”,并新增同级
> “云环境管理”;范围、能力依赖及验收见[主机与云环境 PRD](../remote-host/prd.md)。
> 本节保留现有行为,入口迁移尚未实现。
- 主机名称、地址、端口和用户名。
- 认证方式和凭据配置状态。
- Host Key 算法与 SHA-256 指纹。
@@ -974,12 +982,14 @@ todo/plan,本次不为其增加工具。也不增加 plan 工作模式、计
布局遵循 [UI-DESIGN](../../../UI-DESIGN.md#610-应用顶栏与全局操作)。
默认折叠为一行“执行清单 · 已完成 m/n”,有进行中条目时附简短摘要;展开后按原生顺序
显示内容、状态及原生提供的优先级。行内状态使用图标和“待处理 / 进行中 / 已完成 / 已取消”文字,
在摘要按钮旁的浮层显示内容、状态及原生提供的优先级,不推移或压缩下方消息流。
行内状态使用图标和“待处理 / 进行中 / 已完成 / 已取消”文字,
不用可勾选 Checkbox,也不根据位置推测优先级或进行中状态。请求结束后的摘要附真实
请求结果,例如“已取消”,不改变条目状态。后台更新不强制展开、不抢焦点。
展开按钮提供 `aria-expanded` 和 `aria-controls`,支持 Enter/Space,折叠后焦点留在
按钮;实时区域只播报摘要变化。复用全局颜色、间距和字体令牌,浅深主题均可读。窄窗口
展开按钮提供 `aria-expanded` 和 `aria-controls`,支持 Enter/Space,展开后焦点进入清单。
Escape 收起并返回按钮;点击外部或焦点移出也收起。实时区域只播报摘要变化。
复用全局颜色、间距和字体令牌,浅深主题均可读。窄窗口
摘要截短、展开内容换行并有界滚动,保留完成计数、请求结果与输入区;200% 缩放时仍可
操作。没有清单或收到有效空清单时整行隐藏,历史内容仍按消息归属查看。
+310
View File
@@ -1,5 +1,315 @@
# 工作栏实现与验证进度
## 2026-10-01:自定义 MCP 失败中断回复
已修复共用网关将单个上游发现失败升级为整轮失败的问题,行为见
[本地 MCP 初始化失败恢复](./runtime-process-reuse-technical-design.md#41-本地-mcp-初始化失败恢复)。
正常服务的连接继续复用,失败服务不进入工具列表,动态刷新失败也按服务隔离;
取消和授权撤销继续生效。初次失败的服务在下一请求重试,原有连接超时未改。
验证使用真实 HTTP 上游和网关,覆盖部分失败、无可用工具、非法分页和超量目录、
动态刷新失败后恢复、取消及撤销。Continue 使用真实网关和模拟 Host;新增实际 OpenCode
二进制用例在自定义 MCP 返回 HTTP 503 后完成 Execute 回复,单次本地合成模型调用,
外部模型调用 0 次。网关与 Continue 定向测试 65 项通过;OpenCode 生命周期 4 项通过。
`npm run typecheck`、四个修改源码文件的 ESLint 通过。
完整 `npm test`:5,565 通过、89 跳过、4 失败。失败为 Agent 大依赖清单安装超时、
Magic Notes 采集的 `memo is not defined`,以及终态探针和手动压缩的界面等待超时。
全仓 lint 在并行修改的 `App.tsx` 性能测量代码处报告渲染纯度及不可变性错误;
本次未修改这些文件,不将完整验证记为通过。
远程 Agent 的独立 MCP 实现及 Story Graph 绑定不经过改动方法,本次无需远程 Host 验证。
尚未发布或替换已安装应用。
## 2026-10-01:本地 MCP 晚到初始化覆盖新连接
根因修复见[本地 MCP 初始化失败恢复](./runtime-process-reuse-technical-design.md#41-本地-mcp-初始化失败恢复)。
已核对随包 OpenCode 1.18.29:`MCP.add` 的初始化不随客户端取消而中止,完成后写入同名状态,
并关闭该名称下已有的连接;`disconnect` 只处理当时已登记的客户端。旧实现在 `mcp.add`
超时或取消后仍复用同一名称,前一轮的晚到初始化(授权已撤销,结果为 `failed`)会覆盖下一轮
刚建立的连接。现在响应不确定的名称作废,后续改用唯一替代名称。
验证:新增真实 OpenCode+实际网关用例,通过本地代理挂起第一次 MCP `initialize`,
使第一轮在 3 秒控制超时后结束,第二轮建立连接后再放行挂起的初始化。修复前该用例
复现为第二轮连接状态从 `connected` 变为 `failed`;修复后第二轮连接保持 `connected`
且请求完成。`opencode-runtime`、`opencode-runtime-lifecycle`、`knowledge-mcp-gateway`
三个测试文件 180 项通过;`npm run typecheck` 通过;相关文件 ESLint 通过。模型为本地合成
服务,外部模型调用 0 次。9 月 30 日那次失败的原始时序没有底层日志,未确认就是这一竞态;
本次修复的是已复现的同类失败路径。尚未发布或替换已安装应用。
## 2026-09-30:本地 MCP 连接诊断与恢复
已实现[本地 MCP 初始化失败恢复](./runtime-process-reuse-technical-design.md#41-本地-mcp-初始化失败恢复)。
常驻 HTTP 网关和请求级授权保留;内置及自定义 MCP 对明确的暂时性传输故障最多重试一次,
必须先成功清理失败连接。SDK 返回错误和抛出错误均有覆盖,未知失败、HTTP/API 拒绝和
鉴权错误不重试。界面保留脱敏错误详情,两端失败诊断通过令牌摘要关联。
网关初始化异常也会回收未完成会话。
最终定向验证:`npx vitest run` 指定 `opencode-runtime`、`opencode-runtime-lifecycle`、
`opencode-runtime-permissions`、`knowledge-mcp-gateway`、`knowledge-mcp-gateway.obsidian`、
`create-runtime`、`desktop-diagnostics`、`feedback-service` 八个测试文件,222 项全部通过。
`npm run typecheck` 和 `npm run lint` 通过。真实 OpenCode 与实际网关的恢复用例在连接成功后
注入响应丢失,验证断开先于重连、授权不变、请求完成和网关继续监听;该用例每轮产生一次
本地合成模型请求,外部模型调用为 0。另有真实 HTTP 初始化连续失败后的会话回收测试。
全量 `npm test` 在 600 秒后超时,未取得汇总;超时前报告 `SettingsPanel.test.tsx` 中
中英文磨砂玻璃开关两个用例失败。这些文件存在并行修改,未判定失败归因,全量验证不记为通过。
本次未发布或替换已安装应用。远程 Agent 的 ACP 和 Agent MCP 连接路径未修改,
不将本地测试结果作为远程恢复验证。
## 2026-09-30:OpenCode 终端启动准备
原先每次“在终端中打开” OpenCode 都会把随包插件依赖目录(约 3,600 个文件、52 MB)
复制到临时目录,Windows 本机每次约 2.2–2.4 秒。现在该目录只复制一次到用户数据下的
`terminal/opencode-config`,按 `.goodbuddy-ready.json` 判断是否需要刷新;Skills、Ask
插件和模型配置仍按每次启动单独生成,关闭时清理。
Windows 本机实测启动准备:首次 2.9 秒,之后 19–44 ms。OpenCode 自身 TUI 从进程启动
到显示输入框约 2.9–3.3 秒,由 OpenCode 程序决定,本次未改变。验证:原生终端单元测试
5 项通过;已安装 OpenCode 的 Ask/Execute 真实 PTY 测试 2 项通过(合成模型,真实模型
调用 0 次)。远程 Agent 使用独立的 OpenCode 启动路径,不受本次修改影响。
## 2026-09-28:消息取消与失败展示
消息终态按[Runtime 交互边界](./runtime-interactions.md)保存和展示。取消使用结构化
`terminalStatus`,保持已有回答;终止提示不再补入正文。“重新编辑”只回填并聚焦输入框,
没有可回填文字时隐藏入口。旧快照只对精确重复的形态隐藏状态副本,保留原正文。
深色主题已移除覆盖失败提示颜色的通用规则;检索和压缩记录不再屏蔽终态的无障碍角色。
Windows 本地验证:
- 本次修复完成时 `npm run typecheck`、`npm run lint`、`git diff --check` 通过。
后续并行的应用入口修改新增 `App.test.tsx:14430` 的 `getByRole(..., { exact: true })`,
最后一次类型检查报 TS2769;该选项不属于 `ByRoleOptions`。未修改这处他人新增代码,
当前组合工作区不能记为类型检查通过。最后一次 lint 和 diff 检查通过。
- `npm test`:5,367 通过、84 跳过、1 失败。失败项为
`tests/magic-notes-layout.electron.test.ts` 的原生拖拽测试,90 秒超时;单独复跑仍超时。
该文件未修改,本次检查不记为全量通过。
- 相关五文件复跑为 447 通过、14 失败;14 项均在并行入口改名期间查找“应用”按钮失败。
入口源码更新后,单独复跑 `src/renderer/src/App.test.tsx` 的 304 项全部通过。
其余四文件(数据库、时间线、清单和终态 Electron 探针)的 157 项在上述复跑中通过。
- `tests/terminal-status-probe.electron.test.ts` 通过真实 App、Preload、生产 IPC、
直连 Runtime 和 SQLite 验证空回答取消、部分回答取消、HTTP 400 失败;检查提示唯一性、
浅深主题 computed style、重新编辑回填与焦点,以及数据库重开和页面恢复。
每轮使用 3 次本地确定性 HTTP 请求,外部模型调用 0 次。探针在结束时关闭 Runtime、
HTTP 服务和窗口,并删除本轮独立临时目录。
- 数据库回归覆盖恢复事件事务回滚、失败与取消重放、空正文和已有回答保留、重开后元数据。
App 和时间线回归覆盖新事件结构化判定、旧消息精确去重、正常正文保留、无效按钮隐藏、
活动及工具终态。
本次未连接远程主机,未创建远程资源。远端事件仍通过既有公共事件进入桌面的
`reduceRecoveredAgentEvent`,本次仅修改其消息投影与元数据保存,不改变 Agent、SSH
传输、取消执行、ACK 或重连控制;本地 SQLite 恢复测试覆盖该修改。未验证 Linux/macOS
原生界面,不把本地探针记为远端取消生命周期验收。
## 2026-09-28:DS Web 资源符号链接与 macOS 发布恢复
Desktop `v0.15.0` 原生构建中,两个 macOS 目标均因包外符号链接未通过签名校验。
Linux Host 隔离 fixture 调用实际 DS 资源准备及 electron-builder 文件复制后,
确认相对 CLI 链接变成构建目录的绝对路径。复制现使用 `verbatimSymlinks`,
保留 npm 相对链接;回归移除原始安装和暂存目录后,仍须能读取最终包内链接目标。
Windows 打包聚焦回归 45 项通过,1 项 POSIX 符号链接测试不适用于 Windows;
类型、lint 和 0.15.1 双语说明校验通过。完整冻结快照在 Linux Host 全量
5,372 通过、51 跳过,新增符号链接用例实际执行通过;macOS 原生签名仍待发布 CI。
这只改变 Desktop 的本地 DS Web 资源准备,不改变 Agent 的独立 Runtime 包装、
模型桥或启动控制,模型调用 0 次。恢复版本及最终证据见
[发布恢复记录](../../development/release-preparation-0.15.0.md)。
## 2026-09-27:窄栏添加入口
“+”已移出页签滚动容器;左右箭头和溢出测量使用扣除添加按钮宽度后的页签区域。
Windows Electron 回归覆盖 `300px` 工作栏、默认四页签和 16 个含长标题的页签,验证
原生鼠标打开应用目录、创建浏览器实例、Escape 返回、箭头滚动及 Home/End 激活。
窄栏扩到 `900px` 后,默认页签的溢出箭头消失,缩回后恢复。
中英文、浅深主题均通过,模型调用 0 次。范围与命令见
[窄容器布局修复记录](../../quality/responsive-controls-review-2026-09-27.md)。
## 2026-09-27 Continue Update Status
The shared Continue adapter now skips the update service before it emits the
checking state; behavior is specified in the [native client design](./runtime-native-client-proposal.md#62-continue-与-opencode).
The regression executes the patched method and verifies idle-only state when
disabled, while retaining the original path when the flag is absent.
- Windows production coordinator/PTY and real DS browser validation completed:
Continue made 2 supplier requests, OpenCode 1, and DS Web 2; all five returned
HTTP 200 and the clients displayed the expected answers. Captured terminal
output contained no `Checking for updates`. Original settings stayed byte-identical.
- Current adapter source was bundled and uploaded through pinned-identity SSH to
the shared Linux x64 Host, under a dedicated `/root/tmp/gb-update-check-*`
directory. Continue 1.5.47 reached its initialized Ask TUI and displayed
`Continue CLI`; 15 seconds of PTY capture contained no update-check text.
This startup check made zero model calls and did not run the Agent protocol or
remote model bridge. Agent helper imports this same adapter, but complete
remote shortcut and real-model acceptance remain unfinished.
- Adapter tests: 30 passed. Repository typecheck and lint passed. Full `npm test`
reached the tool's 600-second limit without a final summary; it is not recorded
as a passing full run.
Evidence is in the local temporary `opencode/dsh-017-verify` directory:
`update-check-evidence.json`, `update-check-main-result.json`, and
`remote-pty-run.cjs`. The local action/IPC harness uses jsdom and simulated
Electron transport with real child clients. It does not reproduce or rule out
full desktop-window freezing. The inherited TLS verification override and lack
of a complete background-network capture also limit the evidence.
### Full production App follow-up
A fresh `npm run build` passed. The resulting production Main, Preload and
Renderer were launched in an isolated Windows Electron profile. Native mouse
input selected Continue and opened its terminal; native text input and Enter
submitted a minimal request. The actual Continue output contained the expected
joined answer and no `Checking for updates`. Main HTTP dispatch instrumentation
recorded two additional provider requests, both HTTP 200: this fix's validation
now totals seven external model requests. A subsequent no-submit probe copied
initialized TUI content and typed input from the active xterm buffer.
Early driver attempts targeted a restored inactive terminal, giving an empty
clipboard; selecting the newly opened terminal resolved this without product
changes. Another attempt sent Enter before focus/input settled and dispatched
zero requests. These harness failures are not product freeze fixes. The full App
remained callable and accepted terminal input in the successful runs; the user's
original freeze has not been reproduced or uniquely diagnosed. Captured PNGs
have not been visually reviewed. The production TLS policy still enabled the
verification override, despite the launcher's strict environment setting.
Local evidence: `full-app-model-events.json`, `full-app-calls.json`,
`full-app-buffer.txt` and the `full-app*.cjs` drivers in the same temporary folder.
The isolated profile was removed after checking that no probe-owned Electron or
Node processes remained; the separate diagnostic evidence was retained.
The full-suite rerun completed in 734 seconds: 5,298 passed,
86 skipped and 3 failed tests, plus one failed suite setup. Failures were the
OpenCode isolation fixture (`spawnSync npm.cmd EINVAL`), Magic Notes layout's
90-second timeout, and two HeartbeatCenter tablist assertions. These files were
not changed here; this is not an all-green suite. Original runtime settings were
byte-identical after the full-App probes, and no probe-owned processes remained.
Remote shortcut integration and full Agent/model acceptance remain outstanding.
### Failure triage follow-up
The OpenCode isolation and native Magic Notes layout files passed together through
`npm test` (8 tests). The earlier full-suite supervisor invoked Vitest directly,
without npm's launcher environment; its OpenCode setup error did not recur through
the npm entry point. The layout timeout also did not recur. These focused results
do not replace the failed full-suite result.
Both HeartbeatCenter language cases failed independently at that point. Source inspection
shows the graph contains a separate, named PageTabs for events/entities/relations
inside the page-level tabs; the older test assumes there can only be one tablist
anywhere on the page. Neither that feature nor its test was changed for this
Continue fix. No unrelated UI change or assertion relaxation was made.
A separate 2026-09-27 follow-up corrected the navigation scope assertions after
checking the graph design. HeartbeatCenter, SupervisorWorkspace and shared tab
tests passed together (96 tests); the full suite was not rerun. See the
[validation record](../../quality/responsive-controls-review-2026-09-27.md#heartbeatcenter-断言修复).
The retained full-App log contains no matching unresponsive, crash, timeout or
maximum-update-depth errors. This does not establish the cause of the reported
freeze. This follow-up made zero external model requests (the fix-validation total
remains seven); the OpenCode regression uses loopback model fixtures.
## 2026-09-27 Local Native Client Validation
Scope: [native client proposal](./runtime-native-client-proposal.md), FR-2/3/5/6/7/8,
US-2/4/5/6/8, local portions of P1/P2/P3/P5. Source baseline:
`55efca3ee1d8a491c4d9709888e302188cd988bd` plus the concurrent working tree.
- Fixed the coordinator's browser-open failure path: a ready DS service and its
gateway remain available for owner-scoped reopen/stop. Startup failures still
release their gateway. Focused regressions cover current credentials, work mode,
Node selection, concurrent open, owner isolation and retry.
The DS service also includes the coordinator configuration key in its own match
hash; updated skill digests or launch settings cannot reuse an older instance.
- DS Ask now maps built-in gateway read grants to the official MCP tool names.
Custom MCP remains Execute-only, following existing Runtime behavior. The real
DS process test loads an assigned directory containing `SKILL.md`, rather than
only a synthetic loose Markdown skill file.
- Custom standard Node uses the current local tool environment selection without
a separate npm install. The managed Node fallback still has a bespoke pinned npm
install and remains a delivery gap; it was not exercised by this real-model run.
- The opt-in IPC integration test uses the production action component, preload
methods, registered handlers, saved project/model stores, coordinator, official
DS Web process, and real Continue/OpenCode PTYs. Electron transport is simulated
by the existing IPC test harness and the action component runs in jsdom; this is
not a full desktop-window or packaged-app acceptance result. DS Web itself was
driven in real headless Edge. The hook retains credentials only in process
memory; the temporary settings file stores opaque test-cipher identifiers.
| Client | Configured Model | Supplier HTTP Dispatches | Result |
| --- | --- | ---: | --- |
| DS Web 0.1.7-rc.2 | `deepseek-flash` | 2 | Workspace/model selected automatically; Web displayed `NATIVE_WEB_OK` |
| Continue 1.5.47 | `gpt-6-astra` | 4 | Two prompts and two title requests; second transcript confirms the assistant response, with underscores consumed by Markdown rendering |
| OpenCode 1.18.29 | `gpt-6-astra` | 1 | Native TUI displayed `NATIVE_OPENCODE_OK`; supplier response also contains it |
| Total | | 7 | All HTTP 200; no further dispatches needed |
The first terminal assertion matched echoed prompt text and was rejected as
evidence. The second prompt did not contain the expected joined answer. The
OpenCode probe waits for the rendered TUI and uses bracketed paste before Enter.
The initial six-dispatch bound was increased to eight to cover the remaining
OpenCode path; seven were used in total. The host environment supplied
`NODE_TLS_REJECT_UNAUTHORIZED=0`, so this run does not verify strict TLS.
Evidence under the approved temporary `opencode/dsh-017-verify` directory:
`native-live-evidence.json`, `native-main-result.json`, `native-web.png`, and
`native-{continue,opencode}-terminal.txt`. `native-live-run.cjs` composes the existing
encrypted settings snapshot/safeStorage bootstrap with the production IPC test.
Original `runtime-settings.json` remained byte-identical. The final test verifies
DS stop/reopen/owner isolation and failed fetch after stop, closes both terminal
sessions, and removes its temporary workspace/configuration. Owned live client
processes were absent after completion.
Checks: official DS/coordinator focused tests passed (12) under standard Node 24.19.0;
the opt-in UI/preload/IPC test passed both without provider calls and with the live
hooks. Four installed Continue/OpenCode Ask/Execute tests passed with real clients
and deterministic supplier responses; these made zero external supplier calls.
Repository lint and Agent/Renderer TypeScript checks passed. Main typecheck is
currently blocked by concurrent SSH
test doubles missing `forwardLoopback`; no local native-client type errors remain.
The full `npm test` run finished with 5,285 passed, 79 skipped and one failure:
the desktop runtime-hook fixture lacked the new DS Web package tree. The fixture
was corrected and all 44 build-release tests then passed in a focused rerun. The
full run began before the final configuration-key regression and fixture updates;
the final DS/coordinator and UI/preload/IPC paths were rerun separately and passed.
This is not a claim of a subsequent all-green full-suite run. Full output is in
`native-full-tests.log` beside the live evidence.
Remaining at this checkpoint: remote coordinator wiring and real Host acceptance belong to the
concurrent remote implementation; Continue/OpenCode Ask built-in MCP mappings,
managed standard Node distribution, full Electron/packaged UI acceptance and other
platforms are not verified here. `ipc.ts` was not edited in this work.
2026-09-28 update: native Continue/OpenCode Ask now pass Main-bound read-tool
names to native permission flags and execution hooks. The Story Graph work also
adds Supervisor enablement to native-client reuse keys and gateway checks.
The OpenCode generated plugin is executed by a focused test; packaged native UI
acceptance remains separate. See [Story Graph validation](../conversation-supervision/progress.md#2026-09-28-story-graph-只读工具).
## 2026-09-26 结构升级与历史回收分离(未发布)
- schema 45/46 的回归先复现普通结构升级执行 `VACUUM`,修复后不转换历史、
不回收正常空闲页,保留聊天、笔记、事件及 SQLite 完整性。
- 启动决定在内存中分别保留笔记转换与回收需求,Worker 重试复用原决定;
不增加 schema 或持久标记,不修改正常写入。规则见
[旧库升级](./execution-history-storage.md#旧库升级)。
- 真实 Node Worker 验证结构升级、旧笔记转换、取消与重试、文件大小和内容保留;
启动调度回归验证允许/禁止回收的两种重试,中英文页面区分实际操作。
六个定向测试文件 **47 项通过**,类型检查、lint 和 `git diff --check` 通过。
- 完整 `npm test`:**5,138 通过、74 跳过、1 失败**。未修改的
`tests/magic-notes-layout.electron.test.ts` 原生拖拽断言失败:首次索引列保持 168,
单独复跑则 AI 列保持 280,均未达到拖动后的预期宽度。该 fixture 直接加载
Magic Notes 和模拟 API,不运行本次存储升级路径。随后修正测试的原生输入同步:
等待焦点、拖拽建立、宽度更新及结束,并明确左键按下状态,未弱化断言或改变产品
布局;独立复跑及随后完整测试中的该用例通过。
- 最终完整重跑:**5,137 通过、74 跳过、2 失败**,迁移相关及布局拖拽均通过。
`inline-help.electron.test.ts` 超时,`overlay-layering.electron.test.ts` 子进程退出码
为 null;这两个未修改的用例随后一起独立复跑 **2 项通过**,未提高超时或修改断言。
最终 typecheck、lint、`git diff --check` 均通过。独立复跑不替代完整测试结果,
不宣称本次全量或 CI 全绿。
- 本机与托管 SSH 历史共用 Desktop 数据库升级路径;Agent、Runtime、模型桥和
远程协议未变,不需要部署 Host 补丁。本次未调用模型,未改写用户数据库,
尚无本次修复的 CI 或发布验证。
## 2026-09-21 重复历史迁移补修(Desktop 0.13.9 候选)
- 0.13.8 的检查仍在已有笔记时把 `freelist_count > 0` 当作迁移依据;正常聊天更新
@@ -110,7 +110,9 @@ No SQLite schema migration or separate checklist store is needed.
`ConversationTaskStrip` 与 `RuntimeChecklistStrip` 共用顶部容器,任务在上、清单在下,
各自展开和折叠。没有 Task 时清单仍可显示;远程会话也保留清单入口。总高度不超过
45vh,各自展开内容不超过 20vh 并内部滚动,标题不收缩。清单内容区可用键盘聚焦和滚动。
45vh,Task 展开内容不超过 20vh 并内部滚动,标题不收缩。清单详情复用 `AnchoredMenu`
的 `group` 语义和 `FloatingPortal`,宽度跟随摘要按钮并受视口约束,高度按可用空间限制,
内部可用键盘滚动。展开不改变顶部容器或消息流的高度;点击外部、焦点移出或 Escape 收起。
状态文字与条目首行按文字基线对齐,状态图标单独居中。CN 不推断进行中或优先级。
新轮先选择本轮消息,再读取清单,不回溯寻找旧轮非空清单。后台更新保持展开状态和焦点。
@@ -100,6 +100,16 @@ DeepSeek Harness 按调用 ID 保留名称、输入和摘要,结束事件省
为静态,不凭请求尚未结束暗示仍有进展;会话列表闪点和发送、停止按钮保持原有行为。
持久化快照中的同一条消息仍为生成中时,也不能覆盖本地已收到的终态,即使快照的
会话更新时间更晚;否则连续发送或刷新会让已结束的请求重新显示停止按钮。
- 失败和取消的终止提示只进入消息状态,不补写正文或文本块,已有回答、推理与工具输出
保留。取消使用中性色和礼貌级 `status`,真实失败使用危险色和 `alert`;运行记录的
取消结果标题为“任务已取消”。工具详情已表达同一失败时,不再复制到消息底部。
- 最新失败或取消消息只有在上一条用户消息含可回填文字时显示“重新编辑”。点击只回填
输入框并聚焦,由用户修改后自行发送;附件单独发送且无文字时不显示此按钮。
- 实时事件和 Main 历史恢复均将事件的 `failed / cancelled` 写入消息元数据
`terminalStatus`;既有 `state: error` 继续表示未成功终态,不改 SQLite 枚举。
新事件不按提示文案识别取消。旧快照无此字段时,仅兼容已发布的精确取消标签。
旧记录若正文与状态完全相同,且无文本块或唯一文本块位于末尾并等于该状态,展示时
保留正文、隐藏重复状态;不修改持久数据,不删除正文中的同名句子或部分匹配内容。
- Continue、DeepSeek Harness 和远端 ACP 保留其已接入的处理与工具事件,不伪造未接入
的原生重试状态。直连模型的重试等待与请求开始文案见
[直连模型 UI 设计](../direct-model-agent/ui-design.md#6-错误与通知);本次不改变任何
@@ -0,0 +1,403 @@
# Runtime 原生客户端启动方案
日期:2026-09-26
Status: Local shortcut/coordinator paths and configured-model responses verified on Windows; remote integration and packaged Node delivery remain open.
范围:Continue、OpenCode 的交互终端,以及 DeepSeek Harness(DS)的官方 Web 客户端。
本文集中维护该新增入口的需求、场景、行为规则、接入设计和验收计划。既有终端行为由
[多终端页签 PRD](./terminal-tabs-prd.md)维护,既有 Runtime 权限和资源边界由
[Runtime 交互边界](./runtime-interactions.md)维护;本文描述新增客户端路径,不宣称它已经具备后台 Runtime 的全部能力。
## 1. 方案选择
在聊天输入区“压缩上下文”左侧增加原生客户端入口。Continue 和 OpenCode 打开 GoodBuddy
工作栏中的交互终端;DS 启动官方 Web 服务并在系统默认浏览器中打开。使用当前项目目录和
当前模型,默认建立独立客户端会话,不自动复制 GoodBuddy 聊天历史。
| 当前 Runtime | 按钮文字 | 打开目标 | 本轮计划范围 |
| --- | --- | --- | --- |
| Continue | 在终端中打开 | 工作栏新终端页签,启动原生 CLI | 本地和托管 SSH 项目 |
| OpenCode | 在终端中打开 | 工作栏新终端页签,启动原生 TUI | 本地和托管 SSH 项目 |
| DeepSeek Harness | 打开DSH Web | 系统默认浏览器,访问官方 Web 应用 | 本地项目 |
| 内置模型 | 不显示此快捷入口 | 无 | 保持现状 |
DS 采用官方 `@deepseek-ai/dsh` Web 应用,不开发新的 DS 终端前端,也不在首轮嵌入
GoodBuddy 浏览器工作栏。远程 DS 不在本轮范围:当前共享远程启动契约只接受
`opencode` 和 `continue`,不能通过快捷按钮绕过该范围。
## 2. 调研依据与证据范围
下表保留早期测试报告,作为技术选型线索。2026-09-26 新增的 DS 0.1.7-rc.2
实测单列在表后;Continue、OpenCode 和远程交互终端本轮未复测。
| 历史报告项目 | 报告结果 | 尚需验证 |
| --- | --- | --- |
| Continue 1.5.47 | 标准 Node 与真实 PTY 可显示交互输入、配置和模型名称;直接用 Electron 启动失败 | 启动路径错误、当前模型注入、真实请求和权限 |
| OpenCode 1.18.29 | 随包二进制可显示 TUI,响应终端尺寸变化 | 排除已有用户配置干扰,验证指定模型实际生效 |
| DS Web 0.1.5-rc.3 | Node 24.19.0 启动;Edge 可访问页面及模型设置 | 项目自动选中、模型桥、真实对话、工作模式 |
| DS Windows 依赖 | 首次缺少 Sharp 原生包,补装后可启动 | 正式资源中预先带齐各目标平台依赖 |
| DS 工作目录 | 指定 cwd 后仍要求选择工作区 | 工作区创建及页面选中的完整路径 |
| 终端相关回归 | 报告为 5 个文件、29 项通过 | 新入口尚无回归或端到端验收 |
| 远程 Host | 两条网络路线 SSH 端口可达 | 登录后的当前源码、Runtime、模型桥真实验证 |
早期文档整理和依赖升级报告的真实模型调用为 0 次。本轮核查另发现临时目录
`dsh-017-live` 的 7 次历史 dispatch 记录;本轮在独立 `dsh-017-verify` 目录新增
9 次,两个计数分别保留。测试过程和结果以 [DS 实现进度](../deepseek-harness/progress.md#2026-09-26-真实模型与官方-web-验证)为准。
本轮直接检查了以下仓库材料:
- `src/main/terminal/terminal-session-manager.ts`:已有本地与 SSH 会话创建、窗口归属、事件转发和关闭管理,当前创建路径启动普通终端。
- `src/shared/remote-runtime-launch-contracts.ts`:远程 Runtime 枚举包含 Continue 和 OpenCode。
- `UI-DESIGN.md`:规定 Composer 元信息区、工作栏、通知、键盘及响应式规则。其聊天章节仍写远程只支持 OpenCode,与当前共享契约不一致;实施时需沿 UI 到执行路径核实并同步修正,不能单凭枚举认定远程交互已可用。
上游参考:[官方仓库](https://github.com/deepseek-ai/deepseek-harness)、
[CLI 文档](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md)、
[使用指南](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/index.md)。
这些链接随上游变化;实施时须为选定包版本补充对应源码 revision、启动参数和接口依据。
历史测试版本是候选版本,不作为未经复测的最终发布版本。
2026-09-26 仓库内后台 DS Runtime 已升级至 `0.1.7-rc.2`,Cordis 升级至
`4.0.4`,验证记录见 [DS 实现进度](../deepseek-harness/progress.md)。此版本来自
npm `next` 标签;查询时 `latest` 仍为较旧的 `0.1.0-rc.6`。本轮另用官方
`@deepseek-ai/dsh@0.1.7-rc.2` 验证 Web。Prepared Web resources and backend validation are recorded in section 9.1.
### 2.1 DS 0.1.7-rc.2 实测结论
Windows x64、标准 Node 24.19.0、Edge 154.0.4258.37 下,官方 Web 可承接当前
工作区和已配置模型,完成页面真实对话。Main 使用 safeStorage 解密隔离副本中的模型
配置,经现有 managed model bridge、AgentModelGateway 和 loopback proxy 转发。
浏览器与 DS 只取得本地桥接信息,原始 `runtime-settings.json` 测前测后逐字节一致。
| 实测路径 | 结果 | 供应商 HTTP dispatch |
| --- | --- | ---: |
| 后台 Ask → `read` | 返回文件中未写入 prompt 的随机校验码,收到工具完成及用量事件 | 2 |
| 同一后台会话切 Execute → `write` | 创建测试文件,内容校验通过 | 2 |
| Execute → `pwsh` | PowerShell 读回该文件,结果正确 | 2 |
| 官方 Web → 当前模型 → 文件读取 → 回答 | 页面返回另一随机校验码,刷新后保留结果 | 3 |
| 合计 | 请求中的模型均为 `deepseek-flash`,HTTP 均为 200 | 9 |
`0.1.7-rc.2` 是 Harness/Web 包版本;`deepseek-flash` 是实际配置和请求中的模型 ID。
Web 的 3 次请求包含 1 次未携带工具的请求,计数没有只按用户提交次数计算。
后台使用当前源码的真实 ACP、Host、控制面和供应商流式响应,但 Host 在测试进程内组合;
该结果不等于重新验收 GoodBuddy 完整 UI 或 Utility Process。Web 使用真实 CLI 子进程和
浏览器自动化,未通过尚未实现的快捷按钮。测试继承了环境中的
`NODE_TLS_REJECT_UNAUTHORIZED=0`,因此没有验证严格 TLS 证书校验。
工作区和模型自动准备已验证;官方 Web 到 GoodBuddy Ask/Execute 的工具执行适配、
Skills/MCP 承接、取消和产品生命周期仍待实现与验证。此次可行性验证不要求完成按钮。
## 3. 产品需求
| ID | 要求 | 用户可见的验收结果 |
| --- | --- | --- |
| FR-1 | 在压缩按钮左侧提供 Runtime 对应入口 | 无历史、无压缩能力时仍能打开客户端 |
| FR-2 | 按项目执行空间启动 | 本地使用本地目录,SSH 使用绑定 Host 与远端目录;目录失效时说明原因 |
| FR-3 | 注入点击时选定模型 | 客户端首轮直接使用该模型,无需重复输入供应商密钥 |
| FR-4 | 创建独立客户端会话 | 不修改原聊天历史,不把打开操作伪装成当前聊天的续接 |
| FR-5 | 保持所选工作模式的执行边界 | Ask 的 AI 工具路径只读;Execute 使用相应账号已有权限 |
| FR-6 | 管理启动、重开和关闭 | 不因收起工作栏或切换项目中断;关闭对应客户端资源时释放其桥接 |
| FR-7 | 保留可处理的失败信息 | 启动、模型和远程错误可区分,允许修正后重试,不清空聊天草稿 |
| FR-8 | 明确原生能力与 GoodBuddy 能力差异 | 已分配的 Skills、MCP 等逐项接入并验证;未接入项有明确说明 |
| FR-9 | 交付可直接运行的客户端资源 | 用户无需自行运行 npx、补装 Sharp 或查找 Node |
聊天附件、知识范围、专家提示及历史不自动移入独立客户端。首轮不提供聊天双向同步、
Task 关联恢复或客户端历史导入。原生客户端产生的历史由对应客户端保存,不能在 GoodBuddy
活动记录中显示为已经同步的聊天内容。
FR-8 的实施清单须区分原生工具、GoodBuddy 分配的 Skills、自定义 MCP、内置 MCP、知识工具
以及遵循当前工作模式授权的配置工具。配置应用不再额外要求 GoodBuddy 原生确认,Ask 仍只读。
支持情况以真实调用为准;现有 DS 宿主插件能力不能
直接算作 DS Web 能力。发现现有分配无法承接时,在实施评审中明确差异和处理方式,不能默默忽略。
## 4. 场景与状态规则
### 4.1 用户场景
| ID | 前提与动作 | 预期结果 | 需求 |
| --- | --- | --- | --- |
| US-1 | 本地项目选中 Continue 或 OpenCode,点击入口 | 展开工作栏,新建带 Runtime 与项目名称的终端,进入可输入的原生界面 | FR-1~4 |
| US-2 | 本地项目选中 DS,首次点击入口 | 服务就绪后打开浏览器,当前项目已选中、模型可直接使用 | FR-2~4、9 |
| US-3 | 在支持的 SSH 项目点击终端入口 | 使用已保存 Host 连接,在远端项目目录启动对应 Runtime | FR-2、3 |
| US-4 | 启动期间连续点击 | 同一启动只创建一次;按钮显示启动中 | FR-6 |
| US-5 | 已打开客户端后切换项目或模式 | 旧客户端保持原项目和启动模式;新操作使用新选择 | FR-5、6 |
| US-6 | 启动失败或模型请求失败 | 保留原因与可操作的重试方式;聊天内容和草稿不变 | FR-7 |
| US-7 | Ask 下从原生界面请求写文件、执行写命令或使用写入 MCP | AI 执行边界拒绝写入,不能通过客户端模式开关绕过 | FR-5、8 |
| US-8 | 关闭终端、停止 DS 服务或退出 GoodBuddy | 对应进程和桥接释放,其他会话不受影响;原生历史保留 | FR-4、6 |
### 4.2 行为决策
启动参数按点击时的项目、执行空间、Runtime、模型和工作模式确定。界面后续切换不修改已运行
客户端。这里只需保留进程运行所需的内存记录,不增加持久启动快照、恢复日志或数据库迁移。
Continue 和 OpenCode 启动成功后,每次新的点击创建独立终端。启动中的重复点击合并到同一
进行中操作。终端会话数量沿用现有管理器规则,不新增一套数量控制。
DS 按项目、模型配置及工作模式匹配运行服务。配置相同且服务可用时重新打开原服务;配置变更后
新建匹配实例,保留原实例直到用户停止或应用退出,避免在已有页面中静默改变模型和权限。
同一匹配项并发启动只执行一次。服务已退出则清除内存记录,下一次点击重新启动。
| 事件 | 处理 | 关联场景 |
| --- | --- | --- |
| 启动中切换项目 | 操作仍属于原项目;完成时不抢占新项目的工作栏焦点 | US-4、5 |
| 收起工作栏、切换页面 | 保留终端与其模型桥 | US-5 |
| 关闭终端页签 | 停止该 PTY/远端进程,释放相应配置和桥接 | US-8 |
| 关闭 DS 浏览器标签 | 不推断服务已停止;再次点击可重开 | US-2、8 |
| 用户停止 DS 服务 | 停止该服务和桥接;浏览器页面失去连接;保留原生历史 | US-8 |
| GoodBuddy 退出 | 清理自己创建的进程与桥接,不停止用户其他进程 | US-8 |
| SSH 断开 | 显示断开;收束对应请求,重试时重建连接,不宣称原会话已恢复 | US-3、6 |
| 系统浏览器打开失败 | 保留可重开的就绪服务,通知用户重试;不复制含令牌地址到普通日志 | US-6 |
| 服务启动失败 | 清理本次创建的临时资源,再允许重试;不删除用户原生历史 | US-6 |
Ask 沿用现有 Runtime 按工具命令名判断的只读规则,不改变用户手动终端的已有权限。
DS 后台的 `read` 和原生 `skill` 可在 Ask 执行;`write`、`edit`、`pwsh`/`bash`
在 Ask 拒绝,在 Execute 放行,代理工具沿用现有分类。工具出现在模型请求目录中不等于
获准执行,判断仍在 `tools/execute` 路径。Web 接入复用这些规则,不增加 Shell 文本分析、
目录围栏、逐工具二次确认或新的权限等级。Execute 使用当前账号已有的文件、进程和网络权限。
官方 `dsh-plan-mode@0.1.7-rc.2` 的 Plan 只改变规划提示和状态,工具目录不随之变化;
权限预设由另外的 sandbox/approval 组件处理。因此不能把 Plan 名称当成 Ask 实现,
也不能把官方默认“工作区内修改”当成 GoodBuddy Execute。接入时将启动模式接到现有
工具名规则,原生页面的模式选择保持同一语义。
这些规则覆盖普通启动、使用、关闭和已知失败。尚需技术验证的项目见第 7 节;它们不构成新增
授权层级或通用恢复框架的理由。
## 5. 界面设计
输入区下方从左到右排列“在终端中打开/打开DSH Web”“压缩上下文”和现有用量信息。
两个按钮独立判断可用性。采用同一轻量操作组,避免两组绝对定位相互覆盖;窄输入区允许
操作组换行,保留模式说明、用量信息和对称边距。稳定预留元信息高度的实现应与
[统一 UI 规范](../../../UI-DESIGN.md)一起更新并验证。
按钮使用原生 `button`、共享主题令牌和焦点环,支持 Tab、Enter 与 Space。打开终端后焦点
进入新终端;外部浏览器由系统处理焦点。启动期间保留按钮宽度并显示“正在打开…”,完成后
恢复原文案。缺少项目目录时在相关位置提供选择目录的操作;其他异步失败进入全局通知视口。
DS 服务运行后,打开入口仍显示“打开DSH Web”,旁边显示“停止DSH服务”;操作指向当前匹配服务。
点击停止后,在同一操作行追加紧凑的“确认”“取消”按钮,不再展开整行警告。窄窗口下确认和取消作为一组换行,两个按钮不拆行。
确认按钮的悬停提示和无障碍描述说明网页会断开、进行中的请求会中断;执行时由确认按钮显示“正在停止…”。输入区不常驻显示独立会话和服务生命周期说明。不在每次启动时增加确认弹窗。
按钮快捷入口根据所选 Runtime 显示,不改变工作栏“+”中的普通终端和浏览器入口。已有服务
的停止入口应在切换回来后仍可找到。
## 6. 技术接入
### 6.1 进程职责
Renderer 仅提交当前会话或项目标识及选择标识。Main 重新解析项目、模型配置、工作模式与
程序路径,通过共享 Zod 契约和显式 preload 方法完成调用。保留已有可信发送方校验,供应商
密钥留在 Main 加密设置和模型请求路径中。
建议新增小型原生客户端启动服务,负责配置准备、启动去重和资源释放。终端会话仍由
`TerminalSessionManager` 管理;DS 服务只需内存中的子进程、地址和桥接引用。不要新建平行
终端管理器或通用进程恢复引擎。
共享接口应返回可供界面使用的启动状态、终端标识或 DS 服务标识。外部 URL 由 Main 打开,
不允许 Renderer 提交任意程序、命令行、凭据或任意服务地址。错误保留组件、阶段和脱敏原因。
DS Web 随包依赖通过 `build/native-dsh-resources.cjs` 保留 npm 的嵌套布局。
复制时原样保留相对符号链接,不能将 CLI 链接改写成构建机的绝对路径;
经过 electron-builder 资源复制后,即使移除原始安装和暂存目录,链接仍须能解析到包内文件。
这一交付约束覆盖 FR-9;不改变 Agent 的独立 Runtime 打包路径。
### 6.2 Continue 与 OpenCode
Continue 使用已解析的标准 Node 启动固定版本 CLI,不能直接假定 Electron 的
`process.execPath` 等于 Node。OpenCode 使用已有平台对应可执行资源,启动交互 TUI。
两者均复用现有配置生成机制,但单独验证原生交互模式可接受的参数、模型 ID、环境变量及
Skills/MCP 配置,避免继承测试机器的全局默认模型。
本地 PTY 创建需接受 Main 生成的可执行路径、参数、环境和 cwd,不能先打开普通 Shell 再
模拟键入一段含凭据的启动命令。退出 CLI 后是否保留终端输出沿用既有终端体验;其桥接应
随 CLI 生命周期收束,不等到整个聊天结束。
Continue 的共享宿主适配器在 `GOODBUDDY_DISABLE_CONTINUE_UPDATES=1` 时,直接让更新服务保持 idle,并显示 `Continue CLI`,不进入 checking 状态。仅跳过底层版本查询仍会短暂显示 `Checking for updates`,因此服务入口和版本查询两处都需跳过。本地原生终端与 Agent Continue helper 使用同一适配器;已有缓存按补丁内容哈希重新生成。实测范围见[更新提示验证记录](./progress.md#2026-09-27-continue-update-status)。
模型桥作为客户端会话资源保持有效。复用前检查当前桥接是否依赖单轮请求、单一消息 ID
或短时授权;必要时只提取所需的会话级资源接口。真实供应商密钥不写入 CLI 配置,客户端
仅获得桥接所需的局部连接信息。
### 6.3 DS Web
使用固定版本的官方 Web 包,通过标准 Node 在回环地址启动。以下命令已在
`@deepseek-ai/dsh@0.1.7-rc.2`、Windows x64、Node 24.19.0 下启动成功:
```text
<managed-node> <resolved-dsh-cli> web --no-open --host 127.0.0.1 --port 0
```
生产环境直接使用已准备的资源,不在用户点击时执行不固定版本的 `npx`。Web 资源与当前
DeepSeek Harness 后台 Host 依赖分开管理,避免为了 Web UI 升级而隐式替换后台 Agent 库。
沿用既有受管工具环境和资源分发机制,不新增下载体系。若需在线分发,按该功能文档规范
明确下载源设置范围、固定版本、大小和 SHA-256;镜像与原地址使用相同字节内容。
使用 GoodBuddy 管理的独立 `DSH_HOME`,与用户已有 DS 安装隔离。原生历史和用户设置属于
持久数据,服务退出只删除可重建启动配置。多个运行实例不得共享不支持并发访问的数据库;
在验证官方存储行为后确定按项目及配置分目录的最小方案。
启动后经官方支持的配置/API 创建或查找项目工作区,初始化模型连接,再打开已选中目标
工作区的页面。仅设置 cwd 或成功创建后台工作区都不足以完成该步骤。不得直接修改未经
确认的 DS 私有数据库结构。
本轮验证使用 `$DSH_HOME/cordis.patch.yml` 配置 `llm-pi-ai` 的 `goodbuddy` provider,
设置 `api: openai-completions`、本地桥 `baseURL`、`apiKeyEnv` 和模型列表;
`agent-default-model` 指向同一 provider/model。环境变量中只放本地桥接标识。
官方 CLI 的 `profile-boot` 代码将该 patch 作为用户配置层加载。
取得官方启动地址并建立认证 cookie 后,调用 `POST /api/workspace/create`,请求形态为:
```json
{
"type": "client-request",
"rpcId": "<uuid>",
"method": "workspace/create",
"payload": { "args": { "request": { "path": "<project-directory>" } } }
}
```
该接口在首次运行返回 `created: true`,重开同一测试目录时返回 `created: false`。
随后访问根页面,两次均自动选中 `workspace` 和 `deepseek-flash`,没有手动选择模型或
再次输入供应商密钥。此证据覆盖单项目、独立 `DSH_HOME`;多项目共享服务的选中规则未测。
首次页面保留官方“内测声明”的“继续”操作;测试通过原生按钮继续,没有伪造接受记录。
官方默认权限显示“工作区内修改”,本轮只验证读取;权限适配按第 4.2 节另行接入。
Main 检查服务就绪后,使用 Electron `shell.openExternal` 打开官方规定的访问地址。
如地址带认证令牌,沿用官方认证方式,不写入普通日志。浏览器只访问本机服务;模型请求
通过 Main 中的桥接到达供应商。HTTP、流式响应、取消和错误映射都需实测。
### 6.4 远程 Continue / OpenCode
沿用 SSH Host 身份、凭据存储和 SSH PTY,并通过既有 GoodBuddy Agent 部署资源定位远端
Runtime。远端配置与模型桥需要客户端生命周期,不能直接复用一次聊天请求结束即释放的
桥接实例。远端使用已有模型桥协议及传输路径,是否需要补充生命周期接口由源码验证决定。
开发时必须在共享 Linux x64 Host 上运行当前改动,按
[远程主机技术设计](../remote-host/technical-design.md)执行真实启动、模型请求、取消、
断开和清理。端口可达不等于验证通过;若 Host 不可达,应记录阻塞,不能宣布远程功能完成。
远程 DS 将来若扩展,可评估远端 Web 加 SSH 本地端口转发;本轮不修改 DS 远程能力声明。
## 7. 实施顺序与技术验证
| 阶段 | 工作与应提交证据 | 通过条件 |
| --- | --- | --- |
| P1 原生配置验证 | 固定三个客户端版本;保存脱敏命令、环境与程序路径;隔离已有全局配置 | Continue/OpenCode 使用指定模型;DS 自动选中目标项目 |
| P2 模型与模式 | 每个客户端发送最小真实请求;验证流式、取消、Ask 写入拒绝及 Execute 专用目录写入 | 请求确实经过 GoodBuddy 模型桥;客户端模式切换不能破坏 Ask 边界 |
| P3 生命周期接入 | 配置、PTY、DS 进程和模型桥接入现有管理路径 | 重开、失败重试、关闭及退出能释放对应资源,原生历史保留 |
| P4 远程验证 | 在共享 Host 部署当前代码,验证 Continue/OpenCode 的 SSH PTY 与模型桥 | 启动、请求、取消、断开和重试均有真实 Host 证据 |
| P5 UI 与交付资源 | 接入按钮、通知、停止 DS 服务;完善平台资源与文档 | 从真实按钮到客户端完成对话,无手动补依赖步骤 |
P1、P2 是实施前段的实验任务,完成后继续交付完整入口,不将实验程序或不可用按钮作为最终功能。
DS 单项目工作区选中和当前模型桥协议已通过本轮验证。后续优先接入三个客户端的既有
Ask/Execute 工具规则、Skills/MCP 配置及客户端生命周期,再完成快捷入口和交付资源。
官方 DS 首次预览声明保留原生“继续”流程,模型配置提前准备。
## 8. 验收计划
| 验收面 | 检查内容 | 需求与场景 |
| --- | --- | --- |
| 实际使用 | 每个本地客户端从 GoodBuddy 按钮进入当前项目,使用当前模型完成一轮真实对话 | FR-1~4、US-1、2 |
| 配置隔离 | 预置另一套全局模型后,仍使用当前选择;两个项目不串目录或配置 | FR-2、3、US-5 |
| 工作模式 | Ask 尝试文件写入、Shell 写入和 MCP 写入均不执行;Execute 在专用测试目录成功写入 | FR-5、8、US-7 |
| 能力复用 | 为声明支持的 Skills、MCP 各验证实际加载及最小调用;列出未承接能力 | FR-8 |
| 生命周期 | 连点启动、切项目、隐藏工作栏、关闭页签、重开 DS、停止服务及退出 | FR-6、US-4、5、8 |
| 已知失败 | Node/原生依赖缺失、失效目录、模型错误、浏览器打开失败、SSH 中断 | FR-7、9、US-6 |
| 远程 | 当前源码在共享 Linux x64 Host 上完成 Continue/OpenCode 的 Ask/Execute、模型和断开场景 | FR-2、3、5、US-3 |
| UI | 浅深色、键盘操作、窄输入区、无压缩按钮、空对话、200% 文字缩放 | FR-1、US-1、2 |
| 平台资源 | 对实际交付的 Windows、macOS、Linux 架构逐项验证 Node、PTY、CLI 和 DS 原生依赖 | FR-9 |
完成源码后执行 `npm test`、`npm run typecheck`、`npm run lint`。修改生产构建资源时再按
仓库开发规则执行构建验证。单元测试覆盖行为和回归,不能代替真实按钮、模型和远程 Host 路径。
每份实测报告记录日期、源码 commit、包版本、平台、结果、脱敏证据位置及实际模型调用次数。
## 9. 文档交付状态
本轮完成 DS 后台真实模型与官方 Web 主流程验证,更新方案和
[DS 进度证据](../deepseek-harness/progress.md#2026-09-26-真实模型与官方-web-验证)。
P1 中 DS 单项目启动、模型预配置和工作区选中,以及 P2 中 DS 模型桥请求已实测。
The DS Web backend implementation and its focused validation are recorded below. Current local shortcut evidence is recorded in [workbar progress](./progress.md#2026-09-27-local-native-client-validation); full Electron UI acceptance remains separate.
后续入口交付事实写入工作栏 [progress.md](./progress.md),按 FR、US 和阶段编号关联本方案。
实施时同步更新工作栏产品范围、终端规范、Runtime 交互边界及根 UI 规范;DS Web 的版本、
依赖与兼容性写入 [DeepSeek Harness 文档](../deepseek-harness/README.md),远程部署事实写入
[远程主机文档](../remote-host/README.md)。保持各自职责,不复制多个相互冲突的支持矩阵。
### 9.1 DS Web Backend, 2026-09-26
`src/main/agent/native-dsh-web-client.ts` exports `NativeDshWebClientService`.
Construct it with `rootDirectory`, an async `resolveLaunchEnvironment` returning
`{ nodeExecutablePath, environment }`, and `resourcesPath` in packaged builds.
`start({ projectId, workspace, profile, workMode, configurationKey?, skillDirectories?, mcpServers? })`
returns `{ id, url }`. Main opens this URL with `shell.openExternal`; it must not
log the token-bearing URL or pass it to Renderer. `stop(id)` and `dispose()` close
the owned process tree and bridge. A browser-open failure leaves the service ready
for another call. Matching concurrent starts reuse one instance; changed model
credentials, project, mode, or capability assignments create a separate instance.
The service uses the existing managed model bridge, AgentModelGateway, and loopback
proxy. Provider credentials stay in Main. Each matching configuration has its own
persistent DSH_HOME. Shutdown removes launch configuration and the bridge ledger,
while retaining native history and settings. Startup authenticates through the
official launch URL and calls `workspace/create` before returning.
The host-level `tools/execute` hook permits native `read` and `skill` in Ask.
Assigned MCP servers supply their already-classified `readOnlyTools` as raw MCP
tool names; all other tool names are denied in Ask. Execute uses native full-access
permissions with no additional approval. The native permission picker is hidden;
changing Plan or creating another native session does not change the launch policy.
Skill directories and stdio/streamable-HTTP MCP configurations use official plugins.
Knowledge, built-in MCP, and GoodBuddy confirmation/configuration tools require a
caller-owned MCP adapter and are not automatically exposed by this service.
`@deepseek-ai/dsh@0.1.7-rc.2` is a pinned build dependency. The existing packaging
hook copies its installed dependency tree, including frontend assets and nested
dependency versions, to `resources/runtimes/dsh` outside ASAR. Target Sharp and
Koffi packages come from exact lockfile entries through the existing artifact
preparation code. The Web package itself needs no click-time npm install. The
coordinator now honors the current custom Node selection from the local tool
environment. Its managed fallback still installs `node-<platform>-<arch>@22.22.0`
through npm; this does not yet satisfy the prepared-resource delivery requirement
in section 6.3. No new download setting was added.
Focused tests use standard Node 24.19.0 on Windows x64 and the prepared resource
tree. They exercise Web startup, authentication, project creation, reuse,
mode isolation, stop/restart, retained history, and real Web API tool execution.
Deterministic model responses travel through the production model bridge: Ask
rejects writes, Execute creates the expected file, assigned skills load, read-only
MCP executes, and writing MCP is rejected before dispatch in Ask. These backend
tests make zero external model-provider calls. This evidence does not claim a
signed installer, other platforms, or the complete shortcut UI has been verified.
Validation: seven focused tests passed against the prepared resource tree;
the copied Sharp 0.35.4 native module encoded a PNG successfully; Main TypeScript
checking and `npm run build:bundle` passed. Full `npm run build`/typecheck and
`npm run lint` encountered concurrent Renderer errors in App.test.tsx and
RightAssistantSidebar.tsx; the backend files passed focused lint. Remote Agent
launch paths are unchanged because this service supports local DS Web only.
### 9.2 Local Coordinator Integration
The production coordinator resolves the saved conversation/project and current
settings for both `get` and `open`. Matching includes the selected model profile
and credentials, work mode, capability assignments, knowledge scope, Obsidian
settings and local Node selection. Ready services keep their MCP gateway after an
OS browser-open error and can be reopened or explicitly stopped by their owner.
The coordinator passes its configuration key into the service's existing match
hash, so the inner DS reuse decision also observes updated skill digests and
launch settings even when directory paths are unchanged.
For DS Ask, the built-in gateway's existing read grant supplies the allowed raw
tool names; the Web adapter maps these through the official MCP naming contract.
Custom MCP is exposed in Execute, matching the existing Runtime behavior; custom
`readOnlyHint` annotations do not introduce a separate Ask permission rule.
Assigned skill directories containing `SKILL.md` load through the official skill
plugin. Continue/OpenCode native Ask now retain Main-bound endpoints and map the
granted read-tool names through both native permissions and execution hooks.
OpenCode prefixes names with the bound MCP server name; Continue uses raw names.
Story Graph additionally rechecks Supervisor enablement on every call.
The coordinator remains local-only at this checkpoint. The remote implementation
is being developed separately and must be connected and verified on the shared
Host before remote shortcut support can be claimed. Current checks and exact
supplier counts are maintained in the workbar progress record linked above.
@@ -70,6 +70,9 @@ schema 35 的存储修复与本方案独立,不能回退或删历史来改善
| Cordis | 4.0.2 | Host 根组件与 Agent scope 的生命周期 |
| Continue CLI | 1.5.47 | 实际受管补丁入口的单 `M.session`、QuizService 和权限状态 |
本表保留进程复用探针执行时的版本。2026-09-26 DS 已升级为 `0.1.7-rc.2`,
Cordis 为 `4.0.4`;当前依赖及升级回归见 [DS 实现进度](../deepseek-harness/progress.md)。
已通过的三个隔离探针:
1. **OpenCode HTTP**:调用当前生产 Server launcher,只有一次 spawn;同一个 SDK client
@@ -174,6 +177,38 @@ SelectedRuntimeManager
健康检查也读取 owner 的总 activeRequests,不能从一个空闲 adapter 判断整个 Server
无任务后重启。异常退出使该 owner 的全部绑定失效,明确结算受影响请求,不重放已执行工具。
### 4.1 本地 MCP 初始化失败恢复
桌面 HTTP 工具网关保持监听,MCP 注册和授权仍按请求创建、清理。本地 OpenCode 的内置
工具和自定义 MCP 共用连接逻辑:仅对明确的连接拒绝、连接重置、断管或传输超时,在
一秒内成功断开失败连接后重试一次。SDK 返回的无 HTTP response 网络错误和抛出的网络
错误采用同一判定;HTTP/API 错误、鉴权或协议错误、未知失败不自动重试。
取消立即终止后续尝试。Prompt 和工具调用不重放。
自定义 MCP 上游发现由 `KnowledgeMcpGateway.getCustomMcpBindings` 按服务隔离失败。
某个服务连接、列举或校验工具失败时,记录脱敏诊断并跳过该服务,保留正常服务的连接;
全部失败时返回空的自定义工具列表,Continue 和内置 OpenCode 仍可开始回复。
首次发现的结果在当前授权内复用,初次失败的服务在下一次请求创建授权时重试。
动态刷新失败会清空该服务的工具列表,下次发现时再刷新,不阻断其他服务。
请求取消和授权撤销仍终止操作,工具总数和名称冲突检查保留。发现仍等待连接尝试结束,
因此连接超时可能延迟首个回复;失败隔离不等于后台加载。
OpenCode 1.18.x 的 `mcp.add` 在服务端独立执行:客户端超时或取消不会中止初始化,
`disconnect` 也只清理当时已登记的连接。晚到的初始化完成后会按名称写入状态,并关闭同名
的现有连接。因此,`mcp.add` 未得到 HTTP 响应(抛错、超时、取消或无 response 的网络错误),
或断开未确认成功时,该名称即作废;同一槽位之后改用本客户端内唯一的替代名称
(前缀加 8 位 base-36 序号,仍为 12 个字符)。槽位随请求结束释放,不因断开失败而长期占用。
已得到响应的正常失败继续复用原名称。
界面显示有界、脱敏的失败详情。`runtime.mcp.failed` 诊断记录连接或断开阶段、失败类别、
耗时、尝试次数、内置或自定义类型及可用的 HTTP 状态;通过本轮令牌的 SHA-256 摘要与
网关鉴权、初始化和工具发现失败记录关联。日志不保存令牌、地址、请求正文或原始错误。
网关初始化抛错时也清理未完成会话,防止失败会话累积。
此恢复逻辑属于本地 `OpenCodeRuntime`。远程 Agent 使用 `RuntimeAcpBackend` 和
`AgentImageToolMcp`,不经过本地 MCP 注册或网关 HTTP 处理;本节不代表远程恢复能力。
远程 Story Graph 通过网关的独立绑定调用,不经过上述自定义 MCP 发现方法。
## 5. DeepSeek Harness
### 5.1 共享 Host 的最小改法
@@ -44,7 +44,7 @@ SSH 连接池,但尚不存在工作栏应用实例模型、终端 IPC、终端
### 3.1 工作栏应用
工作栏应用是可以在助手工作栏中打开的一个能力,例如任务中心、工作区、浏览器、成果或
终端。每项应用通过统一注册表声明:
终端、笔记。每项应用通过统一注册表声明:
- 稳定应用 ID。
- 用户可见名称、图标和说明。
@@ -54,7 +54,8 @@ SSH 连接池,但尚不存在工作栏应用实例模型、终端 IPC、终端
- 当前不可用时的原因和修复入口。
目录中的应用可发现性与实例是否已经打开相互独立。应用不可用时保留入口并说明原因,不以
隐藏代替状态。
隐藏代替状态。笔记遵循其业务启用设置:关闭魔法笔记时隐藏目录入口并移除对应 Tab,
关闭前处理草稿,规则见[会话联动逻辑](../magic-notes/logic-design.md)。
注册表字段如下:
@@ -76,14 +77,15 @@ SSH 连接池,但尚不存在工作栏应用实例模型、终端 IPC、终端
`defaultOpen`、`required` 和 `closable` 不互相推导。例如浏览器默认打开但不是必需实例;任务
中心和工作区既默认打开又必须存在。`required` 不表示锁定顺序,顺序只由 `reorderable` 控制。
当前五个应用的 `visibleAcrossContextSwitches` 均为 `true`。该属性记录应用目录的显示策略,
当前六个应用的 `visibleAcrossContextSwitches` 均为 `true`。该属性记录应用目录的显示策略,
不改变内容归属,也不授予其他会话的 MCP 访问权限;当前没有使用 `false` 的应用,尚未实现
按上下文隐藏 Tab 的策略。
浏览器在创建时绑定当前 Conversation;切换会话后仍保留原 Tab、选中状态和页面,导航操作
继续使用原 `targetRef.conversationId`。此时新建浏览器才绑定新会话。默认或旧布局中尚未绑定
的浏览器在首次使用时绑定活动会话,此后不再随会话切换改绑。终端同样保留创建时执行目标;
任务中心、工作区和成果的内容继续跟随当前项目。
任务中心、工作区和成果的内容继续跟随当前项目。笔记使用应用级范围,选择与草稿不随项目或
会话切换,不接受 `targetRef`;重启仅恢复工作栏 Tab 描述,不恢复笔记选择和草稿。
### 3.2 工作栏 Tab 实例
@@ -178,9 +180,9 @@ stdio fallback。首期远程终端随 SSH Channel 结束,不提供跨 SSH 或
- 已打开实例位于一个语义化 `tablist` 中。
- 每个实例使用独立 `tab` 和 `tabpanel`。
- “+”固定在 Tab 行末尾,但位于 `tablist` 之外。
- Tab 过多时实例区域单行横向滚动,不换行、不自动隐藏实例。“+”紧随最后一个标签并随标签一起滚动,滚到末尾时可见。
- 标签全部可见时隐藏左右滚动按钮;溢出时左右箭头透明浮动于工作区两端,不占标签布局宽度。鼠标移入标签栏时显示箭头,移出后隐藏;键盘聚焦箭头时显示,无悬停能力的设备保持可见。悬停箭头时显示轻微底色。点击滚动约一屏,边界方向禁用且不拦截下方标签的指针操作。切换或新增激活标签时将其滚入可见区域。未激活标签使用低对比度灰蓝底色和细边框,激活标签保留亮底与强调色顶线。
- “+”固定在 Tab 行末尾,位于 `tablist` 和页签滚动容器之外,占用独立宽度。
- Tab 过多时实例区域单行横向滚动,不换行、不自动隐藏实例。“+”在 `300px` 工作栏内也保持可见和可点击。
- 标签全部可见时移除左右滚动按钮及其占位;溢出时左右箭头在滚动视口两侧各占独立的 28px 宽度,不覆盖标签、关闭按钮或“+”。鼠标移入标签栏时显示可滚动方向的箭头,移出后隐藏;键盘聚焦箭头时显示,无悬停能力的设备保持可滚动方向的箭头可见。悬停箭头时显示轻微底色。点击滚动约一屏,边界方向禁用并隐藏;溢出期间隐藏和禁用均保留占位,避免布局跳动。切换或新增激活标签时将整个标签(含关闭按钮)滚入可见区域。未激活标签使用低对比度灰蓝底色和细边框,激活标签保留亮底与强调色顶线。
- 终端 Tab 默认使用递增标题,例如“终端 1”“终端 2”;状态栏持续显示项目、Host、
工作目录和 Shell。用户可以重命名终端。
- 任务中心、工作区、成果及新建终端的默认标题随界面语言切换,恢复布局时也使用当前语言。
@@ -218,6 +220,7 @@ stdio fallback。首期远程终端随 SSH Channel 结束,不提供跨 SSH 或
| 浏览器 | `multiple` | 当前 Conversation | 是,一个实例 | 否 | 是 | 是 | 每次创建新 Tab |
| 成果 | `single` | 当前项目 | 是 | 否 | 是 | 是 | 聚焦已有 Tab |
| 终端 | `multiple` | 当前项目 | 否 | 否 | 是 | 是 | 每次创建新 Tab |
| 笔记 | `single` | 应用级 | 否 | 否 | 是 | 是 | 聚焦已有 Tab |
实例策略、默认状态和可关闭策略都是应用注册数据,不能分散硬编码在多个点击处理器中。
@@ -562,7 +565,7 @@ Schema、假数据终端或不可用入口的中间产品。
- [x] 连续选择浏览器会创建页面、导航状态和元素引用相互独立的浏览器 Tab。
- [x] 连续选择终端会创建多个互相独立的终端 Tab。
- [ ] 关闭可关闭实例、切换和重排实例后,“+”仍保持可用。
- [ ] Tab 溢出时单行横向滚动,两端箭头固定,“+”跟随最后一个 Tab,滚到末尾可见,不把 Tab 换成下拉框。
- [ ] Tab 溢出时单行横向滚动,页签区两端箭头固定,“+”在滚动区外持续可见,不把 Tab 换成下拉框。
- [x] 关闭可关闭的活动 Tab 后激活正确的相邻 Tab 并恢复焦点。
- [x] 当前 Tab 的背景、边框、顶部指示和字重清晰可辨;关闭按钮仅在悬停、焦点或无悬停
设备上出现,且不引发布局位移。
+2
View File
@@ -2,6 +2,8 @@
电脑控制包含 GoodBuddy 对浏览器和客户端桌面能力的产品实现与平台适配状态。
内置浏览器 MCP 默认开启,用户可在设置中关闭。已保存的开关选择在重启和升级后保留。
## 文档导航
| 文档 | 权威职责 |
@@ -1,5 +1,10 @@
# 会话 HTML 渲染实现进度
2026-09-27 浮层审查修复:全屏 iframe 已纳入共享焦点循环,跨文档 Escape 通过受限
Main/Preload 通知返回外层。真实 App 中的带链接及纯正文预览均已验证键盘进入和 Escape,
带链接预览另验证正反向 Tab 返回;没有开放 sandbox 权限或执行预览脚本。
实现与测试范围见[浮层审查修复记录](../../quality/overlay-review-2026-09-27.md)。
## 已实现
- [x] 应用级持久开关,默认开启,并提供旧设置迁移。
@@ -56,6 +56,17 @@ iframe 无法按内容自适应高度,因为 `sandbox=""` 不含 `allow-same-o
嵌入预览完全相同的清理后文档,不额外放宽 Sandbox 或 CSP。切换回复时,源码面板与全屏
预览按当前 source 派生开合状态,不使用会引发级联渲染的重置 effect。
全屏 iframe 提供 `tabIndex=0`,共享焦点候选包含 iframe。关闭按钮的正向 Tab 显式进入
预览,支持没有链接的长文;iframe 后的焦点守卫将原生 Tab 返回到后续 Modal 控件或关闭
按钮。守卫本身不作为焦点循环的首尾,通知关闭按钮仍参与循环。
iframe 内的 Escape 不会冒泡到父文档。`registerWindowIpcHandlers` 只在主窗口直接子
`about:srcdoc` frame 获得焦点且收到 Escape keyDown 时,向主 Renderer 发送
`window:preview-escape`。Preload 暴露无参数订阅 `app.onPreviewEscape`;全屏组件仅在
自己的 iframe 当前聚焦且所属 Modal 未被 inert 隔离时关闭。组件卸载移除订阅,窗口 IPC
释放时移除输入监听。此路径不执行预览脚本、不授予同源权限,也不暴露原始键盘事件或
Electron 对象;iframe 的 Sandbox/CSP 保持原值。
## 验证范围
- 应用设置默认值、关闭持久化和版本 10 迁移。
@@ -65,4 +76,5 @@ iframe 无法按内容自适应高度,因为 `sandbox=""` 不含 `allow-same-o
- 源码按钮的 `aria-expanded` 切换,以及全屏预览的初始焦点、背景 inert、Escape 关闭和
焦点恢复。
- 主 Agent、子 Agent、用户、推理和流式状态边界。
- 真实 Electron 中带链接与纯正文 iframe 的进入、正反向 Tab 返回和内部 Escape;通过生产 Main/Preload 事件订阅完成关闭。
- 从设置 Switch 到当前会话重新渲染的应用级集成路径。
@@ -2,11 +2,15 @@
监督者统一工作回顾、会话监督与故事线图谱,以智能心跳作为定时唤醒机制。现有智能心跳页面承载监督者工作回顾入口,手动回顾、定时结果投影、来源原文、实体/关系确认、继续讨论、知识库预览提交和侧栏反馈已经接入。历史回顾定位到对应结果图谱;侧栏按会话或任务查询,并支持固定目标。同 scope 的跨 run 实体复用依赖模型显式选择 Main 提供的候选引用,Main 处理 UUID 和旧文本主键的精确映射,具体规则和验证边界见[实施进度](./progress.md)。
监督者可在“设置 → 模型”选择已有文本模型或跟随应用默认,连接与凭据复用规则及执行期间的模型一致性见[监督者模型选择](./logic-design.md#监督者模型选择)。
## 文档导航
主页面包含工作回顾、故事线图谱、自动监督、活动记录和设置。工作回顾阅读统一监督结果并选择历史,图谱定位同一结果;自动监督管理计划、概览、自动监督报告、记忆与行动建议和心跳审计。活动记录展示统一执行、进度及详情,可按计划 ID 筛选;设置只保留算法参数。当前布局以 [UI 设计](./ui-design.md#应用入口)为准。活动读取实际手动回顾及心跳执行,合并展示同一次心跳的报告与监督回顾阶段;状态规则、历史缺失和保留期限见[活动记录](./logic-design.md#活动记录)。
主页面包含工作回顾、故事线图谱、自动监督、活动记录和设置。工作回顾阅读统一监督结果并选择历史,图谱定位同一结果;自动监督管理计划、监督建议和只读的旧报告。活动记录展示统一执行、进度、建议阶段及详情,可按计划 ID 筛选;设置管理模型选择和算法参数。当前布局以 [UI 设计](./ui-design.md#应用入口)为准。活动读取实际手动回顾及心跳执行,合并展示同一次心跳触发的监督回顾与建议阶段;状态规则、历史缺失和保留期限见[活动记录](./logic-design.md#活动记录)。
自动心跳报告和监督回顾已接入按来源版本及正文位置的增量处理,无变化时跳过模型。手动历史回顾继续重新整理所选区间;进度、失败重试和删除边界见[自动增量与手动重看](./logic-design.md#自动增量与手动重看)。
心跳的职责为“定时触发增量回顾+按配置介入(建议)”,不再先生成独立报告;手动“回顾”默认增量,历史重看改为显式“重新整理”。规则见[心跳职责与介入](./logic-design.md#心跳职责与介入目标设计),需求 FR-S12,验收 US-S32、US-S33,2026-09-30 已实现(主动介入除外),验证见[实施进度](./progress.md#2026-09-30-心跳职责收敛与监督建议)。
监督回顾按来源版本及正文位置增量处理,手动回顾与心跳共享进度,无变化时跳过模型;“重新整理”才重读所选区间。旧的进度、失败重试和删除边界见[自动增量与手动重看](./logic-design.md#自动增量与手动重看)。
| 文档 | 权威职责 |
| --- | --- |
@@ -16,10 +20,14 @@
| [UI 设计](./ui-design.md) | 应用导航、工作回顾、侧栏反馈及页面状态 |
| [User Stories](./user-stories.md) | 监督者与故事线图谱的用户操作和验收场景 |
| [技术设计](./technical-design.md) | Main/Renderer 分层、数据模型、IPC、安全边界和实施顺序 |
| [时态 Story Graph MCP 设计](./story-graph-mcp-design.md) | 只读工具、时间与版本语义、Runtime 接入,以及后续跨范围事实复用和记忆过渡合同 |
| [回顾分块调度设计](./review-scheduling-design.md) | 第 0 节定义生产分页、批次持久化、续跑、当前设置及剩余边界;其余章节保留改造前基线、目标设计和原型证据 |
| [故事线模型改造设计](./storyline-model-design.md) | 全局 › 项目 › 功能 › 子线索的故事层级、故事归属与经验抽取阶段、用户调整及实施顺序(目标设计) |
| [实施进度](./progress.md) | 已验证实现、验证证据和当前边界 |
| [故事线图谱设计](../story-graph/prd.md) | 图谱对象、环绕时间轴与实体交互 |
生产入口已接通完整来源分页、叶子持久化、预算暂停、活动页继续及批次详情;设置可调页大小、批次正文/消息上限、软预算、两个模型超时和共享并发。实际合同及限制见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界)。真实 DeepSeek 验证已覆盖一个完整的 27 条消息会话;较早的 129 条消息实验仍未完成。两次实验共 5 次付费请求,详情见进度。原始 AbortError 原因尚未由日志确定。
生产入口已接通完整来源分页、叶子持久化、持续处理至完成、活动页主动暂停/继续及批次详情;设置可调页大小、批次正文/消息上限、单次响应容量、两个模型超时和共享并发。实际合同及限制见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界)。真实 DeepSeek 验证已覆盖一个完整的 27 条消息会话;较早的 129 条消息实验仍未完成。两次实验共 5 次付费请求,详情见进度。原始 AbortError 原因尚未由日志确定。
实施状态以代码和[实施进度](./progress.md)为准。逻辑待定项见[逻辑设计](./logic-design.md),架构边界见[技术设计](./technical-design.md)。现有心跳行为见[智能心跳](../smart-heartbeat/README.md),原型范围见[Story Graph 演示](../story-graph/README.md)。
Agent 可按需通过三个只读 MCP 工具读取决定、演变和来源。会话设置中的“使用故事图谱”默认开启,仅在监督者应用启用时显示;关闭会话开关或应用后不暴露工具并拒绝旧会话调用。规则见 [MCP 接入合同](./story-graph-mcp-design.md#6-内置-mcp-与-runtime-接入),验证边界见[实施进度](./progress.md)。当前 memory 读取、精确 scope 增量进度和候选身份保留;跨 scope 叶子复用、读取入口切换及 `as_of` 仍属后续能力。
@@ -4,11 +4,17 @@
本文承接[监督者产品设计](./supervisor-prd.md)、[故事线图谱设计](../story-graph/prd.md)和[User Stories](./user-stories.md),定义触发、整理、展示与用户后续操作之间的关系。既有会话监督的证据和介入边界继续由[会话监督 PRD](./prd.md)约束。
## 监督者模型选择
对应 FR-S13。监督者可跟随应用默认文本模型,或固定使用一个已保存的文本模型连接;会话和项目的 Runtime 选择不影响它。提取、导航汇总、故事归属、经验整理及建议措辞均使用监督者的模型设置。
回顾获得执行位置后,读取一次连接配置;同次执行中的批次、输出重试、汇总、故事与经验阶段保持该模型。修改监督者选择、应用默认模型或连接配置,仅影响后续执行。继续暂停或失败的回顾、单独重试故事、生成或重试建议时重新读取当前选择;跨重启不保存模型或凭据快照。显式选择的连接被删除、改为图片协议或缺少所需 API Key 时,执行报错并提示修改设置,不自动换成默认模型。
## 整体逻辑架构
报告、监督叶子与导航整理共用监督专用 FIFO 并发池,普通聊天不入池;监督服务外层 run 仍串行。报告在排队前冻结超时,获槽后领取,实际模型请求前校验并刷新租约;报告提交或无变化后先释放槽位,再执行监督。监督超时在 run 创建时保存,节点获槽并解析 Runtime 后计时。阶段超时、租约、传输时限、后台检查与活动轮询各自独立,取值及生效规则见[当前设置合同](./review-scheduling-design.md#当前设置合同)。
完整回顾的生产接线已增加分页来源清单、逐批输出/offset 事务和导航节点。预算耗尽显示 `paused`,模型或保存失败显示 `failed`;两者均保留此前成功批次,可继续同一 run。存在未读来源、覆盖空洞或未成功导航时不能标 `completed`。手动位置与自动 checkpoint 分开,自动位置只在最终发布事务推进。来源变更目前要求重新回顾,模型配置跨重启固定等差异见[生产接线与剩余边界](./review-scheduling-design.md#0-生产接线与剩余边界)。真实模型验证覆盖 27 条消息的完整会话,较大样本失败单独记录。
完整回顾的生产接线已增加分页来源清单、逐批输出/offset 事务和导航节点。用户主动暂停后显示 `paused`,模型或保存失败显示 `failed`;两者均保留此前成功批次,可继续同一 run。回顾不按累计运行时长自动暂停。存在未读来源、覆盖空洞或未成功导航时不能标 `completed`。手动位置与自动 checkpoint 分开,自动位置只在最终发布事务推进。来源变更目前要求重新回顾,模型配置跨重启固定等差异见[生产接线与剩余边界](./review-scheduling-design.md#0-生产接线与剩余边界)。真实模型验证覆盖 27 条消息的完整会话,较大样本失败单独记录。
```mermaid
flowchart TB
@@ -127,7 +133,7 @@ flowchart LR
## 状态与处理规则
下表包含目标规则;当前活动执行状态以本节后的“活动记录”为准,取消及部分完成尚未接入。
下表包含目标规则;当前活动执行状态以本节后的“活动记录”为准,取消已接入,部分完成状态尚未接入。
| 规则 | 条件或状态 | 处理与用户可见结果 | 需求与场景 |
| --- | --- | --- | --- |
@@ -152,7 +158,9 @@ flowchart LR
旧监督成功记录继续显示;旧心跳与监督之间未保存关联时分别呈现,不用时间接近猜测关系。旧心跳未保存的执行范围显示不可用,不从当前计划反推。此前未保存的手动运行与失败无法恢复。心跳报告沿用计划的历史保留和删除规则;监督结果独立保留,原心跳被清理后仍可读取监督记录。活动不是永久审计库。
当前活动没有 `cancelled` 或 `partial` 状态,也不提供取消按钮。实体和关系修改只有当前状态及结果内容,没有按时间追加的人工修改记录。
同一时刻只允许一个监督回顾执行,包括停止清理阶段;不同 scope 也共用此限制。手动新建或继续遇到占用时立即提示,不排队;自动回顾等待占用释放,再检查未完成运行和增量来源。暂停或失败后可以新建回顾,也可继续有效的原 run。
取消立即持久化 `cancelled`,但执行与清理尚未停止时 UI 显示“正在取消”,此时仍不能开始另一个回顾。停止后显示“已取消”,保留上次成功结果和本次已保存批次,不发布部分结果、不推进自动 checkpoint。已取消运行不能手动继续,自动检查也不恢复它;后续自动检查可新建 run 处理尚未提交的来源。关联心跳活动显示监督取消状态,已保存的报告保持原状态。界面入口及提示关闭规则见[UI 设计](./ui-design.md#活动)。当前没有 `partial` 状态;实体和关系修改仍没有按时间追加的人工修改记录。
收集、叶子整理、导航合并和发布分别展示。阶段推进只依据服务记录,最终完成只依据结果事务;“六批已保存、剩余零来源”仍可处于合并失败或发布失败。继续保留成功叶子和导航节点,缺失阶段不得靠错误文本或来源数猜测。导航摘要允许省略事实数组,叶子缺少事实数组仍失败;导航不得生成替代叶子事实的非空数组。阶段及输出合同见[生产调度](./review-scheduling-design.md#已接入的调度与存储)。
@@ -164,12 +172,59 @@ flowchart LR
工作回顾的手动操作重新整理所选区间,沿用有界历史收集,不读取或推进自动进度。自动监督计划列表中的“立即运行”重新生成心跳报告,其下游监督仍按自动进度处理。删除报告或计划不重置来源进度;重看历史、删除报告和重建图谱是不同操作。当前没有清空或重建图谱功能。
回顾默认增量,重看历史改为显式重新整理。
### 心跳职责与介入(目标设计)
对应 FR-S4、FR-S12,US-S27、US-S32、US-S33。本节为已确认的目标规则,尚未实现;当前代码仍先生成心跳报告再执行监督整理,实际进度以[实施进度](./progress.md)为准。“回顾”默认增量,重看历史改为显式“重新整理”。
### 心跳职责与介入(目标设计)
对应 FR-S4、FR-S12,US-S27、US-S32、US-S33。本节为已确认的目标规则,尚未实现;当前代码仍先生成心跳报告再执行监督整理,实际进度以[实施进度](./progress.md)为准。
回顾整理只有一套流程,手动和自动共用:确定范围 → 分页采集来源 → 分批整理 → 汇总 → 发布到 Story Graph。心跳不参与整理本身,只在其外增加两件事:
1. **定时触发**:计划到期时代替用户发起一次增量回顾。
2. **介入**:回顾发布后,按计划配置的介入方式处理本次图谱变化。
```text
触发(点击“回顾” / 计划到期)
↓
变化检查:比较来源版本与已处理位置,不调用模型
↓ 无变化 → 记录“无新变化”,结束
↓ 有变化
增量整理:只处理新增、修订和未完成片段,关联已有实体(唯一读取原文的阶段)
↓
发布:写入 Story Graph,推进增量进度
↓ 仅心跳触发
介入:读取本次发布的图谱变化,按计划配置生成建议
```
| 规则 | 内容 |
| --- | --- |
| H1 原文只读一次 | 同一来源版本在一次触发中只由整理阶段读取;介入阶段的输入是本次图谱变化及其来源引用,不重新读取原文 |
| H2 不再生成独立报告 | 取消“心跳报告”阶段及其独立增量进度;旧报告只读保留,不再新建 |
| H3 手动默认增量 | “回顾”按钮与心跳整理行为一致,跳过已处理且未变化的来源;手动触发不生成建议 |
重新整理放在更多中,确认后重新分析所选时间区间;不推进也不重置自动进度,不覆盖用户确认内容 |“重新整理”放在“更多”中,确认后重新分析所选时间区间;不推进也不重置自动进度,不覆盖用户确认内容 |
| H5 介入方式 | 计划字段:`仅更新记忆`、`生成建议`(新计划默认)、`主动介入`(会话级实时,暂不实现,不显示为可选项) |
| H6 建议来源 | 候选仅来自本次图谱变化:批次中的未解决事项、无明确取代关系的分歧(contrasts 关系)、由至少三处不同原始来源支持的未确认实体(候选约定)、被修订的实体(revised 变化)。长期未推进的判定尚未实现 |
| H7 建议成本 | 先按规则挑选候选,不调用模型;存在候选时才发起一次输入有界的措辞调用,输入仅为候选及其引用 |
| H8 建议可信 | 每条建议显示依据并可打开来源;用户忽略后,只有出现新证据才再次提出;确认候选约定或偏好后才作为长期背景 |
| H9 频率与成本 | 提高频率只增加变化检查次数;模型调用次数由实际变化决定:无变化 0 次,有变化无候选仅整理,有候选再加一次建议调用 |
| H10 失败隔离 | 建议阶段失败不回滚已发布的图谱与增量进度;活动显示“已更新,建议生成失败”,可单独重试建议 |
介入产物替代旧心跳的报告、记忆建议与行动建议:未解决事项可转任务,候选约定确认后进入长期背景,旧的报告阅读由按时间查看图谱变化替代。已有计划的范围、频率和启停状态保留,迁移后介入方式默认为“生成建议”;已确认记忆与待处理建议保留并逐步迁入,不补造来源。
计划 Modal 的新建草稿不产生持久化或执行副作用,显式保存时创建并启用;编辑沿用已有 enabled。保存失败保留草稿,提交期间禁止关闭,未保存关闭须确认。活动筛选只改变读取范围,不触发执行;同名同范围计划仍按 configId 区分,筛选在分页前应用,结果跳转继续按 resultId 定位。
规则冲突时,先满足所选范围与既有观察边界,再保留用户修订和来源事实,最后应用自动归纳和展示策略。新增证据与已确认结论冲突时显示待核对差异,不能自动断言旧观点失效。
## 完整性与待定项
FR-S11、US-S30 的会话故事图谱开关规则见 [MCP 接入合同第 6 节](./story-graph-mcp-design.md#6-内置-mcp-与-runtime-接入)。应用启用、会话允许和 Runtime 能力分配须同时满足;回顾是否自动运行不由会话读取开关控制。
FR-S11、US-S28 至 US-S31 的规则由[时态 Story Graph MCP 设计](./story-graph-mcp-design.md)统一定义:时间/确认与有效性见第 3 节,读取范围与分页见第 4 节,应用关闭与 Runtime 接入见第 6 节,跨 scope 复用与记忆过渡见第 7、8 节。D1 读取已接入,后两项仍待实现;Global/项目独立 checkpoint 和同 scope 候选身份保留。MCP 不接受 `as_of`,较晚生成的结果也不自动使旧决定失效。
本设计覆盖触发、证据输入、静默检查、正常整理、取消与失败、跨视图共享结果、用户纠正、知识库写入及历史回放。它补充产品逻辑,不代表接口、持久化或迁移方案已经完成。
仍待产品决策:事件触发与阶段识别条件、反馈频率与静默判定、实体合并与拆分的交互、时间轴比例和集中度含义。应用启停按[应用启停与执行](#应用启停与执行)处理;既有入口偏好、计划与运行记录的保留要求见 FR-S1、S2 和 US-S14。
@@ -1,11 +1,372 @@
# 监督者实施进度
日期:2026-09-24。
日期:2026-09-30。
当前记录以已验证生产行为为准。监督者尚未覆盖全部 user stories。
## 2026-10-03 会话故事图谱开关
FR-S11、US-S30 的“使用故事图谱”已接通 Composer、生产 Preload/IPC、SQLite 和工具网关。会话默认开启,仅在监督者应用启用时显示,位置在知识库之前。存储和执行合同见 [MCP 接入](./story-graph-mcp-design.md#6-内置-mcp-与-runtime-接入),没有数据库版本升级或迁移。另保留本轮开始前已有的监督者独立模型选择及并行性能改动。
定向测试共 9 个文件、37 项通过,重复运行不重复计数:
- `npx vitest run src/main/agent/native-client-coordinator.test.ts src/renderer/src/conversation-persistence.test.ts src/renderer/src/Composer.test.tsx src/agent-daemon/story-graph-integration.test.ts`:25 项。覆盖默认值、按会话隔离、保存中及失败、原生客户端绑定、Main/Agent HTTP MCP 发现、旧调用拒绝与重新开启;图谱集成 5 项在最后修改后单独复跑通过,含在途结果交付前关闭。
- `npx vitest run src/main/assistant/assistant-database.test.ts src/main/ipc.test.ts src/renderer/src/App.test.tsx src/main/agent/deepseek-harness-acp-e2e.test.ts -t "story graph|Story Graph|pins"`:11 项通过,630 项按名称未运行。覆盖真实 SQLite 重开、旧自动保存不能覆盖选择、IPC 校验、四种本地 Runtime 的签发与关闭,以及受控 Harness Ask/Execute。
- `npx vitest run tests/overlay-layering.electron.test.ts -t "conversation story graph"`:1 项通过,3 项按名称未运行。真实 App、Preload、IPC 和 SQLite 在 1280px/390px、浅色/深色四种组合下检查共享 Switch、位置和可见矩形;原生鼠标打开菜单、Space 保存、页面重载、应用关闭时移除 DOM、重开后恢复选择及旧工具绑定拒绝均通过。切换没有模型请求,模型尝试计数为 0。
`npm run typecheck` 的 Main、Agent、Web 三组通过,`git diff --check` 通过。修改文件定向 ESLint 除 `conversation-selectors.ts:32,47` 外通过;这两处是同工作树并行标题缓存改动的 `react-hooks/immutability` 报错,本功能只改该文件的 Composer 字段投影,未修改缓存逻辑。没有执行全量测试、发布构建、提交或推送。中文 MCP 说明扫描阻断项为 0,复核项保留必要的数据和执行边界。
### 本轮 Linux Host 证据
[源码探针](../../../scripts/story-graph-host-probe.ts)临时打包后在共享 Linux x64 Host 运行当前 `AgentImageToolMcp`、`AgentOwnedAcpPrompt`、`ContinueHostAdapter`,桌面使用真实 `AssistantDatabase`、`KnowledgeMcpGateway` 和 `MainImageToolSession`。关闭操作改为写会话持久设置,验证其他会话仍开启,以及同一个远程 MCP 会话不再发现工具、旧工具调用被拒绝。
OpenCode 和 Continue 的 Ask/Execute 确定性探针通过,共 10 次测试推理请求(含 2 次标题生成)、10 次桌面图谱读取、0 次外部 Provider 请求。真实 DeepSeek 复测中,Linux OpenCode Ask/Execute 均完成图谱调用并返回完成标记,随后关闭检查通过;该轮 6 次 Provider 请求(含 2 次标题生成)、8 次桌面读取。首轮真实模型已完成 Ask 图谱读取,但探针未拼接流式文字便检查完成标记而失败,消耗 3 次 Provider 请求;修正探针后复测,整个任务外部请求合计 **9 次**。
工具消息通过 SSH 标准输入输出搬运,调用生产二进制工具分发;没有重装共享 Agent,也未把完整 Desktop attach/bootstrap 或远端 Continue 的真实模型请求计作已验证。远程协议与模型桥源码未变,会话开关由 Desktop 的现有可用性回调执行。测试只使用隔离 SQLite 和固定示例内容;临时 Host 探针及运行目录已清理。Electron 界面与 SSH Runtime 分开验收,没有宣称一条自动化场景覆盖整窗点击到远端模型。
## 2026-10-03 监督者独立模型选择
FR-S13 已接通监督者“设置 → 模型”、应用设置持久化和 Main 连接解析。字段只保存已有文本连接 UUID,旧配置跟随应用默认;执行中复用同一 Runtime,继续和重试采用当前设置。规则见[模型选择](./logic-design.md#监督者模型选择)。
定向验证覆盖中英文选择与保存失败草稿、失效连接提示、旧设置默认值及重载、提取到经验阶段的模型一致性、失败后的释放与继续、建议措辞连接及清理。`ipc.test.ts` 的监督/心跳相关 24 项通过,含真实直连 Runtime 到 loopback HTTP 服务的两次请求:固定连接、应用默认各一次,验证模型名、请求定制、无工具及失效连接不回退。外部真实模型请求为 0 次。
验证命令:`npx vitest run src/main/application-settings-store.test.ts src/main/assistant/supervision-production.test.ts src/renderer/src/HeartbeatCenter.test.tsx`;`npx vitest run src/main/ipc.test.ts -t "supervis|heartbeat|unchanged sources"`;`npm run typecheck`;修改的源文件定向 ESLint。未跑全量测试、打包、真实供应商或 Electron 原生界面测试。监督模型请求在桌面 Main 内完成,Agent 和远程模型桥未改动;未进行远程 Host 验证。
## 2026-10-02 长会话验收与 Agent 自主调用图谱
### 长会话验收
`npx jiti scripts/long-session-acceptance.mjs <live.sqlite> .env.deepseek.local <dir> <会话前缀> [failAfter]`:对本机数据库做在线备份,只在副本里归档其他会话、隐藏任务,使回顾只读取一条真实会话;首次运行在第 N 次提取时注入一次提供方失败(不消耗付费调用),再继续同一回顾;完成后用保存的批次片段逐条重建每条消息原文;最后再跑一次心跳和一次默认手动回顾,要求零模型调用。原库不修改。
第一次跑最长会话(389 条消息、约 21.7 万码点、16 天)时,继续回顾三次都失败:两次是模型返回无效 JSON,一次是引用了范围外的来源。原因是一个批次的模型输出不可用时整次回顾立即失败,只能靠反复手动“继续”碰运气。改动:
- 叶子批次输出不可用(无效 JSON、结构不符、引用不存在的来源或实体)时,在同一段待处理正文上缩小到 1/2、再到 1/4(不少于 500 字符)重试,最多 2 次;缩小只是先处理前一部分,其余正文留在待处理里,不跳过、不截断。
- 导航合并输出不可用时,用相同输入再问 1 次。
- 提供方错误、超时、取消和响应超出容量不重试,仍按原来的方式失败并保留已保存批次。
改后结果(DeepSeek `deepseek-flash`):
| 会话 | 首次运行 | 继续 | 覆盖 | 无变化重跑 |
| --- | --- | --- | --- | --- |
| 97 条、6.0 万码点、4.9 天 | 第 4 次提取注入失败,已存 3 批 | 1 次完成,16 次调用 | 97/97 条精确重建,0 缺口、0 重叠 | 心跳、手动均 `no_change`,0 调用 |
| 389 条、21.7 万码点、16.2 天 | 第 7 次提取注入失败,已存 5 批 | 1 次完成,61 次调用(30 提取、31 合并),约 32.5 分钟 | 389/389 条精确重建,0 缺口、0 重叠 | 心跳、手动均 `no_change`,0 调用 |
最长会话共 67 次调用、约 52.7 万输入和 54.8 万输出 Token,发布 237 个事件;失败前保存的批次在继续后全部保留未重算。改动前的同一会话运行(三次继续均失败)记录在 `sl9/long2`,改后中间版本(仅叶子重试,合并失败两次后第三次继续完成)在 `sl9/long3`。
边界:完整覆盖只说明每段原文都进入了成功批次,不代表模型没有遗漏或误读;验收只用了两条会话,没有测多项目全局回顾。
### Agent 自主调用
决定:普通对话不自动注入故事和经验,由 Agent 按需调用 Story Graph 工具。为此:
- 工具说明写明何时应主动调用(继续之前的工作、询问为什么这样决定、在可能有既有约束的领域开始新任务)、何时不调用(闲聊、与项目历史无关的通用问题),用 2~4 个关键词查询,查几次找不到就说明并按当前上下文回答。
- 搜索改为按词匹配:空格和标点分词,中文按相邻两字切分;一条记录命中整句或至少一半的词即返回,按命中程度排序,故事和经验在同分时靠前。此前 Agent 用“监督者回顾调度”这类自然短语搜索,因为要求整句出现而 0 结果。
- 搜索结果的预览先显示名称、标题、描述等可读字段,不再以 ID 开头。
验证(`scripts/story-graph-agent-deepseek.mjs`,DeepSeek 函数调用,只给生产工具定义,不提示要用工具):
| 轮次 | 判断正确 | 说明 |
| --- | --- | --- |
| 改前 | 4/5 | 通用编程题也去搜索;继续工作类问题搜了 9~22 次仍找不到内容 |
| 改后 | 5/5 | 三个项目历史问题都主动调用;数组去重和问候都不调用 |
改后“继续回顾调度,按最后定的方案”能找到并引用 2026-10-02 的故事线模型设计;“导出功能有什么约束”能列出经验和来源。“为什么取消 300 秒自动暂停”仍在搜了十几次后达到对话轮数上限:这次取消发生在数据副本最后一次回顾之后,图谱里确实还没有这条决定,Agent 没能及时停止并说明“没有记录”。
## 2026-10-02 故事与经验质量抽查
数据:同一份 437 个事件的数据副本上已有的故事归属运行(`sl3/off1`、`sl3/cross2`)和经验抽取运行(`sl4/run1`、`sl4/run2`),以及本次改提示词后的新运行(`sl8/off3`,DeepSeek `deepseek-flash`,12 次调用,约 6.2 分钟)。一致性用“两次运行中同属一个功能的事件对”的重合度(Jaccard)衡量。
| 检查 | 结果 |
| --- | --- |
| 故事归属一致性(改前,off1 对 cross2) | 0.51 |
| 主要不一致来源 | “提交与发布”吸收了 87 个事件,其中只有 40% 在另一次运行里属于发布;其余是各功能自己的提交、推送和验证 |
| 改动 | 提示词明确:某个领域的提交、推送、测试和验证属于该领域;发布功能只放版本号、发布说明、标签、打包和发布 |
| 改后(off3) | 发布功能 42 个事件;与 cross2 的一致性 0.74;暂不归类 55 个;再次整理 0 次调用 |
| 暂不归类的事件 | 抽查 cross2 的 49 个:几乎都是问候、知识截止时间、SSL/认证报错、寿命和睡眠问答、案例咨询、权限挂载等一次性问题,归为“暂不归类”是合理的 |
| 经验跨运行一致性 | run2 的 38 条里 23 条能在 run1 中找到形成依据重合(Jaccard ≥ 0.3)的对应经验,表述基本一致 |
| 经验依据抽查(10 条) | 形成依据基本都对得上经验内容;后续“应用”较弱,抽到的 5 条应用里约 2 条和经验关系不大(例如“需求有歧义先确认”被记到“询问微信附件导入能力”) |
结论:功能边界在明确“提交归领域”后明显稳定;仍有约四分之一的事件对在两次运行间归属不同,主要发生在界面、笔记、帮助提示等相邻功能之间,可以通过手动合并修正。经验的形成依据可信,应用记录原先过宽。
随后收紧经验应用:模型给出的应用事件必须与该经验的某个形成事件共享实体,否则丢弃(同批其他内容保留)。在 off3 的故事上重新抽取(6 次调用,约 3.8 分钟):45 条经验,应用记录从此前的 53 条降到 6 条,抽查 6 条全部与经验内容相符;依据无来源 0、应用早于形成 0、再次抽取 0 次调用。代价是跨故事的经验连线变少(3 条),时间螺旋和“可用经验”建议能显示的跨故事复用也随之减少。`supervision-experiences.test.ts` 新增“无关知识的事件不记为应用”。
## 2026-10-02 时间螺旋显示经验节点
对应 [3D 概念](../story-graph/3d-concept.md) 中的桶外经验节点。
实现:
- `experienceLinks()`(`story-graph-3d-model.ts`):经验在当前层中,从最早形成依据所在的木片,连到之后第一个在另一片木片上的应用。只在同一片木片里用过的、应用不晚于形成的、依据不在当前层的经验不画。每层最多 6 条,按应用到的不同木片数排序。
- 时间螺旋:经验画成桶外的菱形,高度在形成与应用两个时间之间,角度在两片木片之间;两段向外弯的弧线分别连到形成木片和应用木片的对应高度。只有选中的经验在画布上显示名称。提供“跨木片的经验”键盘入口(现为工具栏选择框);点击菱形或选择经验打开详情。
验证:
- `story-graph-3d-model.test.ts` 新增 1 项(只连到跨木片的后续应用、只出现一次、层外依据与倒序应用排除、上限)。相关组件测试 3 个文件 32 项通过,Web 类型检查、相关 ESLint 通过。
- Electron 实测(生产组件,第 5 步数据副本的 46 个故事、38 条经验;1440 浅色、390 深色):无报错、无横向溢出;根层 3 条、goodbuddy 层 6 条跨木片经验;从列表选中和在画布上点中菱形都能打开经验。
- 38 条经验中,在当前数据里能形成跨木片连线的只是少数,其余只在同一故事中形成或应用,不在螺旋上显示,仍可在“经验”页签中查看。
## 2026-10-02 故事线模型第 5 步:工作回顾按故事组织
对应[故事线模型改造设计](./storyline-model-design.md)第 5 步“工作回顾页”。
实现:
- 工作回顾在所选回顾结果的摘要之上新增“这段时间的故事”,按该结果的范围和时间区间读取已保存的故事和经验,不调用模型(`supervision-story-digest.ts`、`SupervisionStoryDigest.tsx`)。
- 分组:推进的故事(区间内有事件、区间前也有)、新出现的故事(第一个事件在区间内)、已结束的故事(结束依据事件在区间内)、形成或用上的经验(最早形成依据在区间内,或区间内有应用)、这段时间没有新进展(进行中、区间前有事件、区间内没有;跨项目故事和已结束的故事不算)。功能把子线索的事件计入自己,子线索不重复列出。
- 每组按事件数排序,默认显示 6 条,可展开其余;每条显示项目、事件数和最近一个事件。点击任一故事或经验打开图谱,并选中该对象。原有的工作总结、本次变化、未解决事项保留在下方,作为阅读入口。
验证:
- `supervision-story-digest.test.ts` 2 项(分组规则、子线索计入功能、跨项目与已结束故事不算无进展、经验形成与应用区分);`SupervisorWorkspace.test.tsx` 新增 1 项(按所选结果范围读取故事、各分组内容、点击进入图谱)。相关组件测试 4 个文件 73 项通过,Web 类型检查、相关 ESLint 通过。
- Electron 实测(生产组件,第 5 步数据副本导出的 46 个故事、38 条经验和一条 7 天的全局回顾结果;1440 浅色、1024 深色、390 深色):无报错、无横向溢出、条目无文本溢出;宽屏 3 列、窄屏 1 列;展开后从 13 条到 26 条;点击条目切到图谱。该副本的数据从 09-19 开始,这条结果的区间正好覆盖全部数据,因此只出现“新出现的故事 19”“已结束的故事 1”“形成或用上的经验 33”,“推进的故事”和“没有新进展”两组没有在真实数据中出现,只由单元测试覆盖。
## 2026-10-02 故事线模型第 5 步:心跳故事建议与 MCP 故事、经验查询
对应[故事线模型改造设计](./storyline-model-design.md)第 5 步。工作回顾页按故事组织见上一节。
实现:
- 心跳建议新增两类,候选只由规则选出,不调用模型:
- **暂无进展**(`stalled`):本次范围内进行中的功能或子线索(至少 3 个事件),最近事件距今达到“故事多少天没有新进展时提醒”(设置,默认 14 天)。只提示查看,不改变故事状态,不推断结束。每次心跳最多 3 条,按事件数从多到少;已待处理或依据未变的不计入名额,其余留到后续心跳。接受后转为暂停的后续任务。
- **可用经验**(`experience`):本次回顾归入某故事的事件,与某条经验的形成事件共享实体,而该经验尚未在这个故事中形成或应用。出现在 5 个以上故事中的实体(如产品名)不作为依据。每条经验只指向匹配事件最多的一个故事,每次心跳最多 3 条,按依据数排序。接受即标记已参考。
- 两类候选排在未决事项之前,不会被每次 20 条的上限挤掉;与其他建议一起只用一次模型调用措辞,输入不含来源原文和定位。
- 建议记录 `story_id`、`experience_id`;“在图谱中查看”直接打开对应故事或经验。Schema 56 重建 `supervision_suggestions` 以扩展类型约束,已有行原样复制。
- Story Graph MCP:`story_graph_search` 支持 `story`、`experience` 两种对象;`story_graph_get_context` 对故事返回其事件(功能包含子线索的事件),对经验返回形成与应用事件,再通过事件读取来源。经验带 `automatic` 或 `user_edited` 标记,工具说明要求把经验作为参考而非规则。查询范围规则不变:项目范围只返回本项目的故事,以及在本项目形成或应用过的经验。
验证:
- 新增 `supervision-story-suggestions.test.ts` 5 项:暂无进展的阈值、不改故事状态、不重复、转任务;每次最多 3 条且按事件数排序、其余在后续心跳补上;经验跨故事建议只出现一次、不建议给形成它的故事;MCP 故事与经验的搜索、上下文及项目范围隔离;Schema 55 升级到 56 保留已有建议。新增 `SupervisionSuggestionsPanel.test.tsx`:新类型标签与按故事、经验打开图谱。监督相关主进程 11 个文件 73 项、IPC 定向 21 项、组件 4 个文件 72 项通过;Node/Web/Agent 类型检查、相关 ESLint 通过。未运行全量测试。
- 真实数据(第 4 步修正后运行的数据副本,`npx jiti scripts/story-suggestions-deepseek.mjs <db> .env.deepseek.local <dir> 7`,DeepSeek `deepseek-flash`;数据只有约 13 天,阈值用 7 天,“现在”取最后事件时间):
| 运行 | 规则候选 | 调用 | 结果 |
| --- | --- | --- | --- |
| 第 1 轮(故事候选排在未决事项之后) | 20 条未决事项,0 条故事候选 | 2 | 故事候选被 20 条上限挤掉,因此改为故事候选优先 |
| 第 2 轮(暂无进展不限量) | 20 条暂无进展 | 2 | 13 天的数据里大量故事“停滞”,一次挤满上限,因此加每次 3 条的限制 |
| 第 3 轮(经验按每个实体匹配) | 3 条暂无进展、17 条可用经验 | 2 | 同一经验指向 5~8 个故事,大多经由“监督者”这类通用实体匹配,因此改为排除通用实体、每条经验只指向一个故事、每次最多 3 条 |
| 第 4 轮(最终规则) | 3 条暂无进展、3 条可用经验、14 条未决事项 | 2 | 每次请求 6 条故事候选,都不含来源定位;建议都带故事,经验建议都带经验 |
- 最终规则下的可用经验例如“长任务超时或中断先查取数与分批根因,不以延长超时或调并发修复”指向“回顾算法与并发设置”、“提交只纳入本次相关改动”指向“自动监督开关与设置”。是否真正适用未人工逐条核对。
- 再次生成时没有重复任何待处理建议;只补上了上一次被 20 条上限挤掉的未决事项(1 次调用)。
- MCP:全局范围搜到 6 个相关故事、9 条相关经验;一个功能的上下文返回 96 个事件(含子线索),一条经验返回 5 个依据事件。
边界:是否在普通对话中主动查询故事与经验,取决于 Agent 是否调用工具,未做额外注入。
## 2026-10-02 故事线模型第 4 步:经验抽取
对应[故事线模型改造设计](./storyline-model-design.md) SL-4、SL-5 和第 4 步。
实现:
- `supervision-experiences.ts`:新表 `supervision_experiences`(经验、适用条件、边界、状态、用户调整标记)、`supervision_experience_events`(`formed` 形成依据、`applied` 后续应用及结果)、`supervision_experience_seen`。Schema 55,升级后表为空,由下一次回顾从已有故事中抽取,不重读来源。
- 在故事归属之后、同一回顾名额内执行。候选由规则选出,不调用模型:有尚未处理的决定、变化或里程碑事件,且事件数达到“故事参与经验整理的最少事件数”(设置,默认 5)的功能或子线索。小故事和只有讨论的进展直接标记已处理,不调用模型。
- 候选故事按文本量分块,每块一次模型调用,输入是故事名、事件标题与截断描述以及已有经验,不含原文。回答中的每条形成依据必须是本次输入、且有来源的事件,否则整块不保存;早于经验形成时间的“应用”被丢弃(真实模型会出现这种引用),其余保留。同一表述的经验合并到已有经验。
- 经验生成后即可读取,标记“自动归纳”。用户可以编辑、合并、删除,与故事调整共用撤销;编辑过的标记“已手动调整”;删除的经验不会因同一表述或相同依据再次生成。
- 经验抽取失败不影响故事和已发布的回顾:活动记录显示“故事已整理,经验整理失败”,可与故事一起重新整理,下一次回顾也会补上。
- 界面:图谱左侧新增“经验”页签;选中经验在右侧查看适用条件、边界、形成依据和后续应用(含所属故事与结果),依据事件可直接打开;编辑、合并、删除、撤销。设置新增“故事参与经验整理的最少事件数”。
验证:
- 新增 `supervision-experiences.test.ts` 4 项:规则候选、应用记录、无新进展零调用、小故事与纯讨论零调用、虚构依据整块拒绝、早于形成的应用被丢弃、编辑保护、删除后不再生成、合并与撤销。工作区新增经验页签测试。监督相关主进程测试 7 个文件 174 项、工作区/活动/设置组件测试 48 项通过;Node/Web 类型检查、相关 ESLint 通过。未运行全量测试。
- 真实数据(第 2 步跨项目开启运行后的数据副本,46 个故事,`npx jiti scripts/experiences-deepseek.mjs`,DeepSeek `deepseek-flash`):
| 运行 | 候选故事 | 调用 / 耗时 | 经验 | 应用记录 | 跨故事经验 | 依据无来源 | 应用早于形成 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 首轮(应用早于形成时整块拒绝之前的规则) | 31 | 6 / 约 3.9 分钟 | 40 | 71 | 15 | 0 | 5 |
| 修正后 | 31 | 6 / 约 2.7 分钟 | 38 | 53 | 9 | 0 | 0 |
- 两次再次抽取均为 0 次调用;所有请求都不含来源原文。
- 修正后 38 条经验中 8 条只有 1 个形成依据。内容基本是这段时间真实形成的工作经验,例如“长材料回顾须完整分块,按成功批次保存进度并支持续作,禁止截断”“能力开关关闭时须阻断后台任务领取与执行,不能只隐藏入口”“提交只纳入本次相关改动,其他未完成改动留在工作区”。
- 两次运行的经验条目不完全相同,未比较多次运行的一致性;也未逐条人工核对每条经验与依据是否对得上。
边界:经验尚未接入 MCP、心跳建议和时间螺旋视图(桶外经验节点),这些属于第 5 步和视图后续工作。
## 2026-10-02 故事线模型第 3 步:时间螺旋视图
对应[故事线模型改造设计](./storyline-model-design.md)第 3 步与 [3D 概念](../story-graph/3d-concept.md)。
实现:
- 图谱画布标题右侧新增“平铺 / 时间螺旋”切换。时间螺旋视图(`StoryGraph3D.tsx`)读取故事列表与图谱返回的 `attention`:高度为时间,半径为每圈的对话轮次(一圈一个半径,相邻圈差值受限),木片为故事起止,事件按屏幕距离聚合。层级为 全部 › 项目 › 功能 › 子线索,跨项目故事在根层;点击木片进入下一层并在右侧显示故事详情,单个事件选中后显示事件详情。斜俯视/侧视/俯视/自由视角,拖动、滚轮、方向键、数字键、Esc。
- 计算部分在 `story-graph-3d-model.ts`(树、角度、圈长、半径、聚合),渲染只负责显示。圈长从候选档位中选最接近 8 圈且不超过 14 圈的一档。
- Three.js `0.186.1` 作为依赖,组件通过 `lazy` 加载,只在打开时间螺旋时下载(生产构建中只出现在 `StoryGraph3D` 分块)。颜色取现有 `--graph-node-*` 令牌。WebGL 不可用时提示改用平铺视图。
- 2026-10-02 布局调整:画布下方的图例段落和木片、经验列表移除,中间区域只保留图。图例改为工具栏“图例”按钮,点击后浮在画布右上角,可用 X 关闭;圈长(如“一圈 1 天 · 共 13.1 圈”)常驻画布左下角。木片和经验的键盘与读屏入口改为工具栏中的两个选择菜单(共享 `AnchoredMenu` 样式,每项显示名称和事件数、起止或来源→去向)。按钮按当前层级命名,如“项目与跨项目故事 · 6”“功能 · 15”“子线索 · 9”“经验 · 3”,不再使用“木片”这一内部比喻。菜单内边距 4px,菜单项 8px×12px、行间 2px,两个按钮间距 8px;1440 与 390 宽度下文字无裁切,菜单在窗口内。
- 2026-10-03 图谱中间列不再滚动:平铺与时间螺旋都把标题栏以外的高度全部给图。平铺视图的图例、阅读说明移入标题栏“图例”浮层,可见数量常驻画布左下角;“事件浏览”改为单行(上一个、滑块、下一个、位置、当前事件)。图谱行最低 660px,以免平铺图文字小于 11px;更矮的窗口改为页面滚动。Electron 布局测试新增断言:中间列 `overflow: hidden` 且无溢出,图区一直延伸到列底部;1440×1200 下整页无滚动、底部留 33px,图高 625px。真实数据副本下时间螺旋在 700px 行高中填满,无列内滚动。
- 2026-10-03 图谱页顶部的“图谱范围 · 全局”和时间行并入画布标题下方一行(范围 · 时间区间 · 生成时间 · 事件/实体数),刷新改为标题栏图标按钮;标题由“工作的来路”改为“事件与知识”。图谱行因此多出约 40px:1440×900 下整页不再滚动,图高 552px。布局测试新增断言:范围行位于标题下方,图谱上方不再有独立的范围栏。
- 2026-10-03 修复“时间螺旋没有加载成功”:每次重建场景都复用同一个画布,而上一次释放时已对它调用 `forceContextLoss()`,该画布再也拿不到可用的 WebGL 上下文(Three.js 报 `Cannot read properties of null (reading 'precision')`)。开发模式的 StrictMode 双挂载、父组件刷新时传入新的 `attention` 数组都会触发重建,所以打开就失败。现在每次重建创建独立的绘图画布并在释放时移除,原画布只负责焦点和指针事件;`attention` 按内容保持稳定,刷新不再重建场景。复现脚本(StrictMode + 20 次父组件重渲染)修复前首屏即报错,修复后无报错。真实数据副本的 Electron 检查(1440 浅色、390 深色):画布外无段落和列表,图例浮层位于画布内,选择框逐层打开、视角切换、Esc 返回正常,无报错和横向溢出。
验证:
- `story-graph-3d-model.test.ts` 4 项(树与跨度、圈长档位、每圈一个半径且变化有界、聚合);工作区、活动、设置组件测试通过;Node/Web 类型检查、相关 ESLint 通过;`electron-vite build` 确认 Three.js 只在懒加载分块中。
- Electron 实测(第 2 步跨项目运行的真实故事数据副本,生产组件):1440 浅色与 390 深色均无控制台错误、无横向溢出;画布非背景像素约 26%;根层 6 片(5 个项目、2 个跨项目故事中的果蝇神经仿真等),goodbuddy 15 个功能,监督者 9 条子线索;视角切换、Esc 返回、选中故事回调正常;旋转一帧约 1 ms。数据跨 13 天,一圈 1 天共 13.1 圈。
边界:时间切面(第 7 项)、展开视图(第 8 项)、原位分裂动画和桶外经验节点尚未移植;经验(W)要到第 4 步。Three.js 懒加载分块约 1.15 MB。
## 2026-10-02 故事线模型第 2 步:故事归属与用户调整
对应[故事线模型改造设计](./storyline-model-design.md) SL-1~SL-3、SL-5、SL-8 和第 2 步。
实现:
- `supervision-stories.ts`:新表 `supervision_stories`(功能、子线索、跨项目故事,状态、用户调整标记)、`supervision_event_stories`(主故事唯一,另可关联)、`supervision_story_assigned`、`supervision_story_edits`。Schema 54;升级后表为空,已有事件由下一次回顾归属,不重读来源。
- 回顾发布后在同一回顾名额内执行故事归属:只取本范围中尚未归属的当前事件,按项目分块,每块一次模型调用,输入是事件标题、描述和本项目已有故事摘要,不含原文。没有待归属事件时不调用模型。项目来自事件来源;功能、子线索由模型提出;允许“暂不归类”。
- Main 校验每个事件恰好分配一次、主故事属于本项目、子线索有上级功能、结束标记引用本次归入该故事的事件;任一项不符则整块不保存。功能事件数低于“子线索最少事件数”(设置,默认 20)时,子线索并入功能。
- 跨项目关联关闭时,提示词禁止跨项目故事,也不提供已有跨项目故事;开启时项目依次处理,跨项目故事只作关联,不影响主故事的长度和事件数。
- 用户调整:重命名、合并(同项目同层级)、移除(事件回到上级功能或变为暂不归类,不删除事件)、把事件移到同项目其他故事或暂不归类、撤销上一次调整。调整过的故事不会被自动整理改名或标记结束,移除的故事不会因同名再生成。
- 故事归属失败不影响已发布的回顾:活动记录显示“回顾已保存,故事整理失败”并可重新整理;下一次回顾(含无变化)也会补上。
- 界面:图谱左侧新增“故事”页签,按项目 › 功能 › 子线索、跨项目故事列出,显示暂不归类的事件数;选中故事在右侧查看起止、状态、事件,并可重命名、合并、移除、撤销;事件详情可改所属故事。设置新增“功能拆分子线索的最少事件数”。
验证:
- 新增 `supervision-stories.test.ts` 6 项:归属与无变化零调用、长时间空白后接回原故事、不完整/重复/虚构/证据不符的回答整块拒绝、小功能子线索并入、跨项目开关、调整保护与撤销、回顾内执行及失败后重试。`src/main/assistant` 全目录、Story Graph MCP 集成、监督相关 IPC、监督者工作区/活动/设置组件测试均通过;Node/Web 类型检查与相关 ESLint 通过。未运行全量测试。
- 真实数据(第 1 步升级后的本机数据副本,`scripts/stories-deepseek.mjs`,DeepSeek `deepseek-flash`):
- 跨项目关闭:437 个当前事件,11 次调用约 4.3 分钟;39 个故事(35 个功能、4 个子线索),396 个事件归入,41 个暂不归类;无事件有两个主故事、无主故事跨项目;11 个“已结束”均引用本故事内的事件;再次归属 0 次调用。
- 跨项目开启:11 次调用约 6.4 分钟;44 个故事,其中 1 个跨项目故事关联 dgx 与阿里云-国内的 7 个事件;其余检查同上。
观察到的问题(首轮):
- 功能粒度不稳定:goodbuddy 的发布被拆成 0.13.10、0.13.11、0.13.12、0.13.13 多个功能,两次运行切分不同。
- 监督者相关 77~85 个事件的功能只拆出 0~1 个子线索,模型很少主动建子线索。
- goodbuddy 的 fly-brain 仿真与微信 ClawBot 的果蝇开源项目在开启跨项目后仍未关联。
- 故事起止跨度可以从事件计算,但单个事件的跨度仍多为同一时刻(见第 1 步边界)。
针对上述问题的调整:
- 提示词写明粒度:功能是持续数周的模块、子系统或研究主题;发布、版本、单个缺陷、单日任务不是功能,同一项目的发布与版本同步归入一个功能,版本可作子线索;功能名不含版本号和日期;每个项目通常 3~12 个功能;功能达到“子线索最少事件数”后按子线索放置事件。
- 新功能或跨项目故事的名字去掉版本号、日期后与已有或同批故事相同时,并入已有故事(确定性规则);被并入那一方的“已结束”标记不生效,避免一个版本结束让整条发布线结束。
- 子线索拆分:每个项目归属完成后,自身事件数跨过“子线索最少事件数”下一个整数倍的功能(每项目最多 3 个)各做一次拆分调用,输入只有该功能名称、已有子线索和模型放置的事件标题与截断描述。只把事件从功能移到子线索;新子线索至少需要 max(3, 阈值/5) 个事件,否则不建;用户移动过的事件和用户调整过的功能不动;被用户移除的同名子线索不重建。功能没有跨过下一个整数倍时不再调用。
- 跨项目开启时,提示词附带其他项目的功能名称、描述和事件数(最多 60 个,不含事件内容),已有跨项目故事附带它已关联的功能名称;回答可用 `links` 把本项目功能和其他项目功能挂到同一个跨项目故事,关联该功能的全部当前事件(只作关联,不改主故事)。关闭时不提供,且 `links` 会被整块拒绝。
- 新故事键接受 `new_1_t1` 这类写法(真实模型在一次回答中这样写子线索键,原先整块被拒)。
调整后的真实数据(同一数据副本,DeepSeek `deepseek-flash`,每次各自从空故事开始):
| 运行 | 调用 | 功能 / 子线索 / 跨项目 | 归入 / 暂不归类 | 拆分移动 | 版本号命名的功能 |
| --- | --- | --- | --- | --- | --- |
| 首轮 关闭 | 11 | 35 / 4 / 0 | 396 / 41 | — | 有(0.13.10~0.13.13) |
| 首轮 开启 | 11 | 35 / 8 / 1 | 407 / 30 | — | 有 |
| 调整后 关闭 | 12(含 1 次拆分) | 19 / 23 / 0 | 372 / 65 | 18 | 0 |
| 调整后 开启 | 12(含 1 次拆分) | 21 / 23 / 2 | 388 / 49 | 20 | 0 |
- 改善:goodbuddy 发布收敛为一个“发布与版本”/“提交与发布”功能,版本工作成为其子线索;监督者功能拆出 9~11 个子线索(回顾整理与图谱、回顾算法与并发设置、故事线模型与 3D 图谱、活动记录页、自动监督开关与设置等),界面、笔记与画布也拆出子线索;开启跨项目后“果蝇神经仿真”关联了 goodbuddy“本地果蝇神经仿真”与微信 ClawBot“果蝇神经开源项目调研”(16 个事件),另有“Agent 实例生命周期与回收”关联 goodbuddy 与 dgx(24 个事件)。两次运行都无事件有两个主故事、无主故事跨项目、无小功能子线索,再次归属 0 次调用。
- 未改善或变差:暂不归类增多(关闭 41→65,开启 30→49),模型在功能变宽后把更多零散事件留空,未逐条核对是否合理。首轮的 dgx+阿里云跨项目故事这次未出现。子线索大多在归属调用中直接建出(关闭 18、开启 20 个事件由拆分调用移入子线索),拆分调用每次运行只触发 1 次。关闭、开启各只成功跑了一次,多次运行的切分是否一致未比较。
- 单元测试:`supervision-stories.test.ts` 新增 3 项(版本名功能合并且不误结束、拆分调用的触发上限与用户事件保护及整块拒绝、跨项目功能列表与 `links` 关联及关闭时拒绝),共 9 项;与 `supervision-timeline.test.ts` 合计 13 项通过;Node 类型检查与相关 ESLint 通过。
- 失败:另有两次运行中途失败、未完成。一次是开启跨项目时模型把“已结束”标记引用到未归入该故事的事件,整块被拒,导致整次归属失败(已在提示词中强调证据事件须归入同一故事,之后的开启运行通过);一次是上述 `new_1_t1` 键格式,已放宽校验并加单元测试,但放宽后未再用真实模型重跑。严格校验下单块被拒会让整次归属失败,仍需重试才能补上。
## 2026-10-02 故事线模型第 1 步:共享时间线
对应[故事线模型改造设计](./storyline-model-design.md) SL-6、SL-7 和第 1 步。
实现:
- 处理进度改为一条共享时间线:`review_checkpoints` 的监督阶段只用 `scope = 'timeline'`,项目、全局和多项目组合共用。同一来源版本在任一范围成功发布后,其他范围不再提取;范围只决定读取和展示哪些来源。旧的按范围进度在升级时合并,不重读已处理内容。
- 事件新增 `started_at`、`ended_at`、`project_id`、`superseded_by`,来源新增 `source_key`、`source_revision`(`supervision-timeline.ts`)。起止时间和所属项目按事件引用的来源记录计算,不由模型给出;来源跨多个项目时 `project_id` 为空。同一来源版本的重叠区间被后来的结果重新提取时,旧结果中的事件标记为已被替代,图谱和 MCP 只读当前事件,历史结果快照不变。
- 图谱按范围读取共享时间线上的当前事件、实体、关系和来源,并返回 `attention`:按小时统计的消息轮次与正文量,只表示投入集中程度。
- 新增设置“跨项目关联”(`supervisionReview.crossProject`,默认关闭)。关闭时,每批只把本项目已有的实体作为候选,不同项目的内容不会合并为同一实体;开启后候选来自全部项目。运行开始时冻结到本次配置。
- Story Graph MCP:全局范围可以读到各项目的当前事件;来源可从运行范围、全局或包含该来源项目的项目范围读取。
- Schema 53。
验证:
- 新增 `supervision-timeline.test.ts`:项目回顾后全局只提取其他项目的来源,相同项目再次回顾不调用模型;起止时间和项目来自来源;重新整理与项目回顾重叠后当前事件不重复;跨项目开关决定候选;Schema 52 升级合并进度和重复事件。`src/main/assistant` 全目录 28 个文件 324 项、Story Graph MCP 集成、监督相关 IPC 15 项、设置组件均通过。Node/Web 类型检查、相关 ESLint、`git diff --check` 通过。未运行全量测试。
- 真实数据(`scripts/timeline-deepseek.mjs`,用 SQLite 在线备份复制本机 2.2 GB 数据库,原库只读):升级 2.0 秒,`integrity_check` 为 ok。原有 423 个事件全部得到起止时间与项目;13 个两次回顾重复提取的事件被标记替代,当前 410 个(goodbuddy 358、默认项目 28、dgx 17、微信 ClawBot 5、阿里云-国内 2);全局图谱返回 67 个小时段、790 轮对话的密度。
- 真实模型(DeepSeek,`deepseek-flash`):对 goodbuddy 最近 2 小时的 37 个来源做项目回顾,4 个批次加 3 次导航汇总,共 7 次请求,新增 27 个事件,起止时间都在区间内,未关联到其他项目的实体;再次项目回顾和同区间全局回顾均为无变化,0 次请求,全局重新提取的项目来源为 0。
边界:
- 这个 2 小时区间没有其他项目的消息,所以“全局只补其他项目”只在合成测试中验证过。
- 模型通常让一个事件只引用一条消息,所以真实数据里的事件起止时间多数是同一时刻。按故事计算跨度要等第 2 步的故事归属。
- 测试脚本把单次输出上限设为 8,192 token 时,一个批次的输出被截断,导致 JSON 无效。上限调到 32,768 后通过。产品路径不设这个参数,具体由 Runtime 的提供商配置决定。
## 2026-09-30 心跳职责收敛与监督建议
对应 FR-S4、FR-S12、US-S32、US-S33,规则见[心跳职责与介入](./logic-design.md#心跳职责与介入目标设计)。
实现:
- `HeartbeatService` 不再读取来源或调用模型。领取计划后立即完成自身运行,再通过 `review` 触发共享的 `SupervisorService.run({ trigger: 'heartbeat' })`;审查结果 `no_change` 时标记心跳为无变化。计划领取、单次补跑、单回顾排队和失败记录沿用原有机制。
- 手动“回顾”默认增量:`supervisionRunRequestSchema` 新增可选 `reanalyze`,`isIncrementalReview` 决定是否读取、推进 `review_checkpoints`。只有 `reanalyze: true` 重读区间,且不推进也不重置共享进度。自动检查不再续跑用户主动暂停的回顾,改为新建运行处理未提交来源。
- 计划新增 `intervention`(`suggest` / `memory`,默认 `suggest`)。`supervision-suggestions.ts` 从本次发布结果按规则挑选候选:批次未决事项、`revised` 实体变化、`contrasts` 关系、由至少三处不同原始来源支持的未确认实体。没有候选时不调用模型;有候选时 `supervision-suggester.ts` 只把候选文本交给一次建议调用,不读原文。待处理建议不重复,已处理建议仅在依据变化后再次提出。
- 建议失败写入 `heartbeat_runs.suggestion_status/error`,图谱和进度保留;活动页显示“回顾已更新,建议生成失败”并可单独重试。
- 接受未决事项生成暂停任务,接受候选约定确认为长期背景(记忆)并确认对应实体;分歧和修订标记已核对。
- Schema 50:`heartbeat_configs.intervention`、`heartbeat_runs.suggestion_status/suggestion_error`、`supervision_suggestions` 表。升级时把旧心跳报告中仍为 `proposed` 的记忆迁入为待确认约定,不确认、不删除;旧报告只读保留。
- 界面:工作回顾主按钮改为“回顾”,“更多 → 重新整理…”经确认后执行;计划 Modal 增加“介入方式”;自动监督页用“监督建议”替代成功率、趋势、记忆/行动建议和运行审计,建议可查看依据、打开会话、在图谱中查看;设置移除心跳报告超时;导航角标统计待处理建议。
验证:
- 定向单元与集成测试:`src/main/assistant`、`src/shared`、`src/preload` 共 589 项;`src/main/ipc.test.ts` 心跳与监督相关 29 项;Renderer 监督者、自动监督、活动、设置相关组件测试及 `App.test.tsx` 全文件均通过。新增覆盖:手动增量与重新整理不动进度、不续跑已暂停回顾、候选规则与去重、忽略后仅新证据重提、建议失败隔离、IPC 启停门控、Schema 49 升级迁移。
- Electron 布局:`GOODBUDDY_SUPERVISOR_RECAP`、`AUTOMATIC_OVERVIEW`、`ACTIVITY`、`PLAN_MODAL` 场景通过,覆盖 1440/390px 下监督建议、介入方式字段、重新整理确认和工具栏两行布局,无横向溢出。
- 真实模型(DeepSeek,`deepseek-flash`,隔离 SQLite,`scripts/heartbeat-deepseek.mjs`):首次心跳 1 次回顾调用 + 1 次建议调用(建议输入约 1,150 字符,不含原文),生成分歧、修订、未决事项三条建议且依据均指向本次来源;无变化心跳与默认手动回顾均 0 次调用;追加一条消息后只处理 1 个来源,复用全部 5 个既有实体,再调用 1 次回顾、1 次建议。共 4 次付费请求。
- 未运行全量测试。
边界:主动介入(会话级实时)未实现,界面不显示;项目与全局仍各自维护处理进度(US-S31),已由 2026-10-02 的共享时间线取代。
## 2026-09-28 Story Graph 只读工具
对应 FR-S11、US-S28 至 US-S30 的 D1 读取实现。输入、分页与版本语义由[专项设计](./story-graph-mcp-design.md)负责。US-S31 的跨范围事实复用和 memory 读取切换仍待实现。
实现路径:
- [共享工具合同](../../../src/shared/story-graph-tools.ts)定义 `story_graph_search`、`story_graph_get_context`、`story_graph_read_source`。内置目录与能力设置支持 Model、OpenCode、Continue、DeepSeek Harness 分配;应用关闭时 Main 不签发图谱读取权限。
- [只读 SQLite 投影](../../../src/main/assistant/story-graph-reader.ts)读取精确 scope 下的历史结果、当前对象、事件、变化、关系、来源和成功叶子描述。保留旧决定与修订依据,确认状态和有效性分别返回,未知有效性不按时间推断。没有新增 schema、业务写入或模型调用。
- [Main gateway](../../../src/main/agent/knowledge-mcp-gateway.ts)在发现、读取前和返回前复核监督者开关及 Runtime 分配;IPC 将项目和 Runtime 固定到请求。Model 和 Harness 代理、本地 MCP、原生客户端共用此入口。Harness 的目录、scope schema 转换及 Host Ask 执行钩子均已接通。
- 远程沿用 `ProtocolRemoteRuntimeChannel`、`MainImageToolSession`、`AgentImageToolMcp` 和 `RuntimeAcpBackend` 的现有工具通道。Ask 可以只有图谱描述,没有图像生成描述。OpenCode 根/子会话仅放行当前端点;Continue Ask 保留 Main 会话 MCP,并由原生只读工具名和 Main 双重校验。
- 对象页为 10/50 项,正文页为 4,000/8,000 码点(默认/上限)。Context 用带位置的 JSON 片段续读长对象;游标绑定过滤、scope 和读取修订,变更后返回 `stale_cursor`。来源默认读保存片段;显式 current 通过 locator 读当前记录,version 仅接受可验证的现存版本。知识引用片段不声称是整份知识原文,来源迁出项目、删除、未知历史和响应过大均有明确结果。
定向验证:
```text
npx vitest run src/main/assistant/story-graph-reader.test.ts src/agent-daemon/story-graph-integration.test.ts src/main/assistant/supervision-production.test.ts src/main/capabilities/capability-service.test.ts src/main/ipc.test.ts src/main/remote-agent/protocol-remote-runtime-channel.test.ts src/agent-daemon/runtime-acp-backend.test.ts src/agent-daemon/continue-acp-helper.test.ts src/agent-daemon/opencode-subagent-plugin.test.ts src/main/agent/native-terminal-client.test.ts
```
10 文件、337 项通过,1 项既有条件测试跳过。覆盖真实 SQLite、HTTP MCP、Main/Agent 消息往返、协议准备和释放、四个 Runtime 的 IPC 项目绑定、Supervisor 关闭后不再读取、取消、来源删除/迁移、旧游标、Unicode 正文续页、超长响应及 A → B 修订后 C 仍为选项。冷启动审查发现 current 来源读取依赖运行回顾后才注册的 SQLite 函数,改为按现存 context 直接计算同一修订;新连接回归通过。最终读取相关 3 文件、28 项再次通过。
其他受影响路径也已定向验证:
- `continue-runtime.test.ts`、`opencode-runtime.test.ts`、`deepseek-harness-runtime.test.ts`、`native-client-coordinator.test.ts`、`protocol-remote-runtime-channel.test.ts`、`knowledge-mcp-gateway.test.ts`:6 文件、243 项通过。
- `goodbuddy-harness-control-plane.test.ts`、`continue-host-adapter.test.ts` 及相关桥接/读取测试:6 文件、98 项通过。`model-tool-provider.test.ts`、能力设置及 Continue helper 组合:3 文件、92 项通过。
- `npx vitest run src/main/agent/deepseek-harness-acp-e2e.test.ts -t "reads Story Graph"`:1 项通过、13 项按名称跳过。真实受控 Harness Host 经 ACP 和 Main 代理执行 Ask/Execute 读取,随后关闭监督者并确认工具移除;推理使用测试实现,没有外部模型请求。
- `npx tsc --noEmit -p tsconfig.node.json`、`tsconfig.agent.json`、`tsconfig.web.json` 均通过;最终 Main 类型检查再次通过。本次变更的 TypeScript 源码和测试定向 ESLint 通过,`git diff --check` 通过。
### Linux Host 验证
[SSH 探针](../../../scripts/story-graph-host-probe.ts)从当前源码临时打包,在共享 Linux x64 Host 运行 `AgentImageToolMcp`、`AgentOwnedAcpPrompt` 和 `ContinueHostAdapter`,使用 Host 安装的 OpenCode 和 Continue(1.5.47)。桌面启动隔离内存 SQLite、Main gateway 和 `MainImageToolSession`,通过 SSH 搬运工具通道消息;用户数据库、模型凭据和正文库没有复制到 Host。Host 临时运行目录在结束时清理。
探针通过:HTTP 发现三个只读工具、搜索/上下文/来源读取及分页、`as_of` 拒绝、OpenCode Ask/Execute、Continue Ask/Execute 的实际工具调用,以及关闭后的发现和旧会话调用拦截。成功探针有 10 次本机确定性推理请求,其中 2 次为 Runtime 标题生成,桌面读取 10 次,付费调用 0 次。RPC blob framing 与 prompt 生命周期另由上述 `runtime-acp-backend` 和 `protocol-remote-runtime-channel` 测试覆盖;该 SSH 探针使用标准输入输出承载工具消息,没有执行安装包升级或完整桌面 UI 操作。
复跑时在仓库使用 esbuild 将脚本输出到临时目录,再通过已有 SSH 身份传至 Host 的 GoodBuddy 测试目录。桌面命令参数为:
```text
node <local-probe.cjs> <ssh-host> <remote-probe.cjs> <host-opencode-path> <host-continue-cn.js-path>
```
### 保留边界
没有执行全量测试、付费模型请求、提交、推送或发布。真实模型如何根据冲突证据形成最终建议、跨平台安装包和完整桌面 UI 仍未在本次验收;没有据此声明 US-S28 的建议质量已验证。查询目前同步扫描所选 scope 的已发布事实,大库延迟和运行中断粒度没有性能验收。`replaces`/`contradicts`、历史有效期、`as_of`、跨 scope 事实复用和 memory 消费切换均没有伪造实现;已有记忆与 checkpoint 保留。中文设计扫描无阻断项,复核项是时间、范围和证据限制,按技术含义保留。
## 已验证
### 2026-09-26 单次执行与取消集成复核
对应 FR-S4、FR-S6、FR-S10、US-S24、US-S26。复核当前 Main、SQLite、IPC、Preload 与 Renderer diff,确认手动新建/继续遇到占用立即拒绝,自动回顾等待;取消立即落库,执行位置保留至并行批次及 Runtime 清理结束。UI 在此期间显示正在取消,X 仅关闭提示;已取消运行不能继续,也不会被自动恢复。实现合同见[单次执行与取消](./technical-design.md#单次执行与取消),界面见[UI 设计](./ui-design.md)。
发现并修复一个可达问题:活动页等待整个 `resume` 请求结束才释放操作锁,继续运行后的暂停和取消按钮因此一直禁用。现在发出继续请求后释放提交锁,独立处理完成/失败,Main 仍保证唯一执行。新增回归在修复前因暂停按钮禁用而失败,修复后验证继续中的取消、停止提示,以及原继续请求返回时不清除后续取消操作的状态。
本轮验证:
- `npx vitest run src/renderer/src/SupervisorActivity.test.tsx src/renderer/src/SupervisorWorkspace.test.tsx`:38 项通过,包含上述新增回归、取消清理期间的状态、重新进入页面、手动重复提示及 X 不取消。
- `npx vitest run src/main/assistant/supervision-review.test.ts src/preload/supervision-activity.test.ts src/main/ipc.test.ts -t "reserves before setup|persists cancellation|allows a new review after|cancels navigation|projects cancellation|wakes waiting automatic|production supervision IPC cancels|supervision"`:27 项通过、144 项按名称跳过;其中 Preload 文件未命中过滤,另用 `npx vitest run src/preload/supervision-activity.test.ts` 单独验证,1 项通过。
- `npx eslint src/renderer/src/SupervisorActivity.tsx src/renderer/src/SupervisorActivity.test.tsx` 通过;本轮文件的 `git diff --check` 通过。中文新增内容按 `deai-writing` 清单人工审校,文档链接目标已核对。
上述测试使用隔离 SQLite、生产 handler/工厂及可控 Runtime、Renderer API 替身;本轮未运行 Electron 端到端、全量测试或类型检查,真实模型调用 0 次。用户提供的后端与前端实现代理测试通过记录作为先前证据,不并入本轮计数。远程不受影响的依据是 `ipc.ts` 生产工厂调用 `resolveRequestRuntime({ workMode: 'ask' })`,无项目、Runtime 选择或执行空间,解析直接返回桌面默认 Runtime;不进入 SSH/Agent 路径,未修改远程协议或取消实现。未运行远程实测,未改动其他工作的 Runtime/package 文件,未提交。
### 2026-09-26 移除回顾自动时间预算暂停
对应 FR-S4、FR-S6、FR-S10。生产服务移除提取循环和导航合并中的整次执行期限判断,持续处理至完成;已保存运行中的 `executionSeconds` 继续兼容读取,但不再控制暂停。设置页移除该控件,中英文帮助和活动配置改为持续处理说明。单次模型超时、共享并发、逐批保存、主动暂停及失败续跑保持原路径,具体合同见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界)。主动暂停仍中断在途请求,仅保留此前已保存批次。
`npx vitest run src/main/assistant/supervision-review.test.ts src/renderer/src/SupervisionReviewSettings.test.tsx`:13 项通过。受控时钟超过保存的执行预算后,三个叶子及两个导航合并仍完成并发布;另验证主动暂停后复用成功批次和原分块配置、中英文设置不再出现整理时长控件。测试使用隔离 SQLite 和可控摘要器,不代表真实模型长时间运行实测。
`npm run typecheck`、六个修改 TS/TSX 文件的定向 ESLint 和 `git diff --check` 通过。未运行全量测试,真实模型调用 0 次。核对 Main 的生产工厂接线,监督使用不携带项目的 `resolveRequestRuntime({ workMode: 'ask' })`;本次仅改变桌面回顾编排,不修改 Agent、远端 Runtime 或桌面到 Agent 协议。未提交或推送。
### 2026-09-24 长摘要与内容区对齐
针对全局七天回顾的 `summary max 2000` 报错,代码确认导航输出 schema 的 `max(2000)`、服务层对叶子及导航的二次 2000 字符检查,以及模型提示词中的同值要求。另发现监督旧摘要通过 SQL `substr` 和 JavaScript `slice` 截断。以上监督路径限制已移除;生成内容和事实集合的合同见[生产调度合同](./review-scheduling-design.md#已接入的调度与存储)。未读取本次 16:03 失败的原始模型响应,不能据此声明该运行的具体失败节点。
@@ -400,10 +761,19 @@ npx eslint scripts/review-algorithm.mjs scripts/review-algorithm.test.mjs script
- 结束时间收尾:手动心跳使用真实报告完成时间,下游失败单独记录结束时间。`npx vitest run src/main/assistant/supervision-activity.test.ts src/main/assistant/heartbeat-service.test.ts src/main/assistant/heartbeat-database.test.ts src/main/assistant/supervision-history.test.ts`:4 个文件、21 项通过,新增受控时钟用例区分 10:00 开始、10:02 报告完成、10:03 下游失败。随后 Node typecheck 和这 3 个改动源码/测试文件的 ESLint 通过。
- 已存在的手动失败无法补回;旧心跳没有冻结范围和关联 ID,保留缺失提示。心跳仍按原保留期限清理,监督结果独立保存。取消和人工修改时间序列审计未实现;`no_change` 的后续接入见下节。
### 2026-09-28 继续讨论正文与布局
侧栏与图谱来源详情直接显示可编辑的发送正文,移除隐藏上下文拼接及附带提示。目标会话独立分隔,底部复用共享右对齐操作区。失败保留草稿,取消不发送,目标切换继续使旧预览失效。Main 的来源筛选与发送契约未修改。
- `npx vitest run src/renderer/src/SupervisionDiscussion.test.tsx src/renderer/src/SupervisionCard.test.tsx`:10 项通过,覆盖正文可见、编辑后精确提交、删除引用后不补回、失败重试、取消、重复提交和目标切换。
- `npx vitest run src/main/ipc.test.ts -t 'scopes supervision continuation|dispatches supervision continuation'`:2 项通过,使用真实 SQLite 与队列解析器核验来源范围及空附件发送。
- `npx vitest run tests/supervisor-layout.electron.test.ts` 分别设置 `GOODBUDDY_SUPERVISOR_DISCUSSION=1`、`GOODBUDDY_SUPERVISOR_SIDEBAR=1` 运行通过。图谱覆盖 1440/390px,侧栏覆盖 480/300/200px,均含浅深主题;检查 16px 区块间距、8px 字段间距、目标分隔、右对齐及无横向溢出,侧栏另检查滚动后按钮可命中。截图与测量保存在临时目录 `opencode/discussion-feedback-graph`、`opencode/discussion-feedback-sidebar`。
- Renderer 类型检查 `npx tsc --noEmit -p tsconfig.web.json`、8 个改动 TS/TSX/MJS 文件的定向 ESLint 与改动文件 `git diff --check` 通过。Electron 使用生产组件和模拟数据,未调用模型;未跑全套测试,未提交。
### 其他边界
- 新批次来源读取按 messageId 返回已保存片段及版本;旧历史缺少消息 ID 时保留会话/时间定位。本地知识引用通过现有 `KnowledgeService` 解析文档/分块,失效时显示不可用;外部引用只使用已保存 locator,不调用远端全库。
- 继续讨论和知识库写入使用现有本地会话队列与知识库实体接口,并要求先预览、再由用户确认提交。侧栏已支持结果直达图谱,尚无可编辑的上下文预览。
- 继续讨论和知识库写入使用现有本地会话队列与知识库实体接口,并要求先预览、再由用户确认提交。侧栏支持结果直达图谱;继续讨论的侧栏与图谱入口均已提供可编辑的实际上下文正文,发送规则见 [UI 设计](./ui-design.md)。
- 关系移除保留撤销状态及原始来源;界面没有提供恢复入口。
- 候选集最多 100 个实体,复用依赖模型显式选择;不会自动合并历史重复实体或跨 scope 合并。结果级历史内容已保存,事件滑块仍不重建逐事件的实体演变。Experiment 和完整图谱回放尚未实现;批次暂停/继续已接通。
@@ -2,10 +2,12 @@
日期:2026-09-24。状态:生产分块、持久化续跑、设置及活动入口已接线;27 条消息的完整会话通过真实模型验证,较早的 129 条消息实验仍未完成。当前实现以第 0 节为准,第 1 节保留改造前基线,第 2 至 11 节保留目标设计,第 12 节为早期隔离原型证据。
本文针对全局七天 `supervision:run` 出现 `AbortError` 的反馈,设计完整分页、证据分块、模型任务调度和逐层汇总。用户已明确要求预算耗尽或失败后保留成功批次及位置,重启后可继续,不重复计算成功叶子。生产接线复用 `SupervisorService`、现有数据库及 `review_checkpoints`;不另建定时调度器。schema 47 的实现与目标差异见第 0 节。
本文针对全局七天 `supervision:run` 出现 `AbortError` 的反馈,设计完整分页、证据分块、模型任务调度和逐层汇总。用户已明确要求主动暂停或失败后保留成功批次及位置,重启后可继续,不重复计算成功叶子。生产接线复用 `SupervisorService`、现有数据库及 `review_checkpoints`;不另建定时调度器。schema 47 的实现与目标差异见第 0 节。
产品范围仍由[监督者产品设计](./supervisor-prd.md)定义;本设计对应 FR-S4、FR-S6、FR-S10 及 US-S12、US-S26、US-S27。既有规则见[逻辑设计](./logic-design.md#自动增量与手动重看),实现架构见[技术设计](./technical-design.md),已验证事实见[实施进度](./progress.md)。第 0 节以当前代码为准,其余目标须逐项核对实现状态;大范围回顾仍需独立性能验证。
2026-09-26 修订:回顾持续分页、提取和汇总至完成,不再按整次运行的时间预算自动暂停。下文历史草案中的执行软预算、整次执行硬期限及按时间预算续跑方案已撤销;单次模型超时、并发限制、用户暂停和失败续跑仍保留。
## 0. 生产接线与剩余边界
Main 的手动 IPC 和心跳完成回调均使用 `createProductionSupervisorService`,该工厂连接 `SupervisorService`、`SupervisionReviewStore`、共享模型池及实际 ask Runtime。生产路径不再调用旧的 20 会话 × 20 消息 collector,也不使用旧服务分支的 100 条 / 48,000 字符裁剪。旧的有界服务注入方式仍供原有测试和实验脚本使用;心跳报告本身继续使用原报告预算。
@@ -16,7 +18,7 @@ schema 47 增加运行配置、来源清单、成功批次、导航节点四张
派发按项目及项目内会话轮转,每个会话同一轮至多一个批次,会话内按 sequence/source 排序。批次跨数据库页累计;长消息未装入部分保留待处理。每片最多 8,000 UTF-16 单元、每批最多 50 片,均为封块条件,不是范围上限。位置采用零基、左闭右开的 Unicode 码点;消息 locator 保存 messageId、conversationId、projectId、role、sequence、source、revision、start、end、length。知识引用另保留原 locator 及所属消息,任务使用当前状态和区间内创建/完成时间。
成功叶子的输出、原文片段和连续 offset 在同一事务提交。失败或软预算到期保留此前批次。继续操作使用同一 run ID、时间范围和分块配置;重启后可继续。手动运行不读写自动 checkpoint。自动运行先恢复同 scope 的未完成 run;仅在全部来源连续覆盖、导航完成、最终发布事务成功时写入 `stage=supervisor` 的 checkpoint。空范围无需模型。
成功叶子的输出、原文片段和连续 offset 在同一事务提交。失败或用户主动暂停保留此前批次。继续操作使用同一 run ID、时间范围和分块配置;重启后可继续。手动运行不读写自动 checkpoint。自动运行先恢复同 scope 的未完成 run;仅在全部来源连续覆盖、导航完成、最终发布事务成功时写入 `stage=supervisor` 的 checkpoint。空范围无需模型。
叶子输出仍使用现有事件、实体、变化和关系契约,并校验引用与候选身份。会话、项目、scope 使用二叉递归合并导航摘要,单子透传,总共 N 个叶子至多 N-1 次合并。已成功导航节点按孩子 ID 保存,发布失败后可复用。叶子、导航及最终结果的生成文本不设字段字符上限,事实数组不设独立数量上限;导航节点仍不得生成替代叶子的事件或实体。发布事务逐页读取叶子,保存既有历史/图谱结构,保持历史定位、来源及用户确认状态;完整叶子同时通过独立批次接口分页读取。
@@ -24,7 +26,9 @@ schema 47 增加运行配置、来源清单、成功批次、导航节点四张
导航使用独立的严格输出 schema,要求 `summary`、`changeDigest`、`openItems`;四个事实数组允许省略或为空,非空数组、错误类型和额外字段仍拒绝。验证通过后才在内部补空数组,以复用现有导航存储形状。叶子继续要求四个数组齐全,不将缺字段当作成功;来源、事件实体、变化实体和关系端点均校验。继续时还会核对已保存叶子的引用,导航不覆盖其独立事实。
`supervision:pause` 将父 signal 传入排队及在途模型;取消后的输出不提交。`supervision:resume` 恢复已保存 run;`supervision:batches` 默认每页 10 批、最多 20 批,返回完整叶子及来源。活动返回已保存批次、码点数、剩余来源、当前阶段、排队或执行中的模型任务数及已保存配置。软预算到期会等待已派发批次各自结束,随后显示 `paused`;完成判定不依赖摘要措辞。
`supervision:pause` 将父 signal 传入排队及在途模型;取消后的输出不提交。`supervision:resume` 恢复已保存 run;`supervision:batches` 默认每页 10 批、最多 20 批,返回完整叶子及来源。活动返回已保存批次、码点数、剩余来源、当前阶段、排队或执行中的模型任务数及已保存配置。提取和导航汇总持续运行至完成,不按累计运行时长暂停;用户暂停会中断在途请求,仅保留已保存批次。完成判定不依赖摘要措辞。
2026-09-26 已接入整次回顾 single-flight、`supervision:cancel` 及实时 `supervision:execution`。手动重复请求不排队,自动请求等待;已取消运行从自动未完成查询中排除,也不能手动继续。持久取消与实际停止分开呈现,准入保留至 Runtime 清理结束。完整生命周期见[单次执行与取消](./technical-design.md#单次执行与取消),第 1 节关于 `pending` 队列和缺少取消入口的描述仅为改造前基线。
当前阶段以 `phase` 写入既有 `supervision_review_runs.state_json`,取值为 `collecting / extracting / summarizing / saving`;无新表或迁移。它记录服务最后进入的阶段,失败和重启后保留;完成仍由 `supervision_runs.status` 的发布事务决定。活动的 `navigationNodes` 从已保存导航表计数,`inFlight` 仍来自进程内计数。旧运行没有 phase 时不回填推测值。清单和最终发布为同步事务,短阶段不保证被轮询看到,清单事务失败也不会留下部分阶段记录。批次接口只有成功记录,没有逐个失败/在途任务或项目、会话聚合状态;界面仅按分页返回的真实归属组织详情。
@@ -35,18 +39,20 @@ schema 47 增加运行配置、来源清单、成功批次、导航节点四张
| `supervisionReview.pageSize` | 50 | 1..200 | 每次来源元数据页条数;不改变最终集合或批次边界 |
| `supervisionReview.batchCharacters` | 8000 | 1000..16000 | 每批新正文 UTF-16 单元,不含提示、候选及 JSON |
| `supervisionReview.batchMessages` | 20 | 1..50 | 每批不同消息数;任务等无消息 ID 的来源单独计数 |
| `supervisionReview.executionSeconds` | 300 | 30..3600 | 每次执行软预算,继续后重新计时;到期停止新增工作 |
| `supervisionReview.executionSeconds` | 300 | 30..3600 | 旧设置兼容字段,执行时忽略,设置页不再展示;恢复旧 run 同样不按此值暂停 |
| `supervisionReview.crossProject` | 关闭 | 开关 | 创建回顾时冻结;关闭时每批只以本项目已有实体为候选,也不生成跨项目故事;开启后候选来自全部项目,故事归属可建立跨项目故事 |
| `supervisionReview.storyThreadEvents` | 20 | 4..500 | 创建回顾时冻结;功能的事件数低于此值时,模型提出的子线索并入功能 |
| `supervisorOrganizeTimeoutSeconds` | 240 | 30..600 | 创建回顾时冻结,节点获槽并解析 Runtime 后计时 |
| `heartbeatReportTimeoutSeconds` | 240 | 30..600 | 维持报告发起时冻结及既有租约规则 |
| `supervisorModelConcurrency` | 1 | 1..4 | 回顾保存接纳并发;共享池上限仍随应用设置实时变化,实际执行取较小值 |
应用仍默认关闭。新参数由共享 Zod 合同、ApplicationSettingsStore、App 设置回调和 Renderer 表单保存;没有放置尚未生效的高级字段。
应用仍默认关闭。除上述旧兼容字段外,参数由共享 Zod 合同、ApplicationSettingsStore、App 设置回调和 Renderer 表单保存;没有放置尚未生效的高级字段。
### 未完成与实测限制
- 真实验证覆盖一个 27 条消息的完整会话;较早的 129 条消息实验在第二次输出 JSON 无效后停止。两实验合计 5 次付费请求,没有重复付费或回退。范围及缓存复核说明见[进度](./progress.md#2026-09-24-完整会话生产验证与收尾)。
- 来源被修改或删除时保留旧批次并明确失败,标记需要新建回顾,活动页停止提供无效的继续按钮;下一次自动检查可创建新 run,不会永久重复该失败。尚未实现仅使受影响叶子和父节点失效后重算。清单记录冻结版本,不保存全库正文快照。
- 分块配置、超时、接纳并发及算法版本已保存。实际默认 Runtime、模型及候选背景仍在执行时解析;未实现跨重启模型/提示词版本固定,也未实现共享池排空后切换冻结并发的目标规则。
- 分块配置、超时、接纳并发及算法版本已保存。监督者模型连接在每次执行开始时解析,同次回顾复用;继续或重试读取当前选择,规则见[监督者模型选择](./logic-design.md#监督者模型选择)。候选背景仍在执行时读取;未实现跨重启模型/提示词版本固定,也未实现共享池排空后切换冻结并发的目标规则。
- 元数据清单和最终发布仍是同步 SQLite 事务,缺少大库延迟验证及可中断来源读取。批次详情已分页,既有历史图谱查询仍返回整份结果;大图谱的响应大小尚未改造。
- 没有 token/费用预算、提供商 RPM/TPM、Retry-After 冷却、执行硬期限、独立清理期限或逐层可配置超时。默认正文预算不保证任意模型上下文容量;超长输出、非法 JSON 和引用错误会失败,保留已成功批次。
- 片段以码点边界切分,尚未增加句界优先和重叠背景。事实保留指保留模型已产出的结构化记录,不证明语义无遗漏;生产契约校验引用 token,不要求原型的逐字引文和 reviewedTokens 回执。提供过的 locator.source 仅在本节点唯一对应一个片段时可作为引用别名;歧义及未知来源拒绝。
@@ -221,7 +227,7 @@ run(request):
admit bounded leaf tasks, round-robin by project and conversation
run admitted leaves in a shared worker pool
validate and persist each successful batch with facts, provenance and resume positions
if budget expires, a task fails or cancellation arrives: stop; retain batches; mark incomplete
if a task fails or cancellation arrives: stop; retain batches; mark incomplete
continue paging all project/conversation/message/source groups until exhausted
reduce within conversation, then project, then requested scope
pass through singleton groups; reduce oversized groups in bounded rounds
@@ -231,7 +237,7 @@ run(request):
return completed only after publication; otherwise return resumable incomplete status
```
采集与叶子执行可交错,但入队和在飞任务有界,不使用对全部来源的 `Promise.all`。每批完成后持久化,再接纳下一批;时间不足就暂停。首次派发前用有界探测判断是否直通;穷尽范围后确实只有一个叶子时直接生成最终输出,通过校验后只调用一次模型。因预算只接纳一个叶子时仍是未完成 run。空范围及自动 `no_change` 必须在全部相关目录/来源页检查完毕后判定,不调用模型、不把预算未读误作无变化。
采集与叶子执行可交错,但入队和在飞任务有界,不使用对全部来源的 `Promise.all`。每批完成后持久化,再接纳下一批,持续至全部来源及汇总完成。首次派发前用有界探测判断是否直通;穷尽范围后确实只有一个叶子时直接生成最终输出,通过校验后只调用一次模型。因预算只接纳一个叶子时仍是未完成 run。空范围及自动 `no_change` 必须在全部相关目录/来源页检查完毕后判定,不调用模型、不把预算未读误作无变化。
叶子有多个时输出受限摘要卡及结构化事实,Main 保存卡片 token 到原文片段的映射。会话只有一个卡片时透传,项目只有一个会话摘要时透传,全局只有一个项目摘要时透传。不得为了层级外观额外调用模型。边界必须标清项目和会话,跨项目汇总仍使用原请求 scope 的候选实体集。
@@ -283,7 +289,7 @@ run(request):
建议扩展 `coverage_json`:是否读完冻结范围、已处理新字符和片段数、覆盖项目/会话数、是否仍有来源、停止原因及恢复引用。精确片段继续用 locator,恢复记录关联批次输出,不仅保存一个总计数。总数未知时显示“仍有未读内容”,不用动态发现数充当固定分母。结果读取合同需显式暴露 coverage;当前数据库查询了 coverage,但共享结果 schema 未返回它。
手动和自动均须跨重启继续同一未完成逻辑 run。Main 根据持久化 run ID 读取冻结范围、成功输出及位置,复查来源版本,恢复剩余页和未成功汇总;Renderer 不能自报已处理 offset。手动继续不污染自动位置;自动恢复先于创建同 scope 新 run。恢复后的单次执行重新获得执行预算,但不丢失既有进度。
手动和自动均须跨重启继续同一未完成逻辑 run。Main 根据持久化 run ID 读取冻结范围、成功输出及位置,复查来源版本,恢复剩余页和未成功汇总;Renderer 不能自报已处理 offset。手动继续不污染自动位置;自动恢复先于创建同 scope 新 run。恢复后持续处理剩余工作,不按旧执行预算暂停,也不丢失既有进度。
持久化续跑及手动“继续”入口是本设计的验收要求,不能以建议缩小范围替代。恢复记录的保留期限、主动放弃后的清理规则和具体 schema 尚未确定;不得在 run 未完成时自动删除唯一成功输出或继续所需位置。显式放弃可以终止后续工作,但不能标为完整覆盖。
@@ -297,7 +303,7 @@ run(request):
若后续决定让心跳只触发监督、报告从同一结构化结果投影,必须先定义记忆建议和任务建议的共同输出契约、各自成功语义及旧报告展示,另行修改 FR-S4 和 US-S27;本轮不把该产品调整藏在调度重构里。也不使用多阶段事务将两个结果强行绑定。
报告先获共享槽位再领取租约,并在实际模型请求开始前刷新;报告完成或无变化提交后释放槽位,再执行下游监督。领取与续租条件见当前控制清单。未来整次执行预算不能套到报告租约,也不能在报告完成前等待整棵监督树。现有 tick 防重入不替代共享池上限;监督过长会延迟到期检查,拟通过有界执行和持久化续跑控制,不另建后台调度服务。
报告先获共享槽位再领取租约,并在实际模型请求开始前刷新;报告完成或无变化提交后释放槽位,再执行下游监督。领取与续租条件见当前控制清单。未来整次执行预算不能套到报告租约,也不能在报告完成前等待整棵监督树。现有 tick 防重入不替代共享池上限;监督过长仍可能延迟到期检查;移除自动时间预算暂停不改变 tick 的串行等待关系,不另建后台调度服务。
## 10. 活动与结果展示
@@ -0,0 +1,170 @@
# 时态 Story Graph 内置 MCP 设计
日期:2026-09-28。状态:D1 只读工具及本地、远程接线已实现,验证记录见[实施进度](./progress.md#2026-09-28-story-graph-只读工具)。D2 跨范围复用、D3 记忆读取切换及历史重建仍未实现。
本文负责 FR-S11 的 MCP 工具契约、时间语义、范围复用和记忆过渡设计。产品要求见[监督者产品设计](./supervisor-prd.md),用户验收见 [US-S28 至 US-S31](./user-stories.md#us-s28-依据决定演变继续工作)。既有图谱对象与交互仍由[图谱 PRD](../story-graph/prd.md)定义。可执行输入以[共享工具合同](../../../src/shared/story-graph-tools.ts)为准,数据投影见 [Story Graph Reader](../../../src/main/assistant/story-graph-reader.ts);本文件同时保留后续阶段目标。
## 1. 目标与现状
监督者的主要产出是带时间、演变关系和来源的 Story Graph。工作回顾、侧栏和 Agent 上下文从同一组事实中组织内容。扁平记忆条目无法独立表达“当时决定了什么、后来为何修订、现在应采用什么”,因此不再作为目标架构的主要产物。
用户侧统一为监督者维护的工作记忆:包含长期偏好、决定、事件、未解决事项及其依据。“替代记忆”指统一概念与后续读取入口,保留已确认偏好、用户修订和已有数据。它不授权删除 `memory_items`、抹掉历史或给旧内容补造事件。
| 能力 | 当前代码事实 | 本设计目标 |
| --- | --- | --- |
| 图谱与来源 | 已有结果、事件、实体、关系、结果图谱快照及成功叶子;历史归属存在缺失边界 | 按对象、时间和相关性读取事实与演变,并返回来源版本和覆盖信息 |
| MCP | [内置目录](../../../src/shared/builtin-mcp-servers.ts)和[能力合同](../../../src/shared/capability-contracts.ts)已有 `story-graph`,包含三个只读工具 | 按需读取,沿用应用启停及 Runtime 分配 |
| 时间与关系 | [监督合同](../../../src/shared/supervision-contracts.ts)已有 `occurredAt`、确认状态和 `revised`;关系枚举没有明确替代/矛盾类型 | 分离发生、生成、记录时间;用有证据的关系表达替代和矛盾 |
| 范围与身份 | checkpoint 按阶段、精确 scope、来源区分;候选实体只取同 scope | 同一来源版本的有效叶子事实可跨 scope 复用,范围仍控制可见性与解释 |
| 记忆 | `reviewBackground` 仍读已确认记忆;最多四条、每条 500 字符,作为当前背景 | 逐步由统一查询承接,保留确认状态与适用范围 |
| 历史重建 | 有结果快照,不具备完整来源版本及人工修改时间序列 | 首期只提供 `current`、`timeline`;`as_of` 留待历史重建能力具备后设计交付 |
现状依据还包括 [AssistantDatabase](../../../src/main/assistant/assistant-database.ts) 的 `saveReviewCheckpoints`、`listSupervisionCandidates`、`reviewBackground`,以及[生产分块调度](./review-scheduling-design.md#0-生产接线与剩余边界)。现有结果快照只证明某次回顾保存过哪些内容,不能据此声称能还原任意时刻的认识。
## 2. 对象与稳定身份
复用现有 SQLite、领域对象和来源定位。设计不要求图数据库、独立向量库、完整正文副本、新持久化日志或通用历史快照层。所需读取视图先从已有结果、叶子、关系和 locator 推导;无法推导的内容明确返回未知。
| 对象 | 概念合同 | 身份与关联规则 |
| --- | --- | --- |
| 故事线 `story_line` | `id`、主题、范围、事件与实体引用 | 沿用故事线 ID;不把项目名当 ID |
| 事件 `event` | `id`、类型、描述、发生时间、实体及来源引用 | 决定、讨论、变化、里程碑是事件类型;一次回顾运行不是工作事件 |
| 实体 `entity` | `id`、描述、确认状态、有效性、演变引用 | 对象 ID 与内容修订分离;同名或模型局部 ID 不证明同一实体 |
| 变化 `entity_change` | 实体、事件、变化类型、依据、适用时间 | 指向具体对象版本;无独立主键时以已有所属对象和稳定关联键定位,不以页内下标造身份 |
| 关系 `relation` | `id`、两端引用、类型、理由、来源、确认状态 | 替代/矛盾关系必须能定位两端主张及支持该关系的证据 |
| 阶段总结 `summary` | 结果 ID、范围、区间、正文、事实引用 | 用于导航;检索与上下文仍能访问完整叶子事实 |
| 来源引用 `source_reference` | 引用 ID、来源类型与原 ID、版本、locator、可用性 | 引用 ID 与原消息/任务 ID 分开;跨结果的引用 UUID 不等于新的原始来源 |
偏好、方案、决定涉及的概念沿用实体及事件表达,不为每个业务名词另建基础设施。业务上的“已决定”(`decided`)与“已完成”(`completed`)是不同状态,这里不声明新增任务状态枚举;运行 `completed` 仅表示整理发布成功。决定采用方案、计划执行、模型写出“已决定”均不能推出任务完成,完成须有任务记录或明确执行证据。
返回对象携带 `object_ref`(类型和稳定 ID)、`object_revision`、`result_id` 及适用 scope。版本可使用已有结果身份与可用修订信息表达;没有可靠对象版本时返回 `unknown`,不把 `updated_at` 伪装成完整版本链。旧文本主键及 Main 精确别名映射继续保留,`candidateRef` 只是一次模型输入内的候选引用,不作为 MCP 持久 ID。跨 scope 身份合并须有明确映射依据,不能因复用叶子事实而自动合并同名实体。
## 3. 时间、确认与有效性
| 维度 | 含义 | 禁止的推断 |
| --- | --- | --- |
| `event_time` | 原记录描述的事件发生时刻或区间;带精度、来源依据,未知可空 | 不以回顾时间填补未知发生时间;消息发送时间只有在确实表示该事件时才可采用 |
| `review_generated_at` | 产生该归纳的回顾时间 | 较晚生成不表示事件较晚发生或旧决定已被替代 |
| `recorded_at` | 对象或版本实际写入系统的时间 | 迟录、补录不改变事件先后,也不证明当时已知 |
| `validity` | 该主张适用的区间及当前有效、被替代、存在争议或未知状态 | 不用确认时间、记录时间直接推导有效起止 |
| `confirmation_state` | 自动归纳、已确认、用户修订、已撤销等内容审核状态 | 已确认不等于永久有效;未确认不等于已失效 |
`replaces` 表示有明确证据的新主张替代旧主张,记录方向、两端版本、适用范围、理由和来源。`contradicts` 表示两项主张存在证据支持的冲突,不自动决定谁正确。它们是目标关系语义,不能将当前 `contrasts` 或单纯 `revised` 无条件映射为这两类关系。新增选项、日期较新、措辞更肯定都不足以建立替代关系;没有证据时保留并列主张或待核对差异。
时间区间输入使用带时区的 ISO 8601 时间,查询归一化为 UTC。概念 `time_range` 使用闭区间 `[from, to]`,与现有监督范围保持一致;区间事件按相交判定。检索支持明确的 `time_basis`:默认 `event_time`,也可指定 `review_generated_at` 或 `recorded_at`。响应回显所用维度。未知时间不冒充命中区间,返回可计算的排除数量或 `unknown`,允许通过不限定时间的查询另行查找。
`timeline` 按发生时间和稳定 ID 排序,未知时间单列,展示目前保存的演变及缺口。它可以包含后来补录的旧事件,因而不等于“当时已知”。`current` 给出截至读取时、在已覆盖证据内可支持的当前理解;有冲突或缺口时保留并说明,不能仅挑最新摘要作为答案。
`as_of` 是未来能力,首期输入 schema 不接受该模式或参数,调用必须明确返回不支持,不能悄悄降级为 `current`。只有具备所需对象版本、人工修订记录以及可用来源版本/历史重建,并能验证不会泄漏当时未知的信息后,才另行定义其语义和验收。本文不批准为它新建持久化日志或快照体系。
## 4. 公共读取合同
三个工具都只读已有数据,不调用模型、不自动发起回顾、不更新 checkpoint、不确认或修订对象、不创建任务,也不写入记忆或知识库。检索采用已有数据的确定性过滤与相关性排序,不能在调用中借模型补齐缺失事实。生成面向用户的建议由调用方 Agent 完成,必须引用返回证据并遵守其范围与限制。
默认 scope 为调用会话的当前项目,由 Main 解析并冻结;没有当前项目时要求显式选择 scope,不默认为 Global。调用方可显式请求 Global 或指定项目集合,沿用已有范围与能力授权校验。分页继续使用首请求范围,切换 UI 当前项目不改变旧游标。对象与来源读取也要重新校验归属,稳定 ID 不能绕过 scope。
首期沿用现有精确 scope 结果归属,不把 Global 摘要冒充单项目结论,也不隐式把 Global 结果投影为项目上下文。项目查询缺少该 scope 数据时明确返回空结果及覆盖状态。未来跨 scope 复用只接纳能按来源项目和事实依据正确筛选的叶子,跨项目归纳仍保留真实范围。
| 响应部分 | 必须表达的信息 |
| --- | --- |
| 范围 | `requested_scope`、`resolved_scope`、每项原始 scope、所选时间区间和时间维度 |
| 修订 | 对象/结果修订引用、来源版本、读取时间;区分已知版本与 `unknown` |
| 覆盖 `coverage` | 已知处理范围、来源版本和片段区间、缺失或未处理部分、未纳入来源类型;缺少数据时为 `unknown`,不能用零代替未知 |
| 分页 `page` | `returned_count`、`has_more`、`next_cursor`、本页是否完整及限制原因;集合数量不能计算时为 `unknown` |
| 证据 | 来源引用与 locator、关系依据、确认与有效性、已知冲突和历史缺口 |
覆盖与分页独立:最后一页只证明匹配结果读完,不能证明所有原始消息已被整理;已处理全部入选正文也不证明模型识别了全部事实。旧 `coverage_json` 只有条数时,不能推导完整的时间、版本或语义覆盖。
列表及长字段都需有界分页,超过响应容量时提供可续读的位置,无法分页的响应明确失败。不得静默裁剪后标完整。游标绑定过滤条件、排序和可用结果修订;分页期间内容变化、无法维持同一读取版本时返回 `stale_cursor` 并要求重新查询,不混入新旧页。该规则可基于现有修订检查完成,不要求持久化查询快照。取消、读取失败与真实空页分别返回。
## 5. 工具定义
### `story_graph_search`
输入:`query`、可选 `scope`、`object_types`、`time_range`、`time_basis`、`page_size` 和 `cursor`。`object_types` 对应第 2 节的对象类型;省略时检索全部支持类型。默认 scope 及时间规则按第 3、4 节执行。对象页默认 10 项,上限 50 项。匹配为不区分大小写的连续文本匹配,标题、实体标签和摘要字段权重高于其他字段;同分按类型、稳定 ID 和结果 ID 排序。
输出相关对象页,包含匹配原因或命中字段、明确标为摘录的预览、稳定引用、修订、相关演变和来源入口,以及公共响应信息。过滤须先于分页,按相关性排序并以稳定 ID 消除同分歧义。不能只在最近若干摘要中筛选后声称查完历史,也不能将摘要中的命中替代叶子事实。
### `story_graph_get_context`
输入:`object_ref`、可选 `scope`、`mode: current | timeline`(默认 `current`)、可选时间范围与维度、页大小及游标。`current` 表达当前理解;时间过滤只限定证据集合,响应需标明“在所选证据范围内”,不能称作该区间终点时的历史状态。
输出对象当前描述、明确的决定/工作状态、偏好与适用范围、未解决事项、相关实体及关系、演变和证据引用。`timeline` 分页列出提出、补充、验证、修订、替代和冲突证据,保留旧决定及新修订;多组集合或长正文须各自提供续读位置,不能只对顶层对象分页。返回的是存储事实及其组织视图,不在工具内生成新建议。
D1 返回各结果快照、当前投影、事件、变化、关系、来源及成功叶子的独立描述。既有存储没有可靠的有效期和替代关系枚举,因此 `validity` 返回 `unknown`,保留 `revised`、`contrasts` 及原始理由供调用方核对。当前实体投影不会遮掉旧结果或叶子描述。`content` 是可拼接的 JSON 文本片段,每项附零基码点位置;按对象引用和结果 ID 拼接后解析。`content_page_size` 默认 4,000、上限 8,000 码点,游标可停在对象内部。完整响应超过 100,000 字节时明确失败,要求缩小页大小。
对象不存在、scope 不匹配、版本缺失或只有部分历史时分别说明。已存在的某个结果快照可作为具体版本读取依据,但不能作为任意 `as_of` 查询的替代品。
### `story_graph_read_source`
输入:`source_reference_id`、可选 `scope`、明确的读取种类 `snapshot | version | current`、所需 `source_version`(读取指定版本时必填)及片段页位置。默认 `snapshot`,读取该引用实际保存的证据片段;不默认换成当前原文。
输出正文页、`content_kind`、请求版本与实际返回版本、`source_location`、原来源 ID、已保存片段与整份来源的范围和长度、可用时间元数据,以及公共覆盖和分页信息。位置用零基、左闭右开的 Unicode 码点 `[start, end)`;消息 locator 尽量保留项目、会话、消息、顺序和角色,本地资料保留文档/分块,外部片段保留已有 locator。缺少位置时明确标记,不伪造链接。
`snapshot` 只声称返回当次实际保存的片段;`version` 仅在该来源支持并保留指定版本时成功;`current` 显式读取当前内容并报告与引用版本是否不同。历史版本不可用时返回 `historical_version_unavailable` 及可用读取选项,由调用方决定是否另读当前版本。来源删除或不可访问时返回对应可用性;已保存片段仍可读时准确标注其范围。读取完一个片段不能称整份原文已读完,超长内容必须可续页,无静默截断。
D1 来源页默认 4,000、上限 8,000 码点。`current` 通过既有 locator 精确读取消息、任务状态正文或保存的知识引用片段;不拉取外部知识库原文。`version` 仅在当前可读来源的修订恰好等于请求版本时返回该版正文。旧版正文没有保留时明确不可用;无 locator 的旧引用仍可读 snapshot。当前来源迁出所选项目时拒绝读取。已保存引用的范围与当前位置分别校验。
## 6. 内置 MCP 与 Runtime 接入
内置 MCP 列表提供 `Story Graph`,能力 ID 为 `story-graph`,默认分配给 Model、OpenCode、Continue 和 DeepSeek Harness;监督者应用默认关闭,开启应用后才可发现工具。工具描述说明适用于“继续上次工作”“此前如何决定”“方案为何改变”等请求。普通聊天不强制查询,也不把整份图谱自动注入每轮上下文;Agent 先搜索、再取相关对象上下文,在需要核对原话、处理冲突或详细引用时读取来源。
| 配置或目录 | 当前状态 | 目标映射 |
| --- | --- | --- |
| `heartbeatEnabled` | 监督者应用开关,新设置默认关闭 | 为 Story Graph 暴露和每次调用的必要条件;关闭后不列出三个工具,旧会话直接调用也拒绝 |
| 会话 `storyGraphEnabled` | 每个会话默认开启,缺省字段按开启处理 | 关闭后不向该会话签发图谱工具,并拒绝已有能力会话的发现、调用和在途结果交付;其他会话不受影响 |
| 内置能力 `enabled` 与 `assignments` | [CapabilityService](../../../src/main/capabilities/capability-service.ts)管理现有 MCP 开关与 Runtime 分配 | `story-graph` 沿用同一管理方式;应用关闭时不得由内置能力开关绕过,保留用户原分配 |
| 内置工具目录 | 已有 Story Graph 条目 | 映射 `story_graph_search`、`story_graph_get_context`、`story_graph_read_source`,全部 `access: read`、`readOnlyHint: true` |
| 监督整理参数 | 超时、并发、来源页与批次设置已存在 | 控制后台整理;不让一次 MCP 查询启动整理,也不以正文批次预算截断检索结果 |
| 计划 `enabled` | 决定自动回顾是否到期执行 | 不决定已有图谱能否读取;应用开启且能力可用时,无计划也能查已有结果 |
Main 在工具发现和调用分发时共同检查应用启用、会话开关、能力启用、Runtime 分配和 scope;只隐藏菜单不足以阻止旧会话调用。关闭后拒绝新的读取请求,已在途响应交付前复核状态,避免旧绑定继续返回数据。启用工具本身不产生回顾或图谱写入。Ask 与 Execute 均可读取,同样没有写工具,也不借此扩大 Execute 权限。
会话开关只决定 Agent 能否按需使用这三个 MCP 工具,不自动查询或注入图谱上下文,也不触发回顾。监督者应用关闭时,输入区完全隐藏开关并保留会话选择;再次启用应用后恢复该选择。后台回顾和计划仍由各自设置控制。
开关复用已发布的 `conversations.context_state_json`,字段为可选布尔值,不增加数据库版本或迁移。Renderer 经串行会话保存队列与 `conversations:set-story-graph` IPC 保存,成功后更新显示,失败保留原值并通知。Main 按会话主键只读设置,不加载消息历史;显式设置写入后,旧的自动保存快照不能覆盖它。新会话及新建分支使用默认开启。
本地 OpenCode、Continue 和原生客户端复用 [KnowledgeMcpGateway](../../../src/main/agent/knowledge-mcp-gateway.ts) 的 MCP 注册和分发;直连 Model 与 DeepSeek Harness Main 代理调用同一服务。远程 OpenCode/Continue 复用现有图像工具的受管 HTTP MCP 和二进制通道,`storyGraph` 描述独立于图像生成描述,Ask 可只携带图谱读取。请求仍由桌面 Main 解析范围和读取 SQLite,不把数据库、正文库或凭据复制到 Host。
远程发现每次向 Main 复核启用状态;调用复用同一个范围、分页及版本读取入口。OpenCode 根会话和子会话精确放行当前受管端点的图谱工具;Continue Ask 保留 Main 绑定的会话端点并只允许共享只读工具名。DeepSeek Harness 的 Main 目录、参数转换和 Host 执行钩子均接入这三个工具;其他普通内置 MCP 的 Harness 限制保留。当前远程 Runtime 支持 OpenCode/Continue,不新增远程 Harness。桥接不可用时明确失败。
远程使用须配套部署含此接线的 Desktop 与 Agent;旧 Agent 不识别新增的工具描述字段。本次没有发布或更新受管包,源码验证不代表已发布客户端具备该能力。
## 7. Global 与项目复用
当前 Global、单项目和不同项目集合分别维护 checkpoint 和故事线候选身份,项目集合排序归一化后才比较。项目 A 已处理某消息,不会使 Global 的相同消息自动成为已处理;Global 当前会按自身范围重新收集原始来源,不能描述为“只聚合已有项目图谱”。报告与监督也是独立阶段,不能互相推进位置。当前手动重看还会重新整理所选历史。
目标是同一来源版本的事实提取成功一次后,在 Global 和项目查询/整理中复用可用叶子事实,包括事件、实体变化、关系证据及来源位置,不只复用项目摘要。Global 可在已提取事实之上组织跨项目关系和摘要;范围变化本身不应造成相同正文再次提取。范围控制查询归属和综合判断,不再充当重复提取的唯一分界。
复用须核对原来源身份、版本、已覆盖片段及提取合同是否仍适用。仅部分成功时继续缺失部分;来源改版或用户明确要求重新分析时可产生新的提取结果,不能覆盖已确认内容。任务的当前状态、偏好背景和既有摘要仍须标明时间角色,不能作为跨 scope 复用时新发生的事件。共享事实的可见范围不能被 Global 摘要扩大。
这一目标尚未实现。本轮不批准具体数据迁移、跨 scope 主键重写、历史实体合并、checkpoint 转换或缓存表方案。实施前应从已有成功叶子和来源版本定位验证最小复用方案,列出旧数据的可复用程度与缺口;保留现有精确 scope 行为直到替代方案完成验证,不提前修改现有 checkpoint 含义。
## 8. 记忆过渡与分阶段交付
已确认的长期偏好必须继续可用,并保留 Global/项目适用范围。旧记忆只有更新时间时,只能显示为当前偏好背景和实际更新时间,不能补造“首次决定”“当时生效”或替代关系。新的图谱归纳与已确认偏好冲突时返回待核对差异,不以较新生成时间覆盖用户选择。
| 阶段 | 交付内容与退出条件 | 当前状态 |
| --- | --- | --- |
| D0 设计 | 记录工具、时间、范围、过渡与验收合同 | 已记录 |
| D1 只读查询 | 基于现有数据实现三个工具及本地/远程接线、配置目录、关闭拦截和分页;缺失版本与覆盖返回未知,对应 US-S28 至 US-S30 | 已实现;验证范围见实施进度,当前 memory 读取继续保留 |
| D2 事实复用 | 验证同一来源版本跨 scope 复用完整叶子;明确身份、版本变化与已有数据处理方案,达到 US-S31 的范围验收 | 待设计具体存储调整并实施;无已批准迁移方案 |
| D3 概念与读取统一 | 让统一入口承接已确认偏好及图谱上下文;核对引用、范围、冲突和旧数据可读性后,才切换现有 memory 消费路径 | 待实现;不以删除旧记录作为退出条件 |
| 未来历史重建 | 单独定义并验证 `as_of` 所需版本与当时可知性 | 不属于首期,不承诺交付时间 |
D1 可将现有已确认偏好作为明确标记的背景投影,原读取路径在过渡期仍工作。重复背景按原记忆身份识别,图谱未知历史不能由记忆更新时间补齐。具体迁移与旧入口退出时点须根据实现证据确定,本文没有要求立即删除旧 memory API、设置或 UI。
## 9. 验收映射
核心场景使用可核对的旧决定 A、后来明确修订为 B 的证据,以及“现在继续做应采用什么”的请求。工具返回 A、修订事件、替代依据与 B;Agent 基于它们推荐 B,并引用旧决定和修订来源。新增选项 C 但没有替代依据时,不得把 B 判为失效;决定 B 也不能被回答为 B 已执行完成。
| 验收 | 对应故事 | 通过条件 |
| --- | --- | --- |
| 决定演变 | US-S28 | 完成上述旧决定 → 有证据修订 → 当前建议及引用链;冲突没有证据解决时保持待核对 |
| 时间 | US-S28、US-S29 | 迟录旧事件不变成新事件;确认与有效性分开;timeline 不声称当时已知;`as_of` 明确不支持 |
| 分页与来源 | US-S29 | 匹配项、演变和长原文均可读到末页;覆盖未知与零命中可区分;游标失效不混页;旧版不可用不返回伪历史 |
| 启停与只读 | US-S30 | 应用关闭后发现与直接调用均不可用;开启但无计划可读已有数据;工具自身模型调用与业务写入均为零 |
| Runtime 一致性 | US-S30 | 本地及 GoodBuddy Agent 远程实际链路具有相同 scope、版本、分页、引用与关闭行为,不能以共享 schema 代替接线验收 |
| 记忆及范围复用 | US-S31 | 确认偏好与已有引用保留;同版本成功叶子跨 scope 复用且能检索事实,不只返回摘要;历史缺口不补造 |
2026-09-28 的 D1 验证命令和证据记录在[实施进度](./progress.md#2026-09-28-story-graph-只读工具),当时运行定向测试、类型检查、限定文件 lint 和确定性 Host 探针,没有付费模型请求。2026-10-03 会话开关的 Electron、Host 及真实模型调用证据见[本轮记录](./progress.md#2026-10-03-会话故事图谱开关)。真实模型如何根据冲突证据形成最终建议,仍需与工具读取正确性分开验收。
@@ -0,0 +1,86 @@
# 故事线模型改造设计
状态:2026-10-02 已确认的目标设计。第 1 步(共享时间线、事件起止与密度、跨项目开关)、第 2 步(故事归属与用户调整)、第 3 步(时间螺旋视图)、第 4 步(经验抽取)和第 5 步(心跳故事建议、MCP 故事与经验查询、工作回顾按故事组织)已实现,见[实施进度](./progress.md)。本文定义监督回顾如何产出下一版故事线(时间螺旋、分层故事木片、经验 W),是 [3D 故事线图谱概念](../story-graph/3d-concept.md) 的数据来源合同。现有回顾流程见[回顾分块调度设计](./review-scheduling-design.md),现有图谱工具见 [Story Graph MCP 设计](./story-graph-mcp-design.md)。
## 1. 结构总览
| 层 | 含义 | 来源 |
| --- | --- | --- |
| 全局(根) | 整条时间螺旋本身,即用户的全部经历;不是一片木片 | 所有来源 |
| 项目木片 | 按会话所属项目划分;无项目归属的内容进入“未归属项目” | 会话归属,确定性,不调用模型 |
| 跨项目故事 | 同时涉及多个项目或不属于任何项目的长期事情,例如长期偏好、学习方向 | 故事归属阶段 |
| 功能木片 | 项目内的长期功能或主题 | 故事归属阶段 |
| 子线索 | 事件量足够大的功能再拆分;小功能不拆 | 故事归属阶段 |
| 经验 W | 从故事中形成、可复用到其他故事的经验,默认是全局对象 | 经验抽取阶段 |
全局不作为单独的“全局记忆”重复处理:它是根视图,读取同一套事件、故事和经验。查看某个项目时,只是以该项目为过滤条件。
## 2. 已确认的决定
| 编号 | 决定 |
| --- | --- |
| SL-1 | 层级为 全局 › 项目 › 功能 › 子线索;子线索仅在功能事件量达到阈值时出现,阈值可在设置中调整 |
| SL-2 | 跨项目或无项目的长期事情,作为全局根下的“跨项目故事”,与项目木片并列 |
| SL-3 | 每个事件只归属一个主故事,可另外关联其他故事;关联不影响木片的长度和密度统计 |
| SL-4 | 经验自动生成即可供 Agent 读取,不设确认门槛;标记“自动归纳”,用户可以编辑、合并、删除 |
| SL-5 | 用户对故事和经验的调整(改名、合并、拆分、移动事件、编辑、删除)受保护,后续回顾不覆盖;删除的对象不会因同一证据再次生成 |
| SL-6 | 注意力密度按来源统计:时段内的消息轮次与正文量,不由模型判断;只表示投入集中程度 |
| SL-7 | 每个来源版本只提取一次,项目与全局共用提取结果和增量进度 |
| SL-8 | 设置提供“跨项目关联”,默认关闭。关闭时新内容只关联本项目已有的知识;开启后可关联其他项目中的同一件事,跨项目故事也只在开启时生成 |
## 3. 回顾流程改造
```text
采集来源(按来源版本增量)
↓
批次提取:事件带开始/结束时间(取原始消息时间)、来源引用
↓
故事归属(新增,不读原文)
· 项目:按会话归属确定
· 功能/子线索/跨项目:召回候选故事 → 一次模型调用判断归入已有、新建或暂不归类
↓
经验抽取(新增,低频,不读原文)
· 规则挑候选:有明确结果的决定、多个故事中重复出现的做法、说明原做法问题的修订
· 有候选时一次模型调用:经验内容、形成依据、适用条件、边界
· 记录后续应用及结果
↓
导航摘要 → 发布
```
规则:
- 故事跨回顾保持同一身份,沿用实体候选编号机制;长时间无事件的故事能被接回,不新建。
- 故事状态(进行中、已结束)必须有内容依据;长期无事件只标“暂无进展”,不推断结束。
- 允许“暂不归类”,不为凑层级强行归组;暂不归类的事件在项目下以淡色木片显示。
- 经验的每条依据必须对应到具体事件和来源,否则不保存;没有候选时不调用模型。
- 新增阶段失败时,已发布的提取结果和处理进度保留,可单独重试该阶段。
## 4. 用户调整
图谱是可编辑的结果,不需要逐条审批:
- 故事:改名、合并、拆分、调整层级、把事件移到其他故事。
- 经验:编辑内容与适用条件、合并重复、删除。
- 入口位于故事线图谱的详情面板与 3D 视图选中对象的详情中,共用同一组操作。
- 每次调整可撤销,并记录在活动记录的修改记录中。
## 5. 展示与使用
- 故事线图谱新增 3D 视图,与现有平铺视图并列,几何、时间尺度、聚合规则沿用 [3D 概念](../story-graph/3d-concept.md);Three.js 仅在打开 3D 视图时加载,WebGL 不可用时使用平铺视图。
- MCP 增加按故事、按经验查询;经验返回时附“自动归纳 / 用户编辑”标记和依据。
- 心跳建议增加“故事长期无进展”和“已有经验可能适用于当前故事”。两者都由规则选出:前者按设置的天数判断,只提示不改状态;后者要求共享不过于通用的实体。每次心跳各最多 3 条,排在未决事项之前。
- 工作回顾按故事组织:推进了哪些故事、新出现哪些故事、形成或应用了哪些经验。
## 6. 存储
新增故事表(含层级与父故事)、事件与故事的归属/关联、经验表、经验依据与应用记录、用户调整记录;对应一次数据库升级。已有回顾结果在升级后按新流程重建,原始来源不变。
## 7. 实施顺序与验收
| 步骤 | 内容 | 验收(真实数据副本 + `.env.deepseek.local` 中的 DeepSeek) |
| --- | --- | --- |
| 1 | 统一时间线(SL-7)、事件时间跨度与密度(SL-6) | 重叠回顾不产生重复事件;项目与全局不重复处理同一来源 |
| 2 | 故事归属与用户调整(SL-1~SL-3、SL-5) | 多次增量回顾后故事不碎片化;间断后的事件接回原故事;调整不被覆盖 |
| 3 | 3D 视图接入真实数据 | 长期数据下时间尺度与聚合正常 |
| 4 | 经验抽取(SL-4) | 每条经验依据可追溯到原文;无候选时零额外调用 |
| 5 | MCP、心跳建议、工作回顾页 | Agent 能按故事接手工作、按经验给出建议 |
@@ -11,7 +11,7 @@
| 名称 | 职责 |
| --- | --- |
| 监督者 | 应用名称,负责观察、回顾和解释工作进展 |
| 智能心跳 | 按计划唤醒监督者的机制,保留计划与运行管理 |
| 智能心跳 | 按计划触发增量回顾,并按配置的介入方式生成建议;不单独读取原文或生成报告(FR-S12) |
| 监督反馈 | 会话右侧的阶段回顾、问题提示与证据入口 |
| 故事线图谱(Story Graph) | 呈现事件、知识实体及其随时间发展的关系 |
@@ -19,13 +19,15 @@ FR-S1:将现有智能心跳入口整合、升级为监督者,不增加另一
应用默认关闭,用户显式启用后才接受新的回顾工作;关闭时保留计划和历史。默认值、已保存选择、自动运行条件及在途运行边界由[应用启停与执行](./logic-design.md#应用启停与执行)统一定义。
FR-S2:应用包含“工作回顾 / 故事线图谱 / 自动监督 / 活动记录 / 设置”五个主视图。工作回顾提供统一监督结果阅读、历史选择和手动新回顾,结果显示自身范围与日期;自动监督集中展示计划状态、范围、周期及下次执行时间,以及运行概览、指标、趋势、自动监督报告、待处理记忆与行动建议和心跳审计,创建和编辑使用同一份 Modal 表单;活动记录展示统一实际执行、进度、详情及结果入口,支持按计划 ID 筛选;设置只管理超时、并发、分页、分批及执行预算,不嵌套次级菜单。计划保存失败保留草稿,关闭未保存修改须确认;编辑已暂停计划保持暂停,创建须显式保存并启用。原有运行记录及应用、存储标识保留。
FR-S2:应用包含“工作回顾 / 故事线图谱 / 自动监督 / 活动记录 / 设置”五个主视图。工作回顾提供统一监督结果阅读、历史选择和手动新回顾,结果显示自身范围与日期;自动监督集中展示计划状态、范围、周期及下次执行时间,以及运行概览、指标、趋势、自动监督报告、待处理记忆与行动建议和心跳审计,创建和编辑使用同一份 Modal 表单;活动记录展示统一实际执行、进度、详情及结果入口,支持按计划 ID 筛选;设置管理模型选择、超时、并发、分页、分批及单次响应容量,分类页签见 UI 设计。计划保存失败保留草稿,关闭未保存修改须确认;编辑已暂停计划保持暂停,创建须显式保存并启用。原有运行记录及应用、存储标识保留。
当前设置还支持分别调整报告与监督整理的模型超时,以及两者共用的监督并发数。并发设置不限制普通聊天,也不保证避免上游限流;排队不消耗模型阶段时限,降低并发不打断在途请求。取值及生效规则由[当前控制清单](./review-scheduling-design.md#当前超时与调度控制清单已实现)定义。
FR-S13:监督者的“设置 → 模型”可选择跟随应用默认文本模型或已有文本模型连接,复用连接及其凭据。回顾执行中修改设置不切换模型;生效时点、继续和重试规则见[监督者模型选择](./logic-design.md#监督者模型选择)。
FR-S10:活动同时覆盖手动监督回顾、定时心跳及用户立即运行的心跳。每条记录显示触发方式、范围、开始与结束时间、运行状态、失败原因和已有结果入口。同一次心跳的报告和下游监督回顾合为一条活动,报告保存成功不能掩盖下游仍在运行、暂停或失败。活动读取不调用模型,关闭应用后不查询活动。新批次运行支持暂停及继续,保留成功批次;人工修改的时间序列审计尚未实现,历史缺失不得猜测补齐。
FR-S2:分页只控制数据库读取批量,不能限制最终处理范围。当前已开放来源页大小、每批正文/消息上限、执行软预算;这些配置在新建回顾时保存,继续沿用。当前字段和未来高级参数的边界见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界),交互见 [UI 设计](./ui-design.md#算法配置设置目标)。完整覆盖、来源校验与事实保留不作为可关闭选项。
FR-S2:分页只控制数据库读取批量,不能限制最终处理范围。当前已开放来源页大小、每批正文/消息上限;这些配置在新建回顾时保存,继续沿用。单次响应容量按当前设置生效。回顾持续处理至完成,不按累计运行时长自动暂停。当前字段和未来高级参数的边界见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界),交互见 [UI 设计](./ui-design.md#算法配置设置目标)。完整覆盖、来源校验与事实保留不作为可关闭选项。
## 范围与触发
@@ -33,11 +35,15 @@ FR-S3:首次启用时选择项目、会话及可用来源,生成前显示范
FR-S4:支持手动回顾,并复用已有定时检查机制。自动报告和监督回顾分别只处理所选范围、回顾窗口内新增、修订或尚未处理的来源片段;没有待处理输入时跳过模型及结果生成。失败输入保留重试资格,预算遗漏不得记为已处理。手动回顾重新整理所选历史区间。具体规则见[自动增量与手动重看](./logic-design.md#自动增量与手动重看)。事件触发和阶段自动判定仍待设计。
回顾默认增量“回顾”默认增量,与心跳整理一致;重看历史改为“更多”中的显式“重新整理”。心跳不再先生成独立报告,同一来源版本在一次触发中只读取一次。规则见[心跳职责与介入](./logic-design.md#心跳职责与介入目标设计)。
FR-S12(已实现,主动介入除外):智能心跳的职责限定为**定时触发增量回顾**和**按计划配置介入**。介入方式为“仅更新记忆”“生成建议”(默认);会话级“主动介入”后续交付,当前不显示。建议只根据本次图谱变化产生,附来源依据,替代旧心跳报告、记忆建议和行动建议;无变化不调用模型,有变化而无建议候选时不发起建议调用。验收见 US-S32、US-S33。
FR-S5:会话右侧在跟随模式下展示当前会话的监督反馈;固定到 Conversation、Task 或 Experiment 时保持所选目标并显示其名称,切换聊天不改变监督目标。跨会话总结在完整应用中展示。反馈包含结论、来源和“在图谱中查看”定位入口。用户主动选择“发到当前会话”才加入聊天消息,同一结果不重复触发侧栏和全局通知。
FR-S6:监督者可提出整理与修改建议;已有会话监督的观察边界继续生效。自动更新图谱不能静默覆盖用户修改的描述、已确认的关系或已有知识库正文。取消整理后保留上次结果;失败说明受影响范围并允许重试,部分结果不得显示为完整回顾。
FR-S4、FR-S6、FR-S10 的验收要求:完整分页处理冻结范围内的入选来源;叶子事实和来源完整保留,摘要用于导航。预算耗尽或失败后保存成功批次与位置,重启后继续,不重复计算仍有效的成功批次;未读完或汇总未成功不能标完成。该续跑要求已进入生产接线,当前实现、实测范围与剩余差异见[回顾分块调度](./review-scheduling-design.md#0-生产接线与剩余边界)。
FR-S4、FR-S6、FR-S10 的验收要求:完整分页处理冻结范围内的入选来源;叶子事实和来源完整保留,摘要用于导航。用户主动暂停或失败后保留成功批次与位置,重启后继续,不重复计算仍有效的成功批次;未读完或汇总未成功不能标完成。该续跑要求已进入生产接线,当前实现、实测范围与剩余差异见[回顾分块调度](./review-scheduling-design.md#0-生产接线与剩余边界)。
## 与知识库的关系
@@ -51,8 +57,12 @@ FR-S9:来源事实、自动归纳、待核对关联和用户确认内容分别
## 落地范围
FR-S11(D1 已实现,D2/D3 待实现):监督者以时态 Story Graph 为主要产出,统一用户侧工作记忆概念并保留已确认偏好与已有数据。Agent 在继续工作、查询此前决定及其修订时,按需使用只读的 `story_graph_search`、`story_graph_get_context`、`story_graph_read_source`,读取范围、时间、演变、覆盖及可引用来源;工具不调用模型或触发回顾,监督者关闭时不暴露且拒绝调用。本地和远程 Runtime 共用读取语义。后续目标为同一来源版本提取一次并跨 scope 复用叶子事实;当前精确 scope 独立处理及 memory 读取继续保留。`as_of` 仅在历史重建能力具备后另行交付。合同与过渡阶段由[专项设计](./story-graph-mcp-design.md)定义,验收见 US-S28 至 US-S31;本轮没有数据迁移。
触发、整理与结果共享规则,以及整体架构和图谱对象关系见[逻辑设计](./logic-design.md)。
FR-S11 的会话控制:输入区对话设置在知识库上方提供“使用故事图谱”,每个会话默认开启并独立保存。仅在监督者应用启用时显示,应用关闭时完全隐藏。关闭会话开关后,本地及远程 Agent 不再获得图谱工具,已有能力会话调用也被拒绝;开启仅允许 Agent 自主决定是否调用 MCP,不注入图谱上下文或触发回顾。验收见 US-S30。
首个完整流程为:启用监督者、选择范围、生成回顾、进入图谱、核对来源、携带选定上下文继续讨论。知识库双向定位与内容补充随后接入同一流程。图谱关系辅助检索需单独验证效果,不作为首版默认检索策略。
当前工作回顾和图谱读取生产结果,活动展示实际执行与无变化检查、暂停和继续入口,自动监督设置复用已有计划。完整图谱回放及应用市场尚未实现,验收证据见[实施进度](./progress.md)。
@@ -9,10 +9,12 @@
| 产品设计 | [监督者应用产品设计](./supervisor-prd.md) |
| 行为规则 | [监督者逻辑设计](./logic-design.md) |
| 界面设计 | [监督者 UI 设计](./ui-design.md) |
| 实施进度 | SQLite schema 47;当前算法、设置和验收边界见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界) |
| 实施进度 | SQLite schema 50(心跳介入与监督建议见[心跳触发与建议](#心跳触发与建议2026-09-30));当前算法、设置和验收边界见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界) |
本文回答如何在现有 GoodBuddy 桌面端中实现监督者。它不改变产品范围,也不把模拟 Demo 当作生产数据模型。
FR-S11 的读取架构见[时态 Story Graph 内置 MCP 设计](./story-graph-mcp-design.md)。三个只读工具通过 `readStoryGraph` 投影现有 SQLite,Main 在发现、调用和返回前检查监督者启用、会话开关及 Runtime 分配。绑定携带会话 ID,按主键读取当前设置;原生客户端的复用键包含会话 ID 和开关值。本地 MCP、Model、Harness Main 代理和远程受管工具通道共用此入口;会话开关复用 `context_state_json`,无 schema 迁移。该文独立定义对象/版本合同、时间语义、配置映射及后续跨 scope 复用和记忆过渡。当前 memory 背景读取、精确 scope checkpoint 与候选身份保留;结果快照不支持 `as_of` 历史重建。
生产入口由 `supervision-production.ts` 组装服务、SQLite、共享池和 ask Runtime。`supervision-review-store.ts` 保存冻结来源清单,按有界页和片段供 `SupervisorService` 派发;叶子及连续位置先提交,导航和完整结果之后发布。消息已有知识引用保留本地或外部 locator,已确认记忆通过独立背景输入提供。不会检索外部全库。详细存储、调度及当前限制统一见[分块调度第 0 节](./review-scheduling-design.md#0-生产接线与剩余边界)。
模型实体 `id` 只在单次输出内有效。Main 从同一 scope 的故事线提供最多 100 个未撤销实体候选,生产提示只暴露 `candidateRef`(如 `known_1`)、名称和说明。模型显式选择候选后,Main 转换为严格 UUID 类型的内部 `persistedId`。服务校验候选集成员和重复映射,保存事务再次按实体主键校验故事线归属及撤销状态。没有有效候选引用时分配新 UUID,不按名称或裸模型 ID 合并。来源仍按每次结果分配 UUID,保留原始 `source_id` 和 locator。
@@ -33,30 +35,55 @@ schema 43 在结果上增加 `graph_snapshot_json`,保存当次实体名称、
侧栏通过 `onOpenSupervisionGraph(resultId)` 交给 App 导航;App 遵守现有离页检查,向 HeartbeatCenter / SupervisorWorkspace 传递类型化 `graphNavigation`,复用 `heartbeat` 路由及 keepalive 页面。每次点击创建新的导航请求,打开 graph 页签并更新历史选择共用的 resultId。overview 未包含该 ID 时仍直接请求其 graph;旧 overview、graph、来源和操作响应通过同一请求序号失效,不覆盖新结果。没有新增 window 事件、页面或持久化字段。
继续讨论入队时,无附件请求省略 `serializedContexts`;请求未指定 Runtime 时使用会话保存的选择。知识库预览与提交均检查目标库存在且为本地可写库,更新实体还检查其实际所属库。侧栏确认框展示 Main 返回的实体字段、目标库和来源正文,取消或写入失败后可以重新预览。
继续讨论预览将 `supervision:continue-context` 返回的 `prompt` 直接作为可编辑草稿。侧栏与图谱入口发送当前草稿,不重新拼接原始上下文。`supervision:continue` 沿用现有正文校验(去除首尾空白,最多 12,000 字符),使用空 `attachments` 入队,无附件请求省略 `serializedContexts`;请求未指定 Runtime 时使用会话保存的选择。会话已有的记忆和知识库配置仍按普通会话发送规则处理。知识库预览与提交均检查目标库存在且为本地可写库,更新实体还检查其实际所属库。侧栏确认框展示 Main 返回的实体字段、目标库和来源正文,取消或写入失败后可以重新预览。
监督来源清单将 UI 时间区间归一化为 UTC,先筛选再按稳定键分页。项目必须存在且 active;消息按闭区间筛选,任务按区间内创建或完成时间筛选。手动和自动监督共用该路径,自动清单另读取 supervisor checkpoint;手动心跳报告保留原有 collector。
已确认记忆作为当前背景,不参与新增正文覆盖。最多四条、每条 500 字符,时间来自真实更新时间;模型不能把背景引用为本批新增证据。旧结果继续保留原来源类型和 locator。监督正文上限现用于分批,余段继续处理;心跳报告的有界输入语义没有改变。
## 监督者模型连接
`ApplicationSettings.supervisorModelProfileId` 只保存已有连接 UUID,`null` 或旧设置缺少字段时跟随 `RuntimeSettings.defaultModelProfileId`。Main 的 `resolveSupervisorRuntime` 从 `getResolvedSettings()` 读取连接和已有凭据,校验文本协议及认证后,用 `createModelProfileRuntime` 创建无工具直连 Runtime。Renderer 仅接收公开连接信息,不新增凭据存储或 IPC 方法。
`SupervisorService.admit` 在执行位置内调用生产工厂的 `withExecution`,为提取、导航、故事及经验共享一个 Runtime;执行结束后释放。每次请求仍释放自身临时会话。建议措辞在自身调用开始时解析同一设置,结束后释放 Runtime。生命周期规则见[监督者模型选择](./logic-design.md#监督者模型选择)。这条路径在桌面 Main 内运行,即使证据来自远程项目,也不经过 GoodBuddy Agent 或远程模型桥。
## 模型阶段超时与调度
监督整理超时由 run 创建时冻结;共享模型池仍采用实时设置上限。`supervision:pause` 取消该 run 的排队和在途工作,`supervision:resume` 继续已保存批次;监督整理输出在流中按 `supervisionReview.responseKiB` 检查响应容量,默认 1024 KiB,可在设置页调整为 100 至 16384 KiB。该容量每次请求读取当前设置,继续旧运行也采用新值;超限保留成功批次并提示调高后继续。生成摘要及事实内容不设独立短字符限制,监督旧摘要读取保留全文,具体边界见[生产接线](./review-scheduling-design.md#0-生产接线与剩余边界)。以下报告领取和租约规则继续适用。
手动和自动监督均在桌面 Main 调用生产工厂,Runtime 解析请求只有 `workMode: ask`,不携带 SSH 项目或执行空间。摘要校验、合并和持久化在 Main 完成;本次取消内容长度限制不改变 `gbagent`、远端 Runtime 启动或桌面到 Agent 协议。
手动和自动监督均在桌面 Main 调用生产工厂,Runtime 解析请求只有 `workMode: ask`,不携带项目、Runtime 选择或执行空间。`resolveRequestRuntime` 因而返回桌面默认 Runtime,不进入 SSH 项目或 `selectedRuntimes.getRuntime` 分支;回顾 scope 只筛选证据。摘要校验、合并和持久化在 Main 完成,single-flight、取消及 UI 控制不改变 `gbagent`、远端 Runtime 启动或桌面到 Agent 协议。
`ApplicationSettingsStore` 持久化报告和整理模型超时,均为 30..600 整数秒,默认 240。报告排队前冻结,监督创建 run 时冻结;计时从获槽及 Runtime 解析后开始,覆盖模型执行及其内部重试。流结束校验 signal 和完成事件,finally 释放临时会话,心跳报告保持原有 100KB 上限,监督整理使用上文的可调响应容量。采集、排队、保存和清理不计入模型超时。
`supervisorModelConcurrency` 为 1 至 4 的整数,默认 1。报告和监督整理共用 Main 内 `SupervisionModelPool` FIFO 池,普通聊天不入池;降低上限不终止在途请求。报告槽位从领取前持有至报告完成或无变化提交后,在下游监督开始前释放,避免并发 1 时相互等待。监督服务外层运行仍串行;该池不是完整分块调度器,也不提供跨聊天与监督的提供商全局并发、RPM、TPM 或 Retry-After 控制。
报告租约为 `max(300, reportTimeoutSeconds + 60)` 秒,300 是下限;每次到期领取最多一条。Runtime 解析后、模型请求开始前按冻结超时刷新租约,数据库要求运行 ID、owner、attempt、claimed 状态和未过期条件均匹配,否则拒绝启动模型。直连模型独立的 600 秒传输超时、网络重试、心跳重领及轮询不属于用户阶段时限。设置 UI 提供两个超时、共享并发及分页/分批/软预算字段。
报告租约为 `max(300, reportTimeoutSeconds + 60)` 秒,300 是下限;每次到期领取最多一条。Runtime 解析后、模型请求开始前按冻结超时刷新租约,数据库要求运行 ID、owner、attempt、claimed 状态和未过期条件均匹配,否则拒绝启动模型。直连模型独立的 600 秒传输超时、网络重试、心跳重领及轮询不属于用户阶段时限。设置 UI 提供两个超时、共享并发及分页/分批/单次响应容量字段;整次回顾持续处理至完成,旧 `executionSeconds` 不再参与执行判断。
schema 47 的四张批次相关表使用外键随监督运行清理,未完成运行不新增自动过期删除规则。启动时原有 running 记录会标 failed,但已保存批次和配置保留,活动页仍可继续。发布逐页读取叶子并复用现有结果/来源/图谱写入,在同一事务更新完成状态和自动 checkpoint。
当前设置及冻结范围见[当前设置合同](./review-scheduling-design.md#当前设置合同)。未实现的高级参数继续保留在目标清单,不显示为可保存控件;没有兼容原型数据库的读取器。
### 单次执行与取消
对应 FR-S4、FR-S6、FR-S10;状态规则见[活动记录](./logic-design.md#活动记录)。同一个 `SupervisorService` 在异步配置读取前占用唯一执行位置,`run` 和 `resume` 共用准入。已有执行或停止清理时,手动新建及继续立即返回 `SUPERVISION_REVIEW_BUSY`;`trigger: heartbeat` 等待当前 Promise 结束,醒来后重新检查占用。等待中的自动请求尚未创建监督 run,不占模型池;心跳报告在进入下游监督前已释放自己的槽位。
`supervision:execution` 返回进程内 `{ active, runId?, stopping? }`,配置读取期间可有 `active: true` 而尚无 runId。`supervision:cancel({ runId })` 先在既有 `supervision_runs` 写入 `cancelled`、清除错误并保存取消时间,再向父 controller 传播 abort;重复取消保留原时间。该接口等待执行 Promise 结束,包含并行批次的 `allSettled` 和 Runtime `releaseConversation`,之后才释放准入。暂停只请求 abort,执行停止后保存 `paused`;取消优先于暂停。迟到模型输出不得保存叶子、导航或完整结果,成功批次保留,自动 checkpoint 不推进。发布是同步 SQLite 事务,已完成或无变化的运行拒绝取消,不撤销已发布结果;本次无 schema 迁移。
Preload 暴露类型化 `execution`、`cancel`,取消输入使用 UUID schema 并校验可信 sender。活动 UI 用实时执行状态覆盖持久记录的停止显示;工作回顾挂载期间每两秒读取执行状态,发起前再次查询,Main 仍作最终准入判断。`resume` IPC 直到运行结束才返回,Renderer 发出请求后释放提交锁并独立处理结果,确保继续中的回顾仍可暂停或取消。状态条 X 仅修改本地显示状态。
### 心跳触发与建议(2026-09-30)
对应 FR-S12,规则见[心跳职责与介入](./logic-design.md#心跳职责与介入目标设计)。`HeartbeatService` 只依赖 `HeartbeatActions { review, suggest? }`:领取后 `completeHeartbeatTrigger` 立即提交心跳自身运行(租约 5 分钟,仅覆盖交接),再调用 `review`,由 `ipc.ts` 转为 `supervisorService.run({ trigger: 'heartbeat' }, heartbeatRunId)`。审查失败写 `projection_status/error`,结果为 `no_change` 时标记心跳无变化。心跳不再占用模型池,也不读取 `heartbeatReportTimeoutSeconds`;该设置字段保留兼容,界面不显示。下文关于心跳报告阶段、报告租约与报告增量的描述均为历史行为。
`isIncrementalReview(request)` 统一判断:`trigger: heartbeat` 或未设 `reanalyze` 的手动请求读取并在发布事务中推进 `review_checkpoints`;`reanalyze: true` 从零读取且不写进度。`SupervisionReviewStore.unfinished` 只续跑 `failed/running` 的心跳运行,用户暂停的保持暂停。
计划 `intervention` 为 `suggest` 时,`deriveSuggestions` 调用 `SupervisionSuggestionStore.candidates(supervisionRunId)`:读取该运行叶子批次的 `openItems`(按原始来源映射到本结果 `supervision_sources`)、`revised` 实体变化、快照内 `contrasts` 关系,以及事件关联中不同原始来源数不少于 3 的 `automatic` 实体。指纹按类型与实体、关系或规范化文本生成;依据键使用原始来源类型、ID 与内容哈希,不用每次结果新生成的来源行 ID。每次最多 20 条。有候选时 `createProductionSuggestionPhraser` 发起一次无工具请求,共享监督模型池与整理超时;模型只返回 `ref/title/detail`,未知 ref 丢弃,缺失措辞回退规则文本。建议失败写 `suggestion_status = 'failed'` 与错误,`supervision:retry-suggestions` 只重跑建议。
IPC `supervision:suggestions`、`supervision:suggestion-action`、`supervision:retry-suggestions` 校验可信 sender 与 Zod 输入;监督者关闭时列表返回空、操作拒绝。接受未决事项创建暂停任务,接受候选约定确认迁入记忆或新建已确认记忆并确认实体。Schema 50 内容见[实施进度](./progress.md#2026-09-30-心跳职责收敛与监督建议)。
## 自动增量收集
本节的 100 来源/2,000 码点批次查询现用于原心跳报告及旧服务测试;生产监督使用 schema 47 清单及批次位置,自动 checkpoint 只在完整发布时提交。已有 schema 46 指纹算法和 checkpoint 仍复用,不重置用户已处理位置。
对应 FR-S4、FR-S10 和 US-S27,行为定义见[自动增量与手动重看](./logic-design.md#自动增量与手动重看)。schema 46 在现有数据库增加 `review_checkpoints`,主键为阶段、规范化 scope 和来源标识,保存版本指纹、已处理位置和来源长度。来源标识区分消息 ID、任务 ID 和消息内知识引用位置,不使用消息时间戳作为身份。
@@ -154,7 +181,7 @@ sequenceDiagram
运行状态与内容确认状态分开保存:
- 当前监督运行状态:`running`、`paused`、`completed`、`failed`、`no_change`。心跳及合并活动的映射见[活动记录](./logic-design.md#活动记录)。
- 当前监督运行状态:`running`、`paused`、`cancelled`、`completed`、`failed`、`no_change`。心跳及合并活动的映射见[活动记录](./logic-design.md#活动记录)。
- 内容状态:来源事实、自动归纳、待核对、用户确认、用户修订、已移除关系。
- 运行成功只表示模型结果已按契约保存,不表示所有实体和关系已经被用户确认。
- 失败保留上次完整结果和本次已保存批次;暂停不生成完整结果,继续入口恢复同一 run。
@@ -213,6 +240,8 @@ erDiagram
| `supervision:overview` | 工作回顾、运行状态、未解决事项和最近结果 |
| `supervision:run` | 手动开始一次整理,完成后返回包含 runId 的结果 |
| `supervision:activity` | 有界读取真实执行和阶段状态 |
| `supervision:execution` | 读取唯一执行位置及停止清理状态 |
| `supervision:pause` / `supervision:cancel` / `supervision:resume` | 暂停、终止或继续指定回顾;生命周期见上文 |
| `supervision:graph` | 按故事线、时间区间和回放时刻分页读取图谱 |
| `supervision:object` | 读取事件、实体、关系及来源详情 |
| `supervision:confirm` | 确认、修订或移除自动关系和实体变化 |
@@ -229,7 +258,7 @@ IPC 返回图谱查询结果时使用分页和有界字段;详情中的来源
- 监督者模型调用固定为 `ask` 语义,输入来自 `EvidenceCollector` 的有界快照,工具授权始终拒绝。
- `Global`、Project、多项目和固定 Conversation/Task/Experiment 目标在 Main 冻结;模型不能生成或改变范围。
- 每个来源引用必须属于本次冻结范围,跨范围引用、伪造项目 ID 和不存在的来源 ID 直接使结果失败。
- 心跳运行沿用现有 `heartbeat_runs` 的领取、租约和重试机制;新增监督运行按心跳运行 ID 关联,活动据此合并阶段。没有新增取消协议。
- 心跳运行沿用现有 `heartbeat_runs` 的领取、租约和重试机制;监督运行按心跳运行 ID 关联,活动据此合并阶段。监督取消不撤销已保存的心跳报告,合并活动优先显示监督已取消状态。
- 同一配置同时触发手动和定时运行时,沿用现有幂等键和运行领取规则;已完成结果不能重复写入。
- 用户确认或修订后的实体、关系不被后续整理静默覆盖;新证据以补充或冲突待核对状态写入。
- 外部知识库只读。图谱可以引用保存到本地的外部片段和定位信息,不调用远端写入、不承诺打开远端原文。
@@ -4,57 +4,71 @@
## 应用入口
“设置 → 模型”还提供“监督者模型”单选框:默认“跟随应用默认”,其余选项为已保存的文本模型连接,以连接名称和模型名区分,不列图片模型。失效的已选连接保留“所选模型已不可用”,用户可改选;加载或保存期间禁用选择。沿用该分类的保存按钮、未保存草稿保护和失败重试,常显说明覆盖的阶段及[生效时点](./logic-design.md#监督者模型选择),不增加凭据输入。
应用中心中的智能心跳入口升级为“监督者”,沿用既有启用、常驻与排序能力。未来应用市场使用同一应用身份。应用已启用时,停用单条自动心跳计划仍可手动回顾;关闭应用后,已打开页面显示关闭状态并禁用工作区操作。默认值及运行边界见[应用启停与执行](./logic-design.md#应用启停与执行)。
当前应用页只使用一套共享 PageTabs:“工作回顾 / 故事线图谱 / 自动监督 / 活动记录 / 设置”(英文活动标签为 Activity records)。自动监督独立管理计划;设置只显示模型超时与并发、回顾算法,不含计划、报告、审计或内部页签,也不显示整页全局徽标或独立刷新行。计划范围仍在表单和每条计划内显示。整个页面只有一个一级标题;监督者 bridge 不可用时显示明确错误,设置仍可访问,不回退到旧心跳概览。共享页签保留方向键、Home/End、游标焦点和单一激活面板,窄窗口横向滚动。
当前应用页的顶层导航只使用一套共享 PageTabs:“工作回顾 / 故事线图谱 / 智能心跳 / 活动记录 / 设置”(英文活动标签为 Activity records)。智能心跳独立管理心跳计划;设置不含计划、报告或审计,也不显示整页全局徽标或独立刷新行。设置内用一排分段页签按用途分为“模型 / 回顾整理 / 故事与经验 / 建议”,每次只显示一个表单:模型含整理超时与并发;回顾整理含读取条数、每批文本量与消息数、单次响应容量;故事与经验含子线索与经验阈值及跨项目关联开关;建议含暂无进展天数。各分类单独保存,只提交本分类字段并沿用其他字段的已存值;切换分类保留未保存的草稿。故事线图谱没有数据时,空状态占满可见高度,标题、说明与“工作回顾 / 刷新”按钮水平、垂直居中,不再在上方单独放刷新行。计划范围仍在表单和每条计划内显示。整个页面只有一个一级标题;监督者 bridge 不可用时显示明确错误,设置仍可访问,不回退到旧心跳概览。共享页签保留方向键、Home/End、游标焦点和单一激活面板,窄窗口横向滚动。故事线图谱面板内另有分类页签,见[故事线图谱](#故事线图谱)。
工作回顾只读取统一监督结果。顶部为紧凑的“新回顾”范围、时间、发起与刷新控件,不新增 Modal;简短帮助说明选择仅作用于新请求。历史结果选择与图谱入口位于正文之前,即使只有一条结果也显示选择器。新回顾工具栏、状态、历史选择和结果面板铺满监督者页面壳层的可用内容宽度,与页签左右对齐,不另设阅读宽度上限;结果内部按面板内边距排版,不再单独居中窄正文。正文使用“监督结果”标题,常显生成日期、真实范围和覆盖区间,历史结果不标为最新。工作总结按原有空行分段并保留换行;本次变化和未解决事项直接使用已有字段,不补写结论或截断正文。390px 下新请求字段与操作各占两列,结果元数据可换行。
工作回顾只读取统一监督结果。顶部紧凑排列范围、时间、发起与刷新控件,不重复显示“新回顾”标题和来源说明,不新增 Modal。历史结果选择与图谱入口位于正文之前,即使只有一条结果也显示选择器。新回顾工具栏、状态、历史选择和结果面板铺满监督者页面壳层的可用内容宽度,与页签左右对齐,不另设阅读宽度上限;结果内部按面板内边距排版,不再单独居中窄正文。正文使用“监督结果”标题,常显生成日期、真实范围和覆盖区间,历史结果不标为最新。工作总结按原有空行分段并保留换行;本次变化和未解决事项直接使用已有字段,不补写结论或截断正文。390px 下新请求字段与操作各占两列,结果元数据可换行。
新回顾运行中保留已显示结果及其日期、范围,禁用重复提交,并显示简短状态和活动记录入口;该入口清除计划筛选并聚焦活动页签。工作回顾不读取自动监督报告,也不显示自动报告空态、记忆与行动建议或第二行全局范围与刷新。无监督结果时显示手动回顾入口;自动报告是否为空不影响此判断。
新回顾运行中保留已显示结果及其日期、范围,范围、时间、发起和刷新控件保持可用;重复点击发起只重新显示当前运行提示,不排入新回顾。状态条提供活动记录入口,清除计划筛选并聚焦活动页签;右侧 X 只关闭提示,不暂停或取消执行,同一次运行的轮询不重新打开已关闭提示。范围、时间和活动计划筛选保留可访问名称,不再重复显示字段标题。工作回顾不读取历史心跳报告,也不显示自动报告空态、记忆与行动建议或第二行全局范围与刷新。无监督结果时,空态独占工具栏下方整行,标题与手动回顾入口水平居中;自动报告是否为空不影响此判断。运行中和暂停后使用独立状态条,提示当前状态及活动记录入口。
## 会话故事图谱开关
对应 FR-S11、US-S30。输入区“选项 → 对话设置”在知识库之前显示“使用故事图谱”(英文 `Use story graph`),复用共享 `toggle-row` 滑动 Switch,标签在左、开关在右,支持 Tab 和 Space。仅当监督者应用 `heartbeatEnabled` 为 true 时渲染;应用关闭或设置尚未加载时完全隐藏,不保留禁用控件或占位。
每个会话默认开启。保存期间保留已确认状态并禁用重复提交,成功后显示新值,失败时保留原值并通过应用通知说明错误。切换会话、重新打开或重启后恢复各自设置。开关的执行规则和持久化见 [MCP 接入合同](./story-graph-mcp-design.md#6-内置-mcp-与-runtime-接入)。
## 活动
文案遵循根目录 [监督者文案规则](../../../UI-DESIGN.md#134-智能心跳):不把内部路线图、交付状态或实现缺项展示给用户。活动说明只描述手动回顾、自动监督执行及自动刷新;真实错误、失败和暂停状态仍展示,并保留恢复操作。事实详情提示展开批次并对照原文核对模型内容,不承诺完整语义识别;图谱说明实体状态以所选回顾结果为准。
文案遵循根目录 [监督者文案规则](../../../UI-DESIGN.md#134-智能心跳):不把内部路线图、交付状态或实现缺项展示给用户。活动页不常显重复说明;真实错误、失败和暂停状态仍展示,并保留恢复操作。事实详情提示展开批次并对照原文核对模型内容,不承诺完整语义识别;图谱说明实体状态以所选回顾结果为准。
对应 FR-S10、US-S26。活动按开始时间倒序,每页 50 条,提供较新/更早记录和刷新。列表保留触发方式、执行范围、开始/结束时间、实际状态、阶段状态、错误及已保存摘要。没有结果时不显示结果按钮;有监督结果时可分别进入该结果的工作回顾和故事线图谱,焦点移到目标页签。超过最近 20 条的结果仍按 ID 读取,不能回退最新结果。
活动页只展示统一监督活动、进度、详情及结果入口,不在列表下方重复显示自动监督概览或心跳审计。计划“执行记录”打开此页并按精确 ID 筛选,筛选器显示所选计划并提供清除;监督查询在分页前过滤。同名计划仍按 ID 区分;没有匹配记录时显示筛选无结果,不回退全部数据。自动报告正文和建议处理位于自动监督。
活动页只展示统一监督活动、进度、详情及结果入口,不在列表下方重复显示心跳概览或心跳审计。计划“执行记录”打开此页并按精确 ID 筛选,筛选器显示所选计划并提供清除;监督查询在分页前过滤。同名计划仍按 ID 区分;没有匹配记录时显示筛选无结果,不回退全部数据。监督建议和历史心跳报告位于智能心跳。
自动监督页依次展示范围与“刷新自动监督”、当前状态及计划列表、运行指标、报告趋势、待确认记忆与行动建议、“自动监督报告”历史、心跳报告阶段审计。刷新统一更新此页数据。报告与运行历史每次追加 20 条,报告可展开全部摘要和洞察,切换页签保留展开与分页位置;不再单独复制最近报告。建议保留确认、忽略、带入对话、标记完成及长内容展开。活动的计划筛选不改变自动监督的统计范围。当前状态下复用计划列表的唯一创建按钮,无计划时只显示列表的未配置说明,不叠加第二个计划空态或创建入口;点击直接打开现有 Modal。报告为空时明确写“暂无自动监督报告”,与工作回顾的结果状态区分。
“智能心跳”页依次展示范围与“刷新智能心跳”、“心跳计划”卡片、监督建议,以及只读的历史心跳报告。心跳计划卡片内不再嵌套第二层边框:标题行右侧显示运行状态点,下方一行说明文字与“创建心跳计划”按钮同行,其后直接是计划列表。说明文字按状态三选一:无计划显示“尚未配置心跳计划,目前仅支持手动回顾”,全部暂停显示暂停说明,否则显示频率帮助;不叠加第二个空态或创建入口,点击按钮打开现有 Modal。
加载、空记录和读取失败分开显示;读取失败保留已显示记录并提供就地重试。活动页面激活时,存在运行中记录则在上次读取结束 2 秒后刷新,否则 10 秒后刷新;每次最多一个请求。切换顶层页签、离开监督者或关闭应用后停止定时器,迟到响应不更新界面。错误后等待用户重试,不持续重发失败查询。
加载、空记录和读取失败分开显示;读取失败保留已显示记录并提供就地重试。活动页面激活时,同轮读取活动和实时执行状态;有活动执行(包括停止清理)或运行中记录则在上次读取结束 2 秒后刷新,否则 10 秒后刷新,不重叠轮询。切换顶层页签、离开监督者或关闭应用后停止活动定时器,迟到响应不更新界面。错误后等待用户重试,不持续重发失败查询。
活动列表与工具栏铺满监督者页面壳层的可用内容宽度,与页签左右对齐,不另设仪表盘宽度上限。范围、计划筛选、清除筛选和刷新紧邻排列,空间不足时换行。列表使用共享表面、边框、状态颜色和按钮。每条运行首先显示类型、状态、开始时间和范围;“收集来源 / 整理批次 / 合并摘要 / 发布回顾”紧随其后,宽窗口使用按内容排布的紧凑状态行,窄于 720px 时两列,不为每个阶段铺设独立色块卡片。阶段读取已保存位置和真实运行状态,剩余来源为零不表示发布成功。旧运行没有阶段字段时显示“未记录”,不猜测失败位置;已提交来源清单可确认收集结束。无变化运行不显示新结果跳转,后续无需执行的阶段单独标注。
活动列表与工具栏铺满监督者页面壳层的可用内容宽度,与页签左右对齐,不另设仪表盘宽度上限。范围、计划筛选、清除筛选和刷新紧邻排列,空间不足时换行。列表使用共享表面、边框、状态颜色和按钮。每条运行首先显示类型、状态、开始时间和范围;恢复与事实操作紧随其后,再展示“收集来源 / 整理批次 / 合并摘要 / 发布回顾”。阶段使用编号圆点和连接线,宽窗口四项等宽,窄于 720px 时两列;当前及已完成阶段突出显示,不为每个阶段铺设独立色块卡片。阶段读取已保存位置和真实运行状态,剩余来源为零不表示发布成功。旧运行没有阶段字段时显示“未记录”,不猜测失败位置;已提交来源清单可确认收集结束。无变化运行不显示新结果跳转,后续无需执行的阶段单独标注。
阶段下显示已保存批数、已读字符数、剩余来源和已保存导航合并数,运行中另显示排队或在途数。字符进度按码点统计,界面不展示 Unicode 术语。恢复操作先于详情:运行中可暂停,失败或暂停后沿用同一运行继续;已记录合并、发布阶段时分别显示“继续合并摘要”“重试发布”。成功叶子保留。来源失效时显示新建回顾提示,不提供无效继续。原始错误默认折叠,展开后的完整文本最多占 240px 高度,可滚动、选择复制和键盘聚焦;错误与恢复操作位于批次树之前。结束时间、覆盖区间、报告状态和已有摘要收进“运行详情与配置”,已保存配置在其中再次折叠。
阶段下突出显示已保存批数和剩余来源,运行中另显示合计的排队或在途数,不拆分未经接口提供的数量。已读字符数和已保存导航合并数放入默认折叠的“运行详情与配置”。字符进度按码点统计,界面不展示 Unicode 术语。恢复操作先于详情:运行中可暂停,失败或暂停后沿用同一运行继续;已记录合并、发布阶段时分别显示“继续合并摘要”“重试发布”。成功叶子保留。来源失效时显示新建回顾提示,不提供无效继续。原始错误默认折叠,展开后的完整文本最多占 240px 高度,可滚动、选择复制和键盘聚焦;错误与恢复操作位于批次树之前。结束时间、覆盖区间、报告状态和已有摘要收进“运行详情与配置”,已保存配置在其中再次折叠。
“已保留事实与来源”展开后才查询既有批次接口,每页 10 批,按本页持久记录组成项目、会话、批次层级。项目使用可用名称,会话优先使用保存来源的标题;只有已保存批次标记“已保存”,不创建未落库的待执行、在途或失败叶子,也不把本页数量当作项目总量。批次摘要收起时最多两行,展开后保留完整摘要、事件、实体、变化、关系及原文片段。来源显示消息 ID、码点范围及版本;长原文局部滚动。原生 details/summary 保留键盘展开和可见焦点,轮询不主动展开用户收起的会话。加载、空批次和读取失败分开显示,失败可重试;换页失败不显示上一页批次,切换 run 后旧响应失效。暂停状态为“未完成,可继续”,手动回顾软预算暂停后在工作回顾页提示去活动继续,不显示完成通知。
“已保留事实与来源”展开后才查询既有批次接口,每页 10 批,按本页持久记录组成项目、会话、批次层级。项目使用可用名称,会话优先使用保存来源的标题;只有已保存批次标记“已保存”,不创建未落库的待执行、在途或失败叶子,也不把本页数量当作项目总量。批次摘要收起时最多两行,展开后保留完整摘要、事件、实体、变化、关系及原文片段。来源显示消息 ID、码点范围及版本;长原文局部滚动。原生 details/summary 保留键盘展开和可见焦点,轮询不主动展开用户收起的会话。加载、空批次和读取失败分开显示,失败可重试;换页失败不显示上一页批次,切换 run 后旧响应失效。暂停状态为“未完成,可继续”,手动回顾主动暂停后在工作回顾页提示去活动继续,不显示完成通知。
## 自动监督设置
活动页运行中同时提供“暂停回顾”和“取消回顾”,继续后的运行也可操作。停止期间禁用两项操作,并在运行标题和当前阶段显示“正在暂停”或“正在取消”;即使持久记录已经为已取消,只要执行尚未停止就继续显示“正在取消”。重新进入活动页通过实时执行状态恢复该提示。停止后显示“已取消”,保留事实详情,不显示继续按钮或失败恢复提示;任一回顾执行或停止清理期间,其他记录的继续按钮禁用。
2026-09-23 当前已实现“模型超时与并发”表单:心跳报告与监督者整理分别设置,均为 30 至 600 的整数秒,默认 240;监督模型并发数为 1 至 4 的整数,默认 1。保存真实字段 `heartbeatReportTimeoutSeconds`、`supervisorOrganizeTimeoutSeconds`、`supervisorModelConcurrency`;无效输入不能提交,设置加载/保存中或锁定时禁用,失败保留草稿并提供重试。超时以阶段读取设置的时点为准,不改变已冻结的值;并发数影响后续派发,降低上限不打断在途请求。
## 智能心跳设置
并发说明明确报告和监督整理共用上限,排队不计入模型超时;普通聊天不受此设置限制,但仍与监督竞争上游额度。表单不宣称提供商全局并发、RPM、TPM 或 Retry-After 限流已实现。
没有自动运行、报告或关联建议时,只展示计划配置和已有计划,不显示零值统计、空趋势、建议、报告或审计区。手动回顾不计入心跳统计;产生自动运行或报告后展示对应结果区。
模型超时不包含数据收集与保存,直连模型另有独立 600 秒传输超时。“回顾算法”表单提供“每次读取来源条数”“每批文本量”“每批消息数”和“单次整理时长”,显示默认值及合法范围,使用“保存回顾算法”独立提交。文本量内部仍按 UTF-16 单元计算,界面不要求用户理解编码单位。保存经 App 的现有设置回调进入共享设置存储,加载/保存/锁定时禁用,失败保留草稿。取值以[当前设置合同](./review-scheduling-design.md#当前设置合同)为准。帮助说明执行时间达到软预算后,正在处理的批次完成并保存,再暂停,可从活动记录继续;已保存配置用“同时处理任务数”解释并发,说明实际并发还受当前监督设置限制。
设置的四个分类共用一个设置面板,使用共享表面、边框和卡片圆角。面板内边距为 `--space-4`(16px),分段页签按内容宽度靠左,与下方表单间隔 `--space-6`(24px);页签和表单标题左对齐。表单不再另设边框、底色或内边距,说明段落不叠加浏览器默认外边距。页签及设置面板不参与 Flex 高度压缩,窄窗口页签在自身内部横向滚动,长表单随页面纵向滚动。
对应 US-S22 至 US-S24。“自动监督”先显示计划列表,点击创建或编辑才打开 Modal;复用原表单,不保留页面内副本。表单展示名称、项目范围、每日或每周频率、指定时间、回顾窗口(1 至 720 小时)和历史保留(1 至 365 天);每周显示星期选择。时区常显,新建使用本机时区,编辑保留计划原时区。列表上方帮助说明选择每日或每周时间、保存并启用后自动运行。
模型分类的标题为“模型与运行限制 / Model and execution limits”,包含监督者模型选择、整理超时与并发。整理超时为 30 至 600 的整数秒,默认 240;监督模型并发数为 1 至 4 的整数,默认 1。保存字段为 `supervisorModelProfileId`、`supervisorOrganizeTimeoutSeconds`、`supervisorModelConcurrency`;心跳不再单独调用报告模型,此处不显示报告超时。无效输入不能提交,设置加载/保存中或锁定时禁用,失败保留草稿并提供重试。超时以阶段读取设置的时点为准,不改变已冻结的值;并发数影响后续派发,降低上限不打断在途请求。
无计划时显示“尚未配置自动监督,目前仅支持手动回顾”;全部暂停时另行说明。加载或首次读取失败不显示未配置状态。打开页面、Modal 或填写字段均不创建计划,只有点击“保存并启用计划”才提交创建。已有计划显示范围、周期、时区、回顾窗口、保留期限和启停状态,保留编辑、暂停、恢复、立即运行、执行记录和确认删除;暂停时不显示待执行的下次时间,编辑保存不改变暂停状态。
并发说明明确监督模型调用共用上限,排队不计入模型超时;普通聊天不受此设置限制,但仍与监督竞争上游额度。表单不宣称提供商全局并发、RPM、TPM 或 Retry-After 限流已实现。
模型超时不包含数据收集与保存,直连模型另有独立 600 秒传输超时。“回顾算法”表单提供“每次读取来源条数”“每批文本量”“每批消息数”和“单次响应容量”,显示默认值及合法范围,使用“保存回顾算法”独立提交。文本量内部仍按 UTF-16 单元计算,界面不要求用户理解编码单位。保存经 App 的现有设置回调进入共享设置存储,加载/保存/锁定时禁用,失败保留草稿。取值以[当前设置合同](./review-scheduling-design.md#当前设置合同)为准。帮助说明回顾持续处理至完成,每批保存进度,可从活动记录主动暂停并继续;已保存配置用“同时处理任务数”解释并发,说明实际并发还受当前监督设置限制。
对应 US-S22 至 US-S24。“智能心跳”先显示心跳计划列表,点击创建或编辑才打开 Modal;复用原表单,不保留页面内副本。表单展示名称、项目范围、每日或每周频率、指定时间、回顾窗口(1 至 720 小时)和历史保留(1 至 365 天);每周显示星期选择。时区常显,新建使用本机时区,编辑保留计划原时区。列表上方帮助说明选择每日或每周时间、保存并启用后自动运行。
无计划时显示“尚未配置心跳计划,目前仅支持手动回顾”;全部暂停时另行说明。加载或首次读取失败不显示未配置状态。打开页面、Modal 或填写字段均不创建计划,只有点击“保存并启用计划”才提交创建。已有计划显示范围、周期、时区、回顾窗口、保留期限和启停状态,保留编辑、暂停、恢复、立即运行、执行记录和确认删除;暂停时不显示待执行的下次时间,编辑保存不改变暂停状态。
Modal 按[共享表单 Modal 规则](../../../UI-DESIGN.md#615-表单-modal)复用 header、body、footer:标题左侧、带本地化名称和工具提示的 X 右侧;底部取消次按钮、保存主按钮整体右对齐。标题和操作区保持可见,仅表单内容滚动。表单使用独立字段容器,不嵌套设置表单;窄窗口表单单列,保留共享窗口安全边距。
沿用整窗 Portal、共享遮罩、背景 inert、焦点循环与关闭后焦点恢复;初始焦点进入名称。X、Escape、取消或点击遮罩关闭时,有未保存修改先确认;确认初始焦点为继续编辑,Escape 或 X 只返回编辑,点击遮罩不放弃修改。保存期间禁止关闭和重复提交,失败在表单内显示并保留草稿。
界面及可访问名称统一使用“自动监督 / 回顾”,保留内部 heartbeat 标识及已有计划名称、报告正文。表单继续使用共享控件和容器响应式布局,建议及审计无需先进入即可配置计划。
界面及可访问名称统一使用“智能心跳 / 心跳计划 / 回顾”;心跳负责定时触发回顾和按计划介入,回顾本身由监督者完成,保留内部 heartbeat 标识及已有计划名称、报告正文。表单继续使用共享控件和容器响应式布局,建议及审计无需先进入即可配置计划。
### 算法配置设置目标
以下高级分组为目标交互;已接通的四项算法控件以上节为准。其余参数引用[设置配置清单](./review-scheduling-design.md#4-设置配置清单),不在 UI 文档另设数值,不增加一级页面。
普通配置常显消息每页条数、监督并发、报告/整理模型超时和单次执行软预算。高级配置按“来源与分块”“上下文与输出”“汇总与内存”“超时与续跑”“上游额度”分组。消息页字段的常显帮助为:“每次从数据库读取的消息数;会持续读完所选范围,不限制回顾总消息数,也不决定每次发给模型的消息数。”模型批次消息数、片段数和新增字符数分别标注,避免统称“批量大小”。
普通配置常显消息每页条数、监督并发、报告/整理模型超时和单次响应容量。高级配置按“来源与分块”“上下文与输出”“汇总与内存”“超时与续跑”“上游额度”分组。消息页字段的常显帮助为:“每次从数据库读取的消息数;会持续读完所选范围,不限制回顾总消息数,也不决定每次发给模型的消息数。”模型批次消息数、片段数和新增字符数分别标注,避免统称“批量大小”。
字段显示单位、已确定的默认值和合法范围;补充帮助说明调大后的内存、等待或模型用量影响。允许继承的节点超时显示“继承监督整理值”及解析后的秒数;依赖模型容量的输入/输出预算显示所用模型、容量及 token 是否为估算。跨字段校验覆盖输入与输出预留之和、扇入可容纳性、软预算与硬期限顺序、退避起始与上限;错误附在字段并关联可访问说明,提交时聚焦首个错误,Main 仍作权威校验。
字段显示单位、已确定的默认值和合法范围;补充帮助说明调大后的内存、等待或模型用量影响。允许继承的节点超时显示“继承监督整理值”及解析后的秒数;依赖模型容量的输入/输出预算显示所用模型、容量及 token 是否为估算。跨字段校验覆盖输入与输出预留之和、扇入可容纳性、退避起始与上限;错误附在字段并关联可访问说明,提交时聚焦首个错误,Main 仍作权威校验。
规划中的“保存配置”持久化后只用于新建运行,常显“已开始和待继续的回顾沿用启动时配置”。“恢复本组默认值”只修改该组草稿,经保存才生效,不开始回顾、不修改已有计划;取消编辑恢复已保存值。运行详情展示本次实际配置,继续操作沿用它;想采用新算法配置时使用“重新回顾”,说明会重新计算。当前已实现字段仍按上文的阶段读取和共享池生效方式工作,整次 run 冻结接线完成后再统一调整提示。
@@ -64,11 +78,13 @@ Modal 按[共享表单 Modal 规则](../../../UI-DESIGN.md#615-表单-modal)复
## 工作回顾
2026-09-30 已实现(规则见[心跳职责与介入](./logic-design.md#心跳职责与介入目标设计)):主按钮“回顾”执行增量整理,文案不再称“重新分析”;旁边“更多”菜单提供“重新整理…”,确认对话说明将重新分析所选区间并可能产生模型费用。计划 Modal 增加“介入方式”单选:“生成建议”“仅更新记忆”;主动介入上线前不显示。最近建议替代,每条建议显示依据、打开来源及转任务/确认/忽略。“最近建议”替代,每条建议显示依据、打开来源及转任务/确认/忽略。
当前页面提供全局或单项目范围,以及 1、7、30 天时间区间选择。没有自动计划也能手动回顾;成功结果显示自身冻结的范围、时间和未解决事项,可选择历史结果或进入图谱。来源筛选由本次回顾范围和时间限制;图谱详情只列出所选事件的关联来源,实体或关系列出相关事件的来源。独立的来源类型选择器尚未实现。
选择历史结果后,图谱读取该结果的成员和内容,标题显示其回顾区间和生成时间。切换时清空旧图谱和详情并显示加载状态,迟到请求不得替换新选择。确认、修订和移除反馈更新当前所选结果,后续自动回顾不改写旧结果。
手动“回顾当前进展”生成中禁用重复提交。该操作新建回顾,活动页“继续”恢复原 run;两者不得混淆。失败保留上次结果、本次范围和成功批次。手动操作不推进自动位置,删除报告也不重置自动位置。
手动“回顾当前进展”新建回顾,活动页“继续”恢复原 run;运行中再次发起按上文显示提示,停止清理结束后才能开始新回顾。失败保留上次结果、本次范围和成功批次;已取消回顾不能继续。手动操作不推进自动位置,删除报告也不重置自动位置。
## 会话侧栏
@@ -85,12 +101,20 @@ Modal 按[共享表单 Modal 规则](../../../UI-DESIGN.md#615-表单-modal)复
有匹配结果时提供“在图谱中查看”,打开现有监督者页面的图谱页签并读取该结果;无匹配结果不显示入口。按钮保留原生键盘操作,进入后焦点落在图谱页签,继续使用共享 PageTabs 的方向键导航。固定目标不因跳转改变;再次点击相同或不同结果均重新定位,包括页面缓存后再次进入。结果不可用时显示错误并重试原 ID,不展示默认最新结果。
当前“继续讨论”预览实际发送会话名称、ID 和上下文,确认后发送到该会话;固定任务使用其关联会话,无法确定发送会话时禁用。切换目标使旧来源请求和预览失效。编辑预览中的引用仍属后续工作。
“继续讨论”直接准备上下文,不要求先查看来源。预览顶部单独显示目标会话,下方“发送内容”文本域直接填入实际回顾与来源正文,用户可编辑或删除其中任意内容。发送仅提交当前文本域内容,不再暗中拼接上下文,也不提供额外的展开区。只有点击“发送”才提交,取消不发送,失败保留整份草稿。固定任务使用其关联会话,无法确定发送会话时禁用。上下文仅包含所选来源关联的事件、实体、关系及原文快照,不带入整份全局回顾摘要;没有关联事件时只使用来源。切换目标使旧来源请求和预览失效。查看来源不自动跳转,用户点击“打开会话”才导航。侧栏不再显示实体写入或知识库选择器,现有知识库数据不受影响。
侧栏与图谱来源详情共用编辑区样式:目标区底部使用弱分隔线,区块间距为 `--space-4`,标签与文本域间距为 `--space-2`。文本域默认十行,可纵向调整大小;长内容在文本域内滚动。底部复用 `custom-task-dialog__actions`,取消在前、发送在后,整体右对齐。
## 故事线图谱
关联来源详情属于会话时,显示“打开会话”按钮,使用来源保存的会话 ID 跳转并切换到所属项目;不发送消息或启动回顾。另提供“继续讨论”,使用上述可编辑正文预览,点击发送后提交到来源会话并打开它。详情继续展示整理时保存的原文快照。会话已删除或读取失败时,沿用应用会话导航的提示;其他来源类型不显示此按钮。
左侧使用共享 PageTabs 切换“事件 / 实体 / 关系”,显示各类数量,一次只展示一类列表,无记录时显示本次回顾的分类空态。页签固定在顶部,列表占满剩余高度并独立滚动。切换页签不清除当前图谱选择;从画布、事件浏览或详情选择对象时同步切到对应页签。支持方向键、Home/End 和面板键盘聚焦。
使用 `supervisor` 页面壳层,最大内容宽度为 `1440px`。当前生产图谱左侧为紧凑的事件、实体和关系列表,中间为 SVG 画布,右侧为详情与来源。桌面列宽为 `216px minmax(0, 1fr) 280px`,画布获得主要空间。外圈事件按发生时间排序后等距逆时针排列,不表示实际时间间隔;起止之间保留缺口和方向箭头。内部显示真实实体名称,实线仅连接实际事件与实体,虚线仅连接实际实体关系。
高度:容器宽度不小于 861px 时,图谱这一行的高度等于窗口可见高度减去图谱顶部位置,并在底部保留约 24px 边距(不小于页面底部内边距),最低 660px(再矮平铺图的文字会小于 11px,这时改为页面滚动)。三栏时整页不再出现纵向滚动;左侧列表和右侧详情在这一行内独立滚动,滚动只影响自身,不带动页面。中间画布列不出现滚动条:标题栏和底部单行“事件浏览”栏之外的高度全部给图,平铺与时间螺旋一致。画布标题为“事件与知识”,下方一行小字依次显示图谱范围、时间区间、生成时间和事件/实体数,刷新为标题栏右侧图标按钮;图谱上方不再单独占一行显示范围与时间。没有图谱数据时,刷新按钮保留在页签顶部。图例默认收起,从标题栏“图例”按钮打开,浮在画布右上角,可用 X 关闭;可见事件与实体数量常驻画布左下角。两栏(861~1190px)时第一行按同样方式填满,详情换到下一行,保持自然高度并随页面滚动。单栏保持原有页面滚动和列表 320px 上限。窗口大小或图谱上方内容(提示、工具栏)变化时重新计算。
实体及其影响连线复用现有 `--graph-node-*-border` 令牌;颜色用于辅助辨认实体,不代表故事分类或确认状态。选中项加粗并突出连线,其余节点降低图形强调度但保留可读文字。详情分为名称与说明、图谱关联、确认操作和来源,来源正文按需读取。图例常显,读图说明可展开。
画布每批最多显示 8 个事件、6 个实体;同时间事件分开排布。列表保留全部返回记录和完整名称,选择实体会定位到真实关联事件所在批次,选择关系会优先显示两端实体。滑块明确命名“事件浏览”,只切换当前事件,不还原实体历史状态,也不宣称完整历史回放。当前接口返回单条故事线,不复制 Demo 中的三条模拟故事。后续视图规划见[图谱设计](../story-graph/prd.md)。
@@ -99,6 +123,10 @@ Modal 按[共享表单 Modal 规则](../../../UI-DESIGN.md#615-表单-modal)复
隔离视觉验证使用 `tests/support/supervisor-layout-regression.tsx` 渲染生产 React 组件;所有输入标为模拟 fixture,不读取用户数据库或调用模型。`tests/supervisor-layout.electron.test.ts` 启动 Vite 与 Electron,通过 `executeJavaScript` 测量布局、`capturePage` 截图,覆盖 1440、1024、390px、浅深主题、长名称、密集事件和异常状态。可通过 `GOODBUDDY_SUPERVISOR_ARTIFACTS` 保留截图;交互预览入口为 `node tests/support/supervisor-preview.mjs`。
设置 `GOODBUDDY_SUPERVISOR_CONTENT_LAYOUT=1` 验证工作回顾、空态、活动、待处理及失败场景,在 2000、1440、1024、390px 和浅深主题下共 40 个组合,检查全宽、空态居中及位于工具栏下方、阶段连接线和长内容溢出。
设置 `GOODBUDDY_SUPERVISOR_SETTINGS=1` 只验证设置布局:四个分类分别覆盖浅深主题及 1440×1100、1024×768、390×480,共 24 个组合。检查五个主页签、四个设置页签、单一可见面板、16px 内边距、24px 内容间距、无嵌套表单边框及横向溢出,并保存截图与几何测量;使用生产组件和模拟设置,不调用模型。
设置 `GOODBUDDY_SUPERVISOR_PLAN_MODAL=1` 可只验证五页签 fixture 中的计划 Modal:创建和编辑各覆盖浅深主题、1440×1100、390×1100、390×480,检查关闭按钮、底部按钮对齐、正文滚动、鼠标命中和原生键盘操作。截图及测量写入同一产物目录。
设置 `GOODBUDDY_SUPERVISOR_RECAP=1` 可验证本次工作回顾分区:1440×1100 和 390×1100 下覆盖监督结果与自动报告均有内容、仅有监督结果、两者均空,另检查历史结果运行中仍可阅读以及活动入口。该分支检查每页一处刷新、自动报告和建议的归属、正文段落与宽度,并截取自动页建议、展开报告和审计区块。
@@ -155,7 +155,7 @@
### US-S24 手动回顾当前进展
**状态:待实现;当前智能心跳已有“立即运行一次心跳”。**
**状态:已接入工作回顾;执行状态与取消规则见[活动记录](./logic-design.md#活动记录)。**
作为工作使用者,我希望主动检查当前范围,而不必等待下一次计划运行。
@@ -163,14 +163,15 @@
| Given | When | Then |
| --- | --- | --- |
| 范围有效且有来源 | 点击“回顾当前进展” | 页面显示整理中状态、进度说明和取消操作 |
| 范围有效且有来源 | 点击“回顾当前进展” | 页面显示整理中状态和活动入口,活动页提供暂停与取消 |
| 整理完成并有新变化 | 返回结果 | 工作回顾、监督反馈和图谱引用同一次结果 |
| 没有有效新变化 | 整理完成 | 显示检查时间和“暂无新进展”,不重复推送旧内容 |
| 用户点击取消 | 整理仍在运行 | 显示已取消,保留上一次成功结果 |
| 用户点击取消 | 整理仍在运行 | 清理期间显示正在取消,停止后显示已取消,保留上一次成功结果和成功批次 |
| 回顾正在执行或停止清理 | 再次点击手动发起 | 显示当前运行提示,不新建或排队;关闭提示不取消执行 |
**界面行为**
- 运行按钮在整理期间禁用,取消后可以重试。
- 运行按钮在整理期间保持可用,重复点击按上表提示;取消停止后可以新建,已取消运行不能继续。
- “无新进展”与“失败”使用不同状态和文案。
- 结果必须显示本次冻结的范围,不能因用户后来切换项目而改变解释。
@@ -537,7 +538,7 @@
| 已保存批次跨多个项目和会话 | 展开事实并翻页 | 按本页真实归属分层,标明本页数量;原文可读,失败叶子不伪造为已保存 |
| 原始错误是多行校验 JSON | 展开错误详情 | 默认折叠,展开后有界滚动,完整文本可选择复制;恢复操作和阶段仍在其前面 |
取消和人工修改时间序列审计不属于当前已实现范围;实际状态及保留规则见[活动记录](./logic-design.md#活动记录)。
继续后的运行仍可暂停或取消;任一运行执行或停止清理期间,其他运行不能继续。重新进入页面仍显示实际停止状态。人工修改时间序列审计不属于当前已实现范围;实际状态及保留规则见[活动记录](./logic-design.md#活动记录)。
### US-S27 自动增量与历史重看
@@ -545,13 +546,107 @@
| Given | When | Then |
| --- | --- | --- |
| 自动报告和监督已成功处理窗口内来源 | 同范围再次自动检查且来源未修改 | 两阶段均不调用模型,活动显示无变化,不新增报告和图谱事件 |
| 监督已成功处理窗口内来源 | 同范围再次自动检查且来源未修改 | 不调用模型,活动显示无变化,不新增图谱事件或建议 |
| 消息正文、知识引用或任务状态有修改 | 再次自动检查 | 只处理修改版本及未读片段,结合已有摘要和实体说明,允许复用已有实体 ID |
| 来源多于单次预算或正文较长 | 一次结果成功保存后再次检查 | 继续处理窗口内遗漏或剩余片段,已送入且保存成功的部分不重复处理 |
| 模型、校验或保存失败 | 再次检查相同输入 | 失败阶段仍可重试;已成功的另一个阶段不重复付费 |
| 来源已经自动处理 | 用户手动回顾所选区间 | 重新整理有界历史,保留自动进度;删除报告不等同重看或重建 |
| 来源已经自动处理 | 用户点击
| 只有已确认记忆或背景摘要变化 | 自动检查 | 背景不单独触发模型调用 |
### US-S32 点击回顾只整理变化
**状态:已实现(2026-09-30)。** 对应 FR-S4、FR-S12,规则 H3、H4。
| Given | When | Then |
| --- | --- | --- |
| 所选范围已有成功回顾,来源未变化 | 点击“回顾” | 不调用模型,活动显示“无新变化”,保留原结果 |
| 有新增或修订的消息 | 点击“回顾” | 只处理变化部分,关联已有实体后发布;不生成建议 |
| 用户需要重看一段历史 | 在“更多”中选择“重新整理”并确认 | 重新分析所选区间,保留用户确认内容,不改变自动进度 |
| 首次对某范围回顾 | 点击“回顾” | 完整分批处理所选时间区间 |
### US-S33 心跳按配置给出建议
**状态:已实现(2026-09-30),主动介入除外。** 对应 FR-S12,规则 H1、H5 至 H10。
| Given | When | Then |
| --- | --- | --- |
| 计划介入方式为“生成建议”,来源无变化 | 计划到期 | 不调用模型,不产生建议或通知 |
| 有变化,但图谱变化中没有建议候选 | 计划到期 | 完成增量整理;不发起建议调用 |
| 有变化,出现新的未解决事项或分歧 | 计划到期 | 整理后按候选生成建议,每条附依据并可打开来源;原文不重复读取 |
| 介入方式为“仅更新记忆” | 计划到期 | 只更新图谱,不生成建议 |
| 用户忽略一条建议 | 后续检查没有新证据 | 不再重复提出 |
| 建议生成失败 | 查看活动 | 显示“已更新,建议生成失败”;图谱与进度保留,可单独重试建议 |
| 旧计划升级 | 打开自动监督 | 范围、频率和启停状态不变,介入方式为“生成建议”;旧报告只读可查 |
## 时态图谱与 Agent 读取
### US-S28 依据决定演变继续工作
**状态:D1 工具读取已实现;真实模型建议质量尚未验收。** 对应 FR-S11,读取合同见[时态 Story Graph MCP 设计](./story-graph-mcp-design.md),验证范围见[实施进度](./progress.md#2026-09-28-story-graph-只读工具)。
| Given | When | Then |
| --- | --- | --- |
| 已保存旧决定 A,后来有明确证据将其修订为 B | 用户询问“继续上次工作,现在采用什么” | Agent 按需读取图谱,说明 A 到 B 的变化、推荐 B 的依据,并引用旧决定与修订来源 |
| 新记录只是增加选项 C | Agent 查询当前理解 | B 不因 C 更新而自动失效;替代或矛盾关系必须有明确证据 |
| 记录只表明决定采用 B,没有完成证据 | 用户询问执行进度 | 回答已决定且完成状态未知,不推断已完成 |
| 已确认旧认识与新证据冲突 | Agent 组织当前建议 | 分别展示确认状态、有效性与待核对差异,不静默覆盖 |
### US-S29 分页核对时间与来源
**状态:D1 已实现,缺失历史版本明确返回不可用。** 对应 FR-S11、US-S13。
| Given | When | Then |
| --- | --- | --- |
| 旧事件在较晚回顾中才生成并记录 | 按发生时间查询并查看 timeline | 区分事件、回顾生成和记录时间;不把补录当成新发生,也不声称当时已知 |
| 搜索、演变或原文超过一页 | Agent 继续读取 | 每页说明范围、修订、覆盖与剩余页,能读取全部匹配内容,无静默截断 |
| 旧记录只有证据条数,没有完整覆盖信息 | Agent 读完最后一页 | 返回覆盖未知,不称已覆盖全部原始工作记录 |
| 引用版本原文已不可用或当前内容已修改 | 读取历史来源 | 返回实际保存片段或明确版本不可用;读取当前版本须显式选择并标注差异 |
| 分页期间对象版本变化 | 继续使用旧游标 | 明确提示游标失效并重新查询,不混合新旧版本 |
| 用户要求截至过去时刻的完整认识 | 调用 `as_of` | 明确不支持,不用 current 或 timeline 冒充历史重建 |
### US-S30 按需读取并遵守应用开关
**状态:D1 已实现;跨平台安装包验收未执行。** 对应 FR-S11、US-S14、US-S20。
| Given | When | Then |
| --- | --- | --- |
| 会话属于当前项目 | Agent 未显式指定 scope 搜索 | 只查该项目;无项目时要求明确范围,不自动扩大为 Global |
| 用户进行普通聊天 | 没有继续工作或历史依据需求 | 不强制每轮查询或注入完整图谱;需要核对原话时才进一步读来源 |
| 监督者开启且内置能力已分配,没有自动计划 | 查询已有结果 | 可以读取;查询不触发模型、回顾或业务写入 |
| 监督者关闭,旧会话仍缓存工具 | 列出工具或直接调用 | 不暴露工具,调用被拒绝;已有数据保留 |
| 监督者应用关闭 | 打开输入区对话设置 | 完全不显示“使用故事图谱”开关 |
| 监督者应用开启,会话未保存过该选项 | 打开输入区对话设置 | 知识库上方显示默认开启的共享 Switch |
| 当前会话关闭“使用故事图谱” | 本地或远程 Agent 列工具或使用旧能力会话调用 | 不暴露三个图谱工具并拒绝执行,其他会话仍按自己的设置运行 |
| 开关已保存 | 切换会话、重启或关闭后重开监督者应用 | 恢复该会话的选择;开关不发起回顾或自动注入上下文 |
| 保存失败 | 用户切换开关 | 保留最后确认值并显示错误,可重试 |
| 相同范围与证据分别通过本地和远程 Runtime 查询 | 翻页、读取版本或关闭监督者 | 返回相同业务语义与引用,远程实际桥接须通过验收 |
### US-S31 保留偏好并复用跨范围事实
**状态:待实现;当前仍独立处理精确 scope,读取现有已确认 memory。** 对应 FR-S11、US-S16、US-S27。
| Given | When | Then |
| --- | --- | --- |
| 用户已有已确认的 Global 或项目偏好 | 逐步切换统一工作记忆入口 | 原内容、确认状态、适用范围和引用保留,不删除旧数据 |
| 旧偏好只有更新时间 | 查询演变 | 标记为当前背景和历史未知,不补造决定时间或替代关系 |
| 项目 A 的来源版本已有成功叶子事实 | 目标复用能力交付后进行 Global 整理 | 复用该版本的事件、变化、关系和来源,不因 scope 改变重复提取相同正文,也不只复用摘要 |
| 仅部分片段成功或原来源已改版 | 再次整理 | 识别未覆盖或变更部分,不把旧版本覆盖算作新版本完成 |
| Global 结果含多个项目的归纳 | 从单项目查询 | 不把跨项目摘要伪装成项目结论,复用事实仍保留来源归属 |
### US-S34 独立选择监督者模型
对应 FR-S13,生效规则见[监督者模型选择](./logic-design.md#监督者模型选择)。
| Given | When | Then |
| --- | --- | --- |
| 已保存多个文本模型连接 | 在监督者“设置 → 模型”选择连接并保存 | 回顾和建议使用所选连接,无需重新输入凭据 |
| 尚未配置监督者模型,或选择“跟随应用默认” | 开始执行 | 使用应用默认文本模型 |
| 回顾正在提取或汇总 | 修改模型选择或原连接配置 | 此次回顾后续批次、故事和经验阶段保持原模型 |
| 回顾已暂停或失败 | 修改模型后继续 | 使用当前选择继续已保存进度 |
| 已选连接被删除或变成图片模型 | 打开设置并尝试执行 | 显示连接不可用,执行报错,用户可改选;不自动回退 |
| 模型选择保存失败 | 留在设置页 | 保留选择草稿及重试入口 |
## 验收优先级与依赖
首个可用版本应先打通以下路径:
+9
View File
@@ -16,3 +16,12 @@
[Runtime 进程复用技术设计](../assistant-workbar/runtime-process-reuse-technical-design.md)
统一维护;本 Runtime 的生命周期见[技术设计 §9](./technical-design.md#9-runtime-生命周期),
未发布实现的验收状态见[工作栏进度](../assistant-workbar/progress.md#2026-09-14-runtime-进程复用实施中)。
The separate official Web client is pinned to `@deepseek-ai/dsh@0.1.7-rc.2`.
Its Main service API, Ask/Execute policy, capability adapters, packaged resource
layout, and Windows validation are documented in
[Native Client Proposal, section 9.1](../assistant-workbar/runtime-native-client-proposal.md#91-ds-web-backend-2026-09-26).
This Web resource tree is prepared separately from the background Harness Host.
Current shortcut/coordinator behavior and configured-model evidence are tracked in
[local native client progress](../assistant-workbar/progress.md#2026-09-27-local-native-client-validation),
including the remaining managed Node delivery gap.
@@ -1,5 +1,91 @@
# DeepSeek Harness 实现进度
## 2026-09-26 真实模型与官方 Web 验证
本轮验证 `0.1.7-rc.2` 后台对话/工具路径与同版本官方 Web 的本地接入可行性,关联
[原生客户端方案](../assistant-workbar/runtime-native-client-proposal.md)的 FR-2、3、5,
US-2 和 P1/P2。快捷按钮与完整客户端集成本轮未实现。
环境:Windows x64,标准 Node `24.19.0`,Edge `154.0.4258.37`,官方
`@deepseek-ai/dsh@0.1.7-rc.2`,仓库 HEAD `51166136157a646530e1ad9f6a0242c1c6390e95`
加当前未提交修改。源码哈希记录在本机临时报告 `dsh-017-verify/source.json`。
供应商实际模型为当前 DS 配置中的 `deepseek-flash`,协议为 `openai-chat-completions`。
| 场景 | 实测结果 | HTTP dispatch |
| --- | --- | ---: |
| 后台 Ask | `read` 返回未出现在 prompt 中的随机校验码 | 2 |
| 同会话切 Execute | `write` 创建文件,实际内容为 `DSH_EXECUTE_OK` | 2 |
| Execute Shell | `pwsh` 执行 `Get-Content execute-marker.txt`,返回正确内容 | 2 |
| 官方 Web 页面提交 | 当前模型读工作区文件并返回另一随机校验码;刷新仍保留回答 | 3 |
| 本轮合计 | 9 次均请求 `deepseek-flash`,9 次均返回 HTTP 200 | 9 |
计数在 `AgentModelGateway` 向供应商执行 `fetch` 时记录,包含 Web 的 1 次无工具请求;
4 次用户提交实际产生 9 次供应商请求。后台 3 轮均收到 `done`、工具 `completed`
和每轮 2 个用量事件。调用经过现有 `createManagedModelBridge`、`AgentModelGateway`
和 `ModelBridgeLoopbackProxy`,由隔离 Electron profile 中的 safeStorage 解密配置副本。
供应商凭据未写入 Web 配置或报告;正式 `runtime-settings.json` 测前测后逐字节一致。
环境继承 `NODE_TLS_REJECT_UNAUTHORIZED=0`,本轮不证明严格 TLS 证书校验可用。
官方 Web 以标准 Node、独立 `DSH_HOME` 和回环随机端口启动。通过官方 patch 配置 provider
和默认模型,再调用 `workspace/create`;首次创建和已有目录复用均通过。根页面自动选中
测试工作区和当前模型,无需手动填供应商密钥。保留并通过官方“内测声明”的“继续”按钮,
真实页面输入、发送、回答、刷新及截图检查通过。配置/API 细节统一维护在方案第 6.3 节。
本轮驱动曾因缺少 `skillPackages` 默认值和临时目录动态模块解析失败退出,均为 0 次请求;
修正驱动后后台成功。首次 Web `fill` 后发送按钮未启用,没有发出 Web 模型请求;改为键盘
输入并仅重跑 Web 后成功,未重复后台调用。本轮 9 次 dispatch 逐次记录于同一 `calls.json`。
补查发现此前 `dsh-017-live/calls.json` 已有 7 次 HTTP 200 记录,其中后台 4 次、Web 3 次;
对应历史报告记录模型 `deepseek-flash`、工作区与模型选中、文件结果及配置未改。本轮保留
这些文件,未覆盖或重算为新增调用。该历史批次与本轮合计可核查 16 次,下面依赖升级阶段
“0 次”仅指升级回归本身。
定向命令使用标准 Node 执行 `node_modules/vitest/vitest.mjs run`,目标为
`src/main/agent/deepseek-harness-acp-e2e.test.ts`、
`src/main/agent/goodbuddy-harness-control-plane.test.ts` 和
`src/main/agent/deepseek-harness-runtime.test.ts`:3 个文件、52 项通过、3 项跳过,11.12 秒。
3 个环境开关 live case 未在 Vitest 中启用;上表为独立真实模型驱动结果,不把跳过项算通过。
确定性回归包含 Ask 写入拒绝和 Execute 工作区外写入,无需为验证重复调用真实模型。
本机证据位于临时 `dsh-017-verify/`:`calls.json`、`backend-result.json`、`result.json`、
`main-result.json`、`source.json` 和 `web-conversation.png`。报告保留首次 Web 驱动失败,
最终 Web 结果另见 `result.json`。桥接、worker、Web 和浏览器已关闭,进程检查未发现本轮
目录所属的残留进程;临时 profile、测试工作区和 DS home 清理后仅保留脚本及脱敏证据。
这些本机临时文件不作为仓库长期资源。
证据范围:后台运行真实 ACP/Host/控制面和供应商模型,Host 在测试进程内组合;Web 为
官方 CLI 子进程与真实浏览器页面。未重新验收 GoodBuddy Composer/Utility Process,
也未实现 Web 的 GoodBuddy Ask/Execute 适配。官方 Plan 只控制规划状态,默认
“工作区内修改”也不等同于 Execute;后续复用现有工具命令名判断,不增加权限层级、
目录限制或逐工具二次授权。Skills/MCP 承接、取消、产品生命周期及交付资源仍待验证。
未改远程源码;DS 仍不支持 SSH execution space,本轮没有远程模型调用或 Linux Host 验证。
## 2026-09-26 依赖升级至 0.1.7-rc.2
23 个直接引用的 `@deepseek-ai/dsh-*` 包统一锁定为 `0.1.7-rc.2`,Cordis
升级为上游要求的 `4.0.4`,同步更新依赖锁文件、Runtime 版本及打包测试夹具。
版本取自 npm 发布列表与 `next` 标签;查询时 `latest` 仍指向 `0.1.0-rc.6`。
控制面改为监听 `agent/assistant-stream` 的 chunk 帧,以转发文本、推理和用量;
工具结果从 message 层读取 `toolCallId` 和 `isError`。测试同步适配新版本的
Agent 配置、Shell 执行和系统消息接口。
- DS 定向回归:152 项通过、5 项跳过。
- 最终复跑 DS、Runtime discovery 和发布打包夹具:12 个文件通过、2 个跳过,
170 项通过、5 项跳过,耗时 19 秒。
- 扩大至打包夹具和设置面板:196 项通过、5 项跳过;设置面板套件因并行
Obsidian 修改缺少 `obsidian-tools` 模块而未能加载。
- `npm run smoke:deepseek-harness`:开发构建通过,真实 Electron Utility Host 返回 `ready`。
- 升级后的首次类型检查与 lint 通过;后续类型检查受到并行 Obsidian 修改中的
模块缺失、项目文件列表和类型错误阻塞,不能将当前整个工作区标为检查通过。
- 全量测试两次运行分别在 200 秒、600 秒超时,期间出现 App、SupervisorActivity
和 Electron UI 测试失败;未取得全量通过结果,未修改这些并行工作的文件。
本次依赖升级回归的真实供应商模型调用为 0 次;后续真实模型及 Web 实测见上节。上述升级验证不包含官方 Web 应用,
“打开DSH Web”入口仍按[原生客户端方案](../assistant-workbar/runtime-native-client-proposal.md)实施。
远程启动契约和资源打包仅支持 Continue、OpenCode,`create-runtime.ts` 拒绝 SSH
执行空间使用 DS,因此本次升级未改变远程 Runtime 路径,也未进行 Linux Host 验证。
## 2026-09-14 Windows 工作区路径修复
本机 Runtime 的 Host 启动与 `newSession.cwd` 已共用初始化时解析的 `realpath`,
@@ -7,7 +7,7 @@
| 文档状态 | 实现与发布验收基线 |
| 设计目标 | 将 DeepSeek Harness 作为 GoodBuddy 的第三个 Agent Runtime |
| Runtime 标识 | `deepseek-harness` |
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.2-rc.1` |
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.7-rc.2`,Cordis 为 `4.0.4` |
| 上游状态 | Developer Preview,允许出现破坏性变更 |
| 上游许可证 | MIT |
| GoodBuddy 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
@@ -19,7 +19,7 @@
DeepSeek Harness 的底层库使用 Cordis 组合服务。GoodBuddy 不采用官方产品 profile,也不允许用户配置覆盖内部 Host 或控制服务;GoodBuddy 自行维护 Host、控制协议和生命周期,同时提供一个由 Main 管理、默认关闭的 npm 插件市场。用户显式开启后,市场只搜索带精确 `dsh-plugin` 关键字的公共 npm 包,不代表 GoodBuddy 审核、推荐或承诺兼容这些包。
用户明确安装并启用的插件以当前用户权限运行。Ask/Execute 只控制模型经过 `tools/execute` 发起的工具调用:Ask 只允许 Host 中真实注册的 `read`、`skill` 和 Main 管理的 Web Search/Fetch 代理,Execute 放行 Host 中全部已注册工具。插件不能用同名工具冒充 Ask 允许项。插件安装脚本和初始化代码不属于模型工具调用,不能由 Ask 限制,因此界面在安装前必须明确确认这一边界。
用户明确安装并启用的插件以当前用户权限运行。Ask/Execute 只控制模型经过 `tools/execute` 发起的工具调用:Ask 只允许 Host 中真实注册的 `read`、`skill` 和 Main 管理的 Web Search/Fetch、Story Graph 只读代理,Execute 放行 Host 中全部已注册工具。插件不能用同名工具冒充 Ask 允许项。插件安装脚本和初始化代码不属于模型工具调用,不能由 Ask 限制,因此界面在安装前必须明确确认这一边界。
整体分成两个互相约束的部分:
@@ -145,7 +145,7 @@ Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不
Harness Session 只在当前 Runtime 进程生命周期内存在。释放 GoodBuddy 会话时必须同步释放对应 Harness Agent。
固定 Host 显式加载 `dsh-session-projection` 的内存 SessionProjectionRegistry,这是
`0.1.2-rc.1` AgentLoop 的必需依赖;不加载 Session persistence,也不新增磁盘会话副本。
`0.1.7-rc.2` AgentLoop 的必需依赖;不加载 Session persistence,也不新增磁盘会话副本。
该依赖与其余 DSH 依赖一起由 Vite 打入 Host,不能从 `app.asar.unpacked` 外部导入
仅存在于 `app.asar` 的 npm 包。最终打包检查及 Windows CI 的真实 UtilityProcess
启动探针见 [Runtime 资源与打包](../../../BUILD.md#runtime-资源)。
@@ -368,7 +368,7 @@ GoodBuddy conversationId -> Harness sessionId + workspace + process generation
### 9.5 跨项目复用
锁定 0.1.2-rc.1 的文件和 Bash/PowerShell 工具已按原生 Session header.cwd 路由,
锁定 0.1.7-rc.2 的文件和 Bash/PowerShell 工具已按原生 Session header.cwd 路由,
Provider 的根 cwd 只是默认值。不为每工作区复制本地 Provider;
Main 的 Web/MCP ToolProvider 按工作区保存,最后一个对应 Session 释放时 dispose。
@@ -386,11 +386,17 @@ Main 的 Web/MCP ToolProvider 按工作区保存,最后一个对应 Session
| Ask | 只允许 Host Registry 中真实注册的 `read` 与 `skill`;其他 Host/插件工具一律拒绝 | 能力启用时注册 Main 的精确代理对象,无逐次审批 | 不注册 | 模型工具调用保持只读 |
| Execute | 放行 Host 中全部已注册的内置与插件工具 | 能力启用时注册 Main 代理 | 按分配注册并经过既有 RuntimeAuthorizer | 不增加插件权限层,以当前用户权限运行 |
2026-09-28:内置 Story Graph 的三个读取工具在 Ask/Execute 均通过 Main 代理提供,
Host 只接受 Main 注册的具体工具对象。Main 逐次检查监督者启用、Harness 分配及请求项目范围,
不逐次审批。其他普通内置 MCP 仍不支持 Harness 分配;合同与证据见
[Story Graph 设计](../conversation-supervision/story-graph-mcp-design.md)和
[验证进度](../conversation-supervision/progress.md#2026-09-28-story-graph-只读工具)。
### 10.2 Ask 模式
- Harness Control Plane 在 `tools/execute` 分发边界识别当前 Session 和在途请求。
- 采用所有权感知的只读允许列表,只接受 Registry 中真实的 `read`、`skill` 和 Main 注册的 Web 代理对象;`write`、`edit`、Shell、MCP 及任意新插件工具默认拒绝。只比较工具名不足以授权,插件注册同名工具仍会被拒绝。
- Ask 不注册 Main 代理的 MCP 工具。
- 采用所有权感知的只读允许列表,只接受 Registry 中真实的 `read`、`skill` 和 Main 注册的 Web/Story Graph 代理对象;`write`、`edit`、Shell、自定义 MCP 及任意新插件工具默认拒绝。只比较工具名不足以授权,插件注册同名工具仍会被拒绝。
- Ask 不注册 Main 代理的自定义 MCP;Story Graph 使用共享只读合同,scope 的互斥分支转换为 Harness 支持的 `oneOf`,完整参数仍由 Main 的 Zod schema 校验。
- Web Search/Fetch 的凭据、传输与结果限制保留在 Main;Utility 只看到有界 schema 和结果。
- 只读不等于无限输出,读取仍受字节和工具结果上限控制。
- 插件安装脚本和 Cordis 初始化生命周期不经过 `tools/execute`。Ask 不能把已启用第三方代码变成沙箱,也不能保证第三方代码没有启动副作用。
@@ -405,7 +411,7 @@ Main 的 Web/MCP ToolProvider 按工作区保存,最后一个对应 Session
`goodbuddy/tools/list` 按 Session 保存本次发现的工具定义;`goodbuddy/tools/call` 复用该定义
校验参数与授权,不因调用健康工具而重新等待故障 MCP 的发现。发现重试留在清单刷新,
Session 释放及 Runtime dispose 清除内存定义,不持久化第二份能力状态。
- 所有工具调用仍作为活动事件记录;Ask 和 delegation 路径继续固定拒绝。
- 所有工具调用仍作为活动事件记录;自定义 MCP 的 Ask 和 delegation 路径继续固定拒绝。
### 10.4 Runtime OS 沙箱
@@ -418,7 +424,7 @@ Main 的 Web/MCP ToolProvider 按工作区保存,最后一个对应 Session
- 使用环境变量白名单构造 utilityProcess 环境。
- 不继承 `NODE_OPTIONS`、调试端口、任意 npm 配置、用户 `DSH_*` 覆盖或白名单之外的凭据。
- `DSH_TELEMETRY_DISABLED=1` 必须固定设置。
- `DSH_TELEMETRY_DISABLED=1` 必须固定设置。后台 Host 与独立 Web 启动器均叠加 `src/shared/runtime-privacy-environment.ts` 的通用遥测关闭配置,设置 `DO_NOT_TRACK=1`、`OTEL_SDK_DISABLED=true`,并关闭日志、指标和追踪 exporter。模型、MCP 和用户主动联网请求不受此配置限制。
- Harness Home 指向 GoodBuddy 管理的隔离目录。
- 不调用官方 `loadEnv` 或 `loadLayeredEnv`。
- API Key 由 Main 从加密设置中解析。
@@ -546,7 +552,7 @@ Runtime: GoodBuddy 内置 DeepSeek Harness
模型配置: 跟随 GoodBuddy · 企业网关(qwen-plus)
状态: 已就绪
路径: <受控 Host 路径>
版本: 0.1.2-rc.1
版本: 0.1.7-rc.2
执行权限: 当前用户权限
Host 始终由当前 GoodBuddy 版本提供,不存在自定义 Host 入口。
@@ -689,11 +695,11 @@ Renderer 只接收公开 npm 元数据和受管插件状态。任何凭据、完
- 二进制检测、版本解析和路径规范化。
- ACP 握手、事件转换和请求关联。
- 每个会话单请求、跨会话并行。
- Ask 在工具分发边界只允许真实注册的 `read`、`skill` 与 Main Web 代理,并拒绝同名冒充和任意新插件工具;Execute 放行插件工具。
- Ask 在工具分发边界只允许真实注册的 `read`、`skill` 与 Main Web/Story Graph 代理,并拒绝同名冒充和任意新插件工具;Execute 放行插件工具。
- 握手只接受明确的 `execution.mode = 'host'`。
- 未分配 Skill/MCP 不可见;分配后的 Skill catalog 可调用 `skill` 加载。
- 原生能力快照只包含 Host/插件原生 Skills,不包含 GoodBuddy 分配的 Skills 或 MCP。
- Ask 不注册 MCP 工具;Web 代理可用于 Ask 与 Execute;Execute 每轮刷新有界 MCP schema,并在调用前再次校验活动请求、模式、参数和 RuntimeAuthorizer 结果。
- Ask 不注册自定义 MCP 工具;Web 和 Story Graph 代理可用于 Ask 与 Execute;Execute 每轮刷新有界自定义 MCP schema,并在调用前再次校验活动请求、模式、参数和 RuntimeAuthorizer 结果。
- MCP URL、启动命令和凭据不进入 Utility 启动配置或协议结果。
- 未知授权结果失败关闭。
- 超时、取消、迟到帧和进程意外退出。
@@ -760,7 +766,7 @@ npm run build
- `deepseek-harness` 可被保存、选择、检测和显示。
- Runtime 详情卡内显示状态、路径、版本和当前用户执行权限。
- Skills 与自定义 MCP 设置可把能力分配给 DeepSeek Harness;请求级 GoodBuddy 内置 MCP 当前不支持分配,并在内置 MCP 卡片中以置灰、未选择状态明确显示。布局、键盘语义、文案和保存回显通过真机检查。
- 该阶段的 Skills 与自定义 MCP 设置可把能力分配给 DeepSeek Harness;当时请求级 GoodBuddy 内置 MCP 不支持分配,并在卡片中置灰。布局、键盘语义、文案和保存回显通过真机检查。2026-09-28 的 Story Graph 源码另接入 Harness 分配与 Main 只读代理,验证证据见上文专项记录。
- DSH 市场初始关闭且不加载 npm 目录;显式开启后可以搜索、安装、启停、配置和移除插件,安装前只出现一次准确的当前用户权限确认。关闭市场后已有启用插件继续运行,重新开启后管理状态不变。
- Ask 写入测试在 Runtime 边界失败。
- Ask 可使用已启用的 Main Web Search/Fetch,且插件无法通过同名工具绕过所有权校验。
+11 -5
View File
@@ -1,7 +1,12 @@
# 设备发现与共享
本功能是 GoodBuddy 桌面端管理附近设备、本机共享和 ShareServer 连接的唯一入口。默认连接
方式是局域网;网关由用户额外添加。功能不包含 ShareServer 的组织管理 Web 界面。
设备共享在测试阶段默认关闭,先在“管理应用 → 设备共享 → 应用设置”启用,再从“应用 →
设备共享”或管理列表的“打开”进入。当前首期连接本地未认证
ShareServer 元数据服务,默认地址为 `http://127.0.0.1:8787`,可注册本机、浏览设备与发布目录、
发布能力或知识的元数据并撤销本机 ID 的条目。设备 ID 只用于识别,不是所有权认证。
当前实现不执行远程能力,不读取、搜索或下载知识正文;来源模式和权限均为元数据声明。
局域网发现、配对、组织网关和远程调用仍是后续设计,不是首期交付内容。
## 文档导航
@@ -9,12 +14,13 @@
| --- | --- |
| [PRD](./prd.md) | 用户问题、范围、功能需求和产品验收 |
| [功能逻辑](./logic-design.md) | 不变量、状态、授权优先级和失败结果 |
| [UI 设计](./ui-design.md) | 设置入口、四个页签、配对与调用交互 |
| [UI 设计](./ui-design.md) | 当前应用入口、单页表单与列表,以及后续发现、配对界面 |
| [技术设计](./technical-design.md) | Desktop 进程边界、持久化、IPC、网络和验证 |
| [实施进度](./progress.md) | 首期实现、检查命令、真实 Electron 联调证据及未测范围 |
| [共享网络总体设计](../../architecture/share-network-architecture.md) | 跨端身份、能力、授权、任务、更新和协议权威 |
当前没有单独的 User Stories:正常、失败、撤销、权限和兼容场景已直接进入 PRD 验收与逻辑
决策表。功能尚未实施,因此不创建 `progress.md`。
当前没有单独的 User Stories:首期验收见 PRD 的 `FR-M1` 至 `FR-M5`;后续场景仍在原有
需求与逻辑决策表中保留。下列 Peer、配对、网关术语仅用于后续设计。
## 术语
+22 -1
View File
@@ -4,7 +4,7 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 状态 | 本地元数据首期已接入;完整共享网络为后续设计 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [设备发现与共享 PRD](./prd.md) |
@@ -12,6 +12,27 @@
本文负责桌面端状态、不变量和决策规则,不重复跨端协议定义。
## 当前首期规则
本节对应 [FR-M1 至 FR-M5](./prd.md#当前首期本地元数据)。后文发现、配对、授权和任务状态均为
后续设计,不适用于本地未认证元数据服务。
应用启用状态默认关闭,缺失设置按关闭处理,明确保存的启用状态在重启后保留。启用只开放
页面入口;打开页面才读取目录。关闭后菜单隐藏该项,当前页面返回对话;不清除设备设置、
目录或已发布记录,不停止已有请求。该应用开关不代替后续发现、广播和发布的独立状态。
本机 ID 在首次读取设置时生成并原子保存;并发首次读取共用一次加载。名称与服务 URL 更新
串行保存,不接受 Renderer 写入设备 ID。已存在设置无法读取或解析时直接报错,避免静默
更换身份。切换服务保持本机 ID,但目标服务必须有本机注册记录才能发布。
页面初次加载读取真实目录,空列表、加载中和读取失败分别展示。刷新失败保留旧列表并标记
可能过期,禁用发布和撤销;用户可修改配置或重试。每次请求固定使用开始时读取的服务地址。
页面操作期间锁定重复提交。保存失败保留设置草稿,发布失败保留条目草稿。
发布状态只有 `published` 与 `revoked`。撤销入口匹配本机 ID,Main 再查询本机发布列表并确认
条目 ID;这只是防止误操作,不是认证。撤销只改变元数据状态。能力类型发送三个 `false`
权限;知识类型按独立选择发送。目录读取无需注册,注册记录不表示在线。
## 1. 核心不变量
### INV-1 连接方式独立
+27 -1
View File
@@ -4,13 +4,39 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 状态 | 本地元数据首期已接入;完整共享网络为后续设计 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 适用产品 | GoodBuddy Desktop |
| 关联架构 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
| 关联文档 | [功能逻辑](./logic-design.md)、[UI 设计](./ui-design.md)、[技术设计](./technical-design.md) |
## 当前首期:本地元数据
2026-10-01 首期范围由 `FR-M1` 至 `FR-M5` 定义。后文 `FR-1` 至 `FR-14` 保留为后续共享网络
设计,不能用来推断首期已有发现、认证、调用或内容传输能力。
- **FR-M1 入口与设置**:设备共享为可选应用,测试阶段默认关闭,应用中心提供启用开关;
启用后才出现在应用菜单,关闭时退出当前设备共享页面。旧设置缺少开关时按关闭处理,已保存的
明确启用值保留。关闭不清除目录、设备设置或撤销已发布记录。本机设备 ID 稳定保存,用户可
修改服务 URL 和设备名称。设置保存后重启仍保留;新设备默认连接 `http://127.0.0.1:8787`。
- **FR-M2 注册与浏览**:用户显式注册本机,随后查看服务实际返回的设备及发布列表。注册
包含 ID、名称、平台、应用版本;时间来自服务记录,不从更新时间推断在线状态。
- **FR-M3 元数据发布**:注册本机后可发布名称、描述、能力或知识类型、设备或服务器来源。
知识的搜索、读取、下载三个标志独立选择。服务器来源不会上传、同步或托管内容;权限
标志不提供内容访问实现。
- **FR-M4 撤销与刷新**:只给匹配本机 ID 的已发布条目提供撤销入口,确认后更新服务记录并
刷新目录,显示已撤销状态。该筛选不构成认证,服务本身没有所有权鉴权。
- **FR-M5 失败恢复**:请求有超时,错误可见;失败保留用户输入,列表失败不伪装为空目录。
修改服务 URL 后必须先保存,再向新服务注册或发布。既有应用设置、排序和常驻偏好保留。
验收顺序为启用应用、保存配置、注册本机、浏览返回列表、发布知识元数据、检查三个独立标志、撤销、
刷新并重启读取设备 ID。网络请求由 Main 发出,Renderer 只调用显式 preload 方法。
## 后续设计
以下章节为完整共享网络计划,尚未实施。
## 1. 背景
GoodBuddy 的能力当前主要在本机执行,SSH 远程项目则要求用户预先配置具体 Host。个人和组织
+94
View File
@@ -0,0 +1,94 @@
# 设备共享实施进度
日期:2026-10-01。当前实现范围见 [README](./README.md)。
## 2026-10-01:测试阶段默认关闭
对应 FR-M1,新增持久化 `deviceSharingEnabled`,默认 `false`,旧设置缺少字段时补为关闭,
明确保存的 `true` 保留。应用中心提供启用 Switch,不增加常驻;菜单仅显示已启用项。
从应用中心或设置变更事件关闭时,App 退出设备共享页面并返回对话;设备设置、目录与已发布
记录保留。中英文文案、功能列表和导航文档已同步。
本轮验证:设置存储、配置工具契约及服务、应用中心、菜单和共享页面共 6 文件 112 项通过;
完整 `App.test.tsx` 321 项通过;设置面板、更新设置及笔记工作区 3 文件 246 项通过。
`npm run typecheck` 与 `npm run lint` 通过。首次 App 回归发现关闭逻辑误触其他设置导航,
限定为当前设备共享页后复跑通过;新增测试的查询参数类型错误也已修正。
设置存储回归覆盖版本 11/12 的字段缺失、显式开启/关闭、无关更新、事件与重读;App 回归
覆盖默认隐藏、启用、菜单打开、UI 关闭和外部事件关闭,重新启用不自动返回该页。
真实 Electron/ShareServer 用例 1 项通过,新增默认禁用“打开”、仅一个启用 Switch 的检查,
启用后继续注册、发布、撤销、刷新及浅深主题 480px 检查。该 Electron 用例的应用设置由组件
fixture 更新,持久化由真实设置存储测试覆盖,不据此声明完整生产 App 或安装包联调通过。
本轮未运行全仓测试,未改动 Agent/Runtime 路径,无模型调用。
## 首期接入记录
`a3b41bf` 已接入应用入口、Main 服务、可信 IPC、preload 与页面,
支持配置 ShareServer、注册设备、读取目录、发布/撤销元数据及本机 ID 持久化。
设备 ID 不是认证凭据,权限字段不代表已实现远程操作;正文传输、能力调用、
发现和配对仍不属于本期实现。
当前组合源码类型检查和 lint 通过。仓库包含服务、IPC、页面测试和需指定
`GOODBUDDY_SHARING_SERVER_ROOT` 的真实 ShareServer/Electron 联调用例。
本轮发布准备中的定向测试被用户中断,并要求直接推送交 CI 验证;
不据此声明测试或真实服务联调通过。发布结果以最终候选 CI 为准,
条件跳过的真实服务用例不能计入已验证能力。
## 2026-10-01:桌面实现轮次的独立验证
以下记录来自设备共享实现会话,独立于上文发布准备轮次;已实际运行,不作为发布 CI 或安装包
验收的替代。对应 [FR-M1 至 FR-M5](./prd.md#当前首期本地元数据)。
| 检查 | 结果与覆盖范围 |
| --- | --- |
| 定向 7 文件 | 114 项通过:Main HTTP 契约、稳定 ID、设置重读、可信 IPC、超时与错误、页面发布/撤销、输入保留、旧设置迁移与应用排序 |
| 完整 `App.test.tsx` | 320 项通过,含新增设备共享入口与既有设置 Modal 回归 |
| `goodbuddy-config-contracts.test.ts` | 8 项通过;修正旧四项写入样例,保留重复、未知及缺失项拒绝断言 |
| 真实 Electron/ShareServer | 1 项通过,使用相邻后端源码、独立端口和临时数据目录,真实窗口运行生产页面、preload、可信 IPC 和 Main 服务 |
| 类型与静态检查 | `npm run typecheck`、`npm run lint`、`git diff --check` 通过 |
Electron 联调从应用中心打开页面,注册本机,发布知识元数据,服务返回确认
`sourceMode=server`、`search=true/read=false/download=true`,随后撤销并刷新至
`status=revoked`。注册、发布、撤销确认和刷新使用原生鼠标输入;重读磁盘保持设备 ID。
浅深主题的 480px 视口均无横向溢出。首次窄窗口检查因系统缩放与预期 CSS 宽度不一致失败,
沿用现有 Electron 测试的固定设备缩放设置后通过。该测试是组件组合,未启动完整生产 App,
也未验证安装包、截图视觉效果或 macOS/Linux 窗口。
全仓 `npm test` 用时 760.82 秒:468 文件通过、13 文件跳过、2 文件失败;5,549 项通过、88 项
跳过、2 项失败。设备共享真实 Electron 联调在这次全仓运行中也通过。两项失败分别为:
- 配置工具样例仍提交四项顺序。运行期间修正后独立复跑 8 项通过;随后与服务测试合跑
11 项也通过,服务测试包含对尚不存在设置文件的并发首次读取。
- `inline-help.electron.test.ts` 的 `Hover Escape closed modal: dark 360x320` 断言失败。
未修改该文件或帮助组件,独立复跑 1 项通过,原因未确定;不把复跑结果写成全仓一次全绿。
完整日志位于本次工作机临时目录 `opencode/goodbuddy-sharing-full-tests.log`。没有再次执行整套
全仓测试。本功能测试没有模型调用。中文设备共享文档经 `deai-writing` 扫描无阻断项,复核项
中的权限边界、真实字段及测试列表按事实保留。
### 复现命令
在 GoodBuddy 仓库执行:
```powershell
npx vitest run src/main/device-sharing-service.test.ts src/main/device-sharing-ipc.test.ts src/main/application-settings-store.test.ts src/renderer/src/DeviceSharingPage.test.tsx src/renderer/src/ApplicationCenter.test.tsx src/renderer/src/ApplicationMenu.test.tsx src/preload/preload-sandbox.test.ts
npx vitest run src/renderer/src/App.test.tsx
npx vitest run src/shared/goodbuddy-config-contracts.test.ts
$env:GOODBUDDY_SHARING_SERVER_ROOT = (Resolve-Path ../goodbuddy-shareserver).Path
npx vitest run tests/device-sharing.electron.test.ts
npx vitest run tests/inline-help.electron.test.ts
npm run typecheck
npm run lint
npm test
```
跨仓 Electron 测试需要后端已安装依赖;未设置 `GOODBUDDY_SHARING_SERVER_ROOT` 时明确跳过。
测试会关闭自己启动的服务并清理临时数据,不写后端源码。
### 手工体验
在 ShareServer 仓库运行 `npm run dev:api`,在 GoodBuddy 仓库运行 `npm run dev`。先从“应用 →
管理应用 → 设备共享 → 应用设置”启用,再从应用菜单打开“设备共享”,默认地址为
`http://127.0.0.1:8787`。先保存名称或地址修改,再注册本机、
发布条目、检查目录、撤销并刷新。知识类型提供三个独立权限框;重启后可比较同一设备 ID。
服务关闭时刷新应显示错误并保留旧列表,不应显示为空目录或推断设备离线。
@@ -4,13 +4,47 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 状态 | 本地元数据首期已接入;完整共享网络为后续设计 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [设备发现与共享 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| 共享协议 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
## 当前首期实现
本节实现 [FR-M1 至 FR-M5](./prd.md#当前首期本地元数据)。后文模块树、SQLite 表、共享协议包、
局域网与网关协议都是后续设计,当前不创建这些基础设施。
`src/shared/device-sharing-contracts.ts` 定义严格输入和返回类型;`DesktopApi.sharing` 暴露
`getSettings`、`saveSettings`、`registerDevice`、`getCatalog`、`publish`、`revoke` 六个方法。
生产 preload 对应六个固定 IPC 通道,`device-sharing-ipc.ts` 对每次调用检查可信主窗口主帧,
对变更输入进行 Zod 校验,不暴露通用网络入口。
`DeviceSharingService` 在 Main 内使用 `fetch`,每个请求含响应体读取采用 10 秒超时,非成功
响应保留 HTTP 状态;响应再经 Zod 校验。请求只在页面读取或用户动作时发出,不启动后台发现。
URL 接受 HTTP/HTTPS,禁止内嵌凭据、查询和片段。没有令牌或 Agent/Runtime 改动。
| 方法 | HTTP 请求 |
| --- | --- |
| 注册 | `POST /api/v1/sharing/devices/register`,字段为 `id,name,platform,appVersion` |
| 设备列表 | `GET /api/v1/sharing/devices`,读取 `{devices:[]}` |
| 发布列表 | `GET /api/v1/sharing/publications`,读取 `{publications:[]}` |
| 发布 | `POST /api/v1/sharing/publications`,Main 生成条目 ID 并填入本机设备 ID |
| 撤销 | 先按 `?deviceId=` 获取本机条目,再 `POST /api/v1/sharing/publications/:id/revoke` |
设备 ID、名称与服务 URL 存在用户数据目录的 `device-sharing-settings.json`,版本为 1;
复用 `writeJsonFileAtomically`,加载 Promise 去重、保存串行。损坏设置报错而不重置 ID。
此文件不保存目录副本或凭据。导航沿用已有存储归一化,把 `device-sharing` 追加到旧顺序,
保留既有应用相对顺序与设置;新入口可排序,不增加常驻字段。应用设置中的
`deviceSharingEnabled` 使用共享 schema 的 `false` 默认值补齐缺失字段,显式 `true` 保留;
沿用现有版本与原子持久化、变更事件。更新 schema 将该字段设为可选,避免无关更新重置选择。
菜单复用启用过滤,App 收到关闭设置时返回对话并卸载页面。共享服务和目录数据不受此开关修改。
服务测试使用真实 loopback HTTP,验证契约、独立权限、识别范围、持久化、错误和超时;
Electron 测试通过环境变量选择相邻真实 ShareServer 源码,使用临时服务数据与桌面设置。
具体命令和实测边界见[实施进度](./progress.md)。
## 1. 进程边界
```text
+21 -1
View File
@@ -4,7 +4,7 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 状态 | 本地元数据首期已接入;完整共享网络为后续设计 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [设备发现与共享 PRD](./prd.md) |
@@ -14,6 +14,26 @@
本文只定义桌面端界面。所有颜色、间距、字体、通知、Switch、PageTabs、焦点和响应式行为以
全局设计系统为准。
## 当前首期页面
本节对应 [FR-M1 至 FR-M5](./prd.md#当前首期本地元数据),后文四页签、配对及调用界面为后续设计。
应用中心显示“可选”及启用状态,测试阶段默认关闭,此时“打开”禁用。应用设置复用共享
Switch,常显测试状态和关闭后果,不提供常驻开关。启用后应用菜单显示入口,关闭时退出
设备共享页面并返回对话。主内容区使用 `standard` PageShell、PageHeader 与全局范围徽标。
标题下常显未认证、ID 识别用途和无执行/内容访问的边界。内容依次为本机设置、已注册设备、
发布表单和发布目录。服务 URL 与设备名称可编辑,设备 ID 只读;注册、保存与刷新为次操作,
“发布元数据”为唯一主按钮。
类型与来源使用短标签原生选择框;知识的搜索、读取、下载使用三个独立 Checkbox。服务器来源
不上传或同步内容的说明常显。发布按钮附近显示已保存的目标服务及设备名称,尚未注册或
配置未保存时给出操作提示。列表展示服务返回的名称、标识、平台/类型、更新时间和发布状态,
不展示在线灯或执行按钮。
撤销使用共享危险入口和就地确认,说明保留记录且不删除本机内容。成功与操作失败通过应用
通知反馈;目录加载失败在页面显示并由刷新按钮恢复。宽度收窄时按钮换行,长 URL 和 ID 换行;
表单复用 `.field`,颜色与间距使用共享令牌。页面文案提供中英文。
## 1. 信息架构
设置中心新增一级分类“设备发现与共享”,使用 `standard` PageShell:
@@ -261,6 +261,29 @@ DeepSeek Harness 和托管 SSH 路径不注入这些工具。既有功能的 Win
MCP 代理复用 Provider 发现快照规则,托管 SSH Runtime 不经过该本机构造路径。本次未
修改远端 Agent、桥接或启动器。
## 2026-09-28 本地直连手动压缩
- 本地直连文本会话复用“压缩上下文”按钮、摘要状态和会话保存路径,支持关闭自动压缩时
手动执行。当前连接选择、图像排除、强制压缩和最新轮次保留规则见
[技术设计](./technical-design.md#101-本地直连手动压缩)。未修改远端 Agent、桥接或启动器;
model 入口排除远程会话与 SSH 项目,本地 IPC 仍拒绝 SSH 执行空间。
- 三个目标文件 `App.test.tsx`、`ipc.test.ts`、`model-runtime.test.ts`:569 passed、2 skipped。
覆盖入口点击、自动关闭和低于阈值、运行中禁用、图像排除、摘要保存与后续请求、当前及指定
摘要连接选择。补入远程通道入口排除后,App 手动压缩专项复跑 7 passed;最终类型检查和
本次文件 ESLint 再次通过。
- `manual-compaction.electron.test.ts`:真实 App、preload、IPC、模型工厂和 SQLite 路径通过。
2 次本机 HTTP fixture 请求验证较早历史被摘要替换、最新轮次保留、完整历史仍在数据库,
页面重载后摘要可读,新建 Runtime 实际使用该摘要。
- `manual-compaction-live.electron.test.ts`:真实配置文本模型最终通过,2 次请求完成摘要及
新 Runtime 续问。三轮探针累计 6 次真实文本模型请求;前两轮失败来自探针用未转义摘要
匹配 JSON payload,修正断言后通过。原设置文件未改变,未记录凭据。
- `npm run typecheck` 和本次涉及文件的 ESLint 通过。全仓 `npm run lint` 有 10 个错误,
位于已有侧栏探针 `sidebar-final-probe.ts`、`sidebar-final-run.mjs`,保留未修改。
- 全量 `npm test`:5407 passed、85 skipped、2 failed。失败分别为已有侧栏探针的新建按钮
溢出断言(`magic-notes-capture.electron.test.ts`)及魔法笔记布局 Electron 超时
(`magic-notes-layout.electron.test.ts`);手动压缩 Electron 回归通过。未执行构建或跨平台
真机测试,不将全仓验证记为全绿。
## 进度维护要求
- 只有真实生产路径完成并验证后才能勾选实施项。
@@ -451,6 +451,18 @@ type SubagentActor =
处理。Continue 接收完整历史,已有有效摘要只替换其覆盖的前缀,剩余消息全部交给原生
Runtime;GoodBuddy 不再额外保留最后 20 条或限制为 128,000 字符。
### 10.1 本地直连手动压缩
`agentCompactConversation` 接受 `model`,结果保留实际 provider,压缩策略为
`goodbuddy-summary`。摘要来源为 `current` 时,IPC 使用 `applyRuntimeSelection`
解析后的当前直连连接;来源为 `profile` 时,使用既有指定摘要连接配置。
图像连接不进入文本摘要路径,SSH 执行空间仍由本地 IPC 拒绝。
`ModelAgentRuntime.compactConversation` 复用 `prepareCompressedRequest` 的强制压缩,
跳过自动开关和触发阈值,保留最新轮次。返回的 `contextCompressionState` 沿用会话保存,
不删除历史正文。后续请求校验摘要覆盖范围后,用摘要替换已覆盖前缀;即使关闭自动压缩,
已保存的有效摘要仍会使用。
## 11. 性能
- Shell 探测按执行服务实例缓存,不在每轮模型调用重复探测。
@@ -21,6 +21,11 @@
Job、Subjob 和 Run 继续是内部对象,不创建树形 UI、独立路由或操作菜单。
本地直连文本会话复用输入区已有的“压缩上下文”按钮,不依赖原生 Runtime 快照,
也不因自动压缩关闭或上下文低于触发阈值而隐藏。运行中或压缩中按钮禁用;图像连接、
远程通道会话和 SSH 项目不显示直连模型压缩入口。完成后沿用应用通知及压缩后上下文估算,
摘要随会话保存。压缩行为见[技术设计](./technical-design.md#101-本地直连手动压缩)。
## 2. 设置中的能力目录
直连模型工具目录使用两个本机开发分组:
@@ -64,6 +64,10 @@ Main 先保存资产,再保存草稿或接受队列;消息、分支建立独
会话删除、项目删除及清空数据均核对引用,启动回收已无归属的记录和孤立 UUID 目录。
清理失败保留无引用数据库记录供下次重试,不回滚已接受的草稿。
引用保存按输入顺序去重并查询资产。已有引用的顺序、会话归属均相同,且资产无需从临时目录
转存时,直接返回,不重复开启写事务或删除、插入引用。空引用同样适用;顺序、归属或附件
发生变化时仍按原事务更新,临时资产仍须转存。
助手库 schema 39 标记新附件元数据,阻止旧客户端将其当旧格式读取;没有批量改写历史附件。
知识库 schema 13 新增 `metadata.parsedResultId` 表达式索引,结果按主键定位,不扫描全部文档。
知识库新结果先写入独立 resultId 目录,数据库正文/chunks 发布成功后替换引用,再清理旧目录。
@@ -50,7 +50,7 @@ reading 与知识库 master-detail 壳层,KnowledgeWorkspace 不再嵌套壳
| 应用中心 → 本机推理监控 | [应用定义](../../../src/renderer/src/ApplicationCenter.tsx)仍为四项;[监控](../../../src/renderer/src/LocalInferencePage.tsx)显示本地服务并加载/释放 OCR | 不承担远程 OCR 配置或管理;关闭监控入口不等于关闭解析能力 |
| 监控 → 模型与连接设置 | [IPC](../../../src/main/ipc.ts)的 `localInferenceOpenSettings` 只发出无分类的 `settingsOpen`;[App](../../../src/renderer/src/App.tsx)仅打开设置,[SettingsPanel](../../../src/renderer/src/SettingsPanel.tsx)默认分类为 `runtime` | 文案与默认落点不一致,未直达模型或文档解析;本轮仅记录,产品跳转未修复 |
| 聊天附件 | App 的附件按钮直接选文件;当前卡片显示名称、容量、缩略图或图标、文本 tooltip 和移除 | 尚无逐附件更多菜单、显式 OCR、解析结果预览、重解析或文档选图 |
| 知识库文档 | [DocumentsView](../../../src/renderer/src/KnowledgeWorkspace.tsx)提供任务、打开来源、失败文档重试和分块管理;来源行另有同步,库页头另有“用于当前对话” | 解析文档详情、成功文档重解析及选图须新增,不能把分块管理 Modal 当作已有完整解析预览 |
| 知识库文档 | 当时位于 `KnowledgeWorkspace.tsx` 的 DocumentsView 提供任务、打开来源、失败文档重试和分块管理;来源行另有同步,库页头另有“用于当前对话” | 解析文档详情、成功文档重解析及选图须新增,不能把分块管理 Modal 当作已有完整解析预览 |
| 新建选图目标会话 | App 的 `startNewConversation` 返回布尔值,拒绝消息通道项目,并在复用空会话或新建后切到聊天 | 预览保留、目标 ID 取得及返回确认仍需接线;直接调用不满足 US-I9 |
本轮更正文档中“既有文档详情”的描述,补齐菜单矩阵及 US-H16 / A-15;队列修改统一先
@@ -58,6 +58,12 @@ reading 与知识库 master-detail 壳层,KnowledgeWorkspace 不再嵌套壳
同步标明本轮范围;本地 OCR 仍为现有能力,HTTP 和新附件流程仍为规划。
未修改产品代码、探索脚本或既有应用排序,OCR、文本及视觉模型调用均为 0。
2026-09-27 模块归属复核:上述表格保留实施前状态。当前
[DocumentsView](../../../src/renderer/src/knowledge-workspace/DocumentsView.tsx)
已从工作区入口拆出,提供解析预览、返回列表、重新解析及预览内选图;工作区入口保留库选择、
检索与分块管理。文档与来源统一列表的行为及验证见
[知识库进度](../knowledge-base/progress.md#2026-09-27-文档与来源统一列表)。
## 2026-09-19 实现与验证
HTTP PaddleOCR-VL 已接入 `DocumentParsingService` 的生产 PDF/PPTX 路径,设置支持来源切换、
+17 -9
View File
@@ -6,15 +6,15 @@
## 1. 容器与共享组件
遵循根目录 [UI 设计系统](../../../UI-DESIGN.md),保留当前应用容器结构。下表依据
2026-09-18 源码核对,标为新增的内容需要实现,不能当作现有 API。
遵循根目录 [UI 设计系统](../../../UI-DESIGN.md),保留当前应用容器结构。下表的知识库
模块归属已于 2026-09-27 复核;各入口的实现与验证状态见[进度](./progress.md)。
| 位置 | 现有结构及设计用法 |
| --- | --- |
| 设置中心 | `SettingsPanel.tsx` 整窗 Modal 内的 `DocumentParsingSettingsSection.tsx`;继续使用 `SettingsCategoryHeader`、`settings-section`、`field`,不套第二个 PageShell 或一级标题 |
| 聊天 | `App.tsx` 的 `PageShell variant="reading"`;附件卡位于原 Composer 内,待发送队列仍在 Composer 外,不在助手工作栏增加附件栏目 |
| 知识库 | `App.tsx` 已提供 `PageShell variant="master-detail"`,`KnowledgeWorkspace.tsx` 内含库导航与 DocumentsView;需在文档区域新增解析详情及返回列表,不再嵌套页面壳层或新建知识库级页签 |
| 解析预览 | 新增共享内容视图,分别嵌入设置诊断 Modal、聊天附件 Modal 和知识库文档详情;共享数据和交互,外层容器负责标题、范围、关闭与滚动 |
| 知识库 | `App.tsx` 提供 `PageShell variant="master-detail"`,`KnowledgeWorkspace.tsx` 保留库导航并挂载 `knowledge-workspace/DocumentsView.tsx`;文档区域内切换解析详情与列表,不嵌套页面壳层或新建知识库级页签 |
| 解析预览 | `DocumentResultPreview.tsx` 分别嵌入设置诊断 Modal、聊天附件 Modal 和知识库文档详情;共享数据和交互,外层容器负责标题、范围、关闭与滚动 |
| 页签与选择 | `WorkspacePrimitives.tsx` 的 `PageTabs variant="segmented"` 承载正文、图片、详情;来源及过滤模式使用 `SegmentedControl`,不能用它替换页签语义 |
| 浮层 | Modal 沿用 body Portal、`activateModalFocus` 与 `trapTabFocus`;测试及附件菜单复用 `AnchoredMenu`,由共享 `FloatingPortal` 挂入所属 Modal 并在 top layer 绘制。通知遵循根 UI 规范 |
| 图文内容 | 复用 `MarkdownRenderer.tsx` 的排版、表格滚动和静态 HTML 处理边界;补齐受管图片与 OCR HTML 表格适配,现有 Markdown 渲染器并未直接支持这些资源 |
@@ -33,11 +33,11 @@
| 用户任务 | 主入口与路由 | 范围及目标行为 |
| --- | --- | --- |
| 配置解析与 OCR | 侧栏底部“设置” → “文档解析”(`document-parsing`) | 设备级共享配置,分别保留聊天与知识库工作流;HTTP 地址、凭据和高级选项只在此维护,不随当前项目或会话切换 |
| 管理本地 OCR 文件 | 同一“文档解析”分类的本地模型区 | 下载、校验、ZIP、删除和目录操作;全局模型下载源仍在“平台功能 / 通用设置”,与 OCR 来源选择分开 |
| 管理本地 OCR 文件 | 同一“文档解析”分类的本地模型区 | 下载、校验、ZIP、删除和目录操作;更换下载源入口直接打开“模型连接”页头的模型下载源弹窗,与 OCR 来源选择分开;弹窗交互见[模型下载源设计](../../architecture/model-download-source-design.md#7-模型下载源界面) |
| 查看或释放本地 OCR 内存 | “应用中心” → “本机推理监控”(`local-inference` Modal),或其常驻入口 | 只管理本机执行服务;应用启用及常驻控制入口,不控制解析功能;HTTP 服务不出现在本机服务列表,也不提供远程启停 |
| 添加文件或图片 | “当前对话” → “添加附件”,或输入框粘贴 | 先加入当前草稿,再按输入类型处理;上传按钮继续直接选文件,不改成先选 OCR 的菜单 |
| 查看聊天解析结果 | 草稿或历史消息的附件卡 | 规划中的共享预览;来源消息与目标草稿分别显示,历史输入只读 |
| 查看知识文档结果 | “知识库” → 选库 → “文档与来源”中的文档操作 | 规划新增“查看解析结果”和文档详情;返回恢复库、列表筛选和位置;不改变活动会话 |
| 查看聊天解析结果 | 草稿或历史消息的附件卡 | 打开共享预览;来源消息与目标草稿分别显示,历史输入只读 |
| 查看知识文档结果 | “知识库” → 选库 → “文档与来源”中的文档操作 | “查看解析结果”进入文档详情;返回恢复库、列表筛选和位置;不改变活动会话 |
| 把文档插图用于聊天 | 聊天或知识文档预览 → “图片” → “添加所选图片” | 明确目标会话草稿后复制;设置测试预览无此操作;已有“用于当前对话”仍负责知识库检索范围,不代替选图 |
从解析失败或未配置提示提供“前往文档解析设置”时,须直达 `document-parsing` 分类并保留
@@ -51,6 +51,10 @@
菜单项包含附件名称以区分多个文件。普通图片的“提取图片文字”可作为就近次操作,不能和
菜单形成两套状态或重复执行。菜单复用 `AnchoredMenu` 的定位、键盘和焦点恢复。
草稿与队列中的图片缩略图可点击进入共享图片查看器,优先显示已保存的完整图片,只有
缩略图时使用缩略图预览;查看不触发 OCR。队列附件菜单只提供打开原文件,不读取 OCR
来源或显示解析设置;修改发送方式须先恢复到草稿。解析结果入口仅在已有结果时显示。
草稿、历史消息和队列预览中的“查看解析结果”“更多附件操作”使用共享 `icon-button`
视觉,图标分别为文档查阅和省略号,点击区域为 `32px × 32px`,在独立操作行内排列。
按钮提供含文件名的可访问名称、悬停提示及键盘焦点提示;草稿移除和导入取消使用同尺寸
@@ -65,7 +69,7 @@
| 草稿图片,发送提取文字 | 查看解析结果、打开原图 | 改为发送原图、使用当前设置重新提取文字、移除 |
| 历史消息附件 | 查看已保存结果、打开原件 | 需要重解析或改变发送方式时先添加到当前可编辑草稿;不修改原消息 |
| 已入队附件 | 仅有附件时显示“查看附件”,查看入队时冻结的结果与发送方式;无附件时隐藏入口 | 修改整条输入使用“恢复到草稿”,按 L-H7 合并;不直接修改队列附件 |
| 知识文档 | 查看解析结果、打开来源、管理分块 | 使用当前设置重新解析;失败时使用重试,不再并列一个同义重解析按钮;来源同步仍属于来源级操作 |
| 知识文档 | 查看解析结果、打开来源、管理分块 | 重新解析;失败时使用重试,不再并列一个同义重解析按钮;来源同步仍属于来源级操作 |
| 设置测试结果 | 查看正文、图片、详情及原件 | 重新测试、关闭;没有加入草稿、知识库或发送操作 |
操作依据对象、资源和任务状态显示。解析中仅保留可用旧结果的查看、进度和取消,不允许重复
@@ -138,6 +142,10 @@
聊天标明会话,知识库显示库名与 `ScopeBadge`。下一行固定正文、图片、详情三个 PageTabs,
页签及直接包装层 `flex: 0 0 auto`,仅当前面板滚动。查看已存结果不发 OCR 请求。
知识库将原件与维护操作收进详情头部的共享菜单,并将添加图片工具限制在图片页;聊天与
设置预览保持原有入口。具体顺序、来源与解析时原件的区别及返回规则见
[知识库来源操作](../knowledge-base/ui-design.md#6-来源操作)。
正文面板按来源页或章节呈现 Markdown、HTML 表格及受管图片。页面导航显示真实页号;
PPTX 显示“幻灯片 N”,没有分页的原生文档显示工作表或标题定位,不生成假页码。点击图片
旁的“第 N 页”或“查看正文位置”切到正文并聚焦对应标题;正文图片的“在图片中查看”反向
@@ -172,7 +180,7 @@ reference 使用原路径。reference 原件失效时显示“原文件不可用
和“重试”;有部分产物时可以查看,但不能标为成功。关闭测试预览后释放无引用测试资源。
测试预览不提供“添加到会话”,防止诊断文件进入用户草稿或知识库。
“使用当前设置重新解析”是聊天草稿和知识库文档的次操作,点击前显示将使用的已保存来源。
“使用当前设置重新解析”是聊天草稿的次操作;知识库列表和预览中的同一操作简写为“重新解析”,仍使用当前已保存的设置。
历史消息和队列冻结已接受的输入;其预览只读。历史消息使用“添加到当前草稿重新解析”,
产生草稿附件而不改写已发送正文;队列修改使用“恢复到草稿”,避免原队列和修改稿重复发送。
重解析期间旧结果仍可查看,顶部显示“正在重新解析,
@@ -129,7 +129,10 @@ Main 进程职责、公共 API 对接、失败处理和验收方法。反馈平
> 以及你主动添加的截图。默认不上传桌面诊断;只有勾选后才会把有界诊断摘要追加到描述。
> 不会发送对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。
Checkbox 下方持续说明:诊断摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息。
Checkbox 下方持续说明:诊断摘要含时间、组件、阶段、稳定错误码、错误类型和固定短消息,
以及工具连接失败的类别、耗时、HTTP 状态、尝试次数、工具类型和匿名关联标识。
MCP 附加字段由 `normalizeDesktopDiagnosticRecord` 在写入、读取和反馈导出时按白名单
校验;关联标识为请求令牌的 SHA-256 摘要,不含原始令牌、地址或错误正文。
Renderer 不预览、不读取也不接收诊断内容或路径。勾选后详细描述上限为 3,398 字符,为附加
摘要预留 1,600 字符以及分隔换行;超长草稿保留在表单中并显示字段错误,不静默截断。
@@ -33,10 +33,28 @@
| `src/shared/external-knowledge-contracts.ts` | Provider 配置、实例与绑定输入、目录、测试结果和外部引用定位契约 |
| `src/shared/knowledge-contracts.ts`、`contracts.ts`、`assistant-contracts.ts` | 现有知识结果、列表和会话引用增加外部字段 |
| `src/shared/knowledge-reference.ts` | 预检索与 MCP 共用的引用转换和去重键 |
| `src/main/ipc.ts`、`src/preload/index.ts`、`src/shared/ipc-channels.ts` | 可信发送方检查、Zod 输入解析、类型化跨进程调用 |
| `src/main/ipc.ts`、`src/main/knowledge/knowledge-ipc.ts`、`src/preload/index.ts`、`src/shared/ipc-channels.ts` | 可信发送方检查、Zod 输入解析、类型化跨进程调用 |
| `src/renderer/src/ExternalKnowledge.tsx` | 实例 Modal、绑定表单、Provider 配置、外部详情与检索结果 |
| `KnowledgeWorkspace.tsx`、`App.tsx`、`KnowledgeCitationDialog.tsx` | 统一列表、聊天选择、刷新及历史引用显示 |
Renderer 的 `KnowledgeWorkspace.tsx` 保留库选择、页签路由、检索与分块管理状态及回调。
原有子组件位于 `src/renderer/src/knowledge-workspace/`:
| 文件 | 职责 |
| --- | --- |
| `DocumentsView.tsx` | 来源导入、文档列表与解析结果预览 |
| `KnowledgeSettingsView.tsx` | 索引、分块及图谱配置,向量索引状态轮询 |
| `KnowledgeTasksView.tsx` | 任务筛选、层级展示、取消和重试 |
| `GraphView.tsx`、`GraphEditors.tsx` | 图谱浏览、实体与关系编辑 |
| `KnowledgeGraphChartLoader.tsx` | 图谱画布延迟加载及失败重试 |
| `LibraryDialogs.tsx`、`RemoveSourceDialog.tsx`、`GraphDestructiveDialog.tsx` | 建库表单、库编辑与删除、来源移除及图谱破坏性操作确认 |
| `types.ts` | 工作区 Props 与组件间使用的类型 |
| `formatting.ts`、`helpers.ts` | 多个组件共用的本地化格式化、标签、别名解析、进度和错误处理 |
子组件直接导入所需模块,不反向依赖工作区入口。单个视图专用的格式化与标签留在该视图,
检索结果转换留在入口。模块拆分保留组件 Props、挂载位置、key 与副作用顺序;文档与来源
合并后的列表结构及操作归属见 [UI 设计](./ui-design.md#6-来源操作)。
`ExternalKnowledge.tsx` 的字段改用全局 `.field`,修正 body Portal 脱离知识库祖先后
丢失 scoped 控件样式的问题。CSS 提供 560px 紧凑弹窗和粘附底部操作区;实例选择复用
既有 picker 样式并实现菜单键盘行为。没有新增 Main 模块,具体控件规则由
@@ -911,8 +911,8 @@ Endpoint、编码配置或服务端稳定模型摘要变化。
- 目录只包含元数据,不包含模型权重。
- 每个来源的文件固定不可变 Revision、URL、字节数和 SHA-256。
- 实际下载使用“平台功能 → 通用设置”中显式选择的 ModelScope 或 Hugging Face,完整
来源契约以[平台功能页签与模型下载源设计](../../architecture/model-download-source-design.md)为准。
- 实际下载使用“模型连接”页头的模型下载源弹窗中确认保存的 ModelScope 或 Hugging Face;
向量模型的下载源入口直接打开该弹窗,完整来源契约以[平台功能页签与模型下载源设计](../../architecture/model-download-source-design.md)为准。
- 下载写入随机暂存目录和 `.partial` 文件。
- 禁止未声明重定向到其他主机。
- 每个文件边下载边计算摘要。
+63
View File
@@ -16,6 +16,69 @@
## 验证证据
### 2026-09-27 独立评审 P2 修复
- `DocumentsView` 将任务来源 ID 与维护用来源分开传递。目录和一对多 URL 的文档预览
只筛选当前文档任务,父批次作为上下文保留,同组 B 的任务不再因来源 ID 被纳入。
真正单文档合并行在列表和预览中均保留来源、文档双 ID;判定使用完整文档集合。
- 在飞状态由全局单 ID 改为按操作 ID 计数,完成或失败时只清理本次操作。来源暂停、
同步及文档操作使用各自 ID 判断禁用,A 同步或重解析期间仍可暂停 B。
- 新增 9 项行为回归:目录及一对多 URL 各覆盖列表、预览入口,并在搜索只剩 A 时确认
不纳入 B 或其他来源级任务;单文档合并预览覆盖双 ID。四组 deferred 用例验证
A 同步/重解析与 B 暂停的调用 ID、重复提交禁用、A 先完成及 B 先失败后的独立状态。
预览测试同时确认所属来源的同步和移除入口仍可用。
- `npx vitest run src/renderer/src/KnowledgeWorkspace.test.tsx src/renderer/src/DocumentResultPreview.test.tsx`:
2 个文件、66 项通过;`npm run typecheck`、`npm run lint`、`git diff --check` 通过。
- 本轮仅修改上述组件、工作区测试及本地知识库 UI/进度文档。未重跑全量或 Electron
渲染验收,未提交;此前全量结果及渲染验证边界见下方记录。
### 2026-09-27 文档入口与任务历史整理
对应 US-D1、US-D2、US-D4,完成文档行、解析预览与知识任务中心的界面修改。当前交互
以 [来源操作](./ui-design.md#6-来源操作) 和 [任务历史](./ui-design.md#8-任务批次与历史)
为准,替代下方同日早期记录中的原生折叠操作区。
- 合并 URL 行恢复地址,保留查询参数与片段;同标题、不同查询参数的两条 URL 已验证
可以区分、分别搜索并打开正确来源。
- 文档与来源行使用一个主要按钮和共享菜单,分块、任务、重解析、同步、暂停、重试及
移除继续可达。预览按返回、标题、维护操作分层,保留搜索、焦点与滚动恢复;解析时
原件与当前来源使用各自原有 API。共享菜单补充向下翻转及缩窗后的视口高度限制。
- 任务中心按真实父子记录计批次,对象筛选同步影响计数。历史包含已结束失败批次,
运行批次中的成功子步骤不会单独混入历史。取消、重试继续服从服务契约。
- 最终定向命令 `npx vitest run src/renderer/src/ApplicationMenu.test.tsx src/renderer/src/KnowledgeWorkspace.test.tsx src/renderer/src/DocumentResultPreview.test.tsx src/renderer/src/KnowledgeChunkManager.test.tsx src/renderer/src/WorkspacePrimitives.test.tsx tests/knowledge-layout.electron.test.ts tests/attachment-layout.electron.test.ts tests/overlay-layering.electron.test.ts`:8 个文件、106 项通过。
- `tests/knowledge-layout.electron.test.ts` 使用真实 DocumentsView、KnowledgeTasksView、
DocumentResultPreview、菜单与 CSS,覆盖浅深主题及 1100×720、720×720、420×480。
验证页面无横向溢出、表格内部滚动、菜单展开不改变行高、顶部菜单翻转、原生 End/Escape、
预览焦点恢复及历史默认折叠。截图已检查宽屏列表、窄屏预览与菜单、任务历史。
截图复核发现文档 Grid 原先会被表格最小宽度撑开;列宽改用 `minmax(0, 1fr)`,
并增加内部页面容器无横向溢出的断言,确保导入工具栏不随表格横向滚动。
隐藏窗口无截图时曾停在帧等待;改用可见窗口并确认真实焦点后,无截图运行通过。
- 附件布局和 Modal 浮层的真实 Electron 回归通过,验证共享菜单修改没有破坏这两个入口。
- 完整 `npm test` 最后一次完成:448 个文件通过、1 个失败、11 个跳过;5291 项通过、
2 项失败、83 项跳过。失败仅为下方已记录的 HeartbeatCenter 中英文 tablist 数量断言。
此后补回解析时原件菜单入口、补测锚点滚出视口,并修正窄屏 Grid,均通过上述最终定向回归。
- `npm run typecheck`、`npm run lint`、`git diff --check` 通过。布局 fixture 使用合成数据与桥接替身,
本轮未从完整 App 重做 UI → Main 的文件导入及真实解析服务验收;没有新增外部模型调用。
### 2026-09-27 文档与来源统一列表
在现有 `knowledge-workspace` 拆分基础上修改 `DocumentsView.tsx`,单文件、单 URL 与
唯一关联文档合成一行;目录及一对多来源保留组标题,零文档来源和孤立文档继续可见。
统一搜索、完整移除数量、次要操作折叠及预览返回规则见 [UI 设计](./ui-design.md#6-来源操作)。
`KnowledgeTasksView.tsx` 支持合并行同时传入来源和文档上下文,展示两者相关任务。
- `npx vitest run src/renderer/src/KnowledgeWorkspace.test.tsx src/renderer/src/DocumentResultPreview.test.tsx src/renderer/src/KnowledgeChunkManager.test.tsx src/renderer/src/WorkspacePrimitives.test.tsx`:4 个文件、94 项通过。
- 新增用例覆盖单文件与 URL 去重、来源和文档搜索、一对多过滤、零文档来源重试与暂停、
孤立文档、完整移除数量、合并任务入口、拖放,以及预览返回后的搜索、焦点和滚动恢复。
工作区测试使用解析预览替身;真实预览组件由同次运行的 `DocumentResultPreview.test.tsx` 验证。
- `npm run typecheck`、`npm run lint` 和 `git diff --check` 通过。
- 完整 `npm test` 运行 200 秒后超时,超时前输出 `HeartbeatCenter.test.tsx` 两项失败,
全量测试未完成。独立运行 `npm test -- src/renderer/src/HeartbeatCenter.test.tsx`
复现 40 项通过、2 项失败:zh-CN/en-US 的 `keeps tab titles unique...` 用例在第 213 行
期望 1 个 `tablist`,实际为 2 个。该失败与本次知识库修改无关,本轮未修改心跳中心。
- 未做真实 Electron 浅深主题、窄窗口、原生键盘及 UI → Main 导入验收。
未调用外部模型或知识服务,未提交改动。
### 2026-09-20 清理孤立的内存图谱搜索
- 删除仅剩测试调用的 `searchGraph`、专属评分/遍历辅助函数、搜索上限及测试 fixture。
+65 -2
View File
@@ -51,8 +51,8 @@ Conversation 的检索范围,返回对话并聚焦输入框。
- 向量状态。
- 启用图谱时的图谱状态。
失败文档保留错误信息和“重新处理”。只要仍有可用分块,就允许打开分块管理,避免隐藏
上一版可用索引。
失败文档保留错误信息和“重新处理”,入口位置见第 6 节。只要仍有可用分块,就允许
打开分块管理,避免隐藏上一版可用索引。
## 5. 问答效果与高级参数
@@ -64,6 +64,42 @@ Conversation 的检索范围,返回对话并聚焦输入框。
## 6. 来源操作
“文档与来源”使用一张按来源组织的表格。单文件、单 URL 来源只有一篇关联文档时,
来源与文档合成一行,以文档名为主标题,同步状态、同步时间和索引状态就近显示。
不再单独展示同名来源行,也不重复任务入口。目录及其他一对多来源保留来源组标题,
文档紧随其后;零文档和失败来源仍保留同步、暂停、重试及移除入口。
没有 `sourceId` 或找不到对应来源的文档独立显示。
搜索同时匹配来源名称、来源位置、文档名称和路径。来源命中时显示该组全部文档;
仅文档命中时保留组标题和命中文档。一对多关系按完整数据判断,搜索不能将其临时合成
单文档行。移除确认使用来源的完整文档数量,不使用搜索结果数。
导入文件、目录、URL 及拖放入口保持可用,拖放区使用紧凑的图标与文字提示。
合并行继续显示 URL,包含路径、查询参数和片段,使同标题的不同页面可以区分;
长地址允许换行。本地文件沿用文件名摘要。来源组中的 URL 同样可见。
每行只直接显示一个主要动作及一个“更多操作”按钮。有解析结果时主要动作是
“查看解析结果”;没有结果时,失败文档显示“重新处理”,其余文档有任务时显示
“查看任务”,否则显示“打开来源”。来源组优先提供失败重试或查看任务,没有任务时
提供同步或暂停。状态与错误始终在行内可见。
更多操作复用 `AnchoredMenu`,依次放置打开来源、打开解析时原件、文档分块、查看任务,
再用分隔线区分重新处理、来源同步和移除来源。仅在有对应资源时显示原件或任务入口;
没有分块时禁用分块入口。来源组的菜单提供任务及来源维护。菜单支持方向键、Home、End、
Escape 和焦点返回,展开不改变行高;空间不足时翻转方向或在菜单内滚动。
大小与分块数保持单行,窄窗口在表格内部横向滚动。
操作文案使用“打开来源 / 重新解析 / 同步 / 暂停 / 重试 / 移除来源”,不附带对象名称;更多操作按钮的可访问名称和移除确认框仍保留对象名称。
列表和预览的操作菜单宽度为 `220px`,小视口下收窄并保留左右各 `16px` 边距。
解析预览头部按返回、文档标题与库名及全局范围、维护菜单排列,地址仍可辨认。
正文、图片和详情使用共享结果组件;知识库的会话选择及添加图片工具仅在图片页显示,
切换页签保留选择。返回列表恢复搜索、原触发按钮焦点及页面滚动位置。重新解析继续
调用文档重建,使用当前设置并保留上次结果,失败时显示可恢复错误。维护菜单同时保留
当前来源与解析时原件的打开操作,两者分别读取知识来源和已保存的解析资产。
在飞操作只禁用对应的操作入口。同步或重解析 A 期间,其他来源 B 的暂停仍可用;
并发操作分别恢复可用状态,A 结束不能提前解除仍在执行的 B。
文档行提供“打开来源”,Main 根据知识库和文档归属重新读取保存的文件路径或 URL。
移除来源使用确认对话框,说明文档数、索引、图谱证据和托管副本影响。初始焦点位于
取消,提交期间禁止重复操作,失败时保留对话框和错误。
@@ -77,3 +113,30 @@ Conversation 的检索范围,返回对话并聚焦输入框。
均切换一次选择;选中背景和整行焦点环保持可见。Tab 聚焦、Space 切换,Escape 关闭并
返回触发按钮。点击行内非焦点元素时产生的空目标失焦不关闭弹层;点击外部或焦点明确
移到外部时才关闭,避免原生标签完成勾选前被卸载。
## 8. 任务批次与历史
任务中心按服务返回的 `parentTaskId` 组织一次操作及其文档、阶段子任务。顶层操作按
创建时间倒序排列,一个操作计为一个批次;缺失父记录的旧任务仍独立显示。重试产生的
新操作保留真实历史,使用已有 `attempt` 显示尝试次数,不按标题猜测合并关系。
文档或来源进入任务中心后,列表与计数使用同一对象范围,并保留命中任务的父级上下文。
合并文档行同时匹配来源 ID 和文档 ID,包括没有来源 ID 的文档任务。“清除对象筛选”
恢复当前库的任务集合。
列表与解析预览使用相同的文档任务范围。目录及一对多来源下的文档只按文档 ID 筛选,
即使搜索后只剩一篇也不扩大到整个来源;只有真正的单文档合并行同时包含来源 ID。
预览菜单中的来源同步、暂停和移除仍作用于所属来源,来源维护范围独立于任务筛选。
- 进行中:批次内有排队或运行记录。
- 失败:批次内有失败或中断记录;运行批次也可能同时存在失败子任务。
- 历史:批次内没有排队或运行记录,包含完成、失败、中断、取消和跳过。
计数均按批次计算,失败与历史可以重叠。标题旁的共享帮助解释口径。运行批次中已成功的
阶段不会单独进入历史。切换到进行中或失败时展开相关批次;全部和历史默认收起子任务,
用户仍可逐项展开。批次状态保留服务原值,子任务失败以额外文字提示,不改写父状态。
只有进行中的记录显示进度条;失败和中断保留停止阶段、错误及处理建议。已结束记录
不再显示“当前阶段”、百分比或默认“等待处理”。摘要保留时间、服务消息、子任务数及
重试次数。取消和重试严格使用服务返回的 `canCancel`、`canRetry`,提交期间禁用重复
操作,失败后保留筛选、任务内容及就地错误。
@@ -8,8 +8,17 @@ stdio MCP 提供明确、可诊断的 Node.js 与 Python 环境,同时允许
用户从设置中心的“能力与工具”进入,并在 `Skills`、`MCP`、`工具执行环境` 三个水平
Tab 之间切换。
[Obsidian 接入](../obsidian/README.md)复用本功能的 `LaunchEnvironmentProvider` 启动
随包交付的 MCPVault,不新增 Node 安装或下载源设置。生产组件自动化已覆盖真实 MCPVault
调用,Electron 托管 Node 离线冒烟仍待完成,具体见[验证记录](../obsidian/progress.md)。
## 文档导航
Local native Continue and DS Web clients now honor the current custom Node
selection. They require standard Node rather than Electron's Node mode; the
remaining managed fallback and validation are tracked in
[native client progress](../assistant-workbar/progress.md#2026-09-27-local-native-client-validation).
| 文档 | 权威职责 |
| --- | --- |
| [PRD](./prd.md) | 用户问题、产品目标、范围、需求和产品验收 |
@@ -81,6 +81,49 @@
## 已验证证据
### 2026-09-27 MCP 编辑器遮挡修复
真实 Electron App 经“设置 → 能力与工具 → MCP → 自定义 MCP → 添加 Server”复现。
修复前编辑器层级为 `70`、设置为 `130`,输入框中心命中设置页签容器;名称输入框虽已
获得焦点,父设置仍为 `inert=false`。添加和编辑现接入共享 Modal 层级与背景隔离,
遮罩关闭保留触发按钮焦点。没有修改 Runtime、Agent 或 MCP 传输实现。
`overlay-layering.electron.test.ts` 新增真实 App 用例,复用生产 preload、IPC、
`CapabilityService`、设置存储与 `BrowserService`。已通过的场景:
| 场景 | 验证结果 |
| --- | --- |
| 无浏览器,浅色 `1280 × 900`、深色 `960 × 420` | 添加/编辑鼠标命中、文字输入、父设置隔离、Tab/Shift+Tab 循环通过 |
| 右侧原生浏览器打开,浅色 `1280 × 900`、深色 `1280 × 420` | 设置及 MCP 期间原生视图隐藏;设置关闭后恢复相同 webContents ID、URL 和页面内标记,无重建或刷新 |
| 关闭与焦点 | X、取消、Escape、遮罩、保存成功返回原触发按钮;关闭子层保留设置,关闭设置返回其入口 |
| 持久化 | 四次 UI 保存经生产 IPC 写入临时配置,新建 `CapabilityService` 重读四条记录;编辑重开回显名称 |
原有 overlay Electron 用例也通过,继续覆盖解析菜单、二级结果、通知、应用中心、本机
推理监控和原生 `dialog`。回归禁用 Windows 原生窗口遮挡节流,避免其他桌面窗口暂停
测试的帧等待;未改变产品窗口选项。浏览器页面使用本地 HTTP 服务;MCP 测试配置保存为
停用状态,不启动外部 MCP 命令。真实模型调用 **0 次**。
证据范围为 Windows x64 开发态 Electron。无关启动服务使用 harness 替身,未配置的
向量模型与 Runtime 原生能力服务会返回不可用;未验收这些服务、外部 MCP 连接、远程
Agent 或其他操作系统。短窗口从已打开设置调整尺寸;宽度跨越工作栏自动收起阈值的路径
不作为浏览器保持打开的验收场景。
最终 focused 集合为 6 文件、215 项通过;`npm run typecheck`、`npm run lint` 和
`git diff --check` 通过。本轮未运行全仓 `npm test` 或打包。验证命令:
```text
npx vitest run tests/overlay-layering.electron.test.ts src/renderer/src/McpSettingsSection.test.tsx src/renderer/src/SettingsPanel.test.tsx src/renderer/src/FloatingPortal.test.tsx src/renderer/src/WorkspacePrimitives.test.tsx src/renderer/src/RightAssistantSidebar.resize.test.tsx
npm run typecheck
npm run lint
git diff --check
```
调试期间的失败记录:旧代码在真实鼠标命中断言失败;接入共享焦点后补修遮罩关闭的默认
焦点转移。harness 的帧等待曾受 Windows 窗口遮挡节流影响,浏览器导航曾在创建或输入
状态提交前触发;分别关闭测试进程的遮挡节流,并等待可用控件与实际页面加载后验证。
这些失败未计为产品验收通过。中文文档扫描无阻断项;本节的并列项是实测关闭入口和覆盖
组件清单,保留以便核对。
### 2026-09-03 `v0.12.0` 失败标签恢复
- 不可变标签工作流 `33704483505` 的 Windows x64/ARM64 与 macOS x64/ARM64 托管 Python
@@ -279,6 +279,14 @@ stdio MCP:
Tab、不渲染占位面板。
- 页签直接包装层使用 `flex: 0 0 auto`,并由 CSS 回归测试保证在滚动设置布局中不可收缩。
`McpSettingsSection` 的添加与编辑共用 body Portal,遮罩使用 `--z-dialog`。
打开前记录并聚焦触发按钮,再由 `activateModalFocus` 隔离父设置、聚焦名称并在关闭时
恢复原隔离状态与焦点;键盘事件在子层停止传播。遮罩的 `mousedown` 关闭分支阻止默认
焦点转移,避免恢复到触发按钮后又失焦。原生浏览器继续复用
`browser-viewport-occlusion.ts`,无需 MCP 专用隐藏条件。
真实 App 回归位于 `tests/overlay-layering.electron.test.ts`,使用生产 preload、IPC、
配置存储及 `BrowserService`,验证添加、持久化、编辑、父层隔离与原生浏览器恢复。
### 11.2 工具环境状态与 IPC
Renderer 接收:
@@ -113,6 +113,12 @@ MCP 面板
- MCP 原有四个内层页签继续保留,外层和内层使用不同的 `aria-label` 与 `idPrefix`。
- 窄屏时外层页签保持单行并允许水平滚动,不折成多行,不替换为下拉框。
“MCP / 自定义 MCP”的添加和编辑共用设置上方的子 Modal。打开后聚焦名称输入框,
父设置与工作区保持不可交互;Tab 和 Shift+Tab 在编辑器内循环。X、取消、Escape、
遮罩及保存成功均返回原添加或编辑按钮,Escape 只关闭编辑器。保存期间禁止关闭,
失败保留输入。右侧浏览器沿用应用级 Modal 的视图释放规则,编辑器关闭后仍由父设置
保持隔离,设置关闭后恢复网页。
## 4. 工具下载源
使用原生 Radio Group:
+27 -5
View File
@@ -6,9 +6,12 @@ metadata and indexes, and local files for entry bodies and binary assets.
The open workbench subscribes to successful writes so users can see external
changes without leaving the page or clicking a refresh button.
The workbench opens on a responsive note-card overview. Opening a note replaces
the overview with a continuous record stream, a narrow left index and independent
right AI comments. Both side panes can collapse. Index items show one real
The desktop workbench keeps a compact all-notes list on the left and the selected
note on the right. The list supports search, creation, pin/delete menus, resizing
and collapse. Switching notes preserves list position and remembers each note's
reading position while the workspace remains mounted. Narrow workspaces switch
between list and detail. Note detail contains a continuous record stream, a narrow
record index and independent AI comments. Both detail side panes can collapse. Index items show one real
first-page canvas thumbnail with page count, or a text summary; clicking scrolls
without replacing the editor or losing a draft. Opening a note shows a blank composer
above the history, with text/canvas selection and Save. Creating a record resets
@@ -19,16 +22,35 @@ comments. The divider can be dragged and remembers its width; each note group
can independently collapse its tasks while retaining its title and count.
Narrow containers use on-demand detail with an explicit return to the
mounted list. One content-sized button switches to the other view in the page
header immediately before New note; status filters sit beside search and wrap
header when note detail is closed; New note is always the primary page-header
action to the right of the view switch or Back to overview. The notes list
heading keeps only Manage tags. Status filters sit beside search and wrap
when needed. Note detail removes the surrounding frame and stream padding while
keeping editor borders and the AI divider. Returning from a source note restores the task
context and protects unsaved editor changes. A successful save followed by no
further edits does not prompt again on return. Narrow note layouts use an index
drawer and bounded bottom AI pane.
Conversation capture and the compact Notes workbar panel are implemented. Users
can preview and edit one assistant response or the complete conversation text,
then append it to an existing note or create a note with its first entry. Quick
notes opens the same panel for search, browsing and text appends. Saved entries
carry structured conversation sources with return navigation. The panel and full
workspace share storage and change notifications; drafts remain in App memory.
Real Electron acceptance passed through the App UI, production preload, IPC and
SQLite. Repository validation is not all green; the [progress record](./progress.md)
separates focused passes, repaired schema fixtures and execution-time failures.
## Documents
- [Technical design](./technical-design.md): file storage, schema 38 migration,
- [Conversation integration PRD](./prd.md): scope and acceptance for
conversation capture and the compact workbar panel.
- [Conversation integration logic](./logic-design.md): capture boundaries, source
lifetime, panel state, draft protection and application availability.
- [Conversation integration UI](./ui-design.md): implemented entry points, compact
panel, capture preview and source navigation, following the root UI system.
- [Technical design](./technical-design.md): file storage, schema 38 body migration,
schema 48 source metadata, desktop search and atomic initial-entry creation,
backup boundaries, database notifications, process boundaries, canvas and AI
contracts, refresh lifecycle and regression coverage.
- [Progress](./progress.md): implementation and validation evidence.
+35
View File
@@ -0,0 +1,35 @@
# 会话联动逻辑设计
状态:已按当前实现核对,验证进度见 [progress.md](./progress.md)。需求编号来自 [PRD](./prd.md)。
## 内容与来源
FR-2、FR-3:单条采集读取所选助手消息的非空正文;整段采集先加载该会话完整历史,再按消息顺序收录用户和助手的非空正文,保留角色分隔。预览建立后,切换会话或收到新消息不会替换正文及来源标识。生成中的消息不可采集,停止或失败后已有正文可保存,单条预览注明回答未完成。整个会话正在生成时禁用整段采集,提示等待完成或停止生成。
首版按文字保存,保留原始 Markdown 文本、换行和代码文本,不承诺还原聊天渲染效果。推理、工具调用日志、系统提示、附件二进制和图片不进入正文;预览来源旁提供保存范围帮助,按需展开。用户可编辑预览,来源仍指向本次采集的会话。读取完整会话失败时不以屏幕上已有消息代替。
FR-5:来源独立于可编辑正文。单条来源包含会话和消息标识;整段来源包含会话标识、此次收录的消息标识及采集时间。预览使用采集时的会话标题和项目名称;保存时主进程校验消息归属,并以当时数据库中的会话标题和项目名称写入来源,无项目时显示全局。若预览期间源会话或消息已删除,保存失败并保留预览。保存完成后,修改或删除源消息不回写笔记正文和来源。
点击来源时读取完整会话。消息不存在但会话仍在时打开会话并提示“原消息已不存在”;确认会话不存在后保留来源文字,并在当前来源条禁用跳转。其他读取失败显示可重试错误。打开未渲染的来源消息时扩大聊天历史显示范围,再滚动定位并聚焦。编辑笔记正文不移除来源;删除笔记不影响源会话。
## 面板与草稿
FR-1、FR-4:笔记应用为可关闭、可排序的单实例工作栏 Tab,新布局默认不打开。再次触发入口激活现有 Tab;笔记选择属于应用级状态,切换项目或会话不改写它。列表按置顶优先、最近更新时间排列,搜索匹配标题与记录文字,同一笔记只显示一次。当前应用会话内保留最近选择,重启不恢复笔记选择或草稿。该笔记已删除时回到列表,清除失效保存目标并保留待保存文字。
FR-2、FR-3:面板有浏览、采集编辑、保存中三个状态。触发采集进入编辑态;选择目标只改变写入位置;明确点击“加入笔记”才写入。每次提交新增一条记录,不覆盖旧记录。新建目标时先填写标题,会话标题作为可修改默认值;提交一次创建笔记和首条记录,取消不遗留空笔记。普通“新建笔记”仍可显式创建空笔记。
保存中禁用重复提交和面板离开操作,结果返回后可再次操作。成功后清除本次草稿,显示响应返回的新增记录;失败保留预览、标题和目标。目标已删除时提示重新选择。容量校验使用现有文字记录限制,超限由用户缩减正文后重试;一次提交始终对应一条完整记录。
关闭 Tab、返回面板列表、从面板打开完整工作区、禁用应用或以另一条采集替换草稿前,显示“继续编辑 / 放弃草稿并切换”确认。普通主页面、项目和会话切换以及隐藏工作栏保留 App 持有的草稿及来源。取消采集只在正文或标题相对预览初值有修改时确认;未修改预览可直接取消。完整工作区自身的编辑草稿仍由其离开确认保护。
FR-6:成功写入触发现有通知刷新视图;面板重新挂载或工作栏重新展开时读取最新数据。面板挂载期间保留搜索词;笔记选择和草稿由 App 保留,不受搜索响应或后台刷新覆盖。既有记录编辑继续使用原修订号处理冲突。此处同步仅指本机共享存储的视图更新。
## 启用与评论
FR-1~FR-4:遵循魔法笔记应用的启用状态。禁用时隐藏会话采集及工作栏目录入口,并移除笔记 Tab;禁用前处理草稿。外部设置变更要求禁用时,选择继续编辑会尝试恢复启用设置,失败显示设置错误。应用未常驻不影响会话采集或工作栏可用性。设置继续由应用中心管理。
会话采集和快速追加本身不调用模型。保存后沿用现有 AI 评论模式;只有已有设置要求保存后自动评论时才触发,失败不撤销保存,也不自动重新追加。
## 完整性检查
当前实现覆盖首次创建、追加、取消、重复点击、写入失败、目标删除、来源删除、跨会话切换及刷新与草稿的关系。完整历史读取、来源定位和存储接口已接入;产品范围无待决项。真实 Electron 已验证采集、保存、定位、窄面板键盘操作和持久化,具体范围见进展记录。全仓测试和类型检查仍有执行时记录的失败,聚焦复跑通过不等于全仓通过。
+39
View File
@@ -0,0 +1,39 @@
# 魔法笔记与会话联动
状态:会话联动前后端已实现,真实 Electron 验收通过;全仓验证未全绿。聚焦结果、fixture 修复后复跑及执行时的全仓失败见 [progress.md](./progress.md)。
## 目标与范围
用户在阅读回答时,可将内容保存到已有笔记或新笔记,并从笔记返回原会话。会话界面提供文字预览和保存位置选择,与已有 Agent 笔记工具共用存储。
独立工作区继续承载完整编辑、画布、待办和 AI 评论;右侧笔记面板提供浏览和快速追加。笔记仍属于全局,不因入口所在项目改变归属。
## 功能要求
| 编号 | 要求 |
| --- | --- |
| FR-1 | 右侧助手工作栏增加“笔记”应用,支持搜索、打开已有笔记、新建笔记、查看记录摘要、快速追加文字,以及打开对应完整工作区。 |
| FR-2 | 助手消息操作栏提供“加入笔记”,打开可编辑预览,用户选择已有笔记或新建后明确提交。 |
| FR-3 | 会话标题的更多菜单提供“保存当前对话到笔记”,按触发时的会话内容生成预览,保存为目标笔记中的一条记录。 |
| FR-4 | 通过右侧工作栏“+ → 笔记”打开面板并恢复当前应用会话中最近选择的笔记,首次使用显示列表,不隐式保存当前对话。左侧底部和聊天页头不再提供重复的“快速笔记”入口。 |
| FR-5 | 从会话保存的记录保留结构化来源,显示会话名称、所属项目(如有)和采集时间,支持返回原会话及单条来源消息。源会话删除后保留笔记内容。 |
| FR-6 | 面板、完整工作区和 Agent 写入使用同一份笔记数据,成功写入后更新已打开视图,同时保护未保存草稿。 |
| FR-7 | 笔记支持手动标签。每篇最多 10 个,单个标签最长 24 字,不区分大小写,同名只保留首次写法。在笔记标题下添加或移除标签;添加时提示已有标签,也可以直接新建。 |
| FR-8 | 总览可按标签筛选,多选时只显示同时带有全部所选标签的笔记;搜索框也能匹配标签名。打开笔记后,左侧列表沿用当前筛选,并提供清除入口。 |
| FR-9 | 提供“管理标签”入口,列出全部标签及使用篇数。可以重命名标签;新名称与已有标签相同时,两个标签合并。删除标签时,它会从所有笔记中移除,笔记本身保留。 |
| FR-10 | Agent 笔记工具可在创建、修改笔记时设置标签,并可按标签列出笔记。只在用户要求时修改标签,不自动打标签。 |
标签第一版不提供颜色和层级(如“工作/项目A”),“/”只作为普通字符保存。没有笔记使用的标签会自动删除。
首版不包含自动总结、自动分类、自动选择保存目标、选中文本采集、多条消息勾选、附件文件复制或跨设备同步。“总结后加入”和选区采集作为后续扩展,不预留不可用按钮。
## 产品验收
- 首次使用且没有笔记时,可从一条助手回答创建笔记并保存首条记录;已有笔记时可搜索并追加,原记录保持不变。
- 保存当前对话包含未在屏幕上渲染的历史正文;预览范围明确,超出容量时提示缩减内容,不静默截断。
- 保存成功后显示目标笔记并定位新增记录;取消不写入,失败保留内容和目标供重试。
- 从来源条可返回对应会话;会话已删除时显示“原会话已不存在”,已保存正文仍可阅读和编辑。
- 面板追加后完整工作区可见,Agent 修改后面板可见;后台刷新和切换会话不覆盖未保存文字。
- 给两篇笔记加上“Work”和“work”,只会得到一个标签,计数为 2。选中两个标签后,只显示同时带有这两个标签的笔记。把“Work”重命名为已有的“发布”后,两个标签合并,两篇笔记都不会丢失。删除标签不会删除任何笔记。
状态与内容范围见 [逻辑设计](./logic-design.md),入口和操作细节见 [界面设计](./ui-design.md),接口与存储见 [技术设计](./technical-design.md#conversation-integration)。
+268 -2
View File
@@ -1,5 +1,266 @@
# Magic Notes Progress
## 2026-10-03: Compact Notes List And Narrow Drawer
The notes list now defaults to 200px with a 160px minimum. At workspace widths
of 600px or more it can remain beside detail; below 600px the toolbar toggle
opens the mounted list as an overlay drawer. Escape, outside click, the close
button and note selection dismiss it. Existing desktop width preferences remain
intact. Scroll restoration now runs after read-only content effects populate the
record, fixing a failure exposed by the Electron layout regression.
Validation: 143 workspace and list-window tests passed, along with the real
Electron layout test covering native resizing, drawer geometry, Escape focus
return and note switching. Typecheck and lint on changed code passed. Repository
lint was blocked by concurrent App.tsx changes (max-lines error and a missing
conversationStore hook dependency warning). No provider calls were made.
## 2026-10-01: Header New Note Action
New note is now one primary page-header button that always follows the view
switch (overview, to-dos) or Back to overview (note detail). The `+` icon in the
notes list heading was removed; the heading keeps only Manage tags. From note
detail the button opens the list's title form (desktop reopens a collapsed list;
narrow returns to the list first). Workspace tests (133) and the Electron layout
test pass, with overview and detail screenshots checked at 1280px; the App note
subset (11), typecheck, and lint on changed files pass.
## 2026-09-30: Navigate After Record Mount
Quick Notes navigation now scrolls after React commits the target record, rather
than scheduling from the data request's completion. A regression that runs the
animation-frame callback immediately fails before this fix and passes afterward.
The complete App/workspace tests and Electron navigation/compaction checks pass:
450 tests across four files, plus typecheck and lint. No provider calls were made.
## 2026-09-30: Manual Note Tags
Implemented FR-7 to FR-10 as described in
[Note Tags](./technical-design.md#note-tags): schema 51 tag tables, tag
replacement on note update, AND filtering, rename/merge/delete with note
revision bumps, IPC and preload methods, the overview filter row, card chips,
note-header tag editor, the "Manage tags" dialog, and tag support in
`note_create`, `note_update` and `note_list`. No colours or hierarchy.
- New tests: database tag lifecycle (`assistant-database.test.ts`), MCP tag
normalization and filtering (`knowledge-mcp-gateway.test.ts`), and four
workspace tag tests (filtering, search, keyboard add/remove, manager
rename/merge/delete).
- `npx vitest run` over the workspace, panel, `src/main/agent`,
`src/main/magic-notes`, `src/main/assistant`, storage startup, preload, shared
and the five Magic Notes Electron tests: 142 files passed (4 skipped), 1706
tests passed. `App.test.tsx -t note`: 4 passed.
- The Electron layout test checks the overview filter row and card chips fit
their containers; screenshots of the overview, tag manager and note header
checked at 1280px.
- `npm run typecheck` and eslint on the changed files passed. Full `npm test`
was not rerun.
## 2026-09-30: Overview Page And Header Back Button
"Back to overview" returned to the page header's top-right as a labelled button.
It now leads to a full-width all-notes card grid instead of a list beside an
empty detail pane; the split list appears only while a note is open.
`MagicNotesWorkspace.test.tsx` (129) and the Electron layout, analysis and
navigation tests passed; screenshots checked at 1280 and 720px.
## 2026-09-30: Compact Note Detail
The note detail now prioritizes content: the notes list uses two-line rows
(about 54px instead of 88px); note controls moved from text buttons in the page
header to a sticky icon toolbar; the title appears once as the stream heading;
the record index starts collapsed; AI opens only with comment activity or a pin;
the idle composer is one line; entry actions are hover icons. At 1280x800 with
the list open the stream is about 990px wide (content capped at 880px), versus
525px before, and the first entry starts about 240px from the top of the stream.
- Electron screenshots checked at 1280 (light/dark), 1000 and 720px, with and
without the index and AI panes.
- `MagicNotesWorkspace.test.tsx` 129 tests, `App.test.tsx` 315 tests, and the
Electron layout, analysis, navigation, footer and capture tests passed.
The analysis and navigation fixtures now locate icon buttons by `aria-label`.
- `npm run typecheck` and eslint on the changed files passed. Full `npm test`
was not rerun.
## 2026-09-30: Persistent Notes List
Desktop notes now retain the all-notes list beside the current note. The list
supports independent scrolling, selected state, creation, action menus, resizing
and collapse. Narrow workspaces switch between the mounted list and detail.
Switching notes restores reading positions within the mounted workspace and
protects unsaved titles and bodies. Current-note deletion uses the same draft
guard; failed deletion and deleting another note retain current drafts.
- Workspace, Electron layout, navigation and analysis regression tests passed:
4 files, 132 tests. The analysis fixture no longer waits for Cancel after Save
changes has already closed the editor.
- The full-suite run also passed the production App/preload/IPC/SQLite note-capture
scenario, including reopening the database and returning to conversation sources.
- `npm run typecheck`, `npm run lint` and `git diff --check` passed.
- Full `npm test` did not finish within 600 seconds. It reported two analysis
failures subsequently fixed and passed above, and one supervisor-layout failure.
There is no all-green full-suite result for this change.
- Layout fix, same day: with the list occupying 289px, a 930-1100px window left
the detail container under 800px, so AI stacked below a short stream. At about
880px a CSS container query and the React width state disagreed, briefly
rendering the list above the stacked detail. AI now stays beside the editor
down to a 600px detail width; the record index uses a drawer from 800px; the
list/detail switch uses only the measured workspace width. Electron screenshots
were checked at 1280, 1100, 1000, 930, 880, 700 and 560px.
`MagicNotesWorkspace.test.tsx`, the Electron layout test and the analysis
regression passed: 3 files, 131 tests.
## 2026-09-28: Startup Body Read Reuse
Startup repair now reuses the asset references collected during revision
inspection, as described in [file reconciliation](./technical-design.md#schema-38-migration).
The persisted format and revision recovery rules are unchanged.
- Real temporary SQLite/filesystem fixture: 120 entries across 120 notes,
half rich text and half canvas, with 4,039,560 bytes of body JSON.
Five initializations each read 240 bodies before and 120 after the change.
Logical bytes read fell from 8,079,120 to 4,039,560 per initialization.
- Baseline durations were 148.23, 134.42, 131.06, 140.66 and 172.90 ms;
after durations were 136.89, 119.11, 116.41, 115.14 and 116.26 ms.
Medians were 140.66 and 116.41 ms. These Windows runs used warm filesystem
caches on a shared machine; they do not measure cold application startup.
- `npx vitest run src/main/magic-notes/magic-note-storage.test.ts src/main/assistant/assistant-database.test.ts src/main/assistant/assistant-storage-upgrade.test.ts src/main/assistant-storage-startup.test.ts tests/assistant-storage-worker.test.ts`
passed 154 tests. Coverage includes failed media inspection, missing/invalid
bodies, retained shared assets, revision mismatch recovery and migration.
- Full `npm test`: 5382 passed, 84 skipped, 3 failed. The failures were the
App question-round request wait and Electron note capture focus/layout waits.
All three passed in targeted reruns; the full run was not all green.
The capture rerun exercised production App/preload/IPC/SQLite and reopening.
- `npm run typecheck` passed. Full lint still reports 10 errors in the existing
untracked `sidebar-final-probe.ts` and `sidebar-final-run.mjs`; lint excluding
those two files passed. No provider calls were made for these storage benchmarks.
## 2026-09-27: Shared Destructive Button Styles
The draft discard Portal now receives the global `danger-solid` styles. The
shared rule also covers note deletion, HeartbeatSettings, SupervisorWorkspace,
WorkspaceActionDialog and ToolEnvironmentSettingsSection without caller changes.
Sizing, typography and disabled feedback share the primary/secondary button
rules; theme tokens provide default and hover backgrounds. See
[shared destructive controls](../../../UI-DESIGN.md#66-危险操作).
- `npm test -- tests/magic-notes-capture.electron.test.ts src/renderer/src/WorkspacePrimitives.test.tsx`
passed 2 files / 37 tests. The real App/preload/IPC/SQLite capture test opens the
draft guard outside the notes page ancestor. Both themes render a 36px-high,
10px-radius button with the same font and padding as Continue editing. Native
Tab reaches the visible shared focus ring; continuing editing retains the draft.
- Hover uses Chromium `CSS.forcePseudoState`; disabled is set on the rendered
button to check opacity, cursor and suppressed hover. These are CSS state
checks, not verification of native pointer targeting or an asynchronous submit.
Theme contrast checks include both default and hover backgrounds.
- Windows Electron screenshots were captured and visually inspected for both
themes with keyboard focus. Set `GB_CAPTURE_SCREENSHOT_DIRECTORY` to an existing
directory to retain `draft-danger-light.png` and `draft-danger-dark.png`.
Model calls: 0. Other callers and other OSes were not individually rendered.
- `npm run typecheck` and `npm run lint` passed. Repository-wide
`npm test -- --bail=1` stopped at the unrelated
`HeartbeatCenter.test.tsx:213` assertion (expected one tablist, received two);
this is not an all-green full-suite result.
## 2026-09-27: Analysis IPC Extraction
The three analysis handlers now live in `magic-notes-analysis-ipc.ts`, with
the existing tracked registration injected at the original positions around
note and todo CRUD. Model selection and lifecycle are documented in
[Main IPC Ownership](./technical-design.md#main-ipc-ownership) and
[Canvas Analysis and Comments](./technical-design.md#canvas-analysis-and-comments).
No runtime factory, model-selection rule or Agent protocol changed.
- Before and after extraction, with `GB_NOTES_LIVE=0`:
`npm test -- src/main/ipc.test.ts src/main/magic-notes src/preload/magic-notes-events.test.ts tests/magic-note-canvas-text.test.ts tests/magic-notes-analysis.electron.test.ts`
passed 8 files, 232 tests, with 1 skipped (96.49s baseline, 97.90s after).
Electron exercised vision and text-only analysis through production UI/preload/IPC
with a local model stub; real provider calls for these checks: 0.
- `npm run lint` passed. `npm run typecheck` stopped in the Node project with
11 unrelated missing-`forwardLoopback` errors in `ssh-connection-pool.test.ts`,
`ssh-host-directory-browser.test.ts` and `ssh-terminal-session.test.ts`.
Separate `npx tsc --noEmit -p tsconfig.agent.json` and
`npx tsc --noEmit -p tsconfig.web.json` passed.
- A 120-second-bounded `npm test -- --bail=1` stopped after 94.45s:
20 files passed, 1 failed; 1004 tests passed, 1 failed, 25 skipped.
`RightAssistantSidebar.resize.test.tsx:387` expected two persisted terminal
titles but received one. This was not a complete full-suite run, and the
unrelated failure was not changed.
- `git diff --check` passed with existing LF/CRLF warnings.
## 2026-09-26: Conversation Capture and Compact Notes Panel
- FR-1/FR-4: `notes` is an application-scoped optional singleton in the workbar.
The compact panel supports title/body search, browsing, text appends and full
workspace navigation. Quick notes uses the same tab; app enablement controls
availability independently of sidebar pinning.
- FR-2/FR-3: Assistant-message and conversation-header actions open editable text
previews. Whole-conversation capture loads complete history. App owns drafts
and selection; close, replacement and disable actions use the draft guard.
Success focuses the returned entry ID; failed saves retain input.
- FR-5: Schema 48 adds nullable source metadata. Main validates message ownership
and resolves display labels at save time. Panel and workspace share source
navigation, including missing-message and missing-conversation handling.
- FR-6: Desktop search, preload/IPC and note-level SQL results are connected.
Creating a note with initial content commits note/entry membership in one
transaction. Both views use existing change notifications; file reconciliation
and workspace optimistic revisions retain their existing boundaries. See
[the implemented contract](./technical-design.md#conversation-integration).
Validation reported by implementation subagents in this session:
| Scope | Reported result | Evidence boundary |
| --- | --- | --- |
| Backend focused checks | 362 tests passed | Subagent handoff; not a repository-wide result |
| Frontend focused checks | 8 files / 592 tests passed | Subagent handoff; not real Electron acceptance |
| Web TypeScript and lint | Passed | Subagent handoff; does not establish full-repository typecheck/lint |
The coordinating agent subsequently reported real Electron acceptance passing.
[magic-notes-capture.electron.test.ts](../../../tests/magic-notes-capture.electron.test.ts)
and its [Main driver](../../../tests/support/magic-notes-capture-main.ts) /
[renderer](../../../tests/support/magic-notes-capture-renderer.tsx) mount the real
App UI and use production preload, registered IPC and SQLite/file storage.
Unrelated services use test dependencies; the renderer bridge is not replaced.
- Saving an assistant response to an existing note retains its earlier entry;
whole-conversation capture creates a new note with its first entry.
- All 502 historical messages are compared verbatim in the preview and saved
content, including history initially outside the rendered timeline.
- Full-workspace navigation locates the exact saved entry; source navigation
restores the conversation and focuses the source message. The coordinating
agent also reports source-history expansion coverage.
- Native Electron keyboard input covers returning to the list, opening a note
and appending text in a measured 214px panel, without horizontal overflow.
- SQLite close/reopen preserves both notes. Deleting the source conversation
retains their saved content and source metadata. Model calls: **0**.
The combined Electron, `canvas-ipc.integration`, `MagicNotesPanel` and
`ChatTimeline` run passed **4 files / 70 tests**. Lint and web TypeScript passed.
This validates the App capture path in a test Electron window with isolated data;
it does not establish release-package or cross-platform acceptance.
Repository validation and subsequent fixture repairs:
| Check | Result at execution time | Follow-up |
| --- | --- | --- |
| `npm test` | 457 files / 5,359 cases: 5,272 passed, 4 failed, 83 skipped | No all-green full-suite rerun reported |
| Three schema-fixture failures | Activity-history and two worker fixture variants omitted `DROP COLUMN source_json` when reconstructing old schemas | Fixed; the five-file regression run below passed |
| Remaining test failure | `tests/build-release.test.ts`: missing DSH resources | Unrelated to notes; not fixed in this validation pass |
| Full-repository typecheck | Failed on parallel SSH mocks missing `forwardLoopback`, an IPC native-terminal fixture referencing undefined `terminalManager`, and mismatched arguments in `obsidian-runtime-acceptance` | Execution-time findings; later concurrent changes may alter them |
After fixture repairs, **5 files / 58 tests passed**:
`src/main/assistant/activity-history-io.test.ts`,
`tests/assistant-storage-worker.test.ts`,
`src/main/assistant/assistant-storage-upgrade.test.ts`,
`src/main/magic-notes/magic-note-storage.test.ts` and
`src/main/magic-notes/canvas-ipc.integration.test.ts`.
This focused rerun does not replace the failed full-suite/typecheck results.
Status: **implemented; real Electron acceptance passed; repository validation is
not all green**. Results above come from the session handoffs, not a rerun by this
documentation task. Later fixes require their own verification evidence; older
Electron/full-suite results below do not validate this integration.
## 2026-09-24: Resizable Todo List and Collapsible Note Groups
- Todo list/detail uses a 1px divider with a transparent 9px pointer target and
@@ -467,8 +728,13 @@ Electron cases with substituted model output; live-provider checks remain opt-in
migration and backup requirements are defined in the
[storage contract](./technical-design.md#file-storage-and-writes).
- The startup worker converts one entry per transaction and verifies hydrated
readback before clearing the legacy payload. Reclamation checks the freelist
on every attempt, including retries after conversion has finished. Committed
readback before clearing the legacy payload. The original implementation checked
the freelist on every attempt. This was narrowed by the September 21 startup
fix and the September 26 structure-upgrade fix: only a required legacy
conversion enables reclamation, with its decision retained for same-startup
retries, not later launches. Current rules are in the
[migration contract](./technical-design.md#schema-38-migration).
Committed
saves/deletes/resets remain successful when manifest or GC cleanup fails;
failures are logged and cleanup is retried on later access or reopen.
- Storage implementation handoff reports 135/135 focused tests passed across

Some files were not shown because too many files have changed in this diff Show More