Add native Quill table insertion and contextual row, column, and table actions. Preserve table-only drafts through composer validation, IPC, and storage, and document the saved behavior.\n\nRelease note: 魔法笔记支持插入和编辑表格,空单元格表格也可保存并在重新打开后恢复。
46 KiB
Magic Notes Technical Design
Main IPC Ownership
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,
preserving registration order: note CRUD, entry/draft analysis, todo CRUD, then
todo analysis. 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 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 outer workspace separates the persistent notes list from the detail container.
The list defaults to 200px, resizes within 160-420px and stores notesPaneWidth
and notesPaneOpen in goodbuddy.magic-notes-layout.v1. Existing saved widths are
preserved. Below 600px, selection replaces the visible list with detail while
retaining the mounted list. The detail toolbar keeps its list toggle, opening an
overlay drawer without shrinking detail. Selecting a note, clicking outside,
using the drawer close button or pressing Escape closes the drawer; dismissal
returns focus to the toggle. Drawer state is transient and separate from the
desktop open preference. The right-hand magic-notes-detail query container and its ResizeObserver
measure the detail width for two breakpoints: at 800px or less the record index
becomes a drawer while AI stays beside the editor; at 600px or less AI stacks
below the stream. The 600px list/detail switch is driven by the same measured
workspace width in both CSS class and React state, so the two cannot disagree.
Per-note stream scroll positions live in a workspace-local map; explicit entry
navigation takes precedence over reading-position restoration.
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.
Rich text tables remain native Quill table Delta operations. Insertion stores the active editor
range while the shared table-size dialog is open, rejects invalid dimensions before changing the
Delta, and refuses insertion when that range is already a table cell. Row, column, and whole-table
commands require a cell selection, preserve the selected range, and cut Quill history around each
command so each toolbar action is independently undoable. The read-only rich renderer loads the
same Delta with Quill's readOnly mode, so table content follows the same persisted schema without
an editable toolbar. A table row operation is content even when its cell text is empty: the
workspace keeps the composer open after focus leaves it and submits the Delta through the normal
IPC and storage path. A genuinely empty rich Delta is still rejected before IPC.
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 is collapsed by default (recordIndexOpen, default false); when
open it 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 legacy
indexPaneOpen/aiPaneOpen flags are ignored so existing users get the compact
defaults once. With no note open the workspace renders only the list, full
width, as a card grid (magic-notes-workspace--overview); the split layout
(--notes) exists only while a note is open on a wide workspace. The page
header shows the library switch in the overview and a labelled "Back to
overview" button while a note is open. Note controls live in a sticky icon
toolbar at the top of the stream (notes list, record index, entry count/updated
time, AI). Collapsing hides the entire
index and removes its grid column, border, resize separator and occupied width.
AI visibility is derived, not stored directly: aiPaneOpen = aiPanePinned || (hasAiActivity && dismissedNoteId !== detail.id), where activity means saved
comments, draft analyses, or a running/streaming analysis. Hiding records the
current note as dismissed and clears the pin; starting a new analysis clears the
dismissal. Opening AI with no activity sets aiPanePinned, which is persisted.
The new-entry composer stays mounted. It collapses with CSS
(:not(.magic-note-composer--active):not(:focus-within)) so drafts, focus
management and the unsaved-draft guard are unchanged; composerHasDraft tracks
non-empty text so a draft never collapses after blur. At container widths of
800px or less, the index defaults to hidden and the same toolbar toggle opens or
closes a fixed 168px drawer over the stream, without reserving a rail or showing
an index resize separator; choosing
an item or pressing Escape closes it. AI remains a resizable right column until the
detail container is 600px or less, then 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:
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.
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.
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
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.
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
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 notes list remains visible beside detail on desktop unless collapsed. On
narrow screens, or when opening a source note from To-dos, the originating list
remains mounted but hidden, retaining scroll position and filters. The narrow
notes view can reopen that list through the detail toolbar drawer toggle.
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.
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 while note detail is closed. 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. The compact notes list and independent todo layout retain their
own spacing. These changes do not affect Main, preload or storage.
Note List Actions
Each notes-list row has an open-detail button and a sibling action-menu button,
available while detail is open. Deleting the selected note passes through the
existing draft guard; drafts clear only after deletion succeeds. Deleting another
note retains current selection and drafts. 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.
Note Tags
Implements FR-7 to FR-10. Schema 51 adds two tables:
magic_note_tags(id, name, name_key UNIQUE, created_at, updated_at).name_keyis the NFC-normalized, trimmed, lower-cased name, soWorkandworkshare one row and the first spelling is kept.magic_note_tag_links(note_id, tag_id, position, created_at)with a composite primary key andON DELETE CASCADEon both sides.positionkeeps the order the user added tags in.
magicNoteTagNameSchema and magicNoteTagListSchema in
src/shared/magic-notes-contracts.ts hold all name rules: collapse whitespace,
1–24 characters, no commas, # or control characters; case-insensitive dedup;
at most 10 per note. IPC and MCP inputs go through these schemas before reaching
the database. The MCP tool schemas stay plain string arrays so the published
JSON schema remains simple; the gateway normalizes them.
Database behavior (AssistantDatabase):
- Summaries carry
tags: string[]via onejson_group_arraysubquery shared by list, get and summary search. The summary search also matches tag names. updateMagicNote({ tags })replaces the full list inside the sameBEGIN IMMEDIATEtransaction as the revision check, so a stale revision changes nothing.createMagicNoteaccepts initial tags.- Tags with no remaining links are deleted by the write that orphaned them
(tag replacement and note deletion).
listMagicNoteTags()returns only linked tags withnoteCount, ordered by count then name. listMagicNotes({ tags })is an AND filter onname_key.renameMagicNoteTagupdates the name, or merges when the new key belongs to another tag: links move withINSERT OR IGNORE, then the source tag is deleted.deleteMagicNoteTagremoves the tag and its links. Both bump the revision andupdated_atof every affected note so open editors do not save over the change, and both emit oneonMagicNotesChanged.
IPC: magic-notes:list now returns { notes, tags }; new channels
magic-notes:rename-tag and magic-notes:delete-tag; magic-notes:update
accepts tags. Preload exposes renameTag and removeTag.
Renderer: tag UI lives in MagicNoteTags.tsx. The workspace filters the loaded
summaries on the client (AND by tag key, plus tag names in the search box), so
filter changes do not refetch. Filter keys that no longer exist after a refresh
are ignored; renaming a filtered tag carries the filter to the new key. Tag edits
from the note header use the detail revision and applyDetail, so title and
composer drafts are kept.
Agent tools: note_create and note_update accept tags (update replaces the
list; [] clears it), note_list accepts a tags AND filter, and note
summaries returned to tools include tags. Descriptions tell the model to change
tags only when the user asks. The conversation capture panel does not edit tags
in this version.
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.
Note tools can be authorized with the knowledge-base switch off. The shared desktop gateway binds authorization to each request, without a fixed elapsed-time expiry, so long-running requests retain tool access. Request completion, cancellation, failure cleanup, or gateway disposal revokes the token. Invalid or revoked tokens report unavailable tool authorization rather than a knowledge-base expiry error.
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, logic design and UI design. Real Electron acceptance passed; focused results, fixture repairs and non-green repository checks are recorded in progress.
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, 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.
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. 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.
Validation
magic-note-storage.test.tsuses 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.tsandtests/magic-note-canvas-text.test.ts.canvas-ipc.integration.test.tsruns 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. assistant-database.test.tschecks committed writes with a second SQLite connection, successful mutation coverage, failures, no-op completion and reset.knowledge-mcp-gateway.test.tsexercises 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.tsexecutes the production preload, checking the channel, payload-free callback and listener removal.MagicNotesWorkspace.test.tsxcovers 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.