Repository navigation
Releases: Pranesh-Selvaraj/Enclave
Release list
v1.13.0
A backlog sweep: the remaining open bugs were fixed, the auto-updater now fails closed without a signed release, and Android gets event-driven widgets, a working SAF file flow and JVM tests. On top of that, this release reworks the phone shell and settings for readability, makes the desktop folder tree show its structure, and fixes a batch of UI-size (zoom) layout bugs and a Glass-rendering bug found while sweeping the appearance settings. Anything that cannot be verified without hardware (real-phone RAG, a Windows pass) is called out in the issues rather than silently listed as done.
✨ Added
- 🧪 Android JVM unit tests (#124). The Android project had no test sources;
src/test/kotlinnow covers the widget cache codec, the note-parsing (later moved to Rust), and the hide-when-locked decision. CI runs:app:testUniversalDebugUnitTestbefore the APK build, so parser/codec regressions fail fast. Unit tests need a realorg.json, since the mockableandroid.jarships stubs that throw. - 🧪 A UniFFI binding drift check (#120). The committed Kotlin bindings are now regenerated in CI and
git diff --exit-codefails the Android job if the Rust FFI surface changed without regenerating — previously a stale binding compiled fine and failed at runtime on a device. - 🧪 A locked-capture UI-audit scenario (#22). The tour now stubs a locked password vault and asserts the Quick Capture window renders the inline unlock guard instead of an auto-unlocked composer, on desktop and phone.
- 🖥️ The UI audit runs on Windows now, including display scaling (#126).
windows-125andwindows-150profiles (1440×900 at dpr 1.25/1.5) exercise fractional DPI,--scrollbarsleaves the platform's scrollbars visible for inspection, and the harness discovers Chrome/Edge underProgramFilesand the macOS/Windows Playwright caches. A scheduledui-audit-windowsCI job runs the same tour on windows-latest and uploads its screenshots as a separate artifact; the first full run passed 71/71 scenarios with zero audit issues. - 📁 Folder filtering and a visual folder tree. Choosing a folder — from the phone drawer or the desktop tree — filters the home screen to that folder's notes (
/?folder=<id>), shows the folder name as the heading, and offers a "Show all" reset. The desktop tree draws a guide line from each folder into its notes and labels the root list "Notes · no folder", so it is obvious which notes live inside a folder and which do not; folders list all of their notes (favorites included), with the Favorites section kept as a cross-cutting shortcut rather than a location. (#168)
🔧 Changed
- 🔄 Widget note projection moved from Kotlin to Rust (#122). Widgets used to loop
listDocuments()+getBlocks(id)per note and walk ProseMirror JSON inNoteJson.kt;core-dbnow fetches every document plus its widget/tags/doc payloads in one query andcore-apiprojects typedWidgetNote/ChecklistRowrecords, which Kotlin stores in its unchanged Keystore-wrapped cache. Ticking a checklist row is a Rust call that writes through the same block-upsert path as the editor, so rev, FTS and sync react like a normal save.NoteJson.ktis deleted; the parsing tests moved to the Rust suite. - 🧩 Android widget cache refresh is event-driven (#127). The Rust shell calls a new Kotlin JNI bridge after the mutations that change what widgets show (block saves, title changes, delete/archive/restore, lock, completed sync merges), and the Kotlin side conflates bursts. The old 20-second vault poll is replaced by a two-minute fallback.
- 🔔 The Android sync notification only redraws when its text changes, and idle discovery backs off from 10 to 60 seconds (10 seconds still applies while peers are found or connecting).
- 🎨 Phones force the matte background. Glass
backdrop-filterchrome is desktop-only: the theme store clamps a stored Glass choice to matte at ≤768px and self-heals the setting, and Settings hides the Glass option on phones (tablets and desktop keep it). - 📡 Android settings no longer hold the mDNS multicast lock while sync is off (#123). The lock moved from
MainActivity(where it lived for the whole app lifetime) intoSyncService, which starts exactly when sync is enabled and stops when it is disabled or the vault locks. - 📱 The phone drawer is a folder list with bottom app actions. It drops the "Enclave" heading, the Theme row and the note tree: Home and a plain folder list lead the drawer (tap a folder to filter home; + creates one inline), and Sync / Settings / Lock vault are a simple list pinned to the bottom. The redundant brand/privacy footer is gone, so the actions sit flush with the bottom edge instead of floating above a dead gap, and opening Settings no longer force-closes the drawer first — the sheet covers it, and the stale-overlay history cleanup could otherwise pop the just-pushed entry and close Settings immediately. (#168)
- ⚙️ The phone settings sheet is reworked for readability and touch. Labels sit on their own line with wide controls (segmented choices, colour swatches, text fields) full-width beneath them rather than squeezed to the right and wrapped; choice buttons share the row evenly at thumb height, and compact controls (switches, small buttons, the colour well) stay beside their label. It gains a sheet grab handle, a larger title and close target, roomier section spacing, and a full-width centred Close button. (#168)
🐛 Fixed
- 🧩 The Android widget lock placeholder could stay stale (#132). The old cache tick read
widget_hide_lockedeven while locked, and that call throwsVaultLocked; the exception aborted the tick after mirroring the lock state but before re-rendering, so with “hide widgets while locked” on the last-rendered note content could remain on the home screen until an unrelated update. The dial is now read only while unlocked (falling back to the last value), the placeholder always renders when the lock state changes, and locking triggers it directly from Rust. - 🧩 Android widget updates can no longer take the process down (#121).
NoteListWidgethad no exception guard, so a failure inprovideGlanceor an update broadcast could surface as “Problem loading widget” and crash the process. A shared helper now catchesThrowablearound every widget's render and each widget'supdateAll, logs the widget name/id and cause, and renders the failure placeholder.WidgetStorealso catchesThrowable, not justException, on the JNA boundary. - 📄 Android Markdown import, page export and backup now work (#38). The dialog plugin returns
content://URIs from both Android pickers, but the shell fed them tostd::fs; a newAndroidDocumentsContentResolver bridge reads and writes SAF URIs over JNI,import_markdown/write_fileroute them there, and Settings → Backup offers a SAF location so a phone backup can leave app-private storage. Vault-wide folder export is now explicitly desktop-only on Android (the picker cannot choose a directory), with a clear message instead of a bogus path. Restoring a backup on Android remains desktop-only and is tracked in #38. - 🔐 The Quick Capture window was already fixed for password vaults and is now regression-tested (#22). The capture route checks the shared core's unlocked state and renders the inline guard when locked, which prompts for the password/PIN and derives the master key; the old path that handed the password-encrypted
vault.keyblob tounlock_vaultis gone. - 🧾
SHA256SUMSwas written outside the released artifact set in the new signing step (follow-up to #83); it now takes an explicit output path and CI writes it into the release artifacts directory so the checksums actually ship. - 🧪 The scheduled UI audit had been red on
mainsince v1.12.0 without anyone noticing (#135). All 33 scenarios ran, but four assertions still clicked controls that v1.12.0 made platform-specific (page width on phones, vibration feedback on desktop). The audit matrix now skips hidden-by-design controls and opens the mobile settings panel through the drawer; the Linux job is green on the branch, so the weekly run recovers on merge. - 🧪 The UI audit leaked appearance state between device profiles (#134).
settingsMatrixintentionally ends on UI size Xlarge, so the next profile in the same browser session measured everything underzoom: 1.18and failed the sidebar scenario withafter = saved × 1.18— a harness artifact, not an app bug. Each profile now clears localStorage before its scenarios; the Windows DPI run went from 52/54 to 54/54. - 🪟 The sidebar resize handle drifted off the edge at Large/Xlarge UI size (#169). UI size applies CSS
zoom, which scales rendered boxes but leaves layout px unscaled; the handle is positioned in layout px, yet its width was read fromgetBoundingClientRect()(zoomed px) and drags used raw viewport px, so it floated further from the edge the larger the zoom and dragged by the wrong distance. It now measures withoffsetWidth, converts pointer deltas with the--ui-fittoken, and caps the width in layout px. - 🎨 Glass had no blur on Chromium shells (#170). Vite 8 transforms CSS with Lightning CSS, which treats
backdrop-filterand-webkit-backdrop-filteras the same declaration and keeps only the last, so the shipped stylesheet kept only the prefixed form — unsupported in Chromium — and Glass rendered as flat translucency on WebView2/Android while still blurring on WebKit and in dev. The two prefixes now live in rule blocks that differ in selector syntax (:is(...)vs an explicit list), which Lightning CSS does not collapse; the same split keeps the True-black override'sbackdrop-filter: noneon both engines. - 🪟 Overlay viewport caps ignored UI size (#171).
zoomdoes not scalevh/dvh, so raw caps renderedzoom ×taller and drifted or clipped at Large/Xlarge. The command-palette padding, the shortcuts-dialog pad...
v1.12.0
Unlocking gets flexible: a vault can now be opened with a PIN or a password, a phone can be registered against an existing desktop account with a one-time code instead of retyping the 12-word seed phrase, and the vault can be locked straight from the sidebar. Sync learned about folders and stopped hiding failures, the Android shell got its back gesture back, and the UI-size setting no longer clips the window.
✨ Added
- 🔑 PIN or password unlock. Vault creation, unlock, and the post-seed "set up a faster unlock" step now offer a PIN/password choice, and the old 8-character minimum is gone (passwords accept any non-empty secret; PINs are 4–12 digits). Android defaults to a PIN and opens the numeric keypad; the choice is remembered per device.
- 🤝 Account pairing (desktop → phone). The desktop can hand its account to a phone with a one-time 12-character code (60 bits of entropy, two-minute lifetime, single use, five wrong attempts allowed). The seed phrase never crosses the network in the clear: the payload is encrypted with a key derived from the code (HKDF-SHA256 → XChaCha20-Poly1305). Android's welcome screen gains "Pair with a desktop"; the desktop's sync card gains "Pair a phone" with the code, LAN address and a countdown.
- 🔒 Lock button in the sidebar. The expanded footer and the collapsed icon rail (and therefore the phone drawer) can lock the vault in one tap, using the same path as Settings → Lock now and the idle auto-lock.
- 📁 Folder sync. Snapshots now carry the folder list; a page created in a folder on one device lands in that folder on the peer instead of falling back to the root.
🔧 Changed
- 📱 The Android drawer is navigation-only. The page tree, favorites, folders, tags and trash no longer appear inside the drawer — the home screen already lists them — leaving Home plus Sync/Theme/Lock/Settings.
- ⚙️ Settings are platform-split. Keyboard shortcuts, page width and the Sentinel terminal card are desktop-only; vibration feedback is phone-only; the auto-lock hint and vault-path fallback no longer assume a phone.
- ↩️ The Android back gesture works again. The generated Tauri activity disables WebView history navigation, so back did nothing in the app; it is re-enabled, overlays pop their own history entries, and stale entries left by backdrop/Escape closes are cleaned up.
🐛 Fixed
- 📡 Sync failures were silent. A snapshot merge error was swallowed with no log and no UI signal; merge failures now log the cause and surface as a toast, and the handshake steps (hello/digest/need/snapshot) are traceable.
- 🪟 UI size no longer clips the window. The scale setting is CSS
zoom, which scales rendered boxes but not viewport units, so a100vhshell rendered taller than the window at Large/Xlarge and pushed the sidebar footer off-screen. The app frame, quick-capture and widget routes now divide by the scale (--ui-fit), landing exactly on the viewport. - 💬 The sidebar resize handle showed a stray floating tooltip ("Drag to resize · double-click to reset"); the native
titlewas removed in favour of the existing accessible label.
📝 Notes
- 📦 No new dependencies. Account pairing reuses the HKDF and XChaCha20-Poly1305 crates already in the tree; the only Rust manifest change is the added
net/io-util/timeTokio features. - 📝 Documentation: the README was refreshed with screenshots and corrected structure/test commands, and the release-notes requirement is now recorded in CONTRIBUTING.
v1.11.0
A notes-first redesign of the Android app, an editor engine upgrade with a security fix underneath it, and a clean bill of health from both dependency auditors.
In one line each: the phone gets a Google Keep-style shell — one big search pill with layout and sort controls, four ways to lay out notes, and a FAB that fans out into creation actions; the editor moves to Tiptap 3 because no 2.x patch existed for a prototype-pollution → XSS advisory; every known dependency vulnerability is fixed (npm audit 34 → 0, cargo audit 1 → 0); and Android's status-bar handling was fixed so the top of the app is below the clock and tappable again.
✨ Added
- 📱 Four notes layouts on phones — List, Gallery, Cards and Icons. Picked from a bottom sheet behind the layout button inset into the search pill. Cards (a 2-column, title-first grid with a quiet “time ago” footer) is the new phone default; the choice is persisted in Settings.
- 🔀 A sort sheet — Recently updated, Date created, Title — next to the layout button, so the home list no longer depends on a Settings detour.
- ➕ A FAB creation stack, in the reference design's idiom: tapping + fans out pills for Text, Checklist, Journal, Meeting notes and Folder, and the button inverts to a white ✕ circle while the stack is open. This replaces the old inline “Today's Journal / New Page” buttons, which took permanent screen space.
- ✅ A Checklist template (heading plus three unchecked to-dos) that pre-fills a page when Checklist is chosen, alongside the existing Meeting/Project/Journal/Book templates.
- 📱 A drawer heading — a large “Enclave” title above the drawer's Home entry, matching the reference design.
- 📐 Safe-area CSS variables.
MainActivitynow reads the real Android window insets and publishes them as--safe-top/--safe-bottom(density-corrected, in CSS pixels); the shell consumes those variables everywhere instead ofenv(safe-area-inset-*), which Android WebView reported as0.
⚡ Improved
- 🔍 The phone top bar was a brand title, a separate search icon and a sync dot. It is now a single rounded search pill — “Search Enclave” — centred in the bar, with the layout and sort buttons inside it and a circular sync avatar on the right. The header no longer draws a separator line; it fades into the page so content scrolls under it naturally.
- 🏠 The phone home screen declutters once the vault has notes: the “Your encrypted workspace…” tagline and the two quick-action buttons disappear and creation moves to the FAB stack, giving the notes list the full screen. An empty vault still shows the welcome card and shortcuts.
- 🧭 Phone navigation is simpler: the floating bottom navigation bar was removed because Home, Sync and Settings all live in the drawer, and the FAB covers new-note creation. Graph view is no longer reachable on phones (desktop keeps it).
- ✍️ Editor internals were migrated for Tiptap 3: table extensions were consolidated into
@tiptap/extension-table, StarterKit's newly bundled Link was disabled in favour of the app's explicitLink.configure({ openOnClick, linkOnPaste }), image storage was typed through the newStorageinterface, and the task-item checkbox was rebuilt for the v3 DOM (see Fixes). - 📦 Dependencies were refreshed within their declared ranges: Svelte 5.57, SvelteKit 2.70.3, Vite 8.3, vite-plugin-svelte 7.3.1, Tauri JS/CLI 2.12 and plugins, marked 18.0.14, yjs 13.6.33,
@scure/bip392.4, nanoid 3.3.19 and others.
🐛 Fixed
- 📐 Android: the top bar sat under the status bar and its buttons were untappable. The web view draws edge-to-edge, but
env(safe-area-inset-top)reported0, so the shell never offset for the system bars and the status bar's input region swallowed taps on the top ~24 dp. What changed:MainActivityinstalls an insets listener that converts the system-bar/display-cutout insets to CSS pixels and writes the--safe-*variables, re-applying them after the page loads; all 23 safe-area calculations in the app now use those variables. Verified on the Android 16 emulator — the bar sits below the clock and its buttons respond. - 📝 Pages created from a template came out empty. The template helpers emitted
{ type: 'text', text: '' }nodes. ProseMirror (and therefore Tiptap 3) rejects empty text nodes, so the editor silently fell back to an empty document — no error, no content. What changed: empty paragraphs and list items are emitted without text nodes; the new Checklist template was verified end to end on the emulator. - ✅ Checklist checkboxes were invisible. Tiptap 3 rewrote
TaskItem's rendered DOM: the span that used to be the visible box became a visually-hidden accessibility label with inline styles, so the old.label spancheckbox CSS drew nothing. What changed: the checkbox is drawn from the label itself, keyed off the existingli[data-checked="true"]state (accent fill + white tick), and toggles correctly. - 🎨 The home FAB painted above the drawer's dim layer. The drawer backdrop's stacking order was raised so an open drawer fully covers it.
- ⌨️ Desktop-only keyboard hints leaked onto phones. The drawer's empty state said “press Ctrl+N”; on touch devices it now just offers “create one”.
- 🧪 The UI audit broke after npm hoisting.
tools/ui-audit/run.mjshard-codedapps/frontend/node_modules/.bin/vite; after a clean install the binary can be hoisted to the workspace root. It now resolves either location, so the audit and its CI job work in both layouts. - 📦 Undeclared dependencies were made explicit.
@tiptap/extension-code-blockand@tauri-apps/apiwere imported but never declared (working only by accident of hoisting), and the editor'snpm testscript referencedtsxwithout declaring it. All three are direct dependencies now, so the editor tests actually run.
🔒 Security
- 🛡️
@tiptap/core2.27 → 3.31.3 — GHSA-cp6q-959q-f8rh (moderate).mergeAttributes()assigned an own__proto__key from parsed input with ordinary bracket assignment, replacing the merged object's prototype; ProseMirror's DOM serializer then enumerated inherited values (includingsrcandonerror) onto real DOM nodes, allowing script execution in the app origin when untrusted attribute objects were merged. No 2.x release was patched — the first fixed version is 3.30.4 — so this release performs the Tiptap 3 migration and pins^3.31.3. - 🛡️
nanoid< 3.3.18 → 3.3.19 — GHSA-2v37-7h3g-55p8 (high). Custom generators could loop indefinitely withsize: 0; reached transitively via Vite → PostCSS. - 🛡️
cookie< 0.7.0 → 0.7.2 — GHSA-pxg6-pf52-xh8x (low). Cookie name, path and domain were accepted with out-of-bounds characters. SvelteKit still pins^0.6.0, so a rootoverridesentry forces the patched line; the SvelteKit API surface used (parse/serialize) is unchanged. - 🛡️
rustls0.23.43 → 0.23.45 — RUSTSEC-2026-0285 (medium). TLS 1.3 handshake messages were incorrectly accepted across encryption-level boundaries; reached throughureq(the updater) andhf-hub/fastembed.rustls-webpkiwas bumped to 0.103.15 alongside it. - 📦
chacha200.10.1 → 0.10.2. The 0.10.1 release was yanked; 0.10.2 is the replacement. (The app's own sync stack useschacha20poly13050.10 onchacha200.9, which was unaffected.) - 🛡️ Audit state after this release:
npm audit34 → 0 vulnerabilities;cargo audit1 → 0.
📝 Notes
- 📝 A full
cargo updatewas deliberately not taken. Tauri 2.12 raises its MSRV from Rust 1.77 to 1.90, and this project declaresrust-version = "1.77"; the Rust update was therefore limited to the security-relevant crates (rustls, rustls-webpki, chacha20). The remainingcargo auditentries are unmaintained/unsound warnings (the GTK tray stack via Tauri, tokenizer/pastevia fastembed,urlpattern'sunic-*) with no fix that does not require the MSRV bump. Documented here so it is not mistaken for an oversight. - 🏷️ Android versionCode: committed as
1011000; CI's auto-increment ships1011001, strictly ahead of v1.10.1's1001066. The release attaches the signed APK, the unsigned universal APK, the AAB and the desktop installers for Windows, Linux and macOS.
v1.10.1
Hotfix for an Android launch crash in v1.10.0.
🐛 Fixed
- 💥 Android release builds crashed on launch — R8 minification stripped JNA's own static hook (
com.sun.jna.Native.fromNative), so the first UniFFI core call (the widget-cache tick) threwUnsatisfiedLinkErrorand killed the process. ProGuard now keeps the wholeuniffi.*and JNA surface; verified on a minified release build (launch, vault creation, widget tick). - 🧩 Widget interactions and the widget-cache tick catch
Throwablenow, so a locked vault or a native-bridge hiccup can never take the app down.
v1.10.0
Enclave for Android is now the full app — the same editor, whiteboards, tables, databases, graph and search as desktop — with native home-screen widgets, a share target, launcher shortcuts and restart-safe P2P sync on top. Under the hood the Rust core moved into a shell-agnostic core-api crate, so desktop and Android share one vault, one network stack and one sync loop.
✨ Added
- 📱 Full Enclave UI on Android — the Tauri web view is the app's front door: every desktop feature runs on Android (editor, whiteboards, tables, databases, graph, search, folders, settings, sync). The native Keep-style shell is gone;
MainActivityroutes Android intents into app pages. - 🧩 Android home-screen widgets (Jetpack Glance):
- Notes list (3×2, resizable) — the notes you opted in, with checklist items, text previews and labels; renders while the vault is locked.
- Pinned note (3×2, resizable) — one shared note in full; tapping a checklist row ticks it through to the note (or opens the app while locked).
- Quick capture (1×1) — one tap into a new note.
- Widget-picker configuration for pinned notes, plus Pin this note to the home screen from the page menu.
- 🔐 Widget privacy model — widgets never read the vault. A per-note Show in Android widgets switch puts a synced marker on the note; only those notes enter an opt-in cache encrypted with an Android Keystore AES-256-GCM key (hardware-backed where available) holding just the title, labels, updated time, checklist state and text. Hide widgets while vault is locked (Settings → Android widgets) blanks them to a lock placeholder; the dial lives in the vault so the web app and the widget renderer always agree.
- 📤 Android entry points — the share sheet (“Share → Enclave” pre-fills Quick Capture), launcher long-press shortcuts (New note / New checklist) and a Quick Settings tile.
- 🦀
core-apicrate — shell-agnostic Rust API overcore-dbandcore-network(vault lifecycle, documents, folders, blocks, search, tags, page list, backlinks, settings, embeddings, network + the sync loop) with UniFFI bindings for Kotlin. The Tauri shell delegates to it; the Android app consumes it through JNA — one process, one vault, one network, one sync loop. - 🔄 Restart-safe sync on Android — the foreground sync service is owned by the core (start/stop/re-arm/lock) and its notification reports live state (“N devices connected”, “looking for your devices”); the enabled choice persists per device and re-arms on unlock, after a restart and when the network comes back.
- 🪟 Desktop/mobile UI rework — resizable sidebar with compact chrome, consistent spacing and sizing across breakpoints, theme boot before hydration, long-press action sheets with Escape/back handling, phone-sized whiteboard and split-view toolbars, graph render/resize fixes.
🔧 Changed
- 📁 Android app data directory matches the Tauri app's, so vaults created by earlier Android builds keep working.
- 🤖 Android build: Kotlin 2.0.21 + Compose compiler, Glance widgets, and cargo-ndk in CI (the shared core
.sois compiled into the app). - 🧱 The UniFFI surface is the frozen native API (own record types + one flat error), so core internals can change without breaking Kotlin.
🗑️ Removed
- 🗑️ The native Keep-style Compose shell (list/detail/capture), its QR pairing screen and the shell-only state preferences — superseded by the full app. The zxing dependency is gone with it, so the app no longer requests the camera permission; peers connect through the Sync card's address copy / add-peer flow.
🐛 Fixed
- 🧩 Locking the vault with “hide widgets while locked” on no longer crashes the widget cache rebuild — lock only re-renders from the existing cache, and a lock racing a refresh keeps the last good cache.
- 🧩 Every widget instance (notes list, pinned, quick capture) refreshes on change, and a stale render can no longer land after a fresh one.
- 🧭 Android intent routing —
MainActivitynavigated withtauri://localhost(desktop WebKit only,ERR_UNKNOWN_URL_SCHEMEon Android/Windows). Warm routes now go through a SPA bridge; cold starts load against the web view's real origin. - 📤 Share → capture — Android pauses/resumes the activity on intent delivery, which the auto-lock-on-hide treated as backgrounding and locked the vault mid-share; a short grace period keeps real backgrounding locking. Sharing into a locked app now shows the unlock step inline on the capture screen, keeps the shared text, saves after unlock and lands on the home screen.
- 🔐 capture/widget auto-unlock — both passed the password-encrypted key file to
unlock_vaultas a raw database key; they now use the shared core's unlocked state (the desktop wallpaper widget included). - 🪟 Desktop capture/widget windows —
core:window:allow-closeand thecapture/widgetwindows joined the capability: the capture popup could not close itself and the widget window had no core permissions. - 🪟 Windows cdylib link — the standalone
core_api.dllnow names thecrypt32/user32system libraries its OpenSSL depends on (the main binary got them transitively via WebView2). - 🪟 Prerender error on
/capture—searchParamsis read on mount, not during prerender.
v1.9.0
Mobile ↔ desktop sync finally works end to end: this release fixes the Android mDNS root cause, hardens the P2P layer, ships incremental sync on a versioned wire protocol and keeps Android sync alive in the background — wrapped in a desktop/mobile sync-first UI pass.
✨ Added
- 🔢 P2P sync protocol versioning (v3) — auth frames carry a wire protocol version; mismatched peers refuse the session with "update this/the peer device" guidance instead of passing auth (same-vault keys!) and silently misinterpreting frames. Pre-versioning peers (≤ 1.8.0) are refused and must update alongside.
- ⚡ Incremental sync — the full-vault snapshot on every connect is gone. Peers exchange a metadata digest (id, rev, updated_at, tombstone), diff it against their local index and pull only newer/missing docs (digest → need → partial snapshot → LWW merge → ack). Reconnects move O(changed docs) instead of O(vault); converged pairs exchange two tiny frames and still refresh "last synced". Deletions propagate as tombstones; the doc-level LWW merge path is unchanged.
- 🔋 Android background sync — a foreground service (dataSync) with a high-perf Wi-Fi lock keeps the sync stack alive while the app is backgrounded or the screen is off; started via JNI only while sync is enabled (no notification for users who never enable P2P), stopped on disable/lock. On return to the app the network re-checks itself and reloads docs (resume-reconnect).
- 🏷️ Release version pre-flight (
npm run release:check) — verifies the four version declarations agree (package.json, tauri.conf.json, Cargo.toml, CHANGELOG), previews the exact versionName/versionCode CI will ship from the committed counter, enforces strictly-increasing versionCode vs the last release tag, and--apkaapt-inspects a locally built artifact — no more tag→download→check→re-tag loops. - 🔄 Sync card (sidebar) — one place for sync: live status dot (pulses when peers are online), this-device address with one-tap copy (the manual-connect story), peer list with connect states, inline add-peer, last-sync line, and a one-tap start when sync is off. Collapsed state persists per device.
- ⌨️ Command palette as a launcher — actions are query-filtered (type "sync" to narrow), plus Start/Stop P2P sync, Copy sync address, New folder and Open settings; recents capped at 8 (previously rendered every document on big vaults).
- 📱 Android sync-first UX — Sync tab in the bottom nav (opens the sync card, live dot when peers connect), topbar sync dot on every screen (tap = open sync), home FAB for one-tap new pages with haptics, overscroll containment on the page tree.
- 📦 npm
build:android/build:allaliases.
🔧 Changed
- 🔗 Manual peer connect shows the exact address to dial (
host:portfrom NetworkStatus.local_host) in the sync card, peer tooltips and the palette — no more guessing which IP to dial. - 📡 Sync failures are visible — wrong-vault-key peers, protocol-version skew and failed handshakes surface as snackbars (previously silent eprintlns); manual-connect dial errors toast instead of console.error.
- ✨ Micro-interactions — press-scale on buttons, pop-in entrances for the palette/context menus/confirm dialogs, snackbar slide-in, pulsing online dot.
🐛 Fixed
- 📡 Android mDNS discovery was dead on real devices — Android's Wi-Fi driver filters multicast unless the app holds a MulticastLock; the app never did, so the phone could neither discover desktop peers nor answer their queries (the root cause of "mobile doesn't sync"). Permissions (
ACCESS_WIFI_STATE,CHANGE_WIFI_MULTICAST_STATE) + a lifetime MulticastLock fix it. - 📡 Offline LANs couldn't sync at all — local IP resolution dialed 1.1.1.1 as its fallback and failed without a default route; interfaces are now enumerated via if-addrs (private IPv4 preferred), UDP trick demoted to last resort.
- 🔗 Duplicate snapshot merges — mutual dials left two live sockets per peer pair; the unregistered twin re-delivered every frame. Sessions register first-come and twins back off.
- 🔄 Synced documents stayed invisible — the sidebar never re-queried after a merge; now the document list reloads when a peer's changes land.
- 🛡️ False "synced" state — malformed snapshots/digests merged as empty and reported success; every frame type is strictly parsed and dropped with a log instead.
- 🏷️ Stuck Android versionCode — every release since v1.2.0 shipped the identical code 1001003 (the auto-increment counter was never committed back); the counter now advances per release and
release:checkenforces strict increase against the last tag. - 🧪 Test infrastructure — parallel cargo tests shared the host's real mDNS namespace and dialed each other's peers; network tests are now hermetic (mDNS-off
start_test), plus a vec0 auto-extension registration race in db tests. - 🧪 CODEOWNERS trust-boundary paths pointed at non-existent
apps/desktop/…files — now the realapps/frontendones.
v1.8.0
The liquid-glass look of v1.7.0 becomes a fully opaque matte design language, with new background styles, custom accents and a dense desktop dashboard.
✨ Added
- 🎨 Matte design language — the 1.7 liquid-glass look becomes fully opaque by default: warm charcoal surfaces (dark/light/OLED-true-black), warm neutrals, hairline borders and diffused shadows. Every frosted chrome piece (top bars, drawer, bottom nav, action sheets, export sheet, command-palette scrim, wallpaper widget card) is now solid.
- 🌈 Background styles (Settings → Appearance) — Matte (default), Soft glow (accent-tinted ambient gradient behind content) and Glass (gradient + frosted translucent chrome with backdrop blur). The gradient re-tints automatically when the accent color changes.
- 🎨 Custom accent color — pick any hex in Settings → Appearance; hover and subtle shades derive from the accent via color-mix, so custom accents look native everywhere.
- 🎨 Corner presets — standard / rounded / rounder.
- 🖥️ Dense desktop home dashboard — full-width two-column layout (recent pages lead; quick actions + favorites rail) that uses the whole window instead of a centered column; collapses to a rail-first single column on phones.
- 📐 Responsive editor rails — backlinks/TOC side rails yield width via container queries on common laptop sizes; narrower backlinks rail and responsive page gutters replace the fixed dead space.
- ♿ Android UX + accessibility pass — contextual back arrow in the app bar (drawer trigger only on root screens), single clean title row (emoji tile and mode pill fold into the ⋯ sheet), bottom nav now fixed and visible with 52px labeled tabs and accent states (it previously never rendered), matte bottom sheets with safe-area padding, WCAG AA contrast for muted/faint text on both themes, 40–48px touch targets, tighter home layout on phones.
🔧 Changed
- 🪟 Native desktop window chrome — main window uses native OS decorations and title bar again (min width restored to 680).
🗑️ Removed
- 🗑️ Page comments — the per-page comment thread in the page menu is gone as part of the redesign.
🐛 Fixed
- 🎨 Graph canvas colors — now read live CSS tokens (theme/accent aware) and re-render on theme change instead of using a hardcoded palette.
- 🎨 Editor menu tinting — slash, bubble, mention, link, database, embed and drag-handle menus tint via color-mix tokens; hardcoded violet/red removed so accent customization applies everywhere.
- 🎨 Undefined
--color-hoverreferences in PageEmbedView, DBCell and VaultGuard.
v1.7.0
Split-screen editing, home-screen and desktop widgets, a liquid-glass mobile rebuild, and P2P discovery that survives multi-homed networks.
✨ Added
- 🪟 Split-screen view — two pages side by side with a draggable divider (ratio persisted), each pane with its own editor and autosave; open from the page toolbar or Ctrl/Cmd+Click a sidebar page; panes stack on narrow windows.
- 🖼️ Desktop wallpaper widget — transparent glass panel (recents + quick capture + date) docked to the monitor corner, toggled from the tray; opening a page surfaces and navigates the main window.
- 🧩 Android home-screen widget — frosted card with New note (routes to quick capture) and Open buttons. Launcher-only for now; a live recents feed needs a Rust→Kotlin bridge (follow-up).
- ✨ Mobile rebuild round 1 — liquid-glass UI (floating frosted topbar over a vibrant gradient, glass bottom-nav pill, glass drawer/sheets), action sheet for page actions (favorite, exports, info, delete), 44–52px touch targets, labeled footer actions, mobile-feel near-full-width drawer.
- 🔗 Manual peer connect — "Add peer — host:port" in the sidebar for networks where mDNS is blocked; manual sessions merge into the real peer record and auto-redial.
- 🗄️ Database density — Compact/Spacious toggle per block, persisted with the document; compact mode: 120px columns, 2–3px cell padding.
- 📐 Tighter editor spacing everywhere — line-height 1.55, reduced heading/list/quote/callout/table/toggle margins.
🐛 Fixed
- 📡 P2P discovery — only the first resolved mDNS address was dialed; on multi-homed devices (WiFi + VPN/docker) that could be the wrong interface with no fallback. Dials every advertised address (private IPv4 first) with a 5s per-host timeout; mDNS failure no longer stops the sync server.
- 🍎 macOS widget build — transparency requires the macos-private-api feature (enabled + allowlisted via app.macOSPrivateApi).
v1.6.0
Folders, a right-click editor menu, Markdown tables and source view, plus a rebuilt sidebar, database and toggle blocks.
✨ Added
- 📁 Folders in the vault —
folderstable +documents.folder_id(migrated on existing vaults), commands for create/rename/delete/move; the sidebar gains a collapsible folder tree with inline rename, page counts, a right-click menu, and Move-to-folder on every page. Folder moves sync (rev bump); folders themselves are vault-local. Deleting a folder never deletes pages — they fall back to the root. - 🖱️ Editor right-click context menu — add link (page picker), add external link, cut/copy/paste, undo/redo, select all, and Insert / Paragraph / Format flyouts (flip at screen edges). The caret moves to the click point unless it lands inside a selection.
- 📊 Markdown tables —
@tiptap/extension-tablestack; insert a 3×3 table from the Insert flyout or slash menu, GFM round-trip in markdown export/import, and a table toolbar (add/remove rows & columns) that appears when the cursor is inside a table. - 📝 Markdown source view —
</>toolbar button toggles the editor to an editable markdown textarea (Ctrl+S applies); leaving source mode parses it back through the markdown importer and autosaves. - 🗄️ Database right-click menu — insert row/column, delete row/column, delete database from any cell, header, or row.
- 🧪 TableMenu / insert test harness — headless jsdom harness (
npm run test:insert) that creates the real editor with production extensions and asserts every insert command, table row/column ops, task checkbox toggling, toggle collapse/expand/backspace-delete, and node-viewstopEventbehavior.
🔧 Changed
- 🧭 Sidebar redesigned — the collapsed sidebar is now an icon rail (logo, Home / Graph / New page, folder icons with page-count badges, sync/theme/settings) instead of a bare strip; the extended sidebar keeps its full tree, tags, trash, and footer.
- 🗄️ Database rework — doc sync debounced (250ms, flushed on unmount), toolbar with row count + + Row button, Obsidian-style polish (view switcher, header labels, row hover, cell focus ring, accent buttons).
- 🎨 Editor polish (Obsidian-style) — custom task checkboxes, per-type callout tints, muted blockquote rail, rebuilt toggle blocks (chevron node view with persisted collapse state).
- 🏷️ App name — installer/launcher and Android label now "Enclave".
🐛 Fixed
- 💥 Editor freezes / dead blocks — six node views called Svelte 4's
$seton Svelte 5mount()exports; the TypeError escaped ProseMirror's update pipeline and left the editor frozen. Replaced with Svelte 5 exports (setLanguage,applyData) or clean view recreation. - ⌨️ Blocks vanishing when typing inside them — inputs inside node views (database, bookmark, image, file, page-embed) leaked keypress events to ProseMirror, which replaced the block with the typed character. All views now implement
stopEvent. - 🗄️ Database unresponsive — the prop-sync
$effectre-ran on every edit and reset local state to the stale prop; state now initializes once and syncs via the exportedapplyData(). - 🔽 Toggle blocks — couldn't be deleted (Backspace now unwraps/deletes them) and collapsed state wasn't persisted; rebuilt as a custom node view with a stale-closure fix for consecutive chevron clicks.
- 🎨 Toggle/list blocks styling — custom checkboxes, callout colors, and blockquote rails were using an undefined CSS var or native elements that fought ProseMirror.
v1.5.0
The brand identity and editor UI get a full rework: the keyhole-vault logo, a shared icon set, richer editor blocks and inline PDF support.
✨ Added
- 📄 PDF support — paste or insert a PDF (slash menu → PDF) and it's stored in the vault and shown inline in the page; an "Open" button hands it to the system viewer (tauri-plugin-opener, works on Android via ACTION_VIEW). CSP updated to allow the inline preview.
- 🎨 Brand mark — "the keyhole vault" logo — violet→indigo gradient tile with a white keyhole: in-app SVG component (sidebar, top bar, vault screen, home empty state) plus a full regenerated app-icon set for desktop (PNG/ICO) and Android (legacy launcher + adaptive icon, background color fixed from the default green). Reproducible via
scripts/generate-icons.py; source SVG atsrc-tauri/icons/logo.svg. Three alternative logo concepts (shield-E, hexagon fortress, layered vault) are rendered as previews by the same script (--concepts). - 🎨 Icon system rework — one consistent stroke set (24px grid, Lucide-style geometry, ~60 icons) moved into
@enclave/uiand shared with the editor package. - 🎛️ Editor: slash menu upgraded — real icons in grouped sections (Basic blocks / Lists / Media / Advanced) with accent-tinted selection states; bubble menu (B/I/S/code) now uses icon buttons.
- 🖼️ Editor: image captions — images are figure blocks with an editable caption underneath (stored as a node attribute).
- 💻 Editor: code block polish — language bar with code icon, more languages (rust, go, java, c, cpp, yaml, toml), icon copy button with "Copied" state.
- 🔖 Editor: bookmark card redesign — gradient link avatar, inline URL with external-link icon, hover lift.
- 🧩 Editor: template gallery — slash menu templates render as a two-column tile grid.
- 🖱️ Editor: drag-handle menu — items now have icons (duplicate, copy, cut, delete).
- 🏠 Home page polish — recent/favorite pages are cards (two-column on desktop) with icon chips and relative timestamps ("2h ago"); logo in the empty state.
- ⌨️ Command palette — arrow-key navigation with a visible active item, Enter activates.
- 🔐 Vault onboarding — welcome screen highlights the three core promises (encrypted, P2P, offline-first) with icons; recovery phrase can be copied to the clipboard with one tap.
- 📱 Bottom navigation — Material-style active pill behind the selected tab.
🔧 Changed
- 📦
Icon.sveltemoved fromapps/frontend/src/libto@enclave/ui;@enclave/editornow depends on@enclave/ui. - 🎛️ Slash, bubble and drag-handle menus are now fixed-positioned and clamped to the viewport — they stay next to the caret/selection instead of rendering detached from the editor.
- ⚙️ Settings icon replaced with a Material-style sliders mark.
- 💬 Comments section moved below the editor — it no longer sits between the title and the content.
🗑️ Removed
- 🗑️ Block-type dropdown from the page top bar (the slash menu already covers headings).
🐛 Fixed
- 🪟 Sidebar collapse was ignored under narrow/wide density settings (equal CSS specificity, density rule won) — collapsed now wins.