Skip to content

feat(app): external-spawn via single-instance args (reference impl for #340)#341

Open
akamrume328 wants to merge 1 commit into
fujibee:mainfrom
akamrume328:feat/external-spawn
Open

feat(app): external-spawn via single-instance args (reference impl for #340)#341
akamrume328 wants to merge 1 commit into
fujibee:mainfrom
akamrume328:feat/external-spawn

Conversation

@akamrume328

Copy link
Copy Markdown

Implements #340.

This is a reference implementation using the single-instance args approach described in #340. I'm happy to reshape it to a deep-link handler or a local-IPC endpoint if you'd prefer one of those as the front door — see the design question in #340. Posting the working diff here in case a concrete implementation is easier to react to than prose.


What

Add a way to open a member's pane in the already-running app from outside the GUI:

agmsg-app spawn <team> <role>

A second launch with this shape is caught by the existing single-instance
handler, which forwards (team, role) to the running instance as an
external-spawn event. The frontend validates them against the live member
registry, then calls the existing spawnMember.

Why

The app already delivers agmsg messages to spawned panes via stdin-inject, but
a pane can only be created by a human clicking a member in the GUI. There is
no way for a script, an orchestrator agent, or a shell alias to launch a member.
On Windows this gap is sharper: there is no LaunchAgent-style external launcher,
so headless / automated team setups cannot bring members up programmatically.

This is the smallest possible entry point for that: the single-instance plugin
is already registered and its callback already receives the second launch's
argv/cwd — this change just stops ignoring them for the single spawn verb.

How / trust boundary

  • Rust parses only the spawn verb and forwards the raw team/role
    strings. It never execs anything from the args itself.
  • Frontend validates team (must be the active team) and role (must exist
    in the live member registry) before calling spawnMember. Unknown values are
    logged, not silently dropped.
  • spawnMember is idempotent: an already-running member is just focused, so a
    duplicate spawn cannot create two panes for one identity.

So the trust boundary stays exactly where a manual member click already puts it.

Scope / follow-ups (intentionally out of this PR)

  • Cold-start race: if the spawn launch races app startup before the
    frontend listener is registered, the event can be missed. A small Rust-side
    pending buffer drained by the frontend on ready would close this; kept out to
    keep the diff minimal.
  • Cross-team: only same-(active-)team spawns are honored for now.
  • Richer control-plane (queue/list/status/kill, synchronous success): better
    served later by a deep-link handler (agmsg://spawn?...) or an authenticated
    local IPC endpoint. single-instance args is meant as the minimal bootstrap
    entry, not the final API.

Testing

  • tsc --noEmit: clean.
  • vite build: clean (frontend bundles with the new listener).
  • Manual: agmsg-app spawn <team> <role> focuses the running window and opens
    the member's pane exactly once; unknown team/role is logged and ignored.
  • Windows note: the role may contain non-ASCII (e.g. Japanese member names),
    so it is passed through as a plain arg string end to end (no URL/quote
    transform), which the manual test exercises.

Diff

2 files, +47/-1: app/src-tauri/src/lib.rs, app/src/App.tsx.

Launch a member pane from outside the GUI: a second launch of the shape
  agmsg-app spawn <team> <role>
is forwarded by the single-instance handler to the running instance, which
emits an external-spawn event. The frontend validates <team>/<role> against
the live member registry before calling the existing spawnMember (idempotent:
an already-running member just gets focused). The Rust side never execs the
args itself, keeping the trust boundary at the same place a GUI click is.

Enables scripted / orchestrator-driven launch (the missing counterpart to the
app's stdin-inject delivery), needed for headless team automation on Windows
where there is no LaunchAgent-style external launcher.
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