Skip to content

Add a desktop app: native screens in Go, the engine beside it in Node - #12

Open
imsobear wants to merge 6 commits into
mainfrom
desktop-app
Open

imsobear wants to merge 6 commits into
mainfrom
desktop-app

Conversation

@imsobear

Copy link
Copy Markdown
Owner

What

Loopable for one Mac, in desktop/. The inbox, loops, connectors and agents as native UI drawn by MyGo in Go: no web page, no webview, no account, nothing hosted.

Everything that is not a screen stays the TypeScript the service runs. src/desktop/engine.ts bundles the dispatcher, a runner in the same process (local-runner.ts, the CLI runner with the HTTP taken out) and a JSON API on loopback into one file the app starts as a Node process. Only the app's per-launch token can call it, and the engine stops when the app's stdin closes.

Connecting services

  • GitHub connects by device flow (src/connectors/github/device.ts): a public client keeps no secret, and needs no callback address. The OAuth app needs device flow turned on.
  • Google connects with a Desktop app client, coming back to the engine's loopback address.
  • Slack, Feishu and WeChat connect as on the web.
  • oauthAppRegistration takes a client ID alone when LOOPABLE_DESKTOP=1.

Verified

  • The engine runs standalone and the runner joins with the agents on this Mac.
  • Every screen is drawn without a window in render_test.go, and engine_test.go loads the screens from a real engine in a throwaway folder, saving a loop through the form.
  • go tool mygo dev ran the app against the engine on this Mac; the engine restarted cleanly across a rebuild.
  • pnpm test (417), pnpm typecheck, go vet, go tool mygo vet.

Not yet

  • An app icon of its own, a Developer ID signature and an updater. docs/desktop.md says where each goes.

🤖 Generated with Claude Code

imsobear and others added 6 commits October 13, 2026 21:52
Loopable for one Mac, in desktop/: the inbox, loops, connectors and
agents as native UI drawn by MyGo, with no account and nothing hosted.
Everything that is not a screen is the same TypeScript as the service,
bundled into one file (src/desktop/engine.ts) that the app runs as a
Node process: the dispatcher, a runner in the same process, and a JSON
API on loopback that only the app's per-launch token can call.

GitHub connects by device flow, since a public client can keep no
secret; Google by a desktop client coming back to the engine's loopback
address. The screens draw without a window in tests, and one test
loads them from a real engine in a throwaway folder. CI vets, tests and
builds the app on macOS. How it fits together is in docs/desktop.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…wning agents.

Every page of the desktop app waited on the engine, which detected the
three agent CLIs on every inbox and loops request, and a new page could
not load while an older refresh was still running. Now the engine takes
the agents from this host's runner inventory, which it detects on every
heartbeat anyway, and the app loads each page's own data first, a newer
refresh outranking an older one. Pages answer in tens of milliseconds.

The engine also stops only when its parent is really gone, polling the
parent id rather than trusting stdin's end; and the integration test
waits for the runner to join before it looks at the pages, which it had
reached too soon once they were fast.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nobody using the desktop app should register an OAuth app, so the
client IDs are no longer a Settings section. scripts/build-engine.mjs
writes them beside the engine, from the environment when releasing or
from the desktop entry of the gitignored config/oauth-app.json when
developing, and a build without them says which connector it lacks.
The app then shows that connector as not available rather than asking
the person to fix it.

GitHub's answer for an app with device flow off is device_flow_disabled,
which now gets the same plain message as unauthorized_client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop app now connects GitHub in one click with the account gh is
signed in as on the Mac, which is the account its git already works as:
nothing to register and no code to type. The engine asks gh for its
current token every few minutes, so signing in again carries over, and
only the desktop engine ever asks; the hosted service never looks.
Device flow stays as the fallback when Loopable's app is built in.

A connector that is not ready now says what this Mac is missing, in the
engine's words, rather than blaming the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app looked for the newest Node in every folder a version manager had
made, including fnm's per-shell folders, of which a machine gathers
thousands, and asked each one its version. Starting the engine waited
on all of them. Now it takes the first Node along the login shell's PATH
that is new enough, as a terminal would, skips fnm's per-shell links,
and asks a Node reached by two paths once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app icon, the menu bar icon and the sidebar now use the Loopable mark
from brand.tsx, not a gradient ring, a pixel ring and the repeat arrows.
Connectors and agents show their real logos, copied from the web app's
connector-icon.tsx, in place of two-letter tiles.

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

This branch has not been deployed

No deployments
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