Repository navigation
In-app updates, bundled CLI, instant results and a hands-off UI harness - #32
Merged
Merged
Conversation
scripts/drive.py used System Events keystrokes, a forced frontmost window and cliclick, so it stole focus, typed into whatever app was in front and moved the mouse. It could not run while someone worked on the machine, or next to another agent driving another app. A debug-only `drive` feature adds a Unix-socket driver to the app, active only with both VOID_HOME and VOID_DRIVE set. Keys and clicks go straight into GPUI (Window::dispatch_keystroke / dispatch_event); the window opens unfocused and behind every other window, and each command forces a frame so window-id screenshots stay current while it is covered. Clipboard writes, notifications, Finder reveals and the folder picker are captured for the script instead of reaching the OS. Release builds contain none of it. drive.py now talks to that socket, checks after every step that Void never became the frontmost app (DRIVE_STRICT=1 also pins the mouse and frontmost app), asserts on app state instead of sleeps, and follows the 1.2.1 Group, Sort and Filters menus. CI lints the feature. Builder-Task: T-18
A new manifest job runs after the bundles are attached. It writes manifest.json (version, and each asset's size and SHA-256) and SHA256SUMS, and signs the manifest with the Ed25519 key in the VOID_RELEASE_SIGNING_KEY secret. The script refuses a key that doesn't match the committed public half. deepclean-core gains update::Manifest, which verifies the signature against the embedded release key, refuses a manifest for another version, and checks a downloaded file's size and hash. Its tests use a fixture signed by the real script, so they pin the OpenSSL output format. install.sh is unchanged: its asset pattern doesn't match the new files. Builder-Task: T-10
deepclean-core gains update::release. It reads the repository's release list, skips drafts, prereleases and non-version tags, compares versions numerically, and picks the download each package updates from: the macOS .app.tar.gz, the Windows setup.exe, the Linux AppImage or .deb, or the CLI binary. VOID_UPDATE_FEED swaps GitHub for a local releases file, and a sandboxed run never touches the network. Settings → About has an Updates section: the current status, a "Check for updates" button and a "What's new" link, plus a "Check for updates automatically" toggle, on by default. The automatic check runs at launch at most once a day. Its failures stay quiet; a manual check reports them. What a check found is saved in config.updates without the "Saved" flash. The README documents the request as Void's only network call. Builder-Task: T-11
When the check (this session's, or an earlier one remembered in config) finds a release newer than the running one, a banner above every screen says so. It has What's new, Skip this version, Update… (which opens Settings → About) and a close button. Closing hides it until the next launch. Skipping saves the version and hides the notice until a newer release appears. The tray menu gains "Update to vX.Y.Z…" at the top while an update is pending and not skipped. Picking it brings the window to the update. Builder-Task: T-12
"Update now" in Settings → About downloads the release asset for this install, with progress and a Cancel button. It verifies the manifest signature (and version) against the embedded release key, then the file's size and SHA-256, and deletes anything that fails. Then it installs: - macOS: unpack the .app beside the running one, check its bundle version, clear quarantine (see D-3), swap it in by rename, and put the old bundle back if that fails. Then start the new version once this process has exited. - AppImage: replace the file in place. - Windows: run the NSIS installer silently, then start Void again. - .deb: show the `sudo apt install` command rather than escalating. A sandboxed run can point the key and the install target at test stand-ins (VOID_UPDATE_KEY, VOID_UPDATE_TARGET). It reports "Installed" instead of restarting, and the driver records the relaunch. `void dev release <dir>` writes a matching fake signed release from testkit::release. Builder-Task: T-13
Both reuse the core release check, with Package::Cli picking the void-<version>-<target> asset. self-update works out how this binary was installed. A cargo install gets the `cargo install --force --tag` command, and the copy inside Void.app defers to the app's updater. A release binary downloads the new one, verifies the manifest signature and checksum, and replaces itself in place. Anything that fails verification is deleted. --home keeps both offline unless VOID_UPDATE_FEED points at a local file. The fake release from `void dev release` now also carries a CLI asset, which the new integration tests update against. Builder-Task: T-14
cargo-packager now bundles `void` beside the app's executable: Contents/MacOS, the Windows install folder, and /usr/bin in the .deb and AppImage. find_void_cli() prefers that copy, except inside an AppImage, whose mount path changes every launch. So Install hooks and MCP work without Rust, and hooks point at a path that in-place updates keep. The Agents screen gets a "The void command" card with an Install button: - macOS/Linux: symlink into a writable folder on the PATH, else ~/.local/bin with the export line to add. On macOS an admin prompt offers /usr/local/bin instead. - AppImage: copy instead of link, refreshed at launch when the AppImage's own copy changes. - Windows: add the install folder to the user PATH. Someone else's `void` is never overwritten. A sandbox only writes inside its fake home. The MCP one-liner uses the bundled path until `void` is on the PATH, and install.sh links the bundled CLI after installing the app. Builder-Task: T-16
The scan's results are saved in scan-cache.json beside the settings
(inside the sandbox for VOID_HOME runs) after each scan, clean and item
refresh. The file records its format, the app version and the scan
roots. On launch a matching cache is shown at once ("36 items · scanned
2h ago") and seeded into the results the cleaner reads. Items whose
paths are gone are dropped. A corrupt or mismatched cache is ignored.
A background refresh (Settings → General, on by default) keeps the rows
on screen and collects the new scan aside. When it finishes it merges by
ecosystem, kind and path, carrying over the selection, chosen actions,
open drawer and focus. Filters are untouched. New items whose paths no
longer exist are dropped, which covers anything cleaned meanwhile. The
backend likewise swaps its results only at the end, so a clean started
mid-refresh still finds its items. Cleans re-check their safety gates
when they run, as for any scan.
Builder-Task: T-17
It overflowed the About section's row and was cut off. Builder-Task: T-11
drive.py gains scenarios for this branch's features, all against a sandbox: - updates: the launch check finds a fake signed 9.0.0, then the banner, tray item, What's new, and Update now installing into a stand-in app. - update-skip: skipping survives a relaunch. - update-tampered: a bad checksum is refused and deleted, and the app is untouched. - cli: Install void command links the bundled CLI, and MCP uses the bundled path. - relaunch: the cached scan shows at once, then refreshes. The driver gains a scroll command, and scenarios can set environment for the app they launch. Builder-Task: T-18
On a fast runner the background refresh could finish between reading an item's id and selecting it, leaving the test selecting a stale id. Pick the item by path when selecting instead. Builder-Task: T-17
How far the background refresh gets while the window opens depends on the machine. On CI it could already be done, so asserting it was mid-refresh flaked. Assert instead that a refresh ran: the scan time is newer than the cached one. Builder-Task: T-17
In-app updates, the void command bundled with the app, and results that are there at launch. drive.py pins the fake installed app at 1.2.1 so the update scenarios don't depend on the crate version.
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.
What's in it
Hands-off UI harness (T-18)
drivefeature letsscripts/drive.pysend keys, clicks and scrolls straight into GPUI over a socket.Updates (T-10 – T-14)
SHA256SUMSand amanifest.jsonsigned with an Ed25519 release key, made by a newmanifestjob inrelease.yml..deb: shows thesudo apt installcommand.void version --checkandvoid self-updatedo the same for the CLI. A cargo-installed copy prints the cargo command instead.Bundled CLI (T-16)
voidships inside the app, so Install hooks and MCP work without Rust.PATH.install.shlinks it too.Instant results (T-17)
Before releasing
VOID_RELEASE_SIGNING_KEYwith the Ed25519 private key. Its public half iscrates/deepclean-core/src/update/release-key.pem. Without it, the newmanifestjob fails the release.Testing
cargo test --workspace: 478 passed. Clippy is clean with and without--features deepclean-app/drive.updates,update-skip,update-tampered,cli,relaunch). All checks passed, with focus and mouse untouched..msi/.deb/AppImage bundles, the macOS admin prompt, and restarting a real installed app after an update. CI covers compiling and testing on Windows and Linux.