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.
* 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.
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.
* 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>