feat: spaces, a construct editor kernel, and the 0.8.0 cleanup - #42
Merged
Conversation
- 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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
workspacename behind a documented boundary inclient.ts.store.rs,lib.rs,LibraryStore, andSettingsVieware 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 underdocs/; scaffold residue and shipped OpenSpec changes are cleared out.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
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
**bold**with the markers folded**markers become visible before any keystroke is acceptedRequirement: 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
Verification
npm run check,npm test(272 frontend tests),cargo test --workspace,cargo clippy --all-targets -- -D warnings, andcargo fmt --checkall pass, mirroring the Build workflow.npm run tauri -- build --no-bundlecompletes on macOS, Linux, and Windows.v3_drops_sync_columns_and_preserves_notestest).package.json,tauri.conf.json, and both crates.