Skip to content

th-a59af5: Big Smooth desktop shell (Electron MVP) - #328

Open
brentrager wants to merge 4 commits into
mainfrom
th-a59af5-electron
Open

th-a59af5: Big Smooth desktop shell (Electron MVP)#328
brentrager wants to merge 4 commits into
mainfrom
th-a59af5-electron

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

Problem

Big Smooth has no desktop app. You reach him through a browser PWA on the daemon's port or through a macOS-only menu-bar extra (SMOOTH_MENUBAR) — two half-answers to the same question.

Solution

A new desktop/ package: an Electron app that IS the Big Smooth desktop app. Electron over Tauri deliberately — one Chromium everywhere beats Tauri's per-OS webview fragmentation.

The daemon stays the engine; this is only the shell.

  • src/daemon.ts — resolve + start + stop the native smooth-daemon. Resolution order mirrors th daemon (crates/smooth-cli/src/daemon_launcher.rs) with the packaged copy first: bundled resources → $SMOOTH_DAEMON_BIN~/.smooth/binPATH → cargo target dir (via cargo metadata, so a global build.target-dir works). It probes /health first and attaches to an already-running daemon (th up, launchd) rather than fighting it for the port — and only ever terminates a child it spawned itself.
  • src/main.ts — window + tray. The window is a BrowserWindow on the daemon's /, which already serves smooth-web with the local auth token injected into index.html, so there is no renderer, preload, or IPC code in this package. Close hides to tray, Quit exits, single-instance lock, off-origin links open in the real browser.
  • Tray — the th mark (rendered from images/smooth-icon.svg), with Open / Set Up (Calendar · Reminders · Messages · Full Disk Access, each handing the interactive th doctor --setup-* flow a terminal) / Quit.

Cross-platform throughout; the one mac-only branch is the Terminal.app shell-out, which falls back to a dialog elsewhere.

Verification

Smoke-tested on macOS with pnpm dev against the daemon already running on :8787:

  • App launched, window rendered the real Big Smooth SPA (title Big Smooth — your always-on AI, signed in, model + status live) — confirmed by screenshot, not just by the process existing.
  • Tray item present in the menu bar (1 status item on the Electron process).
  • It attached to the running daemon rather than spawning one (no smooth-daemon child), and quitting the app left that daemon up — the intended asymmetry.
  • pnpm test (2 node:test cases over addr parsing + binary resolution), tsc, oxfmt, oxlint all clean.

Not verified by me: the tray menu items under click (Set Up flows, Quit-from-tray) and any non-macOS run.

Deferred

Packaging and signing. electron-builder.yml is a working skeleton (mac/win/linux targets, extraResources pointing at a per-platform resources/<os>/smooth-daemon that nothing populates yet) — no certificates, notarization, or CI job. Windows/Linux are a config step, not a code change.

Pearl th-a59af5.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4c8d59b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/smooth Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

brentrager and others added 2 commits August 3, 2026 23:35
The browser PWA and the macOS-only menu bar are two half-answers to the same
question: where does Big Smooth live when you're not in a terminal. This is the
answer — an Electron app that IS the desktop app.

The daemon stays the engine. The shell resolves and spawns `smooth-daemon`
(same resolution order as `th daemon`, plus a bundled copy for packaged builds),
waits on `/health`, and opens a window on the daemon's `/` — which already
serves smooth-web with the local auth token injected, so there is no renderer,
preload, or IPC code here at all. If a daemon is already answering on the port
(`th up`, launchd) it attaches instead, and only ever terminates a child it
spawned itself.

The tray carries the `th` mark and three things: Open, a Set Up submenu that
hands the interactive `th doctor --setup-*` flows a terminal, and Quit. Closing
the window hides to the tray; only Quit exits.

Packaging and signing are a separate slice — electron-builder.yml is a working
skeleton with no certificates wired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop app now ships the engine rather than borrowing whatever the host
has: `pnpm stage-daemon` stages `smooth-daemon` into the bundle, electron-builder
packages it (signed .dmg/.zip with the hardened runtime on macOS, NSIS on
Windows), and the nested Mach-O gets its own signature so notarization — reusing
scripts/macos/notarize-and-staple.sh — has nothing to reject. Verified: the
packaged app spawns its bundled daemon, health-checks it, and opens on it.

The tray's Set Up items stopped being a Terminal shell-out. Calendar and
Reminders drive a new `smooth-daemon tcc <what>` as a one-shot child (the
long-running daemon is the wrong process to ask from — it may have been started
by launchd, and TCC would attribute the prompt to that); Messages pokes Apple
Events; Full Disk Access opens the pane and reveals the app.

`SMOOTH_MENUBAR` now decides in both directions. An env var that can only turn a
thing ON is a footgun, and the off direction is load-bearing the moment a daemon
sits inside an app bundle that owns its own tray.

WHAT DOESN'T WORK, measured rather than assumed: the EventKit prompt never
appears. `smooth-daemon tcc calendar` spawned from the signed app — every usage
string present in its Info.plist — returns not-determined silently, while the
identical binary with the identical signature prompts correctly when it is an
app bundle's CFBundleExecutable launched via `open`. Being a bundled app's child
is enough to USE a grant and not enough to ASK for one. desktop/README.md has
the comparison and the two candidate fixes (nested helper .app, or a native
module); neither is in this commit, because both change how the daemon is
embedded and that's a decision, not a detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brentrager

Copy link
Copy Markdown
Contributor Author

Level-up: the Electron app is now the installable — plus one blocker

Shipped

  • Bundled engine. pnpm stage-daemon stages smooth-daemon into resources/current/; electron-builder bundles it into Contents/Resources. resolveDaemonBin() finds the bundled copy first in a packaged build, the dev paths otherwise. Verified: the packaged app spawned its own bundled daemon on :8799, /health returned 200, and the window opened on it.
  • Packaging. macOS .dmg + .zip, hardened runtime, build/entitlements.mac.plist, icon from scripts/macos/BigSmooth.icns. The nested daemon is signed separately (Identifier=smooth-daemon, flags=0x10000(runtime), team DTX9733844) and codesign --verify --deep --strict passes, so notarization has nothing to reject. pnpm notarize reuses scripts/macos/notarize-and-staple.sh. Windows NSIS + Authenticode wiring is stubbed and degrades to unsigned; untested.
  • Tray. No more Terminal shell-out. Calendar/Reminders drive a new smooth-daemon tcc <what> as a one-shot child, Messages pokes Apple Events, Full Disk Access opens the pane and reveals the app. Result reported in a dialog.
  • SMOOTH_MENUBAR decides in both directions (smooth-menubar). It could only turn the native status item on; the off direction is load-bearing the moment a daemon lives inside an app bundle that owns its own tray.

The blocker: TCC does not flow to a spawned child

This is the thing worth stopping on. Measured, not assumed:

Setup Result
smooth-daemon tcc calendar spawned as a child of the signed Electron app, all four usage strings in its Info.plist calendar: not-determined, no prompt
The identical binary, identical signature, as an app bundle's CFBundleExecutable launched via open prompt appears correctly

I also tried moving the daemon to Contents/MacOS (so its main bundle resolves to the .app) — no change, and I reverted it. Being a bundled app's child is enough to use a grant; it is not enough to ask for one. Asking appears to need to be an app bundle's main executable launched through LaunchServices. Messages/Apple Events behaves the same way from a spawned osascript.

Two candidate fixes, neither in this PR because both change how the daemon is embedded:

  1. Nested helper app — ship the daemon as Big Smooth Helper.app in Contents/Resources (which scripts/macos/make-app-bundle.sh already builds) and open it. Grants then attribute to the helper's bundle id, not the Electron app's — so the long-running server has to be launched the same way to use them, which changes daemon lifecycle management.
  2. Native module — call EventKit from the Electron main process. Correct attribution, at the cost of a compiled native dependency.

Full write-up with the comparison table lives in desktop/README.md.

Also verified / not verified

cargo fmt, clippy, and cargo test (199 passing) clean on the two touched crates; pnpm test / tsc / oxfmt / oxlint clean. Windows and Linux have never been run. Notarization is wired but unexercised — only an Apple Distribution identity is on the build machine, and notarization needs Developer ID.

One gotcha found while testing: the web SPA is compiled into the daemon binary, so staging a daemon built without pnpm build:web gives a blank window. Noted in the README.

`hiddenInset` floats the traffic lights over the page, and smooth-web puts its
sidebar toggle in exactly that corner — the two landed on top of each other.
Going frameless properly means insetting the SPA's own header, which is
smooth-web's business and would move the browser PWA with it. A normal title bar
is one deleted line and correct on Windows too.

Packaged builds already take the Dock icon from BigSmooth.icns; an unpackaged
`pnpm dev` showed the stock Electron atom, which is what made this look broken.
Set it explicitly there so dev matches what ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brentrager

Copy link
Copy Markdown
Contributor Author

Polish pass on the two things Brent hit:

Title bar / hamburger overlap — fixed. titleBarStyle: 'hiddenInset' floats the traffic lights over the page, and smooth-web puts its sidebar toggle in exactly that corner. Dropped it for a normal title bar: one deleted line, no CSS, and correct on Windows too. Verified against the real SPA (dev run against a live daemon) and in the packaged build — traffic lights now sit in the title bar, the hamburger clears them.

Going frameless properly would mean insetting smooth-web's own header, which lives in another crate and would move the browser PWA with it. Noted as the upgrade path in a ponytail: comment rather than done here.

Dock — was already correct in packaged builds and I confirmed rather than assumed: LSUIElement is not set, Contents/Resources/icon.icns is byte-identical to scripts/macos/BigSmooth.icns, and the running app appears in the Dock's tile list as "Big Smooth" with the th mark. What looked broken was pnpm dev, which runs out of Electron's own bundle and shows the stock atom icon — now app.dock.setIcon() on the unpackaged path so dev matches what ships.

Gates green (pnpm test, tsc, oxfmt, oxlint). Still review-hold; the TCC blocker from the previous comment is unchanged.

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