Skip to content

feat: spaces, a construct editor kernel, and the 0.8.0 cleanup - #42

Merged
jamubc merged 41 commits into
mainfrom
feat/spaces
Jul 11, 2026
Merged

feat: spaces, a construct editor kernel, and the 0.8.0 cleanup#42
jamubc merged 41 commits into
mainfrom
feat/spaces

Conversation

@jamubc

@jamubc jamubc commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Why

InstantNotes exists to close open loops: capture must be cheap, retrieval must be trusted, and a parked note must resurface at the right moment. This branch advances all three, then pays down the debt that accumulated as the app grew.

Grouping needed to become a place you go, not a label you hunt for, so workspaces are presented as Spaces. The editor's per-feature CodeMirror extensions had drifted into disagreeing about what is hidden, clickable, and safe to type into, so they are replaced by one construct kernel. And a July 2026 audit found the product's logic trapped in a few overloaded hubs (a 900-line library store, a 1200-line Tauri shell, a 1000-line SQLite module), plus dead schema and phantom docs; leaving that in place would tax every future change.

What Changes

  • Spaces: workspaces are presented as Spaces across the UI (inline rename, delete with undo, scoped tag chips). Storage keeps the workspace name behind a documented boundary in client.ts.
  • Editor kernel: a single syntax walk feeds a construct table that derives decorations, caret legality, and click routing, replacing the standalone wysiwyg, link, image, and task extensions.
  • Bulk IPC: multi-select pin, archive, trash, restore, and empty-trash run in one transaction and emit one change event.
  • Structural cleanup: store.rs, lib.rs, LibraryStore, and SettingsView are split into focused modules; the unused sync columns are dropped by a v3 migration; the phantom API and data-model docs are restored as real files under docs/; scaffold residue and shipped OpenSpec changes are cleared out.
  • Tests: component testing is introduced (jsdom plus Testing Library) alongside the existing store, save-queue, and core suites.

ADDED Requirements

Requirement: Spaces group notes as places

The library SHALL present workspaces as "Spaces". A note MUST be allowed to belong to more than one Space, and deleting a Space MUST NOT delete its notes.

Scenario: deleting a Space keeps its notes

  • GIVEN a Space containing three notes
  • WHEN the user deletes the Space
  • THEN the three notes remain in the library, and an undo toast offers to restore the Space and its memberships

Requirement: the editor never hides text under the caret

The preview editor MUST reveal a construct's raw markdown whenever the selection touches it, so a keystroke can never land inside invisible markup.

Scenario: approaching a folded marker

  • GIVEN a note rendering **bold** with the markers folded
  • WHEN the caret moves onto the bold span
  • THEN the ** markers become visible before any keystroke is accepted

Requirement: multi-select actions are atomic

A bulk action over N selected notes SHALL execute as a single transaction and emit a single change event.

Scenario: trashing a multi-selection

  • GIVEN five selected notes
  • WHEN the user moves the selection to Trash
  • THEN all five are trashed in one transaction, and the list refreshes once

Verification

  • npm run check, npm test (272 frontend tests), cargo test --workspace, cargo clippy --all-targets -- -D warnings, and cargo fmt --check all pass, mirroring the Build workflow.
  • npm run tauri -- build --no-bundle completes on macOS, Linux, and Windows.
  • Opening a pre-0.8.0 database applies the v3 migration and keeps every note (covered by the v3_drops_sync_columns_and_preserves_notes test).
  • Versions read 0.8.0 across package.json, tauri.conf.json, and both crates.

jamubc added 30 commits July 10, 2026 14:18
- Reconcile inline tags on body save and promote explicit adds to manual,
  so removed #tokens stop resurrecting and chip removals survive the next
  keystroke.
- Back up the database via VACUUM INTO before every migration and refuse
  databases from a newer schema instead of misreading them.
- Recover from a corrupt database by setting it aside as .corrupt-N and
  starting fresh; corruption is a distinct error class routed to recovery.
- Verify the WAL pragma actually applied and skip no-op update patches.
- Grow the core suite to cover each of these paths, including a real
  v1-fixture migration test.
- Menu, tray, and Dock quits ask the library window to flush queued edits
  and exit through quit_app; a hung webview is covered by an 800ms
  fallback so quit can never wedge.
- The updater's restart exit code passes through the exit interception
  untouched, so an installed update is never stranded by the handshake.
- Wire Store::open_or_recover at startup and tell the user with a dialog
  when a corrupt database was set aside.
- Record global-shortcut registration failures in managed state,
  queryable by the frontend.
- Clicking outside the always-on-top panel dismisses it with the draft
  preserved, guarded against in-flight saves and self-initiated hides.
- A 300ms Saved beat confirms the write before the panel hides, so
  success reads as more than the window closing.
- Cmd+Enter (Ctrl+Enter on Windows and Linux) saves and opens the
  library.
- The quit handshake flushes the debounced draft before the process
  exits.
- Toast queue with a 5s auto-dismiss that pauses on hover, a visible cap
  of three, and FIFO eviction; timer logic is pure and tested.
- Promise-based confirm store and an alertdialog host: Cancel holds
  initial focus, Tab is trapped, Escape and the scrim cancel, and focus
  returns to the invoker on close.
- The dialog is a hard keyboard boundary: no key reaches the global
  shortcuts while it is open.
- Per-note dirty tracking: a failed write retries once, then shows Not
  saved while the edit stays queued for every later flush; Saved can no
  longer lie about unpersisted data.
- Monotonic refresh tokens drop stale list and search responses; search
  keystrokes debounce 150ms with an instant clear.
- flushPendingEdits performs exactly one no-retry write per dirty note;
  retry timers are tracked and cancelled on newer writes, drops, and
  destroys, so nothing fires after a flush settles.
- Soft deletes flush the pending edit first, then offer Undo in a toast;
  permanent deletes act on ids snapshotted when their dialog opened
  (destroyNotes), so a drifted selection cannot be destroyed by mistake.
- All five window.confirm sites are replaced by the shared dialog, and
  the global shortcuts ignore keys while it is open.
- The updater flushes edits before relaunching; a stolen capture hotkey
  surfaces as a dismissible welcome-screen notice.
- search_notes brackets matches in U+0001/U+0002 sentinels: snippet()
  over the body (16 tokens) and highlight() over the title, so a query
  matching only the title still shows why the note hit.
- A tolerant parser turns sentinels into segments rendered as mark text
  nodes; malformed input degrades to plain text and nothing ever goes
  through {@html}.
- Five Rust sentinel tests and ten parser tests.
- An empty query lists the five most recent notes; typing adds up to
  eight ranked search hits beside commands, debounced 120ms with
  per-request stale tokens so a slow reply can never clobber a newer one.
- Arrow keys move across sections with wrap-around; the flattening and
  navigation math lives in palette-sections.ts with its own tests.
- The palette is now a real combobox/listbox for assistive tech.
- Opening a note the current sidebar scope cannot show widens the scope
  first, so the list, filters, and editor stay in sync after the jump.
- Right-click (or Shift+F10) opens a context menu on a tag row with
  Rename Tag and Delete Tag; double-click renames in place. Rows carry no
  resting chrome: management appears only when asked for.
- Rename normalizes input, rejects empties, and surfaces a backend name
  collision as a shake with the friendly message; the tag id survives, so
  an active filter stays put.
- Delete goes through the shared confirm dialog naming the note count,
  clears the filter if it pointed at the tag, and reports a failure with
  a toast instead of silence.
- The context menu is the same keyboard boundary as the dialog: focus is
  managed, arrows cycle, Escape closes, and nothing leaks underneath.
- Twenty behavioral tests driven by fake timers and hand-resolved
  promises: save lifecycle, the single retry, exactly-once flush, destroy
  paths dropping queued edits, write-before-trash ordering for Undo,
  refresh race tokens, search debounce, and init ordering.
- vitest now runs through the sveltekit plugin so Svelte 5 runes compile
  in .svelte.ts modules under test.
- The bump script now rewrites both version fields in package-lock.json,
  anchored on the package name so dependency versions are never touched.
- Adds a regression test and fixes the existing drift (the lockfile still
  said 0.5.2 while the app shipped 0.7.0).
- dtolnay/rust-toolchain pinned to 1.91.1 with clippy and rustfmt
  components declared explicitly; the action installs a minimal profile,
  so without them the lint gate breaks the day runner defaults advance.
- A tag push now runs svelte-check, vitest, and cargo test before
  tauri-action gets to build anything.
- clippy -D warnings and cargo fmt --check gate every PR on all three
  platforms; weekly grouped dependabot for npm, cargo, and actions.
- Drop .gitignore entries that claimed tracked lockfiles were ignored.
- make-update-manifest.sh warns loudly that it writes a darwin-only
  manifest, so a multi-platform release cannot use it by accident.
- README documents the real dev command.
Release notes for the work so far live under Unreleased in the
changelog; retitle that section to 0.8.0 when the release is cut, after
the remaining planned updates land on this branch.
The 0.7.0 notes claimed full Linux support; in reality the AppImage is
untested and may not work properly. Both the GitHub release draft and
the changelog now say so, with the stated focus on macOS and Windows
first.
Workspaces become Spaces: rows adopt the Tags interaction contract
(context menu, double-click rename, no resting chrome), delete becomes
immediate with an undo toast, and a space gains scoped tag chips so one
place can be sliced by the tags captured into it.
Space rows adopt the tag rows' contract: context menu and double-click
rename replace the hover delete button, wired to the already-tested
rename_workspace command. Deleting a space is immediate with an Undo
toast that restores every membership, so delete_workspace now returns
the member note ids (a re-list would miss archived and trashed members).
Inside a space the note list grows tag chips scoped to that space's own
notes, backed by a new list_workspace_tags query.
Capture is discharge, trust is release, resurfacing is closure. Names
the target as powerful rather than simple, and the anti-goal of ever
becoming a task manager.
A note captured through the hotkey and never opened again is an open
loop; after three days it surfaces in a quiet Revisit entry under All
Notes, hidden entirely at zero. Opening a note releases it on the spot,
so the list burns down as you triage. Built on the existing
last_opened_at column via two new NoteFilter fields.
Pinned, Today, Yesterday, Previous 7 Days, Previous 30 Days, then month
names and years, matching the macOS grouping people already read
fluently. Pure clock-injected bucketing with tests on every boundary;
the Revisit view stays flat because it sorts by capture age instead.
shorter messaging, simplified install, development notes, trimmed outdated platform & release details.
The shell stamps the moment it starts revealing the capture panel; the
webview reports back after the textarea is focused and painted, and the
delta lands in a rolling window (never note content). About shows the
median as Capture readiness, and a perf smoke test guards the capture
write path against order-of-magnitude regressions.
A brand-new note can never match the never-opened-and-old filter, so
staying in the mode would hide the note the user just asked for. Same
escape newNote already performs for the trash and archived views.
Replace the per-feature CodeMirror extensions with one kernel: a single
syntax walk feeds a construct table that derives decorations, caret
legality, and click routing, so what is hidden, revealed, and clickable
can no longer disagree. Each markdown construct is a small spec; the old
wysiwyg, link, task, and image extensions become entries in that registry.

Also adds inline image attachments (paste or drop, stored under the app
data dir and served over the asset protocol) and live link preferences on
a new Links settings page.
Add a drag handle on the sidebar border with pointer-capture resizing,
double-click reset, and arrow-key nudging. Width and collapsed state
persist to the settings store, and Cmd+\ or a command-palette entry
toggles the sidebar from anywhere.
Drop the create-tauri-app sample SVGs from static/, move the thirteen
shipped OpenSpec change proposals under openspec/changes/archive/ so the
active list only shows work in flight, and bump the core crate to 0.8.0
to match the app version.
Add docs/API.md and docs/DATA_MODEL.md describing the IPC surface, error
codes, schema, and the tag and title rules that several source comments
already cite. Point the title-derivation and capture comments at real
sections instead of the missing spec ids they referenced.
The sync_state, version, and last_synced_at columns were written on every
note but never read by any logic. Remove them with a v3 migration that
drops the columns from existing databases, and strip the matching fields
from the Note type on both sides of the IPC boundary. A real sync feature
will design its own schema when it lands.
Move the note, tag, workspace, and settings methods out of the
1000-line store.rs into store/{notes,tags,workspaces,settings}.rs. The
struct, migrations, shared row-mappers, and lifecycle stay in store.rs;
each impl block now lives with its domain. No behavior change.
Move the note, tag, workspace, and settings Tauri commands out of the
1200-line lib.rs into commands/{notes,tags,workspaces,settings}.rs. The
window, tray, menu, quit, capture, and run() wiring stays in lib.rs, so
the app shell is in one place while each data command group is easy to
find. No behavior change.
Add set_notes_flags, soft_delete_notes, restore_notes, and destroy_notes
to the core and IPC layer, each a single statement over an id set with
one change event. The library store's bulk pin, archive, trash, restore,
and empty-trash paths now make one call instead of one round trip per
selected note.
jamubc added 11 commits July 11, 2026 12:09
The two rows were nearly identical: the same select, rename, and menu
contract, the same markup and CSS, differing only in the count field, the
# prefix, and the name normalizer. Replace both with a parameterized
SidebarEntityRow, so a row fix is one edit instead of two.
… churn

Collapse the three identical filter-reset blocks (selectWorkspace,
selectRevisit, setTagFilter) into one resetForNavigation helper, and
replace the hand-rolled copy-then-mutate of the save queue's unsaved and
failed collections with small reactive-collections helpers that return a
fresh Map or Set. Behavior is unchanged; the filter and save-queue tests
still pass.
Stand up component tests (jsdom plus @testing-library/svelte, with browser
resolve conditions so components mount) and cover SidebarEntityRow's rename
flow: select, context menu, commit on Enter, the empty-name error, and
Escape to cancel. The existing store and helper tests run unchanged under
the jsdom environment.
Move the debounce/retry/flush persistence machinery (the unsaved and
failed maps, retry timers, and persistBody) out of the 916-line
LibraryStore into a composed SaveQueue class. The store keeps a thin
facade (editBody, saveState, flushPendingEdits) and injects the one
coupling: applying a confirmed write to the open note. The save-queue
tests pass unchanged.
Move the checkbox multi-selection (the id set, its anchor and active-end
cursor, and the toggle/range/step logic) out of the LibraryStore into a
composed SelectionModel that takes the visible-id list as a dependency.
The store keeps the open-note state and the editor-sync orchestration.
Persistence and selection now live apart from the store's core query
engine; all 269 frontend tests pass.
Move window management, capture-latency metrics, the file I/O commands
(theme, note export, attachments), and the quit handshake out of the
970-line lib.rs into shell/{capture,windows,files,quit}.rs. lib.rs keeps
the shared IPC types and run() with its native menu, tray, and setup
wiring, down from 970 to 445 lines. Behavior is unchanged; the moved unit
tests travel with their code, and a small mark_shown method replaces the
one place a window helper reached into the capture metrics' internals.
Break the 661-line SettingsView into a shell (header, breadcrumb, and the
category grid) plus settings/{SettingsAbout, SettingsContexting,
SettingsLinks}.svelte, each owning its markup and scoped styles. The shell
drops to 187 lines, and each page is editable without scrolling past the
others.
Add a component test for the settings shell: the landing grid renders a
card per page, opening a card shows the page under a breadcrumb that
returns home, and Escape steps back to the grid before closing the view.
Guards the per-page split; the command palette's keyboard-nav logic is
already covered by the palette-sections unit tests.
The UI says "Space"; the commands, tables, and Rust core say "workspace".
Document that boundary authoritatively where the two meet (client.ts) and
in the project glossary, so the full through-stack rename (churn with no
user value) stays unnecessary and no layer has to guess which name it uses.
The 'mirrors the previous inline helper' aside pointed at deleted code and
carried no rationale. The other comments the audit called essays are
load-bearing (the client/server normalization boundary, the testability
rationale on the snooze helper) and stay, per when-in-doubt-keep.
cargo fmt --all over the modules split out of store.rs and lib.rs;
whitespace only, no behavior change. Keeps the CI fmt gate green.
@jamubc
jamubc requested a review from a team as a code owner July 11, 2026 20:16
@jamubc
jamubc merged commit aa84fd1 into main Jul 11, 2026
4 checks passed
@jamubc
jamubc deleted the feat/spaces branch July 11, 2026 21:08
@jamubc jamubc mentioned this pull request Jul 11, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant