ClipSpacesClipSpaces Docs
Back to app

All releases

The desktop app grows a title bar, a front door, and a way to update itself

  • The custom Windows installer is gone, and the first run moved into the app. Three attempts at hand-painting an installer produced, in order: a wizard in disguise, a card that was over in a second and a half, and a version that shipped with a checkbox overlapping its own text box, phase labels painted on top of each other, and an entire unstyled grey page nobody had seen — Tauri's reinstall prompt, which only appears when a version is already installed.
    • None of that was a hard bug; the problem was structural. An installer runs before the app exists — no bundle, no design system, no motion, no hot reload. Every control is a numeric id into a Win32 dialog resource, every change costs a two-minute build, and each un-styled path (reinstall, WebView2 bootstrap, error dialogs, the uninstaller) stays invisible until someone hits it. It is a UI surface with none of the tools that make UI good here, and an unbounded number of states.
    • So the installer is boring again — Tauri's stock NSIS wizard, with the app mark as its header and sidebar art and nothing else. The vendored 900-line template, its README and its test are deleted; @tauri-apps/cli goes back to a caret range, since the reason for pinning it exactly was the template.
    • And the part anyone actually reads is now components/desktop/FirstRun.tsx: welcome, the Terms and Privacy agreement, and what happens next. It is the third instance of one idiom — progress bar, stepped body, Back/Next footer, matching SoulWizard and HarnessConsent — because the app should have one grammar for "a first-run flow with a decision at the end", not four.
    • The agreement is a real gate. Continue is disabled until the box is ticked, and the flow is recorded as seen only on completion — closing it brings it back next launch, because nothing was agreed to. It links out to the live Terms rather than embedding a copy that would stop updating the moment the real one changed.
    • It queues ahead of the Claude Code offer: two modals stacked on a first launch is one too many, and an agreement outranks an optional offer.
  • One title bar on the desktop app, and it is the app's own. The window shipped with a grey Windows caption sitting on top of a carefully themed 48px bar — two strips saying the same thing in two visual languages. Frameless had been written for this and then switched off, because it is a contract between two things that ship separately: the shell is compiled from this repo, the page comes off a server, and a shell built decorations(false) against a deployment without components/desktop/ is a window with no title bar, no controls and no way to move it.
    • The page asks now, instead of the shell assuming. The window is built decorated; a page that can draw a caption calls adopt_titlebar and the native one goes. A page that cannot never calls it and keeps what it has. The worst case is a plain window rather than a broken one, so this could ship without waiting on a deploy.
    • Adoption lands while the launcher still covers a hidden window, so nothing is seen to change; and it uses finally, because a failed adoption must not cost the window its reveal.
    • The bar de-chromes into a caption. Under the data-desktop-chrome stamp the space pill and agent toggle drop their outlines and get them back on hover. Same controls, same information — but a caption should read as one surface rather than as widgets parked on a strip. Four CSS tokens, no isDesktop() in any component's styles, and the browser is byte-for-byte unchanged.
    • The window controls take caption geometry: app decoration. 46px cells, full bar height, square, flush to the corner — because a maximised window is closed by throwing the pointer into the corner without aiming, and the old rounded pills inset by 12px gave that away for nothing. The colours, glyphs and hover stay ours. Windows 11 Snap Layouts stays a known gap.
  • The model chip called Claude "default", with a paperclip on it. On the desktop harness the switcher's ids are Claude Code's own aliases — default, opus, sonnet, haiku — and the chip rendered them through shortModelLabel, which only knows raw ids, so the bare word default is what it showed. The badge beside it tested that id for "claude" and, finding none, fell back to the ClipSpaces paperclip: Claude's own reply wearing our mark. The chip now takes its wording from the option it is naming (so it also picks up "Claude Code default · Sonnet 4.5" the moment the CLI says which model it is running), and the Claude test recognises the harness aliases. Both chips — composer and Agent mode — fixed.
  • Two black windows flashed every time the AI panel opened. A GUI process on Windows that starts a console application gets a console allocated for it. The session spawn set CREATE_NO_WINDOW from the beginning; the probe did not — and the probe runs two commands, --version then auth status, so the panel flashed twice on every open because it re-probes each time. Invisible on macOS and Linux, and invisible in dev on Windows where a terminal is already on screen. A test now counts spawns against guards and fails if they ever differ.
  • The AI harness could never work in an installed app. Settings reported "Not installed on this computer" on machines with claude on PATH answering --version and auth status perfectly. The CLI was never the problem and neither was the probe: the probe was never called. Tauri ACL-checks every command — plugin and app alike — as soon as the page comes from a remote origin, and this window always loads one. The capability granted the plugin surfaces (events, window, store, opener, updater) and never the app's own commands, so every invoke into the shell was refused. In a release build the refusal is the bare string "Command X not allowed by ACL", which reached the web layer as a rejected promise and nothing else.
    • Dev could not have caught it. The check is skipped for a LOCAL page and tauri dev loads localhost, so every harness turn ever run took the branch that works. The branch that fails needs an installed build, which did not exist until 2026-08-24.
    • build.rs now declares the commands so allow-* permissions exist at all — nothing generates them by default — and both permission lists name them.
    • The comment that caused it is gone. lib.rs asserted app commands "are not ACL-gated at all": true for a local page, false for a remote one, and taken as settled long enough that the drift test only ever compared the plugin half. A test now reads invoke_handler and fails on any command missing from either list or from build.rs.
    • This is not what makes the harness safe and must not be read that way. The boundary is in harness.rs: Rust picks the binary, owns an empty working directory, and refuses any spawn outside ALLOWED_FLAGS. The ACL decides only which origin may ask.
  • A green build was quietly not producing the updater manifest. uploadUpdaterJson is not a tauri-action input; includeUpdaterJson is. A misspelled input to a GitHub Action is a warning, not an error — the run succeeds and the setting is ignored — so the workflow would have published installers with no latest.json, which is the exact file tauri.conf.json points the updater at. Every install would have checked for updates forever and found nothing, with no failure anywhere to explain it. uploadWorkflowArtifacts was invented outright. A test now checks every input against the action's own valid list.
  • All three platforms build. Linux failed first on an apt conflict (libappindicator3-dev and libayatana-appindicator3-dev exclude each other) and macOS on an empty Apple certificate — listing a secret under env: defines it as an empty string even when it does not exist, and Tauri read the key as an instruction to sign. With both fixed, Windows, macOS and Linux all produce signed bundles, and the five filenames lib/desktop-release.ts hardcodes were confirmed against the real ones rather than assumed.
    • Compiling is not running. No macOS or Linux build has ever been launched, so MapLibre on WebKitGTK and the canvas under load remain exactly as untested as before.
  • The desktop release pipeline points somewhere real. RELEASES_URL and the updater endpoint both named github.com/clipspaces/clipspaces, which does not exist — a download button that 404s and an updater that checks an empty URL forever, neither of them loud. Both now name DeitySama/clipspaces-releases, a public repo holding tags and assets only, because the source repo is private and a private repo's release assets need a GitHub login to download. The build still runs beside the source and publishes across via tauri-action's owner/repo, which needs a PAT — the default GITHUB_TOKEN is scoped to its own repo.
  • The updater keypair exists. Generated, public half in tauri.conf.json, private half in Actions secrets and nowhere else. uploadUpdaterJson emits the latest.json the endpoint reads, and Windows updates come from the NSIS setup rather than the MSI, whose update is an installer UI rather than an update.
  • A test now pins both: the updater endpoint has to live under RELEASES_URL, and the pubkey has to not be the placeholder. The key can never be rotated after a release — the public half is compiled into every installer in the wild.