17 Commits
Author SHA1 Message Date
yetone 7d0e415e62 plugins: two plugins that sign in to one provider are both listed, each row says which serves it, and Use for picks the other (neiko on Discord)
neiko on Discord installed a third-party plugin beside a plugin of their
own, added by its folder. Only the third-party one could be seen. Both
plugins' auth hooks name the same provider id. The host's auths() keeps
the last plugin in plugins.json for an id, as OpenCode does, so the
user's own plugin silently lost it:
- its row read "Signs in to nothing magpie can use";
- its provider was gone from the Add sheet;
- keepUnloaded could list the id a second time, from the
  plugin-providers.json kept before.
A package-name clash and a display-name clash were tried as well. Both
plugins stay visible in those cases, because a folder plugin's name is
its path.

What changed:
- host.js:
  - init gives each loaded plugin the ids it signs in to (provides) and,
    for each id another plugin serves, that plugin's spec (servedBy).
  - auths() honours plugins.json's new prefer map (id -> spec).
  - Only the serving plugin's provider.models hook runs for the id.
- plugin:
  - Clashes() turns provides and servedBy into {id, by, with} per spec.
  - Prefer() records a pick and restarts the host.
  - Remove drops the removed plugin's picks.
  - Re-adding a plugin under a new spec carries its picks over.
  - remove, off, options and prefer find a plugin by its exact spec
    before its package name (indexOf), so acting on one plugin never
    reaches another of the same package.
  - keepUnloaded no longer re-adds a provider from before when another
    plugin serves its id now.
- GUI:
  - /api/plugins rows carry clashes, with the provider's listed name.
  - POST /api/plugins/prefer {spec, provider}.
  - The serving row says "{other} signs in to {name} too; this one
    serves it".
  - The other row says, in amber, "{name} is served by {other}, which
    signs in to it too", with a "Use for {name}" button.
  - New strings in zh, zh-TW, ja and de.
- CLI: `magpie plugin` prints the same lines, with the command that
  fixes it, and `magpie plugin use <name> <provider>` makes the pick.
- Docs: docs/subsystems/provider-plugins.md, docs/reference.md and
  site/public/docs/{,zh/,ja/}plugins.html.

What was run:
- Reproduced under a sandbox HOME with magpie web on 3562: a fake npm
  plugin and a folder plugin, both on id "acme".
  - Old binary: the local row had providers [] and /api/providers
    listed acme twice.
  - New binary: both rows carry the clash; prefer moves acme to the
    folder plugin; remove clears the pick and leaves the other plugin
    serving acme.
- New Go tests:
  - TestTwoPluginsOneProvider, a real Bun host: Prefer, restart,
    off/on and Remove.
  - TestClashes, TestPluginNamedExactlyFirst and
    TestKeepUnloadedLeavesAServedID.
  - TestPluginsClashRows (gui) and TestPluginListSaysClash (CLI).
  - Each fails when its piece is broken by hand: prefer ignored in
    host.js, the keepUnloaded id check removed, name before exact spec,
    Remove keeping picks, no provides, no clashes in the rows, no
    prefer route.
- plugin-clash.test.cjs, in Chromium and WebKit, in en, zh, zh-TW, ja
  and de, at 1100 and 560px: 22/22 pass. It times out on the old
  plugins.js.
- gui-de, gui-ja, gui-zh-tw, i18n-keys and every plugin-*.test.cjs:
  248 tests.
  - 245 passed in the 4-wide run.
  - plugin-moved-signin (webkit) failed once in that run, then passed
    3/3 alone and 26/26 in a smaller 4-wide rerun.
- gofmt -l; go vet ./...; GOOS=linux and GOOS=windows go vet -tags
  nogui ./...; go build ./...
- The full go test -tags nogui ./... under a temp HOME with the caches
  pinned: 44 packages ok, none failed.
- After rebasing onto 05917100 (plugins.css conflict with a3a8e257's
  chip rules, both kept): plugin, gui and the root plugin tests, vet on
  darwin, linux and windows, and the 248 GUI tests above, all green
  (248/248 this time).

Not tried: a real third-party plugin, neiko's own plugins, and Windows
beyond vet.
2026-10-10 00:55:38 +08:00
forever fb9d1a4cc6 provider, plugins: a sign-in reads as done only once its account is listed again, and the command line's plugin sign-in brings the account back and takes its lapsed mark off (#1061)
* provider: a sign-in reads as done only once the account it brings back is listed again, so the window opens it rather than saying magpie can't list it (TestClaudeTeamSeatBesidePersonal failed so on macOS in #1046's CI)

Signing in brings back an account removed from magpie (ShowAccount clears
its Hidden mark). Since 440e535b, (*signInFlow).finish recorded the outcome
and let go of the sign-in's lock before it called ShowAccount, so a poll of
SignInStatus could read done while the account was still hidden. The
window polls every 800ms and, seeing done, looks for the account to open;
missing, it says "{user} signed in, but magpie can't list it — please
report this". #1046's macOS job (run 37502933081) hit the same gap:
  signin_team_test.go:52: account after sign-in: false []
WaitSignIn, which the command line uses, waits for the end of finish and
was not affected.

finish now marks the sign-in finished first. Only the first finish records
an outcome, as before: a cancel or the timeout that comes after it is
refused, and so is a callback (claim). A done sign-in then calls
ShowAccount, and only after that reads as done. ShowAccount's error is
ignored, as before: if providers.json can't be written, the account stays
hidden and the sign-in still ends done. Until then the sign-in reads as
waiting; that takes longer only when the account was hidden, since only
then does ShowAccount write providers.json (and sync the agents' catalogs,
as it did inside finish before).

Every built-in and plugin sign-in's done goes through finish, so this one
change covers them all. The other places that show an account again do it
before their result can be seen: PluginAPIKey returns after ShowAccount,
and ImportLogins and ImportGoogleAccounts call it before they return. The
command line's own plugin sign-in never showed the account again; that is
a change of its own. ImportFactoryKeys shows none either, but the window
offers a Factory key's import only for a Factory it lists, and a removed
one is left out of the Add sheet. providers-accounts.md's sign-in row now
says when a sign-in reads as done.

Verified: TestSignInDoneOnceTheAccountIsBack records what the sign-in reads
as at the moment providers.json shows the account again (catalog.Changed),
for magpie's own callback (Codex), Claude Code's login run by magpie
(Claude) and a plugin's (FakeCo, with Bun). With finish from main all three
fail with "the sign-in read as done before its account was shown again:
the window, seeing it done, can't list it"; with this they pass.
TestSignInCanceledWhileTheAccountComesBack cancels the sign-in at that
moment and checks it still ends done, the account listed; with this finish
guarded on the state alone, as main's was, it fails with a "close of
closed channel" panic in the callback. With a 300ms sleep where main's
finish lets go of its lock, TestClaudeTeamSeatBesidePersonal fails 10 of
10 runs, each in a process of its own, with #1046's message; main as it is
passes 10 of 10 here, the gap being that narrow. With the same sleep after
this finish publishes done, or before its ShowAccount, it passes 10 of 10.
Under a temporary HOME with Bun 1.4.2, -race -count=20 on those three
tests and -race -count=2 on the package's sign-in, plugin and login tests
pass, as do go vet and the darwin, GOOS=linux (nogui) and GOOS=windows
builds; this commit alone builds and passes internal/provider.
With the next commit, go test -tags nogui ./... passes on main at 443f1cbc
and at 0bf9d77d. Three earlier full runs failed, each only in
internal/plugin, which is unchanged and imports nothing changed here:
twice TestHostJSCancellation/ready-wait at its 10-second Node deadline
(signal: killed) while other sessions' tests loaded the machine, the
timeout 76dc5aed and #1036 also record, and once, with Bun 1.3.13,
TestPluginHTTP2TakesTheProxy.
The GUI suite was not run: nothing in internal/gui/assets changes, and its
tests answer /api/signin from fixtures. Not tried with a real vendor's
sign-in.

* plugins: magpie plugin login and magpie accounts add finish a plugin's sign-in as the window does, bringing back a provider removed from magpie and taking the account's lapsed mark off

The window's sign-in to a plugin's provider, once the plugin has saved
the account, takes the account's lapsed mark off and shows again the
agent's own account removed in magpie (clearPluginLapse), and brings the
provider back if it was removed from magpie (ShowAccount): pluginDone
through finish, and PluginAPIKey for a key. The command line's
(pluginLogin, which magpie plugin login runs, as magpie accounts add does
for a plugin's provider or a subscription moved onto one) said "✓ signed
in to …" and did none of it:
- a provider removed from magpie stayed out of magpie providers, the
  agents' catalogs and routing until it was added back (in the window, or
  with magpie provider add);
- an account signed in again after its vendor turned the sign-in away
  still said "sign in again" until a request or a usage read went through
  it;
- an agent's own account (Kiro's, Devin's, …) removed in magpie stayed
  hidden after it was signed in to again, though the window's sign-in
  shows it again (#320).

PluginSignedIn now does all three for a sign-in magpie doesn't follow
itself, and PluginAPIKey and pluginLogin call it. providers-accounts.md's
sign-in row says so.

Verified: TestAccountsAddBringsBackARemovedPlugin signs in to FakeCo, the
plugin host's test plugin, with a key, removes it from magpie and runs
magpie accounts add fakeco with another key.
TestPluginLoginTakesTheLapseOff signs in with magpie plugin login's
browser way (open and xdg-open stubbed), has a usage read mark the
account lapsed as FakeCo's answer says, and signs in again. With
plugins_cli.go from main they fail with "signed in again, but not listed:
no provider "fakeco" — magpie providers lists them" and "signed in again,
but still lapsed: [{… Lapsed:gone@fake's FakeCo sign-in has expired —
sign in again …}]"; with this they pass. With PluginSignedIn's
ShowAccount taken out only the first fails, and with its clearPluginLapse
taken out only the second. Neither covers the own account: FakeCo has
none, and no test covers that part of clearPluginLapse on main either.
Under a temporary HOME with Bun 1.4.2, -race -count=5 on the two and on
TestAccountsOfAPlugin pass. Like TestAccountsOfAPlugin, the two are in
the root package, which has no testenv.Offline, so the plugin host they
start asks models.dev for its catalog.
go test -tags nogui ./... passes on main at 443f1cbc and at 0bf9d77d. Two
earlier full runs failed only internal/plugin's
TestHostJSCancellation/ready-wait at its 10-second Node deadline (signal:
killed) while other sessions' tests loaded the machine, the timeout
76dc5aed and #1036 also record; internal/plugin is unchanged and imports
nothing changed here.
Not tried with a real plugin's sign-in.
2026-10-07 06:24:24 +08:00
yetone 14af0f100b provider, plugins: a plugin's browser sign-in finishes from the address the browser ended on, for magpie on a server or in Docker (Chicring)
The built-in sign-ins that come back to a port on this machine (Codex,
ChatGPT API, Devin, Kiro, Zed, Gemini, Antigravity) already took the
pasted address. Their plugins didn't: a subscription moved onto its plugin
(Devin, Kiro, Zed) or a plugin's own browser sign-in (Trae) showed only
"the account shows up here as soon as you're done", and a browser on
another computer than magpie's ended on a page that won't load with no way
to finish.

Now a plugin's "auto" sign-in whose page names a port on this machine to
come back to (redirect_uri, auth_callback_url, callback on localhost, or
Zed's native_app_port: loopbackPorts) sets pasteCallback, so the GUI shows
the same Callback URL field as the built-ins, and `magpie plugin login` /
`magpie accounts add` ask for it too. SubmitSignInCallback opens the pasted
address on 127.0.0.1 at that port, never through a proxy, so the plugin's
own listener finishes the sign-in as it would for the browser; an address
on another port or host is refused, a page that sends the browser on to a
page that comes back to the same port (Kiro's AWS sign-in) becomes the next
page to open. Command Code's page posts its key from script, which leaves
no address to paste, so it keeps offering the plugin's API key way.

Verified: TestPluginSignInTakesAPastedCallback (fake plugin with a loopback
sign-in, $FAKE_LOOPBACK) fails without the wiring in pluginBegin ("a
sign-in that comes back to a port here takes no pasted address:
{... PasteCallback:false ...}") and passes with it, -race -count=10;
TestLoopbackPorts covers the community plugins' page addresses.
go test -race ./internal/provider ./internal/plugin and the root package's
Plugin|Login|SignIn tests pass; vet, gofmt, darwin/linux nogui/windows
builds pass. With the real community Devin, Zed and Kiro plugins in a
sandbox HOME, each sign-in offered the field and a pasted address reached
the plugin's listener and the vendor (bogus code refused by Devin 401, Zed
"couldn't be decrypted", Kiro 400). signin-paste-plugin.test.cjs (new),
signin-callback and signin-paste-codex pass in Chromium and WebKit, en and
zh, at 360px; no new strings. Not tried: a real successful sign-in from a
browser on another computer.
2026-10-07 00:13:03 +08:00
yetone db812921a6 plugins_cli: magpie plugin off, on and rm take a plugin's short name, as options does (the owner, testing for real) — magpie plugin off think-tags answered no plugin "think-tags" while magpie plugin options think-tags found it, so the names the community READMEs and usemagpie.ai's docs write worked for one command only. installedName turns a short name (think-tags for @magpie-community/middleware-think-tags, plugin.ShortName) into the installed plugin's package name for on, off and rm; a name a plugin has itself, or one no plugin has, is kept as it was. Found and tested end to end in a sandbox HOME with a real DeepSeek account: Model Map and Think Tags installed by clicking Install under Plugins › Discover › Gateway middleware in the real app (from a local verdaccio registry holding the real packages, until their first npm publish; magpie built with npmRegistry pointed at it), an alias set in Model Map's Options editor served by deepseek-flash and named back, think-tags off by short name let the model's <think>…</think> through and on took it out ("51"), rm think-tags by short name and Model Map's Remove in the app left plugins.json empty and the alias unknown. TestPluginOnOffRmShortName is new and fails without the change. go vet, go build (GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. 2026-10-05 22:08:31 +08:00
yetone 2f135ed4d5 plugin, gui, docs: Plugins says which plugins are providers and which are middleware, lists the community's ready-made middleware, and edits a middleware's options in its row (the owner) — the Plugins page still read as if every plugin were a subscription, and nothing showed what a middleware does or let you set its options without editing plugins.json. Every Discover card and Installed row now carries a Provider or Middleware chip (both for a plugin that is both), also for one that is off (isMiddleware/middlewareOnly from the server, read from its files); Discover splits into Subscriptions and Gateway middleware ("runs in magpie's gateway on what every agent sends and gets back, whichever provider serves it"), the latter from market entries with "kind": "middleware" and no providers or icon (the built-in market.json lists the five new @magpie-community/middleware-* packages: param-override, model-map, system-prompt, word-guard, think-tags, mostly New API's channel settings with the same JSON). A middleware row has Options: a JSON editor in the row, starting from the package's magpie.options example when none are set, which validates it is an object, saves (POST /api/plugins/options) for the next request, clears, and cancels with Esc. magpie plugin options takes a short name (model-map for @magpie-community/middleware-model-map; plugin.ShortName, also in SetOptions). en/zh/ja/de. Docs: usemagpie.ai/docs/plugins (en/zh/ja) gains Ready-made middleware and Options and the registry's kind; reference.md, both READMEs and docs/subsystems/gateway-middleware.md likewise. Tested with a real DeepSeek account and real Claude Code through magpie web --gateway in a sandbox HOME, with all five installed and options set by short name: a mapped model named back, max_tokens capped, a prompt appended for claude only, word-guard's 400 and param-override's 403 in the agent's shape, <think> stripped from Chat and Responses streams and a whole Anthropic reply, reasoning_content moved into the text; /api/plugins showed the kinds and the example. TestSetOptionsShortName is new; TestBuiltinMarket checks a middleware entry has no providers or icon and no other kind. plugin-middleware.test.cjs (Chromium and WebKit, en and zh: the chips, Discover's two sections, the options editor with its errors, no select, no scroll, no left border) fails without the change; the other plugin tests and click-scroll pass in WebKit (plugin-market's section name updated; plugin-move fails only under parallel load and passes alone). go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. 2026-10-05 20:50:09 +08:00
yetone c8278a8691 docs: plugins are OpenCode's provider plugins or pi's packages — the Plugins page's intro, magpie plugin's help, the README and the plugin docs (en, zh, ja) said OpenCode only, though pi packages that register a provider run since v0.1.772; the docs gain a pi packages section (what makes a package pi's, what is loaded and left alone, sign-in through onPrompt/onAuth and ctx.ui, pi's files under <config>/pi or PI_CODING_AGENT_DIR, Anthropic Messages requests), the pi-package search and a FAQ entry 2026-10-04 03:10:09 +08:00
yetone 294cf1225d providers: a built-in subscription whose plugin the user installed is the plugin's, and the "+ provider" sheet lists it once (ARNO on Discord: the sheet had Qoder 国际版 and Qoder CN under 订阅, and Qoder ● 2 and Qoder CN ● under 来自插件, the same two subscriptions twice) — a deprecated built-in hands its id to its plugin only through an explicit Move (the editor's Move to plugin) or Adopt (clicking the built-in's tile before signing in, since v0.1.642); the automatic list, Retiring, is empty, and f099d07e (v0.1.659) gave Qoder CN its mover but moves nobody on its own. A user who installed the Qoder plugin from Plugins was never moved: the plugin's providers took the ids qoder-plugin and qoder-cn-plugin (provider.PluginID, as the built-in ids were not moved), the add sheet listed those under 来自插件, and the built-ins stayed under 订阅 as deprecated tiles. Qoder international (moved since 09-30) was listed twice the same way. Existing migration logic, unchanged: movers (built-in id → plugin package, min version, out/back of its accounts), Move (each account checked through the plugin, everything put back on any failure), Adopt (no accounts: install the plugin and mark it moved), MoveBack, the migrations.json states (moving, plugin, back, failed, a failed one retried after 6 h), and KeepRetiringMoved in the gateway (20 s after start, then hourly). Added: provider.HandOver, for each mover whose plugin is installed, enabled and at least its min, serving the built-in's id, not signed in under its own -plugin id, and not moved back or failed in the last 6 h: with no built-in accounts it Adopts at once; with accounts it Moves them, with Move's rollback, only from the hourly loop (HandOver(ctx, true) beside MoveRetiring). The GUI's plugin add/update/upgrade and the CLI's plugin add/update call HandOver(ctx, false) once they succeed. One the plugin is already signed in to under its -plugin id is left as it is: handing the id over would rename the models agents use (qoder-plugin/... to qoder/...). Until a hand-over happens, and whenever it can't, the add sheet hides a deprecated built-in whose plugin serves its id (replacedSub in app.js), keeping it only while it is signing in; a moved one was already one tile. With the plugin not installed the built-in tile stays, as clicking it is how a new user gets the plugin. A provider signed in through the built-in keeps working and stays in the provider list. AGENTS.md says so. The manual Check for updates button for plugins that was also asked for is not shipped: per the owner, official community plugins update on their own. TestHandOver (a fake plugin under a temp HOME: nothing without the plugin; no accounts: moved at once; moved back: left; built-in accounts: left by HandOver(false), moved by HandOver(true), the built-in's emptied and the plugin signed in; the plugin signed in on its own: left) is new. plugin-replaced.test.cjs (WebKit, en and zh: no plugin, both built-ins under Subscriptions; the plugin installed with Qoder signed in to two accounts, the plugin's two tiles alone; Qoder moved, its one tile under Subscriptions and Qoder CN the plugin's; a built-in Qoder CN signed in, its provider row still listed) fails without the app.js change (all four tiles) and passes with it. deprecated-subscriptions, plugin-move, click-scroll, add-button, mobile-web, signin-paste-key and every plugin-* test pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed on its TempDir cleanup, the known flake). 2026-10-02 20:31:19 +08:00
yetone 456208945b plugins: a plugin can be installed from a git repository — github:owner/repo, owner/repo, a GitHub URL or a git+https:// one (Lemon on Discord: can plugins be installed directly from git?) — magpie plugin add and the Plugins field took only an npm name or a path, and turned a git spec away as "isn't an npm package name". A git spec now goes to bun add as given and is kept as given in plugins.json; the package it installs is named by its repository's package.json, which bun records as the dependency whose spec it is, so the plugin is known (list, on/off, rm, its version, its README) by that name or by the spec. update and the row's Update run bun update on it, which fetches the repository's commit now where bun add keeps the one bun.lock has, the spec unchanged; npm is never asked for a newer version of it, nor auto-updates it. Adding the same package from another URL, or from npm, takes its entry's place. The Plugins field says it takes a GitHub repo (en+zh); an installed git plugin shows its package's name, an Update button, and a page with its README, no npm link and a Source link to the repository. Nothing that installed before is turned away. TestGitPlugin (a fake bun fetching from local bare repositories, the real one loading the plugin when on PATH) and plugin-git.test.cjs fail without the change; TestIsGit checks what counts as a git spec. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-git, plugin-local, plugin-market, plugin-updates, click-scroll, old-webkit pass. 2026-10-02 02:24:49 +08:00
AronnaxandClaude Opus 5.5 ec4ee5a3a4 fix(proc): end what a CLI probe started, on its timeout and when magpie exits (#414)
* fix(proc): end what a CLI probe started, on its timeout and when magpie exits

The probes that ask an agent's CLI something (claude auth status,
cursor-agent about/models/status, the --version checks) ran through
proc.CommandContext, which on its timeout kills only the command itself.
A CLI that is a script (a version manager's wrapper, a shim) left what it
had started running, adopted by init; and a probe still asking when magpie
exited (a one-shot command, an update, a restart) was left whole, with
nothing to end it. With Omarchy's mise wrappers stuck in their exec loop
(omacom/omarchy#7234) that was five processes holding about half a core
each for up to six hours.

proc.ProbeContext gives such a probe a session of its own on Unix, as
StartTree does, and its context ending kills the whole group. Probes still
running are listed, and proc.EndProbes kills them when magpie exits (after
run in main) or puts a new version in its place (update.Reexec). On
Windows a probe is killed alone, as before, a job being joined only once
the command has started.

TestProbeTimeoutEndsWhatItStarted and TestEndProbes fail without the
change.

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

* fix(proc): end probes on a terminal's signals and on a Mac's quit

Review of the first change: a probe leading its own session no longer
gets the Ctrl+C, hang-up or SIGTERM sent to magpie's terminal or service,
and magpie ending on the signal never reached EndProbes, so the probe was
left to init where before it died with magpie. main now catches SIGINT,
SIGTERM and SIGHUP, ends the probes and exits as the signal would have
(128+n). A signal magpie was started ignoring (nohup, a background job)
stays ignored. The account sign-in and plugin commands, which end
themselves on Ctrl+C, go through interruptContext and are left to do so.

On a Mac the desktop app's quit ([NSApp terminate:], also the restart to
update) exits without returning to main, so OnShutdown ends the probes.

TestProbeTimeoutEndsWhatItStarted ended its probe on a 500ms timeout,
which on a Mac could come before the script had written its child's pid
(6 of 10 runs failed in review). It now ends the context once that pid is
read, the same Cancel path.

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

* fix: let the app and the TUI quit on a signal their own way

Review of the signal handling: endProbesOnSignal exited ahead of Wails
and bubbletea, which quit on SIGINT and SIGTERM themselves. `magpie tray`
exited 143 without OnShutdown (no downloaded update installed, and
sessions.Saved skipped in main), and `magpie tui` left the terminal on its
alternate screen with the cursor hidden. The app's and the TUI's entry
points now hand the signals back (ownSignals): the app's OnShutdown ends
the probes, and the TUI comes back to main, which ends them too. A
one-shot command keeps the handler.

TestOwnSignalsHandsThemBack fails without the change (magpie exited 143
before the command could quit its own way); TestSignalExits keeps the
one-shot behavior.

childPid waited 3s for a script just written to start its child, which a
busy Mac can exceed (2 of 3 full runs failed in review). It waits 30s now
and, should the probe end first, says the probe's error rather than "the
script started nothing".

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

* fix: keep SIGHUP for the app and the TUI, and die of the signal

ownSignals handed SIGHUP back along with SIGINT and SIGTERM, but neither
Wails nor bubbletea handles it: the terminal closing under the TUI or an
app started from a shell ended magpie on the spot, and its probes, in a
session of their own, out of the hang-up's reach, kept running (before
this PR they were in the terminal's group and ended with it). SIGHUP now
stays with endProbesOnSignal. A tray under a pty whose terminal closes
left 2 processes before, none now.

The handler asked whether a command was ending itself on Ctrl+C only
after EndProbes, which can take up to a second; a sign-in done in the
meantime got magpie ended in the middle of its own cleanup. It is asked
first.

It exited 128+n, which a shell takes for a child that handled the signal:
a loop running magpie went on to the next run on Ctrl+C. It now dies of
the signal (handling reset, the signal sent again); Windows, which can't
send one, still exits with the code.

agentProbe and agentCommand share withProxy; the Windows probeGroup
comment no longer says EndProbes ends what a shim started.

TestOwnSignalsKeepsHangUp and TestInterruptWhileEndingProbes fail without
the change.

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

* fix: hand signals to the TUI and the app only once they listen

Review: runTUI handed SIGINT and SIGTERM back before tui.Run, whose
newModel asks agents' CLIs (claude auth status, up to 5s) before
bubbletea listens for signals. Ctrl+C, SIGTERM or SIGINT while it loaded
ended magpie by Go's default and left the probe and its child running.
tui.Run now takes ready, called once the model is made, and runTUI passes
ownSignals.

The app had the same window: runGUI handed them back before gui.Run,
which starts the backend (probes included) before Wails' Run starts
listening. ownSignals is now gui.Started, called on ApplicationStarted,
which Wails emits after its signal handler is set up. A tray sent SIGTERM
0.05s after starting, a probe under way, left 2 processes before and
none now; one sent it once started still quits through OnShutdown
(exit 0).

TestTUIKeepsSignalsWhileLoading fails without the change.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:42:48 +08:00
yetone c3197ac217 plugins: magpie keeps the Bun plugins run on up to date by itself (the user: 我们能否自动去管理一个bun的版本?去自动的下载并维护这个bun的版本到最新版) — plugins ran on the one Bun magpie pinned, or on whatever $MAGPIE_BUN pointed at. Every six hours (first two minutes after start), while there are plugins, magpie now asks GitHub for Bun's newest release and takes it once it has been out two days: downloaded, its SHA-256 checked against Bun's SHASUMS, tried (it says its version, and runs a script with AsyncLocalStorage, fetch and fs), then switched to, the host restarted gracefully so a streaming reply finishes on the old one. BunVersion is the floor; the Bun before stays on disk, and a new one whose try fails, or that the host won't start on, is set aside for the one before and never taken again (bun/state.json). $MAGPIE_BUN is left alone. Plugins' page and magpie plugins show the version in use. TestCheckBunTakesASettledRelease, TestCheckBunWaitsAndKeepsTheFloor, TestCheckBunSetsAsideOneThatFailsItsTry, TestTryBunOnRealBun and TestHostFallsBackWhenTheNewBunDies pass on macOS, Linux and Windows; a real run from 1.3.14 to GitHub's 1.4.2 started the host on all three. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFetchNewLeavesListedPlugins failed once under load, passes alone). 2026-10-01 19:33:04 +08:00
yetone 4ba51bb627 plugin: a plugin names its own icon and the key field's title and hint (Lemon on Discord) — a plugin's provider took only the market's icon, and an api method's key field always read "<name> API key" with no hint. auth.icon (or package.json "magpie": {"icon"}) as an https URL, fetched once through the public-only icon path, or a data:image URI now comes before the market icon; an api method's label titles the key field as in OpenCode, and a new placeholder hints it, in the window and in magpie plugin login. README has a For plugin authors section. TestPluginOwnIcon, TestPluginOwnIconAndKeyHint and plugin-key-hint.test.cjs (Chromium, WebKit, en/zh) fail without the change. 2026-10-01 11:42:45 +08:00
yetone cc6fc56b3b cli: magpie plugin list says a built-in runs the plugin's provider rather than "not signed in" (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — after a move back the plugin stays when the user had installed it, and its rows read "not signed in · magpie plugin login kiro", as if broken; they now say "runs on magpie's built-in (2 accounts) · magpie plugin move kiro". go build -tags nogui passes. 2026-10-01 11:15:25 +08:00
yetone f9e5c8dfe6 cli: magpie --help lists plugin move|migrate and move-back|unmigrate (记得把整套迁移逻辑做得丝滑优雅精致优美简单方便易用) — the commands existed but only magpie plugin help named them, and without their other names. go build -tags nogui passes. 2026-10-01 11:15:24 +08:00
yetone 2ca1373db5 plugins: removing or switching off the plugin a built-in is moved onto moves the built-in back first (插件版…保持一模一样) — the provider went with the plugin, its accounts out of sight in plugin-auth.json; now its accounts go back to the built-in, then the plugin is removed or switched off, in the app and with magpie plugin rm|off. Installed marks such a plugin ("Serves Zed", a dot, no stripe) and asks in its row, not a dialog, naming what goes back; only a yes sends it. TestReleasePlugin and plugin-release.test.cjs (Chromium, WebKit, en/zh) fail without the change; plugin-market, click-scroll, old-webkit pass. 2026-10-01 04:04:23 +08:00
yetone 3bec6c0e40 providers: a built-in subscription moves onto its community plugin and back, its accounts with it (the user: 慢慢下掉内置的那些 Subscription Provider…不要让用户一升级完之后,突然发现他的所有服务都不可用了) — eleven built-ins (Zed, Qoder, Factory, MiMo, Devin, Grok, Command Code Plan, Cursor, Kiro, WorkBuddy, ZCode) each get a mover to their @magpie-community package. A move installs the plugin (updated when older than the mover needs), hands it every account in the plugin's shape, checks the plugin lists models for them, and only then lets the plugin serve the same provider id, so agents' model ids don't change; any failure rolls back and the built-in keeps serving. The built-in's accounts are kept as a backup, and "Run built-in again" (magpie plugin move-back <id>) takes the plugin's current sign-ins back, never copying a rotating token: they are taken out of the plugin in one step, and an account the built-in renewed meanwhile keeps its newer token. An account only the plugin can hold (Cursor's browser sign-in) stays with it. The provider editor shows "Runs on" with the button, a failed move's reason, and the CLI has magpie plugin move / move-back. Automatic moves (Retiring) are wired in but the list stays empty until the packages are on npm. Checked with real accounts in a sandbox HOME: Command Code Plan and Devin moved, answered a chat through the gateway on the plugin, moved back and answered again on the built-in. TestMove*, TestPutBackKeepsNewer, the movers' round-trip tests and plugin-move.test.cjs (Chromium, WebKit, en/zh) are new. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit pass. 2026-09-30 21:21:58 +08:00
yetone b63a0f1fd5 plugins: a plugin's provider takes more than one account, as a built-in subscription does (the user: 插件当然要支持多账号了) — a plugin kept one sign-in per provider, and signing in again replaced it. Now each sign-in goes beside the others: plugin-auth.json keeps the first under the provider's id, as OpenCode does (so what is signed in already stays the first account), the rest under id#slot; logins.json lists them as it lists the built-ins' accounts, so the editor shows them all, one first, the others ticked on behind it or off, any made first, any removed (which signs it out). The host runs every request in its account's scope, so a plugin's getAuth, client.auth.set (a refreshed token), loader and provider.models see that account alone; a sign-in to an account already there (the same id, else the same secret) replaces that one's rather than listing it twice. The gateway spreads and fails over across the accounts on, as for the built-ins. Accounts with no id are told apart by the end of their key ("API key …abcd"), else by their slot. Also on the Devin page: the header shows the plugin's icon, and Cognition's opencode-devin is off the market (its gateway answers 404 model_not_found for models and chat). TestPluginAccounts (plugin: per-account requests and refreshes, dedupe, sign-out; provider: first, on/off, switch, forget, labels), TestPluginProvider (gateway: a 429 on the first account goes on the second) and plugin-signin.test.cjs (add a second account in the editor, make it first; fails without the change) are new. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-signin, plugin-market, old-webkit, click-scroll pass. 2026-09-30 19:58:22 +08:00
yetone a31f97b30d plugin: OpenCode's provider plugins sign in to subscriptions in magpie — an npm package OpenCode users install for a plan (its auth hook: loader, methods, oauth authorize/callback, api keys, chat.headers) runs in a Bun host magpie downloads the first time a plugin needs it, speaking JSON-RPC over stdio; its sign-ins are kept in plugin-auth.json (0600). A provider a plugin signs in to is a provider like the others: its models the plugin lists, each spoken to on the API its AI SDK package speaks (anthropic → Messages, openai/azure → Responses, google → Gemini, Copilot's GPT-5+ → Responses, else chat completions), its requests carried by the plugin's fetch to the base URL its loader gives; an id magpie already has is <id>-plugin. CLI: magpie plugin [list|add|rm|update|on|off|login|logout]. App: Settings → Plugins adds, updates, switches off and removes them and says why one didn't load; Add provider → From plugins signs in with the plugin's methods, its questions (picks and checked text), the browser and a pasted code or a key, followed as the built-in sign-ins are; removing the account signs out. The built-in subscriptions stay. TestFakePlugin, TestPluginProvider (chat, Anthropic and Gemini models through the gateway) and TestPluginSignIn (a wrong code fails, the right one signs in, removing signs out, a key) run a fake plugin on Bun; plugin-signin.test.cjs (Chromium, WebKit, en/zh) covers the add sheet and Settings. signin-callback, add-sheet, zed-sub, account-proxy, own-signin-remove, routing-signin, click-scroll GUI tests pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked, known flakes under load, pass alone). Not yet tried with a real plugin and account. 2026-09-30 16:44:32 +08:00