Skip to content

feat(desktop): add an Electron app that runs Ovis on bundled herdr-core and Bun - #2691

Merged
devswha merged 7 commits into
mainfrom
feat/electron-shell
Nov 4, 2026
Merged

devswha merged 7 commits into
mainfrom
feat/electron-shell

Conversation

@devswha

@devswha devswha commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Summary

Adds app/electron, a desktop app (Electron, Linux and macOS) that carries everything Ovis needs and runs it itself: the Ovis server on a bundled Bun, and a bundled herdr-core (hc) for the terminals. Nothing else has to be installed. The existing Mac (app/mac) and Windows (app/windows) shells are untouched, and nothing in server/, src/ or shared/ changes: the app only launches the server and opens a window on it.

  • Pinned, checked runtimes. scripts/runtimes.ts pins herdr-core 0.1.17 (releases.herdr.dev manifest) and Bun 1.4.2 (the CI Bun, oven-sh SHASUMS256.txt) per platform with their SHA-256. scripts/build.ts stage downloads them at build time, refuses a checksum mismatch, checks each binary reports the pinned version, then stages the server as the installer runs it (sources, built client, production packages). No binaries are committed.
  • Children with isolated state. The app copies the bundled hc into its data folder and starts its own core with HC_SESSION=ovis (never the default session another hc client uses), then runs the server with OVIS_RUNTIME=herdr-core, OVIS_HC_BIN, HOST=127.0.0.1 and its own OVIS_STATE_DIR. The core outlives the app as hc runs it for every client; a newer bundled hc replaces the copy with hc update --from, so terminals keep running. The server keeps its port between runs while it is free (page storage stays), and a server left behind by a killed app is ended on the next start, only when it is still this app's Bun on its server.
  • Connection screen (renderer/connect.html): the two steps, what failed with the end of its output, Try again and Show logs; English and Korean.
  • Tray: Open Ovis, Open in Browser, Restart Ovis Server, Restart to Update, Quit. Closing the window keeps Ovis in the tray; one copy runs at a time.
  • Auto-update plumbing: electron-updater from a generic feed. OVIS_UPDATE_FEED at build time bakes the feed in; OVIS_UPDATE_URL overrides it at run time; OVIS_UPDATES=0 turns it off. Off in development and without a feed (so off by default today).
  • Window rules: external links open in the default browser, only the server's own pages may open more windows, only microphone, clipboard, notifications and full screen are granted, and only to the server's pages; the connection screen's IPC bridge is exposed to file: pages only.
  • Licenses: only permissive npm packages are bundled (electron-updater and its dependencies: MIT, ISC, Blue Oak 1.0.0); the build writes their license texts into the app. herdr-core's (Apache-2.0) and Bun's license files ship beside the binaries. THIRD_PARTY_NOTICES.md lists all of it, including that the unmodified Bun binary statically links JavaScriptCore/WebKit (LGPL-2), as Bun's own license file states.

CI (needs the maintainer's OK)

.github/workflows/desktop.yml builds the app on ubuntu-24.04 (AppImage) and macos-15 (dmg + zip, unsigned) for PRs touching app/electron, runs its unit tests and type check, and keeps the installers as artifacts. It publishes nothing. It is a workflow change, so it waits for the maintainer's OK; it is in its own commit and can be dropped.

Validation

  • bun run check fast on the branch: workflow syntax, generated types/themes, typecheck, build, unit tests (10891 pass, 0 fail), actionlint: all ok.
  • app/electron: bun test (22 pass) and bun run typecheck, on Linux x64 and on macOS arm64 (eamac, fresh /tmp clone, deleted afterwards).
  • Linux x64: bun scripts/build.ts staged herdr-core 0.1.17 and Bun 1.4.2 (checksums verified, versions checked) and built Ovis-0.4.22-x86_64.AppImage. Ran the dev app and the AppImage (APPIMAGE_EXTRACT_AND_RUN=1, no FUSE on that host) under Xvfb with an isolated hc session (HC_SESSION=ovis-qa, mktemp XDG_*/HOME/user data): connection screen while the core starts, then the Ovis window; a terminal started in that core (hc run --bg) showed live in Ovis's Terminal view; killing the server switched to the "Ovis stopped" screen and Try again recovered it; a restart reused the saved port and the running core; a broken server showed its error output on the failure screen (English and Korean); quitting stopped the server and left the core running, as designed.
  • hc update --from: swapping an isolated 0.1.16 core to 0.1.17 kept its terminal running (terminals kept, output still readable).
  • macOS arm64 (eamac): bun run dist built an unsigned .dmg and .zip; the app, opened with an isolated hc session and user data, started its core and server and loaded Ovis; Quit stopped the server.
  • Not run: signed/notarized macOS build, the update flow against a real feed (no feed exists yet), Windows (not a target), the new workflow on GitHub (it runs on this PR).

devswha and others added 3 commits November 4, 2026 08:17
…re and Bun

app/electron downloads pinned herdr-core 0.1.17 and Bun 1.4.2 at build time, checked against their SHA-256, stages the server as the installer runs it, and packages an AppImage or a dmg and zip. At run time it starts its own hc core (HC_SESSION=ovis) from a copy in its data folder, runs the server with OVIS_RUNTIME=herdr-core on loopback with its own state folder, and shows a connection screen until the server answers. It adds a tray and electron-updater plumbing that stays off without a feed. No server, client or shared code changes.

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
Runs app/electron's unit tests and type check and builds an unsigned AppImage, or dmg and zip, as artifacts for pull requests that touch it. Publishes nothing.

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
…uring a quit

A quit asked for inside the cancelled before-quit was dropped, so the app needed a second SIGTERM when no server was running. Also sets the Linux desktop name so window managers link the window to its launcher.

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 21 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 40dcae1 and cd1287e.

Files ignored due to path filters: 1
  • app/electron/bun.lock is excluded by !**/*.lock
Files selected for processing: 20
  • .github/workflows/desktop.yml
  • THIRD_PARTY_NOTICES.md
  • app/electron/.gitignore
  • app/electron/README.md
  • app/electron/electron-builder.yml
  • app/electron/package.json
  • app/electron/renderer/connect.css
  • app/electron/renderer/connect.html
  • app/electron/renderer/connect.js
  • app/electron/scripts/build.ts
  • app/electron/scripts/runtimes.test.ts
  • app/electron/scripts/runtimes.ts
  • app/electron/src/children.test.ts
  • app/electron/src/children.ts
  • app/electron/src/main.ts
  • app/electron/src/preload.ts
  • app/electron/src/updates.test.ts
  • app/electron/src/updates.ts
  • app/electron/tsconfig.json
  • changelog.d/electron-desktop-shell.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

- The core and the server start through bash, which closes the descriptors Chromium leaves open, so the long-lived core no longer holds the app's files, sockets and pipes.
- A server is recorded with its start time as soon as it is spawned; a quit during its start stops it, and the leftover check matches the start time, not just the command line.
- Start, restart and Try again run one at a time.
- Only microphone media is granted, never the camera.
- A blank window a page opens for a sign-in is allowed and hands its address to the default browser; only the connection screen's own file URL is a local page.
- HC_SOCKET is not passed to the app's core.
- Each build re-checks cached downloads against their pins and unpacks them afresh, and adds the release's Rust PTY host when it matches this checkout.

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
@devswha

devswha commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

Review round 1: what changed

Reviewed at 7531287e by the omo reviewer and a Codex adversarial review. Fixed in 8d7d2558:

Finding Source Resolution
Quitting while the server starts leaves an unrecorded server omo MEDIUM, Codex MEDIUM The server is recorded as soon as it is spawned; a quit during its start stops it. Checked: SIGTERM to the app while a slowed server starts leaves no process and desktop.json clears.
about:blank sign-in popup is denied (CLIProxyLogin) omo MEDIUM A blank window opened by the server's page is allowed; when it heads for another address, that address goes to the default browser and the window closes. Checked in the app: window.open('about:blank') returns a window, which closes once sent to an outside address.
media permission also grants the camera Codex MEDIUM Only audio is granted. Checked with Chromium's fake devices: video and audio+video NotAllowedError, audio granted.
Concurrent restarts can leave the server stopped Codex MEDIUM Start, restart and Try again run one at a time; Restart is disabled while one runs.
Leftover check can match a reused pid Codex MEDIUM The record carries the process start time (ps -o lstart=), and the leftover is ended only when it matches.
Extracted cache bypasses the checksum Codex MEDIUM Every build re-hashes the cached archives against the pins and unpacks them afresh.
No Rust PTY host in the package Codex MEDIUM The build adds the release's host (scripts/pty-sidecar.ts install, checked against the release manifest) when the release matches this checkout's PTY sources, as on a release tag; otherwise Node is the fallback, as for other installs.
HC_SOCKET could point the app at another core omo LOW Not passed to the app's core.
Any file: URL allowed to navigate omo LOW Only the connection screen's own file URL.

Found while checking these: the hc core inherited ~60 descriptors Chromium leaves open across exec (cache files, IPC sockets, the DevTools listener), and kept them after the app quit. The core and the server now start through /bin/bash, which closes every descriptor above stderr before it becomes the program; the core now holds only its own files (lock, log, its sockets). Unit test added.

Not changed:

  • Codex HIGH, loopback API reachable by other local accounts without a token. This is the server's existing rule for every install (an unforwarded loopback request gets drive); the app does not change it and the PR changes no server code. Passing a per-install OVIS_TOKEN would also put a sign-in in front of Open in Browser and change paired-device behaviour, so it is left for the maintainer to decide.
  • Codex LOW, desktop CI path filter does not include server/, shared/ and root manifests. Kept narrow so the macOS job does not run on every PR; the workflow itself still needs the maintainer's OK.
  • Saved port never moving back (omo LOW, unreproduced): left as documented.

CodeRabbit does not review drafts, so no CodeRabbit review has run on this PR.

- Settings dropped from the server's environment are dropped under their names from before the rename too, which the server would otherwise take back.
- A leftover server is ended only when its start time was recorded and matches.
- No server is started when a quit came during the steps before it; a failed page load no longer clears the record of a running server.
- Each run's log is closed when its server ends.
- The descriptor-closing wrapper reads /proc/$$/fd as intended; its test checks the exact descriptors the parent left open.
- Packages carry Ovis's own LICENSE and THIRD_PARTY_NOTICES.md.

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
@devswha

devswha commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

Review round 2: what changed

Round 2 at 8d7d2558: the omo reviewer returned CLEAR (LOWs only), and the Codex adversarial review returned six MEDIUMs and no HIGH. This is the last fix round, in 6c0b4be5:

Finding Source Resolution
Token under its pre-rename name comes back through the server's legacy-env adoption Codex MEDIUM, omo LOW (reproduced) serverEnv also drops the old names of the settings it removes. Unit test.
Leftover check without a recorded start time falls back to pid + command line Codex MEDIUM, omo LOW A record without a start time is left alone. Unit test.
A quit during the steps before the server spawns still starts one Codex MEDIUM launch returns before spawning when a quit came meanwhile.
A failed page load cleared the record of a running server Codex MEDIUM The page load happens after startup and its failure only logs.
Packages lack Ovis's own license and notices Codex MEDIUM LICENSE and THIRD_PARTY_NOTICES.md are staged into resources/ovis/.
Wrapper read /proc/$/fd (worked only through the /dev/fd fallback); test filter missed fd 3 omo LOW Fixed to /proc/$$/fd; the test asserts the exact descriptors the parent left open are gone. Live check: the core holds 12 descriptors, none from Electron; the server none from Electron.
Log stream left open after a server ends omo LOW Closed when its server ends.

Left as they are (no further fix rounds):

  • Rust PTY host not independently pinned (Codex MEDIUM): it uses the same check as every install (scripts/pty-sidecar.ts install: the release manifest's SHA-256 and crate tree). Pinning it in the checkout would need a per-release pin; a candidate for the release job.
  • A socket = in hc's own config overrides HC_SESSION (omo LOW): needs an explicit user setting, and then the user's own terminals show in Ovis.
  • Saved port is replaced when taken (LOW): documented.
  • Codex HIGH from round 1, loopback API without a token for other local accounts: the maintainer decided to keep the existing server rule (usability first); it is listed as a known limit in the PR description.

Validation for this head: bun run check fast passes; app/electron bun test (26 pass) and bun run typecheck; restaged on Linux (license and notices present) and the app started against an isolated hc session, opened Ovis, and stopped its server on quit.

@devswha
devswha marked this pull request as ready for review November 3, 2026 23:49
devswha and others added 2 commits November 4, 2026 08:54
… with the pid

- A copy of hc newer than the bundled one is kept instead of asking it to go back, which hc refuses (an app rolled back after a newer one).
- The server's record is written once, with its start time read synchronously, so a crash right after the spawn never leaves a record without one.
- The listening line is found after any amount of earlier output: the end of the output is kept, not its start.

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
@devswha

devswha commented Nov 4, 2026

Copy link
Copy Markdown
Owner Author

Review at 6c0b4be5: what changed

The omo reviewer blocked on one reproduced finding; fixed in 5d4338c1:

Finding Resolution
Rolling the app back to an older hc fails every start: hc update --from refuses an older build (reproduced) installCore compares versions first and keeps a copy that is the same or newer. Checked with the real binaries: a running 0.1.17 copy and a bundled 0.1.16 start fine and stay on 0.1.17. Unit tests.
A crash right after the spawn could leave a record without a start time The record is written once, with the start time read synchronously from ps. Unit test.
More than 64 KiB of startup output hid the listening line The end of the output is kept, not its start. Unit test with 200 KiB of output.

Left as known limits (LOW): a socket = in hc's own config overrides HC_SESSION (in the PR description), and builds without a matching Rust PTY host fall back to Node.

Also merges origin/main (with #2693). Validation: bun run check fast passes on the merged head; app/electron bun test (29 pass) and bun run typecheck.

@devswha

devswha commented Nov 4, 2026

Copy link
Copy Markdown
Owner Author

Review at cd1287e5: CLEAR

The omo reviewer returned CLEAR at the exact head. Notes left open, not reproduced or by design (no further fix round):

  • MEDIUM (unreproduced): a leftover server that ignores SIGTERM for more than 10 seconds is left running, and its record is replaced. Needs a server hung on shutdown; candidate follow-up: SIGKILL after the wait, or stop with an error and keep the record.
  • MEDIUM (documented): builds without a matching Rust PTY host need Node on the PC for a followed sign-in. Listed under Known limits.
  • LOW: desktop.yml triggers only on app/electron/**; kept narrow so the macOS job does not run on every server PR.
  • LOW: HC_SESSION and OVIS_STATE_DIR are taken from the launching environment (documented overrides).
  • LOW: startUpdates has no .catch for a failed electron-updater import.

CodeRabbit skipped this PR (no CodeRabbit review ran); the merge pass's own reviews stand in.

@devswha

devswha commented Nov 4, 2026

Copy link
Copy Markdown
Owner Author

Merge note: owner approved the desktop shell screens and the new desktop.yml workflow on 2026-11-04, and chose to keep the existing same-PC loopback rule (noted as a known limit). CodeRabbit skipped these heads; merged on the omo review (CLEAR receipt at cd1287e), two Codex Terra rounds (findings fixed or noted above) and green checks. app/mac and app/windows stay as they are.

@devswha
devswha merged commit 08dcf9e into main Nov 4, 2026
14 checks passed
@devswha
devswha deleted the feat/electron-shell branch November 4, 2026 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant