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.
This commit is contained in:
mesalogo
2026-09-27 08:17:54 +08:00
parent 55efca3ee1
commit 1094433a7d
51 changed files with 1949 additions and 56 deletions
+119 -1
View File
@@ -449,6 +449,124 @@ These note tools run in the desktop gateway. Remote gbagent ACP sessions
currently inject only the image MCP server, so this change requires no
daemon implementation update.
## Conversation Integration
The desktop API and renderer integration are implemented. Scope and behavior
are owned by the [PRD](./prd.md), [logic design](./logic-design.md) and
[UI design](./ui-design.md). Real Electron acceptance passed; focused results,
fixture repairs and non-green repository checks are recorded in [progress](./progress.md).
### Workbar and Capture
`WORKBAR_APP_DEFINITIONS` registers `notes` as an optional, closable, reorderable
singleton with application scope, no `targetRef` and `defaultOpen: false`.
`RightAssistantSidebar` opens or activates it through the existing
[workbar tab model](../assistant-workbar/terminal-tabs-prd.md), and removes it when
Magic Notes is disabled. Sidebar pinning does not control this availability.
`MagicNotesPanel` renders the compact view; `WorkbarShell` supplies the shared tab
controls. The footer shortcut and collapsed-sidebar chat-header shortcut both
activate the same instance.
`App.captureToNote` receives the message identity from `ChatTimeline`, or calls
`ensureConversationHistory` for whole-conversation capture. It freezes text and
source IDs before opening the preview and waits for queued conversation persistence
so Main can validate newly completed messages. Whole-conversation text includes
nonempty user/assistant bodies with role headings. It excludes reasoning, tool
logs and attachment binaries, and rejects streaming conversations.
`useMagicNoteDraft` lives in App and owns the draft, selected note, saving state
and one shared confirmation promise. Selection and drafts survive panel remounts
and conversation/project switches, but are not persisted across app restarts.
The guard rejects leaving while saving and confirms discarding pending input;
explicit cancellation compares text/title with their initial preview values.
Application disablement and tab closing call the same guard. Workspace navigation
also passes through the full workspace's existing leave check.
`capturedNoteContent` converts text into v1 Quill string inserts, preserving
newlines and adding the terminal newline when needed. It splits inserts at the
200,000-code-unit operation limit without splitting surrogate pairs; the complete
record still passes the shared rich-content schema. Markdown stays literal text.
No summarization or attachment copying runs during capture. New-note titles are
limited to 100 characters. A synchronous submission ref prevents duplicate writes.
`magicNotes.create({ title, content?, source? })` returns `MagicNoteCreateResult`,
with `createdEntryId` when initial content exists. `createMagicNote` inserts the
note, first entry, source and derived todos inside one SQLite transaction; failure
leaves no committed empty note and sends no success notification. This atomicity
applies to database membership; filesystem writes retain the reconciliation
boundary in [File Storage and Writes](#file-storage-and-writes).
`magicNotes.createEntry({ noteId, content, source? })` always returns the exact
`createdEntryId`. The panel uses that ID to scroll, focus and briefly highlight the
new record. Opening the full workspace passes `{noteId, entryId?, requestId}` into
its existing guarded note-navigation path.
`magicNotes.search({ query, limit? })` is exposed by `DesktopApi`, preload and
`magic-notes:search`. Trusted-sender and shared-schema validation precede
`AssistantDatabase.searchMagicNoteSummaries`. The trimmed query has a 1,000-character
limit; result limits are 1-200, defaulting to 200. The panel requests 100 results
and debounces nonempty queries by 180 ms. SQL searches note titles or indexed entry
text using `EXISTS`, so each note occurs once before `LIMIT`. Empty queries list
notes; ordering is pinned first, then updated time and row ID descending.
`%`, `_` and backslashes are escaped for literal matching. Agent `note_search`
retains its separate entry-level output and 1-100 limit.
### Source Metadata
`magicNoteSourceSchema` defines `kind` (`message` or `conversation`),
`conversationId`, nonempty unique `messageIds`, ISO `capturedAt`,
`conversationTitle`, and optional `projectId` / `projectName`. A message source
requires exactly one message ID. Create/create-entry inputs accept `source`;
note creation requires initial content when source is supplied. Entry reads
return it independently of editable content, while update inputs cannot replace it.
The IPC helper `resolveMagicNoteSource` loads the stored conversation and verifies
that every message ID belongs to it; a single-message source must identify an
assistant message. It replaces renderer-supplied title/project labels with current
database values while retaining the supplied capture time and IDs. Preview labels
therefore describe capture time, but stored labels describe save time. Missing
source rows reject creation; later source deletion does not block note reads or edits.
Schema 48 adds nullable `magic_note_entries.source_json` in a transaction and
advances `PRAGMA user_version`. Existing entries read without a source. It adds no
conversation foreign key or deletion cascade, and does not rewrite file-backed
bodies. Schema 38 remains the historical body migration. Structure-only upgrade
does not independently trigger body conversion or VACUUM; see the
[database migration guide](../../development/database-migrations.md). Coordinated
SQLite/files backups include the source metadata.
`MagicNoteSource` is shared by panel summaries and workspace records. Its callback
`App.openNoteSource` reads `conversations.get`, then navigates through the workspace
leave guard. Message navigation increases the mounted timeline range before
scrolling and focusing. A missing message opens its conversation with a notice;
a confirmed missing conversation disables that source bar's jump buttons. Other
read errors remain retryable. No deleted-source flag is stored.
### Refresh and Validation
Both views use the same Main store and `magic-notes:changed` notification. The
panel reloads search and selected detail when active, and ignores superseded
responses. Search/loading state belongs to the mounted panel; input and selected
note belong to App. A deleted selected note clears the selection and matching
target ID while preserving draft text. Read and save failures have local recovery
feedback. The full workspace retains its existing revision and dirty-editor rules.
After a successful save, only `after-save-auto` starts entry analysis using the
returned `createdEntryId` and configured comment format. Analysis errors notify
without undoing the save or recreating the entry. Other comment modes perform no
panel analysis.
Regression cases are in `MagicNotesPanel.test.tsx`, `App.test.tsx`,
`ChatTimeline.test.tsx`, `MagicNotesWorkspace.test.tsx`, workbar contract tests,
database/storage tests and production preload/IPC integration tests.
`tests/magic-notes-capture.electron.test.ts` mounts the real App with production
preload and capture IPC, backed by SQLite/files. It validates existing/new-note
saves, verbatim 502-message history, workspace/source navigation, native keyboard
input in a narrow panel, database reopen and retention after source deletion,
with zero model calls. Failed writes and external refresh retain their focused
regression coverage; this Electron result does not claim every failure path.
Repository test/typecheck failures and repaired migration fixtures are documented
separately in [progress](./progress.md).
## Validation
- `magic-note-storage.test.ts` uses real files and SQLite for migration equality,
@@ -476,6 +594,6 @@ daemon implementation update.
coverage includes portal placement, keyboard navigation, dismissal, delete
confirmation, target selection, revision failures and draft preservation.
Schema 38 and the file layout above govern current persistence; refresh
Schema 48 adds source metadata to the schema 38 file-backed body layout; refresh
notifications need no separate storage or network service. Dated validation
results and remaining repository checks are owned by [progress](./progress.md).