Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
f085e98
Merge remote-tracking branch 'origin/feat/desktop-app-core' into feat…
roodboi Nov 14, 2026
d201f43
Merge remote-tracking branch 'origin/feat/desktop-app-setup' into fea…
roodboi Nov 14, 2026
3b861b4
feat(desktop): add evidence-gated upgrade review flow
roodboi Nov 14, 2026
30008c8
test(desktop): qualify upgrade evidence and offscreen states
roodboi Nov 14, 2026
cbb4d47
merge: integrate setup (Doctor, next e47681ea) into upgrade-flow
roodboi Nov 15, 2026
4490b29
docs(upgrade): describe the desktop upgrade review preview on the sta…
roodboi Nov 15, 2026
d202ba8
merge: integrate setup (Doctor interpreter check) into upgrade-flow
roodboi Nov 15, 2026
9656449
merge: integrate setup (Doctor shebang argument check) into upgrade-flow
roodboi Nov 15, 2026
a11eb31
merge: integrate setup (Doctor shebang allowlist) into upgrade-flow
roodboi Nov 15, 2026
25f1edd
merge: integrate setup (Doctor interpreter option check) into upgrade…
roodboi Nov 15, 2026
1abca35
merge: integrate setup (narrowed Doctor shebang rule) into upgrade-flow
roodboi Nov 15, 2026
5d5f337
merge: integrate setup (CLI pinning, XDG_CONFIG_HOME, Mutagen interpr…
roodboi Nov 15, 2026
a4a7a82
fix(desktop): read the hackd version from the CLI's socket in upgrade…
roodboi Nov 15, 2026
bd02ead
merge: integrate setup (CLI interpreter check) into upgrade-flow
roodboi Nov 15, 2026
ba65b13
merge: integrate setup (next 345dc0d1: #835, #831) into upgrade-flow
roodboi Nov 15, 2026
5619675
test(desktop): answer the version --json envelope in fake CLIs
roodboi Nov 15, 2026
b5b6679
test(desktop): escape the version envelope in format! fake CLIs
roodboi Nov 15, 2026
4338a7d
fix(desktop): read the current v2 upgrade check report
roodboi Nov 15, 2026
a4c696f
merge: integrate setup (Doctor pin, Docker selectors, adapter check) …
roodboi Nov 15, 2026
72a9fa9
merge: integrate setup (terminal temp root check) into upgrade-flow
roodboi Nov 15, 2026
729aae7
merge: integrate setup (canonical terminal temp root) into upgrade-flow
roodboi Nov 15, 2026
20c2556
merge: integrate setup (Doctor startup-variable strip) into upgrade-flow
roodboi Nov 15, 2026
300a39f
merge: integrate setup (loader-variable strip) into upgrade-flow
roodboi Nov 15, 2026
a25ce8c
merge: integrate setup (loader-variable test) into upgrade-flow
roodboi Nov 15, 2026
1d53b15
merge: integrate setup (next 34b50534: Env tab) into upgrade-flow
roodboi Nov 15, 2026
4f94c3a
docs(desktop): note that the CLI update check exists but the Upgrade …
roodboi Nov 15, 2026
110e1f3
merge: integrate setup (explicit CLI check, native selectors) into up…
roodboi Nov 15, 2026
1306657
merge: integrate setup (verified checked runner) into upgrade-flow
roodboi Nov 15, 2026
caba7c4
merge: integrate setup (Doctor local-endpoint check) into upgrade-flow
roodboi Nov 15, 2026
5887342
merge: integrate setup (Doctor preflight selector fix) into upgrade-flow
roodboi Nov 15, 2026
316a470
merge: integrate setup (exact Docker context lookup) into upgrade-flow
roodboi Nov 15, 2026
b8e1fb4
merge: integrate setup (context ACL check) into upgrade-flow
roodboi Nov 15, 2026
54c2131
fix(desktop): run upgrade reads in the checked context and bind copie…
roodboi Nov 15, 2026
06527b3
merge: integrate setup (daemon self-invocation, global config selecto…
roodboi Nov 15, 2026
e34f6f4
fix(desktop): run Doctor's Docker preflight before upgrade checks and…
roodboi Nov 15, 2026
8123383
merge: integrate setup (unreadable-tool refusal) into upgrade-flow
roodboi Nov 15, 2026
c718578
merge: integrate setup (dirname wrapper, pinned copied commands) into…
roodboi Nov 15, 2026
7fca325
test(desktop): expect Doctor's dirname wrapper in upgrade tool PATH
roodboi Nov 15, 2026
d020370
Merge commit '77f25083871dfc52f450a1a68934858cac57b611' into HEAD
roodboi Nov 15, 2026
02a3d80
Merge commit '865df7f8563c06b80afef14a7fa45af17081c048' into HEAD
roodboi Nov 15, 2026
1944129
fix(desktop): keep Git overrides and the host-probe guard in upgrade …
roodboi Nov 15, 2026
9da4883
Merge commit '8f0cd942c733b54d0f60a9fedba3590565168b55' into HEAD
roodboi Nov 15, 2026
9e75928
test(desktop): expect the nested unavailable native pin
roodboi Nov 15, 2026
f42b1a7
Merge commit '3ee37641ade9b4c0b2a358931301a7bff27d5d00' into HEAD
roodboi Nov 15, 2026
751916f
style(desktop): format merged upgrade test
roodboi Nov 15, 2026
5a4bb6f
fix(desktop): keep only controlled PATH directories in copied upgrade…
roodboi Nov 15, 2026
42b934f
test(desktop): expect the system default PATH when none is controlled
roodboi Nov 15, 2026
8eb2620
Merge commit 'd6eb90d974b3b1c4485efec771a38524396cf835' into HEAD
roodboi Nov 15, 2026
0d6f411
fix(desktop): show the CLI's reason when a snapshot is refused
roodboi Nov 15, 2026
f48ab39
Merge commit 'db5642f93d51de523551b60a798d4d205c94322d' into HEAD
roodboi Nov 15, 2026
4ed00e5
Merge commit '6f47a1cbc44b9a005d40f46038a0b4a0060454cd' into HEAD
roodboi Nov 15, 2026
052316a
Merge commit 'b15338c609a8d98fb5ae03ee3597eaff3efa465f' into HEAD
roodboi Nov 15, 2026
ee26371
Merge commit '20e79a96646bdd80e1bd140c188618aef1e0f4bf' into HEAD
roodboi Nov 15, 2026
c37f8ad
Merge commit 'ac40fbbe9298ccc4af5cfde2724e1014419004fc' into HEAD
roodboi Nov 15, 2026
8ac49b5
Merge commit 'f601833c583f32cdeba98b034e60d3186660d57c' into HEAD
roodboi Nov 15, 2026
d17eed2
Merge commit '52cef18415d494b77a032449c7788845bd9a09f7' into HEAD
roodboi Nov 15, 2026
f48569d
Merge commit 'c886c226374e4f9762a5dfbbde99ca48325fe844' into HEAD
roodboi Nov 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 101 additions & 3 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,9 @@ observations, not locks against concurrent same-user changes.

## What v0 does not do yet

The Activity tab, CPU and memory, always-on, broader daemon controls and
upgrades come in later units. The design and the CLI/daemon requests they need
are in `DESIGN.md` (PR #800).
The Activity tab, CPU and memory, always-on and broader daemon controls come in
later units. The design and the CLI/daemon requests they need are in `DESIGN.md`
(PR #800).

## Setup

Expand Down Expand Up @@ -386,6 +386,104 @@ home. Daemon remedies retain the original backend. Login-service installation
alone does not pass when available evidence says inactive, unloaded, or failed
launchd exit status; these states need attention even if hackd runs separately.

## Upgrade review (preview)

![Upgrade evidence flow](diagrams/upgrade.svg) ([source](diagrams/upgrade.mmd))

**Upgrade Hack** opens without probing for updates. **Review readiness** runs the
Comment thread
roodboi marked this conversation as resolved.
Resolved
selected CLI's read-only `upgrade check --json` on a background worker, keeping
valid reports from exit 1. Only `report_version` 2 is read: every plan step must
carry its `count` and `targets`, and a step covers the machine (no targets), one
checkout (its name) or several (`projects`). A target must name a live checkout,
by name and root, with exactly one finding for that step; any other report stays
Unknown. Grouped cards list each checkout with its own instruction and, when it
has one, its own quoted command. The hackd version comes from the socket named in the
CLI's own read-only `daemon status --json` answer.

Every upgrade read and write (readiness, CLI version, daemon status, snapshot and
rollback) runs the CLI by its checked absolute path with the same non-secret
execution context as Setup (home, `HACK_HOME`, backend, service guards, the CLI's
`HACK_HOST_PROBES_DISABLE=1` hard deny when it is set, and Docker selectors;
nothing else from the app's environment) and Doctor's private tool
PATH: per-operation wrappers only for tools no other user can change, with
interpreter startup variables such as `BASH_ENV` removed. Like Doctor, every run
disables repository `core.fsmonitor` and hooks through command-line Git config,
because `upgrade check` reads Git in registered checkouts, and pins the native
runtime Doctor checked. Copied commands keep the host-probe hard deny too. They
run later, without the private wrapper PATH, so their PATH keeps only directories
no other user can change (owner- or root-owned, not group- or other-writable, with
the same ancestry and ACL rule as Doctor); with none left, it is `/usr/bin:/bin`. If that PATH cannot be
built, or a tool such as `docker` or `git` on PATH is in a location other users can
change, or PATH is too long to check fully, the operation is refused before the CLI
runs and the message names the tools. No `update --check` probe or daily check runs. The
CLI now has a read-only update check (R14), but this desktop adapter does not
consume it yet, so availability stays Unknown for every live install kind, including candidates and
Homebrew. Readiness is separate from update availability. Actual Upgrade stays
disabled until verified target and installation evidence exists; the current
live adapter never supplies that evidence. Doctor's
helper and Docker preflight also runs first, so a remote Docker endpoint (for
example `DOCKER_HOST=ssh://...`) refuses the operation as well.

**Save file snapshot** is an explicit CLI write, available after a complete ready
review. The app accepts a recovery point only for `status: created`, `complete:
true`, an empty `gaps` list and an absolute `manifest.json` path under the returned
snapshot id. Refused, malformed and incomplete receipts enable neither Upgrade
nor rollback. For a refused receipt the pane shows the CLI's own reason (control
characters removed, at most 600 characters) so the user knows what to fix
before retrying. The app never opens the saved files or reads file values or keys.
The CLI remains responsible for private-manifest validation and file ownership.

With proposed R14 evidence, the fixture flow offers **Upgrade in Terminal** only
after a complete snapshot. The authored sequence is `brew upgrade hack` followed
by `brew upgrade --cask hack-desktop`, stopping on failure. The shared terminal
adapter quotes each argument, displays the sequence and waits for Enter. It never
runs finding `command` text. Plan argv may be displayed/copied only after quoting,
bound like Setup's commands to the reviewed CLI and context (`/usr/bin/env -i
HOME=… HACK_HOME=… <cli> …`), so a terminal run addresses the reviewed home,
backend and guards, plus `HACK_MCP_COMMAND` naming the reviewed CLI so a copied
daemon command restarts that CLI. A review without a checked context offers no copy;
R17 safe metadata and an authored allowlist are required before any plan fix can
be run in the app. Today those cards offer Copy and documentation only. No admin
action, password prompt, self update or relaunch happens in the app.

Terminal launch and window focus never confirm success. **Check again** requires
fresh CLI and Homebrew-installed app versions at the exact target, hackd matching
the CLI, and a live, fully readable ready report with no remaining plan. Every
previous step needs the same scope, checkout root and explicit `ok` finding;
a grouped step needs that for every checkout it covered, under the id of the
finding that put the checkout in the step (an `apply_fix` group may differ).
A disappeared step, missing scope, unreadable checkout or unchecked registry
entry stays Unknown. The snapshot plan entry is proven by its separate complete
receipt. A read adapter for `brew info --json=v2 --cask hack-desktop` uses the
installed version, never the catalog version; it stays unwired until the desktop adopts
R14's installation-ownership evidence. **Restart hackd** is explicit, shares the machine-action
gate with setup/recovery, and re-reads versions after the command. No poll starts
or restarts a service. Upgrade reads run only on demand, including while hidden;
there is no eager or background daily upgrade check.

**Restore files from snapshot** calls only `upgrade rollback --rollback-manifest
<exact snapshot path> --json`, without overwrite. Results list each path, status,
kept-as path and reason. Incomplete restores stay failed. A complete result says
**Files restored. Versions, containers and services were not changed.** Fresh
readiness is required again. All state and receipts stay in memory for this app
session. The desktop has no signing/cask distribution or production updater in
this unit (DESIGN sections 4 and 8).

Offline proposed-capability examples (no CLI, Homebrew or terminal effects):

```sh
cargo run -p hack-desktop -- --upgrade-fixture ready --theme dark
cargo run -p hack-desktop -- --upgrade-fixture availability-unknown
```

Fixture names: `availability-unknown`, `available`, `checking`, `blocked`, `ready`,
`snapshotting`, `incomplete`, `applying`, `unknown`, `restarting`, `done`, `failed`,
`rolling-back`, `restored`. `--screenshots` renders all fourteen in both themes
without a window. Evidence: [light](screenshots/upgrade-light.png),
[dark](screenshots/upgrade-dark.png). Core tests use only fake executables, isolated homes, disabled
autostart, injected launchers and pure evidence inputs. They do not qualify live
Homebrew, terminal, platform focus, a published app or a real upgrade.

## Logs tab

`hack logs --json` for the selected project, following by default:
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/app/src/doctor_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub enum Pane {
Project,
Doctor,
Setup,
Upgrade,
}

pub fn render(pane: &DoctorPane, cx: &mut Context<HackApp>) -> AnyElement {
Expand Down
22 changes: 20 additions & 2 deletions apps/desktop/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod logs_view;
#[cfg(feature = "screenshots")]
mod screenshots;
mod setup_view;
mod upgrade_view;
mod view;

use std::sync::Arc;
Expand All @@ -26,14 +27,15 @@ use hack_desktop_core::source::{FixtureSource, LiveSource, Source};
pub(crate) struct Args {
pub fixture: Option<Fixture>,
pub setup_fixture: Option<String>,
pub upgrade_fixture: Option<String>,
pub logs_fixture: Option<hack_desktop_core::logs::fixtures::LogsFixture>,
pub theme: Option<ThemeMode>,
pub screenshots: Option<std::path::PathBuf>,
}

fn usage() -> ! {
eprintln!(
"usage: hack-desktop [--setup-fixture missing-cli|partial|ready|native-unknown|service-unavailable] [--fixtures <state> | --logs-fixture <state>] [--theme light|dark] [--list-fixtures] [--screenshots <dir>]"
"usage: hack-desktop [--upgrade-fixture <state>] [--setup-fixture missing-cli|partial|ready|native-unknown|service-unavailable] [--fixtures <state> | --logs-fixture <state>] [--theme light|dark] [--list-fixtures] [--screenshots <dir>]"
);
std::process::exit(2)
}
Expand All @@ -42,13 +44,21 @@ fn parse_args() -> Args {
let mut a = Args {
fixture: None,
setup_fixture: None,
upgrade_fixture: None,
logs_fixture: None,
theme: None,
screenshots: None,
};
let mut it = std::env::args().skip(1);
while let Some(arg) = it.next() {
match arg.as_str() {
"--upgrade-fixture" => {
let name = it.next().unwrap_or_else(|| usage());
if hack_desktop_core::upgrade_fixtures::load(&name).is_none() {
usage();
}
a.upgrade_fixture = Some(name);
}
"--setup-fixture" => {
let name = it.next().unwrap_or_else(|| usage());
if hack_desktop_core::setup_fixtures::load(&name).is_none() {
Expand Down Expand Up @@ -131,6 +141,7 @@ fn main() {
gpui_kit::application().run(move |cx| {
gpui_kit::init(cx);
let setup_fixture = args.setup_fixture;
let upgrade_fixture = args.upgrade_fixture;
let fixture = args
.fixture
.or(args.logs_fixture.map(|l| {
Expand All @@ -140,7 +151,8 @@ fn main() {
Fixture::Running
}
}))
.or(setup_fixture.as_ref().map(|_| Fixture::Running));
.or(setup_fixture.as_ref().map(|_| Fixture::Running))
.or(upgrade_fixture.as_ref().map(|_| Fixture::Running));
let logs_fixture = args.logs_fixture;
let theme = args.theme;
let source = source_for(fixture);
Expand Down Expand Up @@ -173,6 +185,12 @@ fn main() {
{
app.show_setup_now(report);
}
if let Some(flow) = upgrade_fixture
.as_deref()
.and_then(hack_desktop_core::upgrade_fixtures::load)
{
app.show_upgrade_now(flow);
}
if let Some(a) = fixture.and_then(|f| fixtures::local(f).in_flight) {
app.simulate_in_flight(a);
}
Expand Down
39 changes: 39 additions & 0 deletions apps/desktop/app/src/screenshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,45 @@ pub fn run(dir: &Path, fixture: Option<Fixture>) {
shots.push((l.name(), base, Some(l)));
}
for mode in [ThemeMode::Light, ThemeMode::Dark] {
for name in hack_desktop_core::upgrade_fixtures::NAMES {
cx.update(|cx| Theme::change(mode, None, cx));
let opts = WindowOptions {
focus: false,
show: false,
..crate::window_options()
};
let (handle, _) = cx
.update(|cx| {
gpui_kit::open_window(opts, cx, |window, cx| {
cx.new(|cx| {
let mut app = HackApp::new(
crate::source_for(Some(Fixture::Running)),
false,
window,
cx,
);
app.show_upgrade_now(
hack_desktop_core::upgrade_fixtures::load(name)
.expect("known upgrade fixture"),
);
app
})
})
})
.expect("offscreen upgrade window");
cx.update_window(handle, |_, w, cx| w.render_frame(cx))
.expect("render upgrade");
let image = cx.capture_screenshot(handle).expect("upgrade renderer");
let suffix = if mode == ThemeMode::Dark {
"dark"
} else {
"light"
};
image
.save(dir.join(format!("upgrade-{name}-{suffix}.png")))
.expect("save upgrade");
cx.update_window(handle, |_, w, _| w.remove_window()).ok();
}
for name in hack_desktop_core::setup_fixtures::NAMES {
cx.update(|cx| Theme::change(mode, None, cx));
let mut opts = crate::window_options();
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/app/src/setup_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ impl HackApp {
self.setup.refresh_again = true;
return;
}
if self.setup.action_busy || self.recovery_busy {
if self.setup.action_busy || self.recovery_busy || self.upgrade.busy {
return;
}
let generation = self.setup.begin();
Expand Down Expand Up @@ -65,6 +65,7 @@ impl HackApp {
&& !self.setup.loading
&& !self.setup.action_busy
&& !self.recovery_busy
&& !self.upgrade.busy
{
self.pane = Pane::Project;
cx.notify();
Expand Down
Loading
Loading