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.
629 lines
39 KiB
Markdown
629 lines
39 KiB
Markdown
# Magic Notes Technical Design
|
|
|
|
## Main IPC Ownership
|
|
|
|
[magic-notes-ipc.ts](../../../src/main/magic-notes/magic-notes-ipc.ts) owns
|
|
note search/list/get/create/update/delete, entry create/update/delete and todo
|
|
list/status/update handlers, including conversation-source validation and entry
|
|
content mapping. Its two registration functions receive the existing tracked
|
|
`registerHandler`, window and database from [ipc.ts](../../../src/main/ipc.ts),
|
|
preserving registration order: note CRUD, entry/draft analysis, todo CRUD, then
|
|
todo analysis. [magic-notes-analysis-ipc.ts](../../../src/main/magic-notes/magic-notes-analysis-ipc.ts)
|
|
owns the three analysis handlers through two registration functions. It receives
|
|
the window, database, settings readers, usage persistence callback and existing
|
|
error formatter; it imports `createDefaultModelRuntime` directly. `ipc.ts` retains
|
|
tracked registration/removal and shared usage persistence.
|
|
Sender checks and shared input schemas stay at each handler boundary.
|
|
[rich-content.ts](../../../src/main/magic-notes/rich-content.ts) remains shared
|
|
by CRUD and draft analysis. Analysis retains revision checks, hidden task status,
|
|
stream events, error causes and nested release/dispose cleanup.
|
|
|
|
## Continuous Record Workspace
|
|
|
|
The detail mounts every entry in a continuous newest-first stream. The left index
|
|
and AI source buttons scroll to an entry ID without changing the active editor,
|
|
clearing a composer, flushing content or invoking the draft-switch guard. Opening
|
|
a note shows a blank composer below the title and above the stream, without a New
|
|
record button. Editing another entry or
|
|
leaving the note still checks unsaved content after flushing canvas changes.
|
|
`MagicNotesWorkspace.onBeforeLeave` registers this same check with the App navigation
|
|
gate, including sidebar, application launcher and conversation jumps. The gate waits
|
|
for canvas flush, then offers Continue editing or Discard draft and switch for dirty
|
|
body or title content. Workspace cache eviction does not serve as draft protection.
|
|
Non-title detail responses retain a dirty title. A title-save response acknowledges
|
|
only the submitted input; text entered while that request runs remains a draft.
|
|
The shared store returns `MagicNoteEntryCreateResult`, the existing detail shape
|
|
plus a required `createdEntryId`. Production IPC and preload preserve that ID.
|
|
Creating an entry selects this exact ID and resets the composer through its key;
|
|
it does not enter editing, including after automatic analysis. Text/canvas selection
|
|
is retained. Canvas capture for automatic analysis completes before the reset.
|
|
the renderer never infers it from a list difference or timestamp. Agent/MCP
|
|
creation delegates to the same store; its existing public note projection is
|
|
unchanged.
|
|
Explicit Edit opens a saved record. Update and automatic-analysis responses advance its revision, so subsequent
|
|
saves use `updateEntry` and the returned revision. Failed writes retain the draft.
|
|
|
|
The dirty baseline belongs to the mounted editor. `MagicNoteEditor.onReady` reports
|
|
Quill's normalized contents after loading; `MagicCanvasEditor.onReady` reports the
|
|
first completed core flush, after Fabric restoration and flow pagination. Canvas
|
|
input stays disabled until this baseline exists, and imperative flush waits for
|
|
the same initialization promise. Raw stored JSON is not a valid comparison baseline
|
|
for an initialized editor: Quill can add its terminal newline, and Fabric/flow
|
|
serialization can add defaults or change field order. Later changes and navigation
|
|
flushes compare with the initialized baseline without resetting it. A successful
|
|
save remounts at the returned revision and establishes a new baseline; an unchanged
|
|
background revision retains the existing editor and baseline. Aborted canvas
|
|
initialization and file reads for an unmounted text editor cannot write back.
|
|
|
|
The real saved formatted-text probe reproduced identical values with different
|
|
Delta key order: storage returned `{insert, attributes}` and Quill emitted
|
|
`{attributes, insert}`. The old `JSON.stringify` comparison therefore reported
|
|
dirty after a successful save. The saved PDF probe's initialized content and
|
|
subsequent flush were byte-for-byte equal; readiness is still awaited for canvas
|
|
restoration rather than assumed from the save response.
|
|
|
|
Saving or leaving an editing context clears the immediate-analysis timer, queued
|
|
content and streamed draft comments through the shared draft-analysis cleanup.
|
|
The context counter rejects late results and prevents dispatch after abandoned
|
|
settings preparation. This cleanup does not clear the new-entry composer's content.
|
|
|
|
The left index defaults to 168px and resizes from its right edge between 140px
|
|
and 320px. It shares the AI pane's pointer-capture handlers, separator styling
|
|
and keyboard semantics (16px arrow steps, Home/End limits). Both widths are
|
|
stored in `goodbuddy.magic-notes-layout.v1`; missing index width defaults to
|
|
168px. Displayed widths clamp together to reserve at least 300px for the stream,
|
|
without overwriting saved widths when the window shrinks. The page header places
|
|
the index toggle immediately after the back button, followed by the AI toggle.
|
|
All three use icons and visible localized text; pane toggle labels reflect their
|
|
current show/hide action. Collapsing hides the entire index and removes its grid column,
|
|
border, resize separator and occupied width. AI has a separate
|
|
right pane with its existing resize separator and header toggle. Both panes scroll
|
|
independently and persist independent desktop visibility. At container widths of
|
|
800px or less, the index defaults to hidden and the same header toggle opens or
|
|
closes a fixed 168px drawer below the header, without reserving a rail or showing
|
|
an index resize separator; choosing
|
|
an item or pressing Escape closes it. AI moves below the stream, limited to 40%
|
|
of layout height and 280px. Overview search, scroll, task selection and return
|
|
focus stay mounted.
|
|
|
|
`MagicCanvasThumbnail` lazily queues visible records through a temporary core
|
|
renderer and caches results by content identity in a WeakMap. Core `capturePages`
|
|
accepts `firstPageOnly` and `thumbnailWidth`; thumbnails capture page zero at 240px
|
|
width, including paper/PDF background, Quill flow, images and Fabric objects.
|
|
They do not capture all pages or leave an editor mounted for every record.
|
|
An abort listener calls core `destroy()` immediately: PDF render cancellation and
|
|
the Fabric abort happen synchronously, before waiting for capture to settle.
|
|
The `finally` path awaits the same destruction promise and removes the temporary
|
|
surface, without destroying twice or waiting for capture before cancellation.
|
|
AI capture uses the configured page prefix described under Canvas Analysis and
|
|
Comments. Main canvas page height, wheel handling and file storage are unchanged.
|
|
|
|
Background refresh preserves dirty editing content and its original optimistic
|
|
revision, including externally deleted entries. Clean editors adopt refreshed
|
|
entries; deleting a clean selection updates `selectedEntryId` to a remaining
|
|
entry or the empty state. Later appends preserve that actual selection, rather
|
|
than recalculating a render-only fallback. Dirty deleted entries remain open
|
|
until explicitly discarded. No background refresh saves a draft.
|
|
|
|
## Canvas Storage Contract
|
|
|
|
`MagicNoteContent` is the union of unchanged v1 Quill rich content and v2
|
|
`paged-canvas`. The shared schema is authoritative. Canvas pages carry dimensions,
|
|
template/PDF backgrounds, opaque Fabric object records and optional `flowAuto`.
|
|
The optional Quill flow accepts native PeopleLib `version: 1` and page-break
|
|
embeds. Template names are `blank`, `ruled`, `grid`, `dot`; PDF backgrounds use
|
|
one-based `pageNumber` and optional extracted `text`.
|
|
|
|
API assets are document-local `{id,name,mimeType,dataUrl}` records. Backgrounds and
|
|
image objects reference stable asset IDs; image objects can also embed raster
|
|
data directly. Main validates data encoding, image/PDF signatures and references.
|
|
Canvas has no v1 resource-size budget. `image_bytes` counts decoded embedded
|
|
resource bytes for new entries. Bodies and binary assets use the file storage
|
|
contract below; there is no separate resource table, resource staging or extra
|
|
resource IPC. Renderer, preload and IPC continue to exchange hydrated v1/v2
|
|
content with data URLs.
|
|
|
|
Schema 38 replaces SQLite body payloads with file markers. Schema 37 was the
|
|
earlier canvas compatibility marker; its embedded-payload layout is historical.
|
|
Plain text combines flow text, PDF extracted text and object text for search.
|
|
Checklist indexes depend only on flow checklist lines; moving objects does not
|
|
change todo identity. Completion updates flow without replacing pages or assets.
|
|
Entry edits retain optimistic revisions. Canvas comment retention depends on the
|
|
actual analysis input, as described below.
|
|
|
|
`note_get` reports content kind/version and `plainTextEditable`. The text update
|
|
tool rejects canvas entries before mutation. These tools execute in the desktop
|
|
gateway; remote gbagent's injected image MCP server is unaffected. Browser page
|
|
renders can be sent as `analysisOptions.canvasImages`; analysis comments and draft
|
|
results can retain the actual `inputMode` used.
|
|
|
|
## File Storage and Writes
|
|
|
|
`MagicNoteStorage` owns files under `dirname(assistantDatabasePath)/notes`:
|
|
|
|
```text
|
|
notes/<noteId>/
|
|
note.json # Derived {id, entries} membership manifest
|
|
entries/<entryId>.json # Authoritative body envelope
|
|
assets/<sha256>.<ext> # Binary assets deduplicated within this note
|
|
```
|
|
|
|
Each body envelope contains `id`, `noteId`, `revision`, `updatedAt` and `content`.
|
|
SQLite owns note metadata, entry membership, search/plain-text and byte indexes,
|
|
optimistic revisions, todos and comments. Entry `content_json` contains only
|
|
`{storage: "file", version: <content version>, revision: <body revision>}`.
|
|
Comment-only revision increments do not change the indexed body revision.
|
|
|
|
Known media fields become `{$asset: "assets/<sha256>.<ext>", mimeType: "..."}`
|
|
on disk and hydrate back to data URLs on API reads. These fields are v1
|
|
`insert.image`, `localVideo.dataUrl` and `attachment.dataUrl`; canvas
|
|
`assets[].dataUrl`, `preview`, flow embeds and image-object `src`, including
|
|
nested objects. Ordinary text, insert strings, names and arbitrary `$asset`
|
|
objects remain unchanged. Canvas asset IDs and PDF references are preserved.
|
|
Write, read and garbage collection use the same scoped field traversal.
|
|
|
|
Revision checks run under `BEGIN IMMEDIATE` before file writes. Assets are
|
|
written before the temporary body file is atomically renamed over the entry
|
|
file; the database then commits its marker and indexes. This is not a single
|
|
atomic transaction spanning SQLite and the filesystem. If a replacement body
|
|
was renamed but indexing failed, the next database access or reopen reads it
|
|
back, repairs plain text, byte counts and todos, advances revisions and
|
|
invalidates old analysis. Missing bodies raise an error instead of becoming
|
|
empty content. Failed creates leave no accepted database membership; their
|
|
unaccepted files are removed on the next access or reopen.
|
|
|
|
After commit, reconciliation derives `note.json` from database membership and
|
|
inspects all retained entry files before removing unreferenced assets. Startup
|
|
repair reads each current body once to inspect its revision and collect asset
|
|
references, then passes the complete per-note reference set to reconciliation.
|
|
If media inspection fails, cleanup uses its existing deferred retry path rather
|
|
than trusting a partial set. Revision mismatch recovery still hydrates the body;
|
|
legacy conversion still verifies its written content. Manifest or
|
|
GC failures log `console.warn` with the note ID and error and remain in an
|
|
in-memory retry set. Later database access retries; reopening derives cleanup
|
|
again from membership. These cleanup failures do not turn a committed save,
|
|
delete or reset into a failed operation or suppress its notifications. Body
|
|
persistence and index-repair failures still propagate. Entry deletion, note
|
|
deletion and assistant-data reset share this cleanup path; deferred whole-tree
|
|
cleanup checks current membership so it preserves notes created after reset.
|
|
There is no persistent retry journal, and renderer-only drafts create no files.
|
|
|
|
## Schema 38 Migration
|
|
|
|
The existing startup storage worker runs before the business connection opens.
|
|
It upgrades the schema and converts legacy payloads one entry per SQLite
|
|
transaction: write assets and body, read back hydrated content, compare JSON
|
|
equality, then replace the old `content_json` payload with its file marker.
|
|
Already converted entries are reconciled on retry. Matching body revisions do
|
|
not require rehydrating binary payloads. Direct `AssistantDatabase.initialize`
|
|
callers and tests also run storage repair.
|
|
|
|
Space reclamation is separate from structure upgrades and conversion. Startup
|
|
detects legacy note payloads and retains the conversion/reclamation decision in
|
|
memory for that startup's worker retries. Only a required legacy conversion
|
|
permits checking `PRAGMA freelist_count` and running VACUUM. A structure-only
|
|
upgrade does not repair notes in the worker or reclaim ordinary free pages.
|
|
Cancellation after conversion or a VACUUM failure can retry reclamation within
|
|
the same startup. After quitting, startup checks the schema and remaining legacy
|
|
payloads again; free pages alone do not trigger another upgrade, and SQLite can
|
|
reuse them. No additional persistent migration marker is stored.
|
|
General migration requirements are in the
|
|
[database migration guide](../../development/database-migrations.md).
|
|
|
|
## Backup and Restore
|
|
|
|
A restorable backup must pair a consistent SQLite backup with the adjacent
|
|
`notes/` tree from the same period with note writes paused, or with the app
|
|
closed. Handle active WAL through SQLite's consistent backup mechanism; copying
|
|
only the database main file while it is active is insufficient. Restore both
|
|
parts together. SQLite-only backups of file-backed entries cannot recover
|
|
bodies or assets, and `note.json` alone cannot recover database metadata,
|
|
revisions, todos or comments.
|
|
|
|
The product has no one-click assistant backup action. This is an operational
|
|
backup boundary, not a new UI capability. The tested backup/restore and migration
|
|
scope, including the lack of a real user historical database run, is recorded
|
|
in [progress](./progress.md).
|
|
|
|
## Canvas Editor and Export
|
|
|
|
`MagicNotesWorkspace` selects rich-text or canvas content for new entries and
|
|
opens saved canvases in `MagicCanvasEditor`; `MagicNoteContent` delegates saved
|
|
canvas rendering to the paginated `MagicCanvasContent` viewer. The React adapter
|
|
wraps the retained PeopleLib Fabric + Quill core. Fabric owns page annotations;
|
|
Quill owns the continuous body flow. Saving awaits `flush()` so queued imports,
|
|
text edits and pagination reach the existing preload/IPC create/update handlers.
|
|
There is no autosave or separate canvas persistence path.
|
|
|
|
Editor and viewer heights follow the current paper element's displayed height
|
|
through natural CSS layout, including wrapped toolbars and viewport padding.
|
|
No fixed preview height or viewport-height cap clips the page. The outer note
|
|
stream scrolls vertically; the paper viewport retains horizontal scrolling.
|
|
View zoom is local to each mounted core instance. A displayed-size outer box and
|
|
one transformed logical-size surface keep PDF, flow and Fabric aligned; Quill
|
|
screen measurements are converted back to logical coordinates. Neither zoom nor
|
|
fit-width resize calls `onChange`, changes the persisted model, invalidates AI
|
|
comments or alters PNG/PDF export dimensions. See the
|
|
[view zoom implementation](../../../src/renderer/src/magic-canvas/README.md#view-zoom)
|
|
for controls, bounds, page-switch policy and observer cleanup.
|
|
|
|
Paper templates, pen, highlighter, whole-object eraser, selection/transforms,
|
|
floating text, images and PDF page backgrounds are integrated. Native PDF text
|
|
is stored on its background for search and analysis; scanned pages remain visual
|
|
without automatic OCR. The toolbar exports a raster PDF through jsPDF, composing
|
|
background, flow text and annotations per page. It does not retain a searchable
|
|
PDF text layer. `capturePages()` provides PNG data URLs for AI input, not a PNG
|
|
download button. Undo/redo is per annotation page or Quill mode; page operations
|
|
and PDF imports are not global undo steps.
|
|
|
|
Flow page-break removal and rejected-input history handling follow the
|
|
[canvas editing and limit rules](../../../src/renderer/src/magic-canvas/README.md#editing-and-assets).
|
|
`canvas-core.electron.test.ts` feeds the real Electron flow's before/after content
|
|
into production `AssistantDatabase` create/update and reopen, checking that the
|
|
derived checklist todo and its identity survive page-break removal. History
|
|
coverage exercises accepted typing, rejected overflow, selection and redo.
|
|
|
|
The [canvas integration reference](../../../src/renderer/src/magic-canvas/README.md)
|
|
owns core limits, import behavior and lifecycle details. Vite bundles Fabric,
|
|
Quill, jsPDF, PDF.js workers and binary assets; PDF workers use explicit module
|
|
Workers and local assets under the production `file://` CSP. The renderer does
|
|
not need Node integration or remote asset services.
|
|
|
|
## Canvas Analysis and Comments
|
|
|
|
Draft, saved-entry and todo analysis use the existing typed preload/IPC methods
|
|
and `createDefaultModelRuntime`. Each canvas request reads the current default
|
|
model profile's `supportsImageInput` (or the resolved default setting when no
|
|
profile is selected). Main resolves settings again for the actual request. This
|
|
does not change an Agent runtime or desktop-to-Agent protocol. All three handlers
|
|
read global resolved settings per request, without applying chat/project runtime
|
|
selection. The factory creates a tool-free direct `ModelAgentRuntime`, using the
|
|
resolved default model connection and that profile's headers, body options and
|
|
output limit. Environment-managed default credentials retain the settings store's
|
|
environment overrides. The existing context-compression configuration can select
|
|
a separate summary model; it does not select the main analysis model.
|
|
|
|
`magicNoteCanvasPageCount` is an application preference: an integer from 1 to 8,
|
|
defaulting to 1 when absent in historical settings. The application center and
|
|
`goodbuddy_config` application.update/get share the settings schema, store and
|
|
change event. It counts pages within the canvas being analyzed, never entries.
|
|
The first N pages follow the current `content.pages` array order, independent of
|
|
the viewed page, page IDs or PDF source page numbers. Fewer than N pages selects
|
|
the whole canvas. Saving and PDF export retain the 50-page canvas limit.
|
|
|
|
Renderer and Main share `selectMagicNotePages`. The core selects the prefix before
|
|
compositing, so it never captures 50 pages just to discard 49. Analysis capture
|
|
returns the selected pages' Quill text measured from the actual CSS columns;
|
|
binary searches locate the character boundaries for explicit and automatic page
|
|
breaks. Text-only requests skip image compositing but still read page text.
|
|
The transient `canvasPageText` IPC field carries these page IDs and text, without
|
|
changing saved canvas content. Main reads the persisted page count for all three
|
|
handlers, selects page text, PDF text, objects and images in the same order, and
|
|
ignores captures outside the prefix. A partial canvas with flow requires matching
|
|
page text; missing data fails instead of including the full cross-page flow.
|
|
Prompts state the selected and total page counts. A changed setting applies to
|
|
the next analysis; existing comments are not automatically regenerated.
|
|
|
|
| Default model capability | Analysis input and result |
|
|
| --- | --- |
|
|
| Image input supported | Renderer captures the selected prefix; Main requires each selected page's PNG/JPEG capture and sends it alongside that page's flow, PDF and object text. Comments carry `inputMode: canvas-images`. |
|
|
| No image input | Only text from the selected prefix is sent, with an explicit instruction that handwriting, images and layout were not seen. Comments carry `inputMode: text-fallback`. |
|
|
| No image input and no extracted text in the selected entry/draft pages | Analysis fails with guidance to switch to an image-capable default model; no visual understanding is claimed. |
|
|
|
|
Todo analysis includes its title and instructions plus the source canvas context,
|
|
and uses the same capability and capture rules. Non-canvas content keeps the text
|
|
analysis path. All analysis remains read-only with no tool calls.
|
|
|
|
Saved-entry requests accept `expectedRevision`; todo requests accept
|
|
`sourceEntryRevision`. Nonempty `canvasImages` or `canvasPageText` requires the corresponding revision,
|
|
even when the resolved model uses text fallback. Main loads the source after
|
|
resolving settings and checks the supplied revision before model invocation.
|
|
Existing rich-text callers may omit these fields. Entry writeback retains its
|
|
revision check; canvas todo writeback also requires the source entry revision,
|
|
including when no comments existed at request time. No additional stored snapshot
|
|
or schema migration is needed.
|
|
|
|
Canvas `immediate` mode provides an Analyze canvas draft action without a persistent
|
|
instruction banner; pen strokes do not trigger automatic requests. `after-save-auto`
|
|
prepares analysis input and then saves manually submitted content. Settings or
|
|
capture errors are caught so storage still proceeds; after a successful save,
|
|
the UI reports that automatic analysis failed. A later model error likewise
|
|
does not roll back the save. Failure to flush the document itself still prevents
|
|
saving incomplete content.
|
|
|
|
For v2-to-v2 entry updates, identical content retains saved comments. If all
|
|
comments have `text` or `text-fallback` input mode, equal analysis text from shared
|
|
`magicNoteCanvasAnalysisText(content)` (including page numbers)
|
|
also retains comments across layout-only changes. Other changed canvas content,
|
|
including layout changes with `canvas-images` comments, clears saved entry
|
|
comments and `analyzed_at`. Source canvas changes apply the same rule to saved
|
|
todo comments and increment the affected todo revision without replacing its ID.
|
|
In `after-save-auto`, visual comments trigger renewed
|
|
analysis when content changes, while existing text-only comments trigger it when
|
|
analysis text changes. A changed draft clears its prior canvas draft analysis.
|
|
The UI labels the actual input mode so a text fallback is visible to the user.
|
|
|
|
Workspace captures the saved entry and its revision before awaiting image capture;
|
|
an external refresh cannot pair an old capture with a newer revision. Automatic
|
|
post-save analysis takes the revision from the save response using `createdEntryId`
|
|
or the edited entry ID. Todo capture uses the selected source entry's actual page
|
|
IDs and revision. Renderer text-only change detection uses the same shared
|
|
analysis-text helper as persistence, so inserting a blank page before text triggers
|
|
renewed analysis when its cited page number changes. Draft analysis remains unversioned.
|
|
|
|
## Change Notifications
|
|
|
|
`AssistantDatabase` accepts `onMagicNotesChanged: () => void`. Every successful
|
|
note/entry create, update or delete emits once after the write commits, including
|
|
empty-note creation, initial content, title/pin updates and cascading deletion.
|
|
Saved note analysis, todo completion, saved todo analysis and assistant-data
|
|
clearing also emit. Revision conflicts, missing rows, rolled-back writes and
|
|
unchanged todo completion do not emit. Startup schema migrations run before the
|
|
workbench loads and do not publish notifications.
|
|
|
|
Desktop IPC and `KnowledgeMcpGateway` use this same database instance. The latter
|
|
also serves Agent tools; no tool-specific notification or daemon protocol change
|
|
is needed. Raw SQL writes from an unrelated process are outside this callback
|
|
contract.
|
|
|
|
Main queues notification delivery to the live window on `magic-notes:changed`.
|
|
The typed preload method `magicNotes.onChanged(listener)` exposes no Electron
|
|
objects and returns a function removing that listener. The existing
|
|
`onTodoStatusChanged` event remains the separate sidebar-count signal.
|
|
|
|
## Refresh Lifecycle
|
|
|
|
The overview's `libraryView` and the optional note-only `detailView` are independent:
|
|
opening a todo's source note does not lose the originating task view.
|
|
The overview remains mounted but hidden while detail is open, retaining scroll
|
|
position and filters. Returning restores the initiating control's focus and
|
|
invalidates outstanding detail requests. Initial loading fetches summaries and
|
|
todos without selecting the first note or task. Navigation away from unsaved content or
|
|
an unsaved title uses the existing draft confirmation; active writes finish
|
|
before navigation. No new persistence or IPC contract is required.
|
|
|
|
`MagicNotesWorkspace` subscribes on mount and coalesces notifications with a
|
|
100 ms debounce. Pending notifications wait for an active local save or analysis
|
|
to finish. Cleanup removes the listener, clears its timer and invalidates pending
|
|
refresh results. This does not introduce periodic database polling.
|
|
|
|
The existing list/detail loader reloads notes and todos, then the preferred note.
|
|
Request counters reject superseded results and preserve newer user selections.
|
|
Todo selection survives list reordering while the item exists; replacing the todo
|
|
snapshot also reloads the selected todo's source entry. Search, filters and layout
|
|
state are not reset.
|
|
|
|
Background application of a detail reads the current draft state after the
|
|
asynchronous fetch. A dirty title is retained, and the composer is not remounted.
|
|
A dirty entry editor uses its original editing content and revision for both its
|
|
React key and save request; external revisions cannot remount that draft or bypass
|
|
optimistic concurrency checks. Clean editors adopt newer revisions. Deleted entries
|
|
remain displayed only while their editor has a draft. If an externally deleted note has a draft, its editor remains available
|
|
with a deletion explanation instead of moving the draft to another note.
|
|
|
|
Read failures use the existing retryable refresh error and retain successful
|
|
data and drafts. Retry uses the same draft-preserving loader. Interaction rules
|
|
are owned by [the UI design system](../../../UI-DESIGN.md#137-魔法笔记).
|
|
|
|
## Todo Task View
|
|
|
|
The To-dos panel has its own search/status toolbar and independently scrolling
|
|
task list, grouped by source note. `selectedTodoId` controls one sibling detail pane;
|
|
the selected item is derived from the filtered list. Completion updates the
|
|
existing item and source cache without selecting or opening another task. Items
|
|
that no longer match the status filter leave the list. A removed focused control
|
|
returns focus to an available list/filter control.
|
|
|
|
Detail content includes instructions, the source entry, its existing note
|
|
navigation action, and AI controls/comments. AI requests still use the existing
|
|
analysis options and streaming subscription; failed analysis retains saved
|
|
comments. Source reads are guarded against superseded selections, report failures
|
|
through notifications and offer inline retry. The grid reserves both columns even
|
|
without a selection, so selecting or switching tasks cannot move list rows. The
|
|
detail is keyed by task ID to start each newly selected task at its top.
|
|
At a page container width of 700px or less, CSS hides the mounted list while a
|
|
task is selected and shows an explicit return button in the detail area. Returning
|
|
clears selection and restores row focus without scrolling; source-note return
|
|
retains selection and focuses this visible return button on narrow layouts.
|
|
There is no todo detail route or separate AI sidebar. A shared accessible resize
|
|
separator displays a 1px line with a transparent 9px hit area and 8px adjacent
|
|
content padding. It adjusts the list width, initially 320px, with a 240px minimum and at
|
|
least 300px reserved for detail. Pointer dragging and 16px arrow steps/Home/End
|
|
persist `todoPaneWidth` in `goodbuddy.magic-notes-layout.v1`. Narrow layouts hide
|
|
the separator; window constraints do not overwrite the saved desktop width.
|
|
Each source-note heading is a disclosure button with its matching task count.
|
|
Groups start expanded and can independently hide their mounted rows without
|
|
clearing the selected detail. Collapse state lasts while the group is mounted.
|
|
Note detail retains its existing AI pane preferences.
|
|
|
|
A single secondary button lives in `PageHeader.actions` before New note. Its
|
|
label and icon describe the destination, and `requestDraftSwitch` retains the
|
|
existing draft guard. Focus returns to the same `magic-library-switch` button
|
|
after switching. The overview section keeps its localized list `aria-label`;
|
|
there are no tab, tablist or tabpanel roles or tab-label references. The button
|
|
follows content width at all sizes. The shared status `SegmentedControl` follows
|
|
search in one wrapping toolbar.
|
|
|
|
Only note detail applies `magic-notes-layout--detail` to remove the layout frame
|
|
and corner radius. Its stream pane has no extra padding; editor borders and the
|
|
AI divider remain. Overview cards and the independent todo layout keep their
|
|
existing frame and spacing. These changes do not affect Main, preload or storage.
|
|
|
|
## Note List Actions
|
|
|
|
Each overview card has an open-detail button and a sibling action-menu button. The menu
|
|
uses the conversation action styles and shared `DestructiveConfirmActions`, and
|
|
is portalled to `document.body`. Its position follows the trigger on scrolling,
|
|
window resizing and confirmation-size changes. Entering detail, filtering out the
|
|
target or leaving the notes panel closes the menu.
|
|
|
|
Pinning uses the target summary's ID and revision through the existing update
|
|
IPC. It updates the sorted summary and matching selected detail without resetting
|
|
title, composer or entry-edit drafts. Deleting uses the explicitly confirmed
|
|
target ID and refreshes the overview after success, without opening another note.
|
|
These actions are available only in the overview. Database and Agent contracts
|
|
are unchanged.
|
|
|
|
## Agent Search Contract
|
|
|
|
`note_search` accepts an integer `limit` from 1 to 100, defaulting to 8.
|
|
The shared schema supplies both model tool definitions and MCP validation;
|
|
the existing 128 KiB output budget can reduce the returned result count.
|
|
The built-in MCP gateway returns invalid scoped-tool arguments as an
|
|
`isError: true` tool result with the field and validation message, allowing
|
|
the model to correct its arguments without an MCP internal-error response.
|
|
Validation failures do not execute the tool.
|
|
|
|
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,
|
|
reopen/repair, binary deduplication, coordinated backup/restore and physical
|
|
database shrink. Failure injection covers body rename, manifest/GC cleanup,
|
|
delete/reset retries, post-conversion cancellation and VACUUM retry.
|
|
- Canvas integration coverage includes `MagicCanvasEditor.test.tsx`,
|
|
`MagicNoteContent.test.tsx`, `rich-content.test.ts`,
|
|
`magic-note-analyzer.test.ts` and `tests/magic-note-canvas-text.test.ts`.
|
|
`canvas-ipc.integration.test.ts` runs production preload and registered IPC
|
|
handlers against real SQLite with mocked Electron transport and model output.
|
|
Interactive workspace/SQLite and production core export evidence is recorded
|
|
separately in [progress](./progress.md).
|
|
- `assistant-database.test.ts` checks committed writes with a second SQLite
|
|
connection, successful mutation coverage, failures, no-op completion and reset.
|
|
- `knowledge-mcp-gateway.test.ts` exercises real database CRUD through Agent/MCP
|
|
access, including revision failure without an extra notification. A real HTTP
|
|
MCP client also checks searches above ten results, the published limit schema,
|
|
invalid arguments and successful retry against SQLite.
|
|
- `magic-notes-events.test.ts` executes the production preload, checking the
|
|
channel, payload-free callback and listener removal.
|
|
- `MagicNotesWorkspace.test.tsx` covers external updates and deletion, selection,
|
|
source reload, editor identity and draft saves, coalescing, deferred writes,
|
|
asynchronous selection races, error retry and unmount cleanup. List-action
|
|
coverage includes portal placement, keyboard navigation, dismissal, delete
|
|
confirmation, target selection, revision failures and draft preservation.
|
|
|
|
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).
|