Conversation
Post-0.8 batch addressing accumulated user feedback. Added: - Settings front page is now a dashboard: live library stats (notes, tags, Spaces, attachments, capture readiness, trash) and the installed version's release notes parsed from the bundled changelog. - Images settings page with copy-in vs link-original storage, a preview height control, and attachment location, count, and size. An Image button in the editor toolbar inserts from a file honoring the storage mode; linked files render through a narrow per-file asset scope rather than widening it. - Editor settings page with a "show exact save time" toggle; the exact save time, to the minute, is always available on hover over any note date. - Contexting image handling with an absolute-path default, so copying a note as context can hand a tool the real image path, keep the reference, or drop it. - In-app feedback: a Feedback page that saves a local record and opens a prefilled GitHub issue, backed by a submit_feedback command. Fixed: - Stray caret when switching notes: each note now loads a clean editor state, so no caret lingers in a note being viewed, and undo no longer reaches into the previously open note. - Links set to open with Cmd/Ctrl+Click now show the pointer cursor while the modifier is held, so they read as clickable. Verified: svelte-check clean, vitest (298) green, cargo test green, build ok.
Only the bump-version half of 0decff9; its NoteEditor hunk cleaned up a duplication introduced by the whiteboard merge and is not needed here.
Default WEBKIT_DISABLE_DMABUF_RENDERER=1 on Linux in both the app entry point and the dev script, without overriding an explicit setting; the WebKit compositor otherwise renders a blank or corrupted window on many Linux systems. README gains Linux prerequisites and Arch/CachyOS build notes.
SEQUENCE.md defines the order work lands in and the definition of done that gates each unit. It records that everything between v0.8.0 and this branch is unfinished: ten streams, none of them decided, including a whiteboard backend that migration v4 already has a column for. feat-portable-vault-sync carries the vault and sync design, blocked on that decision.
The branch held ten unfinished streams, seven of them entangled in the already pushed 44bea94, so those are finished in place as four grouped changes. The whiteboard was local-only and comes off cleanly onto feat/note-whiteboard, taking migration v4, three runtime deps, and four defects out of the release. Sequencing it after the vault makes its sidecar an additive file type instead of a user-data migration.
Settle the props on SegmentedRow and ToggleRow, give them the keyboard and focus behavior their roles promise, and move the row shell each of them was carrying a copy of into one PrefRow component. SegmentedRow is a real radio group now: the selected segment is the only tab stop, the arrow keys move and wrap, and Home and End reach the ends. The Links page hand-rolled both controls and now uses them, which is what proves the API holds before three more settings pages depend on it.
Describe library_stats and every field it returns, and say where each number comes from: seven counts out of the store, and the two attachment numbers off the filesystem, where a missing directory reports zero rather than failing the whole call. Cover the two changelog states the dashboard has to render, an undated heading and a version with no section at all, and correct the release note to the tiles that actually ship.
Tick the tasks that are done and write down what was settled: the frozen row API, why it carries no error state, and where the row shell now lives. The one task left needs a running app, so it says so.
cargo test, npm test, and npm run check all pass clean. The live dashboard-number check and the archive step stay open for jam to verify in the running app.
The "Insert image..." toolbar action hand-wrote the same attachments/<name> markdown that editor/images.ts already builds for paste and drop. Reuse that helper so the two insertion paths produce their reference from one place instead of two copies that happen to match.
…exting SettingsImages.svelte (210 lines) and the imagePrefs store it reads from were both untested. imagePrefs.init() gates a single settings read behind a private flag, which breaks Svelte's effect context under vi.resetModules() + dynamic import, so it's covered separately from the mounted component: images.svelte.test.ts drives init/persistence/clamping directly, SettingsImages.test.ts covers what the page renders and persists. Also extends contexting-format.test.ts: a linked image (an absolute path, not an attachments/ reference) now has explicit coverage across keep, absolute, and strip, alone and alongside a copied image.
import_image_file, allow_image_file, and open_attachments_folder were missing from the attachments section. Also notes how link mode's absolute-path reference relates to the vault portability requirement, which feat-portable-vault-sync's serializer will need.
…mage-handling Resolves the open decision feat-portable-vault-sync deferred to this unit: link mode survives as a live-editing convenience, and the vault serializer materializes a copy into attachments/ at flush time rather than carrying the vault's portability guarantee on a path that can break on another device. Recorded in tasks.md with the reasoning, since the vault serializer reads it. Every task in the unit is done and its suites are green, so the change moves to openspec/changes/archive/.
An earlier truncated terminal read missed the 22-test migration_tests module. The real count was always 92 Rust tests, not 70.
feedback.jsonl accumulates every submission with nothing surfacing it to the user, unlike attachments, which already had "Open attachments folder". Add open_feedback_log, mirroring that command: it reveals the file in the OS file manager rather than opening it, creating an empty file first if none exists yet. Wire a "Reveal saved feedback" button and a line on the Feedback page stating nothing is pruned automatically. Also fixes the send() error handling: a GitHub hand-off failure after a successful local save used to report "Couldn't send feedback", which is wrong since the feedback was saved. The two failures are now distinguished, and the draft only survives a local-save failure.
Only the title was capped; a long feedback body had no limit, and open_url hands the result to the OS shell, whose URL length tolerance is tighter than a browser address bar in some environments. The message is now truncated past 1500 characters with a note that the full text is saved locally; the diagnostics block is never truncated.
SettingsFeedback.svelte (185 lines) was untested. Covers category switching, empty-body rejection, submit success, a local-save failure that keeps the draft, a GitHub-hand-off-only failure that still clears it, and the reveal-log button. feedback.test.ts gains coverage for the new body-length cap, including that diagnostics survive truncation.
The Feedback section was missing entirely. Documents FeedbackInput's shape and states plainly that no note content ever reaches this path.
…app-feedback Recorded: no pruning (a submission is bytes, not the megabytes attachments needed a different answer for), but revealed, since invisibility was the real gap the proposal called out. Reasoning is in tasks.md. Every task in the unit is done and its suites are green, so the change moves to openspec/changes/archive/.
Neither existing suite exercised loadDoc's fix for the stray caret and undo-reaches-into-the-previous-note bug: library.svelte.test.ts's "undo" coverage is workspace-delete undo, and kernel.test.ts's "caret" coverage is fold behavior over a headless state. Mounts the real component with a real EditorView in jsdom, recovers it via EditorView.findFromDOM, and switches notes on the same instance via rerender rather than a fresh render, which would mount a new view and pass trivially either way. Verified by temporarily reverting loadDoc to reuse state across notes and confirming the test fails against it before restoring the fix.
editorPrefs.init() reads three settings, then assigns all three unconditionally once the read resolves. A setter called after init() started but before it resolved (setShowExactTime, setZoom, toggleToolbar) got silently overwritten back to the old persisted value moments later. Guard each field with a #touched set so a live change always wins over a slower stale read. Found via a test asserting exactly this sequence in editor.svelte.test.ts (6 tests), which also covers init()'s read/default/error-fallback behavior directly, since a component mount can't drive a #loaded-guarded singleton's read path without breaking Svelte's effect context under vi.resetModules().
SettingsEditor.svelte (57 lines, one toggle) had no test.
The cursor feedback for the two link-open modes that need Cmd/Ctrl (preview + modclick, and edit mode) depends on ModKeyCursor toggling cm-mod-held correctly; linkMarkClass's existing tests only proved which modes lack cm-link-clickable; the fallback mechanism itself was untested. Covers meta/ctrl detection, release, window blur, and cleanup on destroy so a stale modifier state can't leak into the next note.
Every task is done: the missing regression test exists and was verified to actually catch the regression, SettingsEditor and editorPrefs are tested (surfacing and fixing a real race in the process), and the modifier-held cursor mechanism is covered across all three link-open modes. Suites are green, so the change moves to openspec/changes/archive/.
The prior commit's git add ran against both the new and old (already-moved, nonexistent) paths in one call; git appears to reject the whole add when one pathspec doesn't match, so it silently staged only the earlier git-mv snapshot rather than the fully-edited content sitting in the working tree. Same content this unit's task list always described; only the commit that carried it was wrong.
Opening a database with a user_version past the last known migration returned an error, but the setup hook let it flow into build().expect(), which panics inside a call that cannot unwind and aborts the whole process. Give AppError a distinct SchemaTooNew variant so setup can catch it before that expect, show a message telling the user to update, and exit cleanly instead.
The pinned 1.91.1 channel had no rust-analyzer component available, so VS Code's extension fell back to a bundled version too new for the toolchain to satisfy.
…gs-dashboard Verified against dev-0.9.0.db, a real v3-schema library, after the app's own pre-migration backup made that possible without touching the drifted-to-v4 primary database. All counts matched.
Every task is done: the frozen row primitives, the dashboard counts confirmed live against real data, the attachments and capture-readiness gaps closed, and the changelog view's edge cases covered. Suites are green, so the change moves to openspec/changes/archive/.
Dates the changelog and adds the Linux and release-script fixes that were already done but had no changelog line of their own.
A note can now be a whiteboard: its canvas lives in surface_data and its body holds the text written on the board, so search and inline tags keep working. Converting is one-way, only a whiteboard holds a canvas, and an auto title freezes on convert since the body moves with the drawing. List rows leave the canvas out, since a board can hold pasted images.
A whiteboard's note file now carries kind: whiteboard, and its canvas is a standard .excalidraw file beside it with the same name, openable in Excalidraw. The canvas moves, trashes, and deletes with its note under the mirror's ownership rules, tracked by a hash that migration v6 adds, and Check vault compares it by content. A one-time export writes it too.
Whiteboards return from the pre-release branch with its four defects fixed. Board edits ride the same save queue as typing, so a stroke survives a note switch, trash, or quit. The board follows the app theme, and converting a note lays its text onto the board instead of hiding it. New Whiteboard sits in the palette and the File menu, Export writes a board as .excalidraw, and the board's keys stay the board's. Excalidraw loads on first use, with its fonts served locally.
Pin the fix for the defect that kept whiteboards off 0.9.0: a stroke still waiting on the canvas reaches the save queue when the board closes, flushes, or switches. Also cover conversion carrying text onto the board, the board's text and fingerprint, and the keys a board keeps.
Add contentKind and surfaceData to the notes commands, note that update_note's reply omits the canvas, and list the File menu events and the .excalidraw export.
Describe what a whiteboard stores and why its body is the text on the board, and how migration v6 carries its canvas file through the vault mirror.
Archive feat-note-whiteboard with how each of the four pre-release defects was fixed, add the Whiteboard Notes requirement, mark unit 12 done, and close the vault's stage 3 whiteboard gate.
An image stays while anything references it as attachments/<name>: a note in any state, a whiteboard's canvas, or the capture draft. The store re-checks each candidate itself before removing it, and takes the live vault's copy only while it is unchanged.
Deleting notes for good now also removes the images only they used, under the same store lock as the delete so no save can start using one mid-cleanup. Settings > Images reports how many stored images no note uses and removes them on request, leaving anything added in the last hour alone.
Cover the unused count and size, the disabled state when every image is in use, and that removal happens only after the confirm.
Add unused_attachments and remove_unused_attachments, and what a permanent delete now removes and keeps.
Archive feat-attachment-cleanup with why removal happens at permanent delete and in an explicit sweep rather than in the background, add the Image Cleanup requirement, and mark unit 11 done.
Live notes, every tag and Space, and one link per membership, read from the existing tables each time it is asked for. Nothing about the graph is stored.
A Graph place in the sidebar lays the library out with a seeded force layout, so it draws the same way every visit, and settles it across frames so a large library never freezes the view. Hovering lights a note's neighborhood, and choosing a node opens the note, filters by the tag, or opens the Space. It opens around the note you have open.
Pin that the layout is deterministic, keeps known nodes nearly still on refresh, and settles in steps to the same place; that the view offers every node as a button and routes each kind; and that every other place leaves graph mode.
Archive feat-graph-view with its decisions (tags and Spaces as edges, a sidebar place, nothing stored), add the Graph View requirement, and mark unit 13 done.
Several Tauri projects share the default 1420 dev port, so running InstantNotes alongside another one collides. Give this project its own port (1422) across vite.config.js, tauri.conf.json, tauri.dev.conf.json, and the tauri-dev script so multiple projects can run side by side.
Move @tauri-apps/plugin-updater from 2.10.1 to 2.12.0, window-vibrancy from 0.6 to 0.8, and rusqlite from 0.32 to 0.40, along with the resulting lockfile churn in package-lock.json and Cargo.lock.
Replace the update-available modal with a synthetic Update Space in the sidebar: a green asterisk that holds two notes, the version you are going to and the one you are coming from, the install button, and the release notes as a readable note. It removes the old 'remind me later' snooze in favor of a Space you can simply ignore, and the welcome pill and tray check now open it directly.
Introduce src-tauri/src/error.rs, src-tauri/src/events.rs, src/lib/api/error-codes.ts, and src/lib/api/events.ts as the single source of each language's error codes and event names, plus src/lib/api/contract.test.ts to hold the two halves equal. Replace roughly 40 hand-rolled code: literals and 12 bare event-name literals with references into the registries. This is behaviour-preserving: it comes from docs/CODE_SMELL_AUDIT.md phases 0-4 and 6, and the VALIDATION code it removes was latent and never user-visible, since the paths that emitted it surface e.message directly rather than going through friendlyMessage.
The unit landed on 0.9.0-pre on 2026-09-24 with every task checked, but no entry was ever made in SEQUENCE.md, so the file that says what a release contains left it out. It takes 13a by landing order and the boundary registries move to 13b. Add a test that fails when an archived change records landing on a release branch and the ledger does not name it, since nothing was checking.
…resolved Dependabot's three grouped PRs were closed rather than merged: the cargo group was superseded by 6197b97, and the npm and github-actions groups are stale and belong to the dependency baseline unit. Unit 6 now says so, and corrects its own claim that the github-actions group is CI only, since tauri-action v0 to v1 is the action that builds and publishes the release artifacts. The two whiteboard safety refs are deleted, with the evidence that 0.9.0 holds everything they carried. Also clears the em dashes this file had collected.
A whiteboard could only be created from the File menu or the command palette, so the feature was invisible to anyone reading the UI: the toolbar's one create button always made a document. The button becomes a split control. The + still makes a document in one click, and a chevron beside it opens the existing context menu with both kinds, each labelled with its keyboard shortcut so the menu teaches the keys rather than replacing them. Right-clicking the control opens the same menu. ContextMenu grows an optional shortcut hint per item and an optional anchor element. Presses on the anchor no longer count as outside the menu, so the control that opened it can own the toggle instead of the menu closing on press and reopening on the click.
Set the release date to 2026-09-25, the day it is cut. The Whiteboards entry now names the note list's new create button as the first way to start a board, since that is the one a reader will see without knowing the shortcut.
A Windows checkout converts CHANGELOG.md to CRLF, and the parser split it on "\n" alone, so every line kept a trailing carriage return. The bullet pattern ends in `(.+)$` with no `\s*`, and `.` does not match a carriage return, so no bullet matched and the Settings dashboard showed an empty "What's new" on Windows. Split on either ending instead, which fixes it for every pattern rather than one.
The new CRLF tests replaced "\n" to make their fixture, which on a Windows checkout turned the already-CRLF file into "\r\r\n" and tested an input no checkout produces. Convert from either ending instead, so the fixture is the same on every platform.
The export path test hardcoded "/tmp/a.md". On Windows a leading separator is not an absolute path without a drive prefix, so every fixture was rejected for being relative before its extension was considered, and the test failed there. Build the paths from temp_dir(), which is absolute on every platform, and keep one explicit relative case for the check it is meant to cover.
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 keeps notes in a SQLite library that only InstantNotes can read. That is fast and safe, but it asks the user to trust a single opaque file with everything they write, and it makes the library a dead end: nothing else can open it, and nothing carries it to another machine. 0.9.0 answers that directly with a Vault, a plain Markdown folder the app keeps in step with the library, readable in any editor with the app closed.
The release then widens what a note can be. A note has been one thing since 0.1.0, a Markdown document, so anything spatial had to live outside the app. Whiteboards make a note a canvas without making it a second class of object: a board is still a note, still searchable, still tagged, still exported. Graph answers the companion question, which is what the library looks like as a whole rather than as a list.
The rest is debt this growth exposed. Images accumulated with no way to reclaim the space. The two halves of the app named error codes and events in two places that could silently drift apart. Updates arrived as a modal dialog that interrupted the user to ask a question they could not act on yet.
What Changes
.excalidrawfile beside its note. Text on a board stays searchable and its#tagsstill tag the note. Creating one is now visible in the note list rather than only in the File menu and the command palette.ADDED Requirements
Requirement: the vault mirrors the library without owning it
The app SHALL keep every non-deleted note as a Markdown file in the chosen folder, carrying its tags and Spaces, and MUST treat the SQLite library as the source of truth. A vault write failure MUST NOT fail the note write that triggered it.
Scenario: a note edit reaches the folder
Requirement: a whiteboard is a note, not a separate object
A note SHALL be either a document or a whiteboard, and a whiteboard MUST remain subject to every behavior a note has: search, tagging, Spaces, trash, and export. Converting a document to a board MUST carry its text onto the canvas.
Scenario: searching for text written on a board
#portson the canvas#portstagRequirement: a whiteboard can be created from the note list
The note list SHALL offer creating a whiteboard from its create control, so a board can be started without knowing a keyboard shortcut or opening the command palette. The plain create action MUST continue to create a document in one click.
Scenario: starting a board from the list
Requirement: removing images never removes one still in use
Deleting a note permanently SHALL delete only the images no surviving note references. Images referenced by a note in the Trash or the Archive MUST be kept, because those notes can be restored.
Scenario: emptying the Trash with a shared image
MODIFIED Requirements
Requirement: a newer library is refused without crashing
Opening a library whose
user_versionis past the last migration this build knows SHALL surface a message telling the user to update. The app MUST NOT abort at launch.Scenario: opening a library from a newer build
Requirement: an update informs rather than interrupts
An available update SHALL be presented as a Space in the sidebar and MUST NOT open a modal dialog or require the user to dismiss a reminder. Installing MUST remain the user's explicit action.
Scenario: an update arrives while the user is writing
Requirement: the dashboard reads the changelog on every platform
The bundled changelog SHALL parse identically whichever line ending the checkout produces. A CRLF copy MUST yield the same release, sections, and items as an LF copy.
Scenario: a Windows checkout renders What's new
Verification
.github/workflows/build.yml:1.1
npm run checkpasses: 610 files, 0 errors, 0 warnings.1.2
npm testpasses: 54 files, 490 tests.1.3
cargo test --workspace --manifest-path src-tauri/Cargo.tomlpasses: 220 tests, 1 ignored.1.4
cargo clippy --workspace --all-targets --manifest-path src-tauri/Cargo.toml -- -D warningsis clean.1.5
cargo fmt --all --check --manifest-path src-tauri/Cargo.tomlis clean.1.6 All three platform jobs pass, including windows-latest. That runner had never run against this branch before the PR opened, and it caught two defects: the CRLF changelog parsing above, and an export-path test whose fixtures assumed a Unix absolute path.
2.1 A 0.8.0 library (schema v3) opens and applies v4, v5, and v6 with every note intact.
2.2 A library touched by a pre-release whiteboard build opens instead of being refused, and its boards render as whiteboards.
2.3 A library at a schema past v6 shows the update message and does not abort the app.
3.1 Turning the vault on writes every note, and "Check vault" reports no mismatch.
3.2 Editing a note updates its file; trashing one moves the file to the vault's trash folder.
3.3 A whiteboard writes a
.excalidrawfile beside its note.4.1 The note list create control makes a document on click and offers New whiteboard from its menu, as does a right-click on the control.
4.2 Cmd+Shift+N and the File menu still create a board, and the palette still converts a note.
4.3 Text written on a board is returned by search, and a
#tagon the canvas tags the note.5.1
package.json,src-tauri/tauri.conf.json, andsrc-tauri/Cargo.tomlall read 0.9.0. The core crate versions independently by design and stays at 0.8.0, perscripts/bump-version.mjs.5.2
CHANGELOG.mddates 0.9.0 as 2026-09-25 and the dashboard's "What's new" finds that section for the running version.