In WSL, with only magpie tui and Cursor's subscription, Claude Code said ECONNREFUSED. The terminal set agents to the gateway but never served it, and nothing listened at 127.0.0.1:3425 unless the app, magpie web or magpie serve ran.
magpie tui now serves the gateway in its own process while it is open, as the app does, whenever no magpie answers at its address. One that does (the app, magpie web, magpie serve) keeps it. The terminal looks every 15s and takes the gateway up once that one is gone.
The status line says "● gateway <url> · served while magpie tui is open" when it has nothing else to say, or "✗ gateway not running: <why>" when the port couldn't be had. On quitting after serving, it says the gateway stopped with it and what serves it otherwise. The usage text and docs/reference.md say so too.
Tested in a sandbox HOME on port 3486 with a real DeepSeek key: magpie tui alone answered /v1/models; Claude Code (sandbox HOME) set to deepseek/deepseek-flash ran a Bash tool turn through it; the status line showed the gateway; q printed the note and freed the port.
TestTUIServesTheGatewayWhenNoneDoes (serves, status, lets go at quit) and TestTUILeavesTheGatewayToAnotherAndTakesItUp (leaves another magpie's gateway alone, takes it up once it is gone) are new and pass 3x under -race; they don't build without the change.
go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
* Add German translation and harden paths against unset env vars
Add a German locale to the GUI, tray menu, usage alerts and website home page, and refuse to start when the home folder is unset or relative so nothing is written under the working folder; the pi plugin no longer adopts the user's PI_CODING_AGENT_DIR, and an inherited APPIMAGE no longer selects portable mode. Scripts refuse to run from the wrong folder or empty a shared out folder, and a new internal/testenv sandboxes every test package from the user's real files, which also fixes the Claude picker and Termux installer tests on Windows.
* Makefile: know magpie's folder by its go.mod, so paths with spaces work
The check compared $(CURDIR) with $(dir $(abspath …)), which make splits on spaces, so a checkout under 'My Projects' or 'John Doe' refused every target. It now reads go.mod relative to the working folder and wants magpie's module there: make -C and a symlinked Makefile work, make -f from another folder still stops before any recipe runs.
* i18n: German for the strings main added since, with a parity test
75 strings main added (group order, patterns, slow-start wait, usage caps, RTK savings …) get their German, and two that main dropped go. gui-de.test.cjs checks, as gui-ja does, that de has every zh string with its placeholders and tags, and that the window reads in German when picked or on a German system.
* appdir: ignore relative folder variables when reading, keep them for children
CheckEnv no longer unsets anything. magpie reads the XDG folders, APPDATA/LOCALAPPDATA and every agent's folder variable through appdir.Getenv/LookupEnv, which pass over a relative value; the environment stays as it is, so the agents magpie starts get the variable as the user set it. That covers what proc.UserPath lends from the login shell too, which is set after main's check and reached the desktop app unchecked. On Windows a path rooted on the current drive (\Users\x, Git Bash's /c/x) counts as usable, and magpie version and help answer without a home, before run makes any folder.
* Sandbox the tests main added since
internal/wslrun gets testenv's TestMain, and four tests main added that set HOME alone set their home with testenv.SetHome, so on Windows they don't read the runner's profile through USERPROFILE.
* agentenv: PI_CONFIG_DIR and OPENCODE_DB take relative names on purpose
omp puts $PI_CONFIG_DIR under the home and OpenCode a relative $OPENCODE_DB in its data folder, so neither depends on the working folder: appdir no longer passes them over.
* i18n: German for the Library's multi-select and System One's decision API
The 19 strings main added since (#791's skill multi-select, the decision API snippets) get their German; the one main replaced (four APIs → five APIs) goes.
* appdir: tidy the import into magpie's group; say what Go makes of /c/x
The appdir import joins the group of magpie's own imports where a file has one. rooted's comment now says that Go takes a Git Bash path like /c/x as \c\x on the current drive, not C:\x.
* aside: magpie picks Aside's model, its thinking level and its four task models (X: junjie — Aside Settings → Models has a Default model row magpie could not touch, and Fast/Standard/Deep/Visual rows it could not reach at all)
Aside reads the same provider list Pi does, so magpie hands it the whole
catalogue in a provider block of its own, carrying the magpie-aside token so
its requests are told apart in usage. What differs is the default: Aside keeps
it as one object in ~/.aside/u/0/settings.json naming the provider, the model,
the thinking level and fast mode together, so the model field reads and writes
two keys where Pi's are two strings, and both go through Aside's own settings
API — the running daemon holds its default and writing the file behind it
leaves the daemon on what it already had, read back the same way with the old
model still named after magpie had written the new one. The API is reached
through the repl, the settings command having no set of its own.
The four task roles are the same shape as the default and are wired the same
way, each a Quiet field following the model, so a role the user has not set is
not a line of its own and follows the default. A role is written whole:
thinkingLevel is required by Aside's schema and a role without one is refused,
so a role keeps the level and fast mode it had, a new one takes the default's,
and where neither is set it takes the level Aside gives its own roles. Taking
magpie out puts every role back, the user's own model where there was one and
nothing at all where magpie was the only thing that set it, and a role the user
moved onto a model of their own is left alone.
Each change checks the value that came back: Aside answers a set with the
settings as they now are and drops what it will not take without a word — a
role naming a model it has not resolved yet is the one that happens — so
without the check a change that went nowhere read as one that landed. Caught,
it is written to the file, which is what Aside reads at its next start, and
the Agents page says so.
The account magpie wires is 0: the folder is ~/.aside/u/0 while the CLI names
it u0, and TestMain stands in for the binary so a test can never change the
real Aside's default. A models.json carrying a magpie provider of someone
else's is refused rather than written over, and one that will not parse is the
user's to fix.
With the owner's real Aside under a sandbox HOME: a pick reached the gateway
and was attributed to a client named Aside, the default put the user's own
minimax model back and took the magpie provider out, and a role set through
magpie was read back by the running daemon the moment it had a magpie model to
resolve. Not tried: a change written to the file taking hold at Aside's next
start, which needs a restart. go vet, builds (darwin, GOOS=linux,
GOOS=windows) and go test -tags nogui -count=1 ./... pass (TestBunThroughBridge
fails, reproduced on a pristine worktree of the parent; internal/plugin passes
alone and times out only under the full run's load).
* aside: the row appears for an Aside that is installed and not yet used (the CI preview of #789 could not find .row.agent[data-id="aside"])
Detected() took ~/.aside/u/0, the account folder, and that only exists once
someone has signed in. The binary was the other look and the CLI installer
links it into ~/.local/bin, which a magpie launched from Finder has no PATH
entry for, so an Aside that was installed and never used showed no row at all —
the preview installed one, drove the UI and could not find it in either the
installed list or the row selector. The installer writes ~/.aside/cli and the
daemon writes the rest, so ~/.aside is the folder that is there from the
install, and it is the one Detected() should ask about. Path stays the
account's settings.json, which is the file magpie edits and the only one
worth reading a model out of.
TestAsideIsDetectedBeforeAnyAccount fails on the old Dir: it takes PATH away
so the binary cannot be found either, makes the installer's folder and no
account, and expects the row.
* aside: a wedged repl gives up, and the model comes off through Aside (yetone on #789)
Three things about the way out of an agent and into one, all found on a real
Aside.
The repl had no timeout. proc.Command(...).Output() waits as long as the
command does, so a daemon that has wedged or one waiting on input leaves a pick
in the Agents page or the CLI hanging on it, and switching off asks up to five
times over — the default and four roles. proc.CommandContext with ten seconds:
Aside answers in milliseconds, and a run that does not is written to the file
instead, which is what Aside reads at its next start, with the notice that says
so. TestAsideGivesUpOnAReplThatHangs runs the real command against an aside
that never returns: it passes in the timeout, and without CommandContext it
hangs until the test's own timeout kills it.
Switching off with no model of the user's behind magpie's wrote settings.json
directly. By then the provider block was gone, so the running daemon went on
using magpie/… with no way to reach it, and asideStale was never set, so
nothing said the change needed a restart. It goes through asideApply like
every other change, with the value that came back checked.
The provider block now goes last, after the default and the four roles are put
back. A step that fails on the way out otherwise leaves the settings naming a
provider that is no longer there. TestAsideDefaultTakesTheProviderOutLast
watches the block from inside the settings step and fails on the old order.
The icon is Aside's own brand-embossed-light.svg with the embossing's
transparency taken off, so the mask is solid and takes the row's text colour
like every other agent. As it was, the white fills at 8–20% opacity the dark
and light brand files are built with left the mark almost invisible on a light
row, and it vanished entirely in dark mode. Same paths, same shape, now solid;
checked in the app next to Pi's.
Tested against Aside 1.0.928.1 (daemon 1.26.928, CLI 1.26.916.1741) on
darwin/arm64. go vet, builds (darwin, GOOS=linux, GOOS=windows) and
go test -tags nogui -count=1 ./internal/agent pass (408).
* aside: the model Aside draws with is magpie's to set too (X: Aside Settings → Images had no row for it)
Aside keeps a fifth model setting beside the default and the four task roles:
imageGenerationModel, the provider and the model it draws pictures with and
nothing else — no thinking level, no fast mode, since neither means anything for
a picture — and no key at all on an account that has never picked one.
It is a magpie field like the roles: Quiet, so it is not a line of its own on an
account that has none, and taking the model after while it is unset. A model of
magpie's needs the provider block to reach it through, and what it named before
is remembered the same way a role's is, so `magpie aside model default` puts the
user's own picture model back and takes a picture model of magpie's off rather
than leaving it naming a provider that is no longer there.
Not wired: customModels, which is the user's own model definitions with their own
credentials, and asideModelCatalog, a list of model ids to pin or hide. Neither
is a model magpie picked, so neither is a setting of magpie's to own.
Aside 1.0.928.1, daemon 1.26.928. go vet, the agent/gui/profile tests, the three
CLI cross-builds and the CLI verification under a temp HOME all pass.
- The opened row follows the boards: what to do in the agent, running copies with the old list, Claude Code's tiers (each with its own effort beside its model again, #536), the model list by whose models they are with what's hidden, the model new sessions start on, what connecting costs (Gemini's Google sign-in), and the files put back on disconnect, shown line by line.
- Disconnecting asks first and shows the lines it puts back. Secrets are masked, and a config of tens of thousands of lines diffs in a fraction of a second.
- Connecting prefers the agent's own subscription when magpie serves it (Claude, Codex).
- The dry run goes through proc.
- Every model hidden still keeps the row connected, with the way back to the list (#356).
- Routing groups are named in Chinese.
- Phone widths: the pickers and squares wrap inside the opened row, and the name stays beside its logo.
GUI tests moved to the new rows: connected fixtures carry refs and open the row from its link. tier-effort and mobile-web each found a real gap and now guard the fix.
Tried in a sandbox HOME with the owner's real accounts:
- Claude Code: connected, a request answered, and disconnect restored the settings exactly.
- OpenCode: a request answered.
- Gemini CLI: connected.
- Codex: connected.
- Model list picks were saved.
go vet, builds (darwin, GOOS=windows, GOOS=linux -tags nogui) and go test -tags nogui ./... under a temp HOME pass. The agent GUI tests pass except cli-update's tight-row case, menu-scroll and old-webkit, which fail on main too.
* agent: configure Zed models and gateway credentials automatically
Add Zed to the CLI and Agents page with an OpenAI-compatible magpie provider, a synced model catalog and default model selection. Preserve existing JSONC settings and restore the previous provider and default model on reset.
Store the gateway credential in Secret Service, macOS Keychain or Windows Credential Manager so Zed needs no manual API key entry. Cover config preservation, catalog sync, model limits, Flatpak paths and credential failures.
Verified with the full nogui test suite, go vet, macOS and Windows cross compilation, and a local Zed conversation through the gateway.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd03-00d7-75fd-956f-c505e07ba60d
Co-authored-by: Amp <amp@ampcode.com>
* agent: store Zed credentials directly through Secret Service D-Bus
Replace the Linux and FreeBSD secret-tool subprocess with the existing godbus client. Open a session, create the default collection when needed, unlock it and replace the matching Zed credential. Handle collection, unlock and item prompts, dismissal and timeout.
Verify the protocol on isolated D-Bus instances with no helper executables on PATH, including immediate completion signals and failed writes. The full nogui suite, targeted vet and race checks, macOS and Windows cross compilation pass; local credential readback confirms replacement without duplication.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd03-00d7-75fd-956f-c505e07ba60d
Co-authored-by: Amp <amp@ampcode.com>
* docs: remove the separate Zed configuration paragraph
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd03-00d7-75fd-956f-c505e07ba60d
Co-authored-by: Amp <amp@ampcode.com>
* agent: fix Zed reset, keychain access and translated notices
Avoid restoring magpie’s own provider after a model change in Zed, and cover reselection followed by reset. Remove the unsupported Flatpak config path.
Authorize the installed Zed app and security command to access the macOS gateway credential. Translate Zed’s notice and apply translation when selecting or reapplying an agent model.
Verified with the full nogui test suite, go vet, Zed race tests, cross compilation and browser checks. macOS keychain behavior still needs runtime verification on a Mac.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd70-98a1-737f-93bd-77e181f904c1
Co-authored-by: XYenon <register@xyenon.bid>
* agent: preserve Zed model picks when resetting magpie
Restore the stashed default model only while Zed still selects magpie. Preserve a model chosen in Zed itself and discard the stale model stash, while restoring the previous provider.
Find Zed Preview in Applications folders and Spotlight without requiring a CLI, and use any in the isolated D-Bus tests.
Verified the reset regression fails before the fix and passes afterward. The full nogui suite, go vet, Zed race tests and macOS/Windows test cross compilation pass.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd70-98a1-737f-93bd-77e181f904c1
Co-authored-by: XYenon <register@xyenon.bid>
* agent: retain Zed provider backup when reselecting magpie
When Zed’s provider already points at the gateway, update only the model stash and preserve the original provider backup. Reset can then restore the user’s provider after a native model pick followed by another magpie pick.
Add a regression covering the full sequence and checking the restored provider, model catalog, custom headers and latest native model. Confirm it fails before the fix and passes afterward; all Zed tests, the full nogui suite and go vet pass.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd70-98a1-737f-93bd-77e181f904c1
Co-authored-by: XYenon <register@xyenon.bid>
---------
Co-authored-by: Amp <amp@ampcode.com>
* routing: Least used puts first the account with the most of its week left per hour until it renews — it ranked by the share used alone, so with two subscriptions, one 80% through its week and renewing in an hour, the other fresh and renewing in five days, every new conversation went to the fresh one and the first's remaining 20% was lost at its reset. Allowance.Pace: over the windows that count the model and run a day or longer (the five hours are a rate cap, not a budget; Kiro's month counts), (100 − used) / hours to reset — a week not started or not known runs its whole span from now, one whose reset has passed is full again, of several (Opus's own beside the general) the tightest; an account whose vendor tells the five hours alone (Claude's own usage command) counts as a week not started with their share used, so those go by what they have used, as they did. weigh's LeastUsed case: one at 98% or more last whatever its week, by how far; then pace, highest first, in bands alike within a tenth drawn from the top before sorting (a pairwise tenth isn't an order: three paces a twelfth apart go round in a circle); within a band the tokens sent lately, then their order, for the cache; an account not known counts as a fresh week (before, "used 0" put it first), a key follows the subscriptions. Renewing the same hour, pace is most-left-first, as before. Smart untouched. The trace carries pace; the Routing tab's story, the provider and group editors' hints and the docs' routing table (en, zh) say what the option goes by now. TestAllowancePace, TestLeastUsedPace (fails on the old weigh: "imminent renewal first: ab"). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gui/ pass; ./internal/gateway/ passes but TestPluginModelListThroughAccountProxy, which fails on main too (a concurrent Body.Read panic in its fixture).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* routing: weekly pace is its own option, "pace"; Least used keeps its meaning (yetone, #517 review) — a user who had picked Least used was being switched to another strategy under the same name. Weekly pace: the account with the most of its week left per hour until it renews goes first, the one with the most to lose at its reset; Least used is most-left-first again, Smart untouched; the CLI takes routing=pace (weekly-pace), the TUI cycles to it, the group and provider editors, the Routing tab, README and the docs' routing tables (en, zh) list it. Review points: the tiers are Smart's — one at 90% or more of any window that counts the model, the five hours too, waits behind the healthy ones, by share, one all but used up behind those (an account 95% through its five hours with a healthy week ranked ahead of a fresh one until 98%, and the allowance is read about once a minute, so it was sent into 429s); among the healthy, one not known that tells what it has left as it answers (a Claude account) goes first while it isn't known, as in Smart, else it would never answer and never be known; a window that says no span (a plugin may not) is a budget for as long as its reset says, else a week, where it fell among the five hours and made the week not started; the hours until a reset are never fewer than one, or a window thirty seconds from renewing paced at 6000; the trace carries due, the reset of the window the pace went by, and the Routing tab's story says the week's share left over the hours until that, not the first reset it finds; Routing's comment in provider.go and README's routing= line name the option. The bands of pace alike are drawn among the healthy known accounts alone: one running low would otherwise set a band's top and part two alike behind it. TestWeeklyPace (the tiers and their 90% edge, the probe and its end, the bands drawn without the low, keys by tokens and after subscriptions, the trace's pace and due, Least used unchanged), TestAllowancePace (due, the span-less budget at 48 h and at 90 min, a day the shortest budget, the hour floor, normalize keeps pace). go vet ./internal/... ., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gui/ ./internal/tui/ . pass, the two new tests 25 times over; ./internal/gateway/ passes but TestPluginModelListThroughAccountProxy, which fails on main too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
dsh resolves a session's model from the patch list before it contacts the gateway, so a
route that lists a model magpie no longer gives dsh fails every session, with nothing
reaching magpie to show for it.
While magpie serves the gateway it now writes the fields it manages on existing dsh
magpie routes again every 30s where they are not what magpie would write now, from
gateway.WhileServing next to the WebDAV sync. A route the user removed stays removed,
agent-default-model stays the user's choice, and an empty catalog writes nothing.
* 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>
* feat(gui): show and filter usage for each API key
* feat(gateway): issue per-user caller keys and attribute usage
* feat(gui): manage gateway users and filter caller-key usage
* test: verify caller-key failover and document user management
* refactor(gateway): use named caller keys without user accounts
* feat(gui): manage named caller keys with existing account styles
* fix: remove user breakdown from CLI and leftover gateway styling
* fix(usage): drop stale user field from request response
* feat(gateway): authenticate LAN calls with named caller keys
* fix(gateway): preserve LAN key rotation in the named caller store
* fix(gui): retain native LAN key copy and rotation controls
* fix(gui): inherit the global theme for API key inputs
* fix(usage): show client API keys using the existing terminology
* fix(gateway): rotate named keys without changing usage identity
* fix(gui): unify gateway keys and selectable connection examples
Keep sharing controls in Settings and manage credentials only in Gateway. Refresh the named-key list on navigation, ignore stale reads after key actions, and generate all API examples from the selected address and enabled key.
* fix(gui): keep caller key names literal in connection menus
* fix(access): use Magpie as default key and drop user migration
* refactor(gui): name caller key source and tests consistently
* refactor(usage): split provider-key accounting out of caller-key PR
* fix(gateway): preserve LAN compatibility and add headless key management
* fix(gui): clarify gateway keys and confirm destructive actions
* test(gateway): guard caller attribution under 120 concurrent requests
* fix(gateway): keep open access, tolerate migration errors and revoke legacy keys
* fix(backup): preserve named gateway keys and honor no-keys restores
* fix(gui): keep local API-key semantics and disambiguate shared credentials
* docs(gateway): keep review scope focused and append browser test documentation
* fix(backup): retain existing default keys when old backups lack their store
* fix(gateway): attribute video creation usage to the caller key
* fix(access): explain default key revocation blocked by read-only settings
* test(usage): preserve provider and caller identities after rebase
* test(gui): reconcile caller key fixtures with merged main
* docs(usage): clarify caller coverage and independent provider accounting
* fix(gui): initialize fast members for model-shortcut groups
Latest main introduced fast-member arrays but omitted the model shortcut's draft. The existing unlisted-models browser regression fails unchanged on main. Initialize fast to an empty list and assert a standard-speed group is saved, without relaxing existing assertions.
* fix(access): retain legacy LAN access when the key store cannot be created
Allow only the configured legacy credential before either migration marker exists. Refuse revoked mirrors, disabled matches and unreadable stores; keep the fallback's caller identity stable when migration later succeeds. Cover read-only configuration directories, remote credential transports and revocation.
* fix(backup): save settings before replacing gateway credentials
Leave the key store untouched when settings validation or writes fail. Restore the previous settings association when the key store refuses its write, and report a rollback failure separately. Cover invalid settings and read-only settings/store failures.
* fix(gui): reset missing caller filters and focus key controls without scrolling
* test(provider): synchronize Zhipu quota fixture observations
* gateway: draw through a Grok subscription (Grok Imagine)
A Grok subscription (SuperGrok, X Premium+) could not draw: Drawers() returned
nothing for it and draw() refused with "Grok (SuperGrok) can't draw: magpie
draws only through an OpenAI-compatible API, and Grok (SuperGrok) has none".
Grok Build's own image_gen / image_edit tools call the backend's Imagine API
at <cli-chat-proxy.grok.com/v1>/images/generations and /images/edits with the
CLI's sign-in, which magpie already uses for chat.
- drawsGrok / grokDrawers: a Grok account draws with grok-imagine-image and
grok-imagine-image-quality, like a ChatGPT account does with GPT Image
- the Imagine API takes an aspect ratio, not a size or quality: size
WIDTHxHEIGHT is matched to the nearest ratio it accepts (aspectAmong,
aspectOf is unchanged); the image is asked for as base64
- an edit is JSON with the images as data URLs
- a plan's images cost nothing extra, so AutoDrawer ranks Grok with ChatGPT
(ChatGPT still wins a tie on release date)
- provider.grokBase is exported as GrokBase, like CodexBase, for the test
Tested against a real SuperGrok account through the gateway: generation,
16:9, the quality model and an edit. Video is not covered: the Grok CLI's
video tools need zero data retention off or a storage bucket.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* gateway: film through a Grok subscription, and a generate_video tool for agents
A Grok account that draws (c6578bc) can make videos too: Grok Imagine's
video endpoints sit behind the same sign-in.
The gateway speaks OpenAI's Videos API, so its SDK works as it is:
POST /v1/videos, GET /v1/videos/{id}, GET /v1/videos/{id}/content. A
request is taken on at once and polled; the id carries the provider and
the vendor's request id, so nothing is kept in the gateway and a restart
loses no video. Status is mapped (pending at 0% -> queued, pending ->
in_progress, done -> completed, failed/expired -> failed, with why).
seconds (1-15, the vendor's limit, which it says when exceeded), size
(-> aspect ratio and 480p/720p/1080p by the shorter side), input_reference
for image-to-video (URL, data URL, {image_url}, or a multipart file) and
reference_images are taken; a model that isn't a Grok video model is
refused by name. /content answers 409 until the video is ready.
`magpie mcp image` lists a second tool, generate_video: it starts the
video, waits (up to 10 minutes), saves the mp4 into the project
(generated-videos/ unless path says where, never over a file) and says
where, so an agent can animate an image it just drew by passing its path.
Checked against a real SuperGrok account: text-to-video, image-to-video
(input_reference), reference_images, both video models, the OpenAI
Python SDK (videos.create/retrieve/download_content), the MCP tool end to
end with two videos at once, and the error paths. Unit tests use a fake
Grok upstream.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* gateway: videos follow the Settings' off switch, and an id can't change the vendor's path
Review of the video change:
- Image generation "off" turns off the video a request that names no model
gets (videomaker, beside drawer), so generate_video says it is off too.
- A video id is put in the vendor's URL with the account's token: its parts
must be [A-Za-z0-9_-]+ (parseVideoID) and the vendor id is path-escaped, so
video_grok.x%3Fa=1.1 and %2F answer 404 and ask the vendor nothing.
- The id carries a short mark of the account that started it; after a switch
the video says it was started with another account instead of the vendor's
not-found.
- generate_video waits up to ten minutes, and Codex gives a tool a minute:
magpie-image's Codex entry gets tool_timeout_sec (Goose's timeout) of
eleven minutes, the user's own value kept.
- grok-imagine-video stops at 720p (the vendor turns 1080p away for it; 1.5
makes 1080p), so a 1080p size is asked at 720p there.
- A model may be named without its provider (grok-imagine-video).
- "seconds": 6.0 is 6; 6.5 is still refused.
- completed_at is left out: the vendor doesn't say when a video finished,
and a time made up at each poll changed each time.
Checked against a real SuperGrok account: bare model name, seconds 2.0,
1080p on the base model asked at 720p (the mp4 is 1280x720), 1080p accepted
on 1.5, both malformed ids, no completed_at.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* gateway: TestVideoModelByItsBareName asks its two models in a fixed order
It checked the body of the second request the upstream got, but asked the
two models by ranging over a map, so it failed about one run in seven
(Ubuntu CI). Ask them from a slice.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* library: an installed magpie-image gets the longer tool timeout on the next sync
magpie-image already ships, so many users have it in Codex and Goose as it was
written before: no tool_timeout_sec in Codex, and the timeout: 300 every Goose
server was given. syncMCP skipped an entry whose command, args and env were
unchanged, so an upgrade left Codex at its one-minute default, and Goose kept
300 as if the user had chosen it.
- mcpFile.behind says an entry lacks the timeout magpie gives the server now:
Codex's tool_timeout_sec is missing, or Goose's timeout is still exactly the
300 magpie wrote. syncMCP rewrites such an entry; merged() doesn't keep that
300 as the user's. A value the user chose (any other number) is left alone.
- Test: an entry as main wrote it is given 660 in both, one the user set stays,
and a second sync changes nothing. It fails without the change ("changed []").
Also, from the review:
- seconds is validated once in readFilming: a decimal number that is whole and
at least 1 (hex, inf, nan, underscores, negatives and 0 are refused), and the
object echoes 6 for 6.0.
- imagemcp: Run's doc comment is above Run again.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* agents: omp's subagents and small role pick their own models, through magpie too
* agents: omp keeps a role's thinking level and the model it had before magpie, and offers the slow and designer roles
A role set to "anthropic/claude-opus-5:max" and switched to a magpie model lost its ":max", and resetting it deleted the key rather than putting the user's model back. The level now goes with the new model (omp clamps one the model lacks, never fails), the model the role had before magpie is stashed and restored, and the reviewer's @slow and the designer's @designer roles are fields beside subagents and small.
* gui: omp's slow and designer roles follow the model, as its subagents do
Unset, both roles take the session's model, yet the picker offered them omp's Default and no typed model. They are now squares beside subagents, with Same as model first and a model typed in taken as it is.
* agents: omp's thinking level no longer hides a role's model from magpie, and the model alone clears it — magpie omp magpie/x/y:high is taken, a role moved back to omp's own is drift, a list of models comes back on reset, and designer (gone in omp 18.1.5) goes
A role on "magpie/deepseek/pro:max" matched the picker only through the
option offered for its level beside the model, and only for the value in
the file now. So the CLI refused a model typed at a level, a recorded
value at a level the current one didn't share wasn't known for magpie's
(no "replaced" drift), and a level could never be taken away: a model
picked without one took the old one on.
An agent may now say what it keeps after the model (Agent.SplitSuffix;
omp's is its level). Drift, Reseat, RenameProvider, Spell and the CLI's
field-for-value match the model and put the suffix back after the one
they move to; agents without it are unchanged. The option at the level
stays: it keeps the level. Another model keeps it too; the same model
picked alone clears it.
A role omp falls back through (a YAML list, or the comma list omp joins
it into, settings.ts modelRoleValueFromUnknown) read as nothing, so it
wasn't stashed and a reset deleted it. It reads as that comma list now,
is stashed and comes back; a list has no level of its own.
omp 18.1.5 removed the designer role (CHANGELOG), so its field goes. The
smol role, like task and slow, gives its agents the session's model when
unset; its field is labelled smol, as omp names it, so the GUI can show it
as the others without touching other agents' "small".
* gui: omp's smol role follows the model as its subagents and slow role do, and designer goes
smol unset gives omp's scout and sonic the session's model, so it is a
square after the pickers with "Same as model" first, as subagents and
slow are (a feather). Its label is smol, not small: OpenCode's, Crush's
and OpenChamber's small model stays a picker. designer, a role omp 18.1.5
removed, leaves FOLLOWS_MODEL, the glyphs and i18n.
* agents: the note on omp's fallback chains says their keys are roles, and from omp 18 models too
ompRefKeys' comment had retry.fallbackChains keyed by model. omp 16.3.5 takes only a role's name there (session/agent-session.ts: "a mapping of role names to selector arrays"); omp 18.4.4 takes a role's name or a model selector (session/retry-fallback-chains.ts). Walking the keys stays: a role's name is no magpie model and is passed over. Follow-up to the review of #340.
* agents: omp reads whether it is on magpie one way, and a role at a level that a fallback chain names too is renamed once in each
Rebased on #340, one scan over omp's config (ompRefs) answers both questions: magpie's provider stays in models.yml while anything under ompRefKeys is on magpie, as #340 has it, and Check looks at the wiring while any role under modelRoles is, as this PR had it; a fallback chain or enabledModels alone keeps the provider without being checked.
A provider renamed moves a role at a thinking level through SplitSuffix first, then RenameRefs the rest, which finds no old name left in the roles: each place moves once, the level kept.
* agents: a fallback chain alone on magpie has omp's wiring checked, and a role set off magpie mends it
Check now reads the same keys dropMagpie does (ompRefKeys: every role, retry.fallbackChains, enabledModels, task.agentModelOverrides): whatever keeps magpie's provider in models.yml has its baseUrl checked, so a chain on magpie whose wiring something else moved no longer fails its fallback silently. Applying again sets the model role, which may be omp's own; a role set off magpie while something else still names it now writes magpie's provider afresh instead of leaving it as it is, so that mends the wiring. Review of #325.
* agents: an omp role that is a list of models stays the user's own, even starting on one of magpie's, and comes back as it was written
A role omp falls back through ([magpie/deepseek/flash, openai/gpt-6], or the same as a comma-separated string) was taken for one of magpie's models because it started with magpie/: magpie taking the role over stashed nothing, and a reset deleted it. Only one model, no comma, is magpie's now: SplitSuffix answers whether a value is one model, and Spell, drift (magpieValue), Reseat, RenameProvider and the CLI's fieldForValue leave a list alone. The stash keeps a YAML list as the YAML it was written in (edit.GetYAMLText), and a reset puts it back so, flow or block; a comma-separated string comes back a string. A provider renamed moves the names inside a list through RenameRefs, once, and in the lists the stash keeps too. Review of #325.
* gui: a test holds omp's subagents, smol and slow roles as squares that follow the model, and OpenCode's small model a picker
omp-roles.test.cjs, in Chromium and WebKit, English and Chinese, API faked: the three roles are squares after the model's picker, unset named "same as model" (「同主模型」) and set naming their model; smol's picker opens on "Same as model" with the model beside it, and a pick sends it and shows it. OpenCode's small model stays a picker. With smol and slow taken out of FOLLOWS_MODEL it fails at "small is a square". Review of #325.
* gateway: a Claude Code run whose subagent ended on StructuredOutput is let go, and runs left waiting on tool calls are bounded (#345) — a workflow's subagents end on StructuredOutput, which the client answers itself, so the bridge's claude -p waited 30 minutes for a result that never came, one process per subagent. Such a run now waits 30s (for a schema error the client sends at once) and ends; of other runs waiting 5 minutes or more on their caller's tools, 16 are kept, the longest waiting let go first (one that then gets its results starts anew with the whole conversation). TestClaudeRunEndsAfterStructuredOutput and TestClaudeParkedRunsAreBounded fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
* plugins: a built-in moved onto its plugin looks and works as it did (你确保插件版的订阅 provider 和其对应的内置的…都保持一模一样了吗?) — moved onto its plugin, a built-in lost pieces: its tile's name, icon, plans and risk note went to the plugin's own, "Add account" still opened the built-in's sign-in (in the app, on the server and in `magpie accounts add`), an agent's own sign-in the built-in still found showed a second usage card of the same account, the account's plan badge went, WorkBuddy's daily check-in stopped, the provider's icon and site were the plugin's, WorkBuddy AI's card was named WorkBuddy, and Devin, which had no card of its own, lost the plugin's. Now a moved built-in keeps its tile and risk sheet and signs in through its plugin; its cards take the built-in's place and name, and it has none of its own; the plan the plugin tells stays with the account; WorkBuddy checks in through the plugin, which signs the call; a card is left out only where its built-in's slot showed it. TestMovedBuiltinQuiet, TestWorkBuddyCheckinThroughPlugin, TestMoveToPlugin (Devin-like card) and TestPluginUsage (plan kept) fail without the change. Command Code and Devin moved and back in a sandbox HOME: one card each, plan, name. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-move, plugin-signin, commandcode-risk, old-webkit pass; click-scroll's WebKit fails the same on origin/main under this load.
* provider: the Providers and Gateway pages no longer wait on `claude --version` (deluxe on Discord: on Omarchy the two pages never loaded) — on Omarchy `claude` is a mise wrapper that installs or updates Claude Code before it answers, which on a slow network takes minutes or never ends; magpie asked it with no limit and with a lock held, so /api/providers (fetching the Claude account's models) and every Claude request waited with it. The CLI is now asked in the background, given a minute, and a caller waits at most 3s before the version it has stands in; a late answer is taken once it comes. Reproduced on an Omarchy VM (/api/providers unanswered after 100s on v0.1.516, 3.7s with the change). TestClaudeVersionDoesNotWaitForAStalledCLI and TestClaudeVersionTakenWhenItComes fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
* plugins: a plugin's account behaves as its built-in's (the user: 插件版的订阅 provider 和内置的…保持一模一样了吗) — a plugin's failed sign-in now says why ("the sign-in failed: the sign-in page expired") instead of a bare failure; a plugin account the provider refuses with 401 is marked lapsed, "<user>'s <provider> sign-in has expired — sign in again", beside the account and on its usage card as a built-in's is, and the mark goes when it answers again or is signed in anew; a Devin or Command Code account taken from the agent's CLI moves to the plugin marked as the CLI's and back as the agent's own, so the one it follows stays the one the CLI is signed in to. TestPluginProvider (lapsed), TestMoveSideState (cli) and TestPluginSignIn (reason) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
* agents: "Hide all" in an agent's model list keeps its line, and an uninstalled Gemini CLI goes from the Agents page (#356, #230) — the line under an agent ("Showing 0 / N models") came only when a picker still listed one of magpie's models, so hiding them all took away the one way back; it now stays while any is hidden. Gemini CLI was taken as present from what it leaves in ~/.gemini (settings.json, .env, installation_id…), which stays after it is uninstalled, so its row stayed, offering to set up a CLI that can't run; it is now its gemini binary. TestAgentModelLineAllHidden and TestGeminiNotAntigravity fail without the change; agent-models-all-hidden.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/agent ./internal/gui ./internal/library ./internal/provider pass; agent-models, click-scroll, old-webkit pass.
* gateway: Codex's Responses Lite tools reach SuperGrok and every other backend, and a group moves on when a member can't read a request (#350) — Codex sends a Lite model's tools as an additional_tools input item, its own functions in a "functions" namespace; magpie relayed the item to xAI, which 422s on it (unknown item type), and translated routes (Cursor, Anthropic, Chat…) dropped it, so the model had no tools. Outside OpenAI the item's tools now go in tools, the functions namespace opened into plain tools and earlier calls un-namespaced; OpenAI and the ChatGPT account still get Codex's own form. A 400/422 over the request's shape (failed to deserialize, unknown item type/parameter…) now moves to the group's next member, each asked once, the member not resting; the Routing page says so. TestAdditionalToolsRelayed, TestAdditionalToolsTranslated and TestGroupFailsOverARequestShape fail without the change; routing-shape.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, routing-wb-refused pass.
---------
Co-authored-by: yetone <yetoneful@gmail.com>
Each visit re-reads the few session files agents appended to since the last
read, which takes milliseconds. But refresh() then saved the whole ~20 MB
~/.cache/magpie/sessions.json before clearing the indexing flag, and that
save took over a second, so /api/sessions/progress said "indexing" past the
0.7s threshold and the hero played every time. The flag now drops as soon as
the parse ends; the index is written from a snapshot (parses are never
changed in place, so a shallow clone is safe) by one background writer that
coalesces saves. Reset and the CLI's exit wait for the write.
Tested: TestSaveBehindTheRead holds the save open and requires List to
return and Indexing to be off meanwhile, then the file to be written (fails
with the synchronous save). Against a copy of the real index (196 sessions)
the flag stayed on at most 22ms per visit. go test -race ./internal/sessions,
go test -tags nogui ./... (TestAddSecondOfPreset flaky, passes alone), go build
-tags nogui (and GOOS=linux, GOOS=windows), go build ./..., and the GUI tests
sessions-overview, old-webkit, menu-scroll, click-scroll pass. Not tried in the
real Wails window.
* cli: magpie webdav — WebDAV sync from the terminal, what Settings › WebDAV sync does, for a machine with no window (WSL, a server over SSH)
magpie webdav shows it; on <address> [user=…] [password=…] [keys=no] [agents=no] [library=no] turns it on and syncs at once, asking for the passphrase (and the password, when a user has none); set k=v… changes what is given and keeps the rest; now, dismiss and off are the page's Sync now, OK and Turn off. Through the same davsync.Configure, Now, Dismiss and Off.
A sync, Off and Dismiss hold a lock the system keeps on sync.lock (flock; LockFileEx on Windows): the gateway's sync every 3 minutes and one from a terminal — or from the app's window while magpie serve has the gateway — would each save sync-state.json over the other's. The system lets it go when the magpie holding it ends, however it ends.
Configure keeps the saved password only for the same server (scheme and host) and user: an address moved to another server, or another user, no longer takes it there — in the app too, where a new address with the password left empty was sent the old one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* davsync, cli: from the review of #127 — the setup read again under the lock, Configure locked too, the password never an argument, one left behind asked for, a wait that runs out said in the status
Now reads the setup again once it has the lock: one that waited while another magpie turned sync off went on with the setup it had read, and saved sync-state.json back after the Off. Configure takes mu and the lock too, after a sync in progress.
magpie webdav takes neither the password nor the passphrase as an argument: password= asks for it, as passphrase= does.
Configure with the saved password left behind — another server or user, the password empty — says to type it and saves nothing, where the Settings page dropped it and the sync failed with 401 under a field that still said saved.
A wait for the lock that runs out is davsync.ErrBusy. Dismiss returns it, and the davsync routes put ErrBusy from a sync in the status they answer with, where the page shows a sync's failures; Save's own errors stay in its form, what was typed in it kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* davsync, cli: the lock before mu, a wait bounded under the page's, one called off not called busy, the password rule in one place, a bare address with = in it
The lock comes before mu everywhere, so a wait for another magpie never holds mu, and Configure, Off and Dismiss wait for it 30 seconds at most, less than the Settings page gives a request: before, a request could hang on mu and then on the lock for minutes.
A wait for the lock called off, as when the page is closed or the gateway stops, is its context's error, not ErrBusy: nothing said another magpie was syncing.
What becomes of the saved password is decided in one place, davsync.SavedPassword, which Configure and magpie webdav both go by, where the two had rules of their own that had come apart: an account with a password and no user name, moved to another server, had its password dropped with nothing asked, and the sync failed with 401. It is asked for now, as for an account with a user.
magpie webdav takes a bare argument with :// in it as the address, = in it or not: https://host/dav?token=… was read as a field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>