Native macOS .app launcher + bundle - #16
Open
MuJiongan wants to merge 2 commits into
Open
Conversation
Adds a one-window native Mac app so you can launch gorchestra from Spotlight/Dock instead of running 'make backend' and 'make frontend' in two terminals. - launcher.py: pywebview window pointed at FastAPI on a pinned port (8765) so localStorage settings persist across launches. SPA mount serves frontend/dist/ from the same origin. private_mode=False keeps WKWebView's data store between runs. - scripts/build_app.sh: builds gorchestra.app with a tiny C wrapper in Contents/MacOS/ (real arm64 Mach-O so LaunchServices reads the arch correctly and doesn't falsely demand Rosetta), then ad-hoc signs for macOS 15+ App Management. - runner: child subprocess now spawned with start_new_session=True; events.cancel uses killpg() so cancellation reaps the runner's grandchildren (shell tools etc.) too. Window-close hook reuses the same teardown path. - Makefile: app-install, app, app-bundle, install-app targets. The install-app target is the one-shot 'build everything + drop into /Applications'.
MuJiongan
force-pushed
the
jiongan/mac-app-gui9u
branch
from
May 3, 2026 22:55
d7ae94a to
80447e7
Compare
Adds a pipe-EOF watchdog to the runner child so abrupt parent death (SIGKILL, force-quit, segfault — anything that skips atexit and signal handlers) is detected instantly and the child tears down its own process group, taking any 'shell' tool grandchildren with it. - runner.py: open a pipe before Popen; pass the read end via pass_fds with PARENT_DEATH_FD env var. Parent holds the write end open for the duration of the run and closes it in a finally block when the child has exited. - child.py: install a daemon thread early in main() that blocks reading the inherited fd. EOF means parent is gone; we double-check via getppid()==1 to avoid spurious teardown, then SIGTERM our process group and _exit. - README: fix incorrect bundle-id caveat. Both 'make app' and gorchestra.app run through Homebrew's embedded Python.app and share a WKWebView store under ~/Library/WebKit/org.python.python/, so localStorage carries between them. Only 'make dev' (browser) is separate. Also note the new force-quit teardown guarantee. Verified end to end: SIGKILL'd a parent runner that had spawned a node which had in turn spawned 'sleep 60'. Within 2s both the runner child and the sleep grandchild were gone. 51/51 backend tests still pass.
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
A real macOS
.appbundle so you can launch gorchestra from Spotlight/Dock/Launchpad instead of runningmake backendandmake frontendin two terminals.Then
Cmd+Space→gorchestra→ Enter.How
launcher.py— runs FastAPI in a daemon thread on pinned port8765, mountsfrontend/dist/as an SPA fallback, opens a native WKWebView viapywebview.private_mode=Falseso localStorage (API keys, default models) persists across launches. Port pin is required because localStorage is origin-scoped — a shifting port would orphan settings every relaunch.scripts/build_app.sh— producesgorchestra.app. The executable inContents/MacOS/is a tiny C wrapper compiled to native arm64 Mach-O thatexecvs the project's venv Python onlauncher.py. A shell-script wrapper here would make LaunchServices misread the architecture and falsely demand Rosetta with a misleading -10669 error. Bundle is then ad-hoc signed so macOS 15+ App Management lets it launch.start_new_session=True;events.canceluseskillpg()so cancellation reaps the runner's grandchildren (shell-tool subprocesses) too. The pywebviewclosingevent reuses the same teardown, so closing the window kills any in-flight runs cleanly.killpgs the child's own process group +_exits. Verified: SIGKILL on a parent with an active sleep grandchild reaps both within 2 seconds.Targets
make app-installpywebviewinto the backend venvmake appmake app-bundlegorchestra.appin the project rootmake install-app/ApplicationsCaveats
make install-apprebuild. For a fully relocatable bundle, swap the C wrapper forpy2app.make devruns the frontend in your browser and uses that browser's localStorage. The native-window launches (make appandgorchestra.app) both run via Homebrew's embedded Python.app and share a single WKWebView store under~/Library/WebKit/org.python.python/, so settings carry between those two — but not from the browser.Verification
make devstill works (backend on 8000, vite on 5173, vite proxy unchanged).make install-append-to-end: builds, installs to/Applications, launches viaopen, FastAPI binds 8765, WKWebView loads UI.Popen'dsleep 60; SIGKILL on parent → both runner child and sleep grandchild gone within 2s.