Files
Gateway → Gateway keys → Add gateway key has a second field, "Your own key
(optional)"; `magpie gateway-key add <name> --key <value>` (or `--key -`
from stdin) does the same. Left empty, magpie makes a sk-magpie-key- as
before. A value is 8–256 printable ASCII characters with no spaces, can't
start with sk-magpie-, and can't be another key's. The list shows only its
last four characters ("…0042"); rotating it gives a magpie-made key.
From another computer any enabled key was already checked, so an own value
works there as is. From this computer the gateway only identified a request
whose key looked like sk-magpie-…, so an own value would have been let in
unidentified: not counted as the key, nor held to its limit, models or
accounts. managedKey now asks access.Named, which also knows the store's
own values (cached by the file's size and time).
Tested: go test -race . ./internal/access ./internal/gateway ./internal/gui;
-count=20 on the new and LAN guard tests; TestOwnKeyValueIsTheKey fails with
managedKey back on access.Managed. gateway-own-key.test.cjs passes 16/16
(chromium and webkit, en/zh/ja/de, 1000 and 440px), and the gateway-key
suites, i18n-keys, gui-ja and gui-de pass. A built binary in a sandbox HOME
with sharing on: own key from the LAN address 200, a wrong or no key 401,
from 127.0.0.1 200, the old value after rotate 401. GOOS=linux and windows
builds pass.
gui, agent: the models picked for a WSL agent are the ones its picker, its own menu and the gateway list, and a model taken out leaves the picker at once (#927, cjyrainbow)
gateway, settings, gui: web pages the user lists in Settings › Network may call the gateway from a browser, each request with a gateway key (SkyAerope, #1051)
gui tests: a test run leaves no gui.test folder in the real ~/Library/WebKit or ~/Library/Caches (waitingsnow) (#1047)
archive: a request the archive kept is opened and downloaded from the Usage page, with its bodies whole (jorben, #447: 请求存档 reachable from usage history, full bodies, download) — the archive's name was only on the Gateway page's recent calls, gone once a call left the list or the gateway restarted, and the archive kept the first 256 KB of each body, the recent list's cut. The usage ledger's row now names the call's archive (archive in usage.jsonl, kept through the Requests page's packed rows), and its details on Usage → Requests show the Request archive panel, Fetch from archive and Download; with the archive on, a request it has no copy of says "Not archived". The bodies are kept whole up to requestArchiveMaxMB in settings.json (32 MB unless set, at most 1024): the response is teed to a temp file as it is sent, the request body written to one when the call ends, so the upload queue holds file handles, not bodies, and one upload at a time reads them; a body past the limit is kept to it and marked cut, with its whole size. What goes upstream is unchanged, the archive stays off unless turned on, headers and bodies are scrubbed as before, and the temp files are removed after the upload or when the queue is full. The page is sent a body to show only when it is 256 KB or less (a longer one is said by its size), an archive over 80 MB is only said to be; Download streams the object whole, to Downloads in the app (archive/export, -2, -3… for a name taken) and as the browser's download in magpie web (archive/file). TestArchiveWholeBodies, TestArchiveLimit, TestRowKeepsArchive and TestArchivePreviewAndDownload fail without the change; TestArchiveOffUsage, TestArchiveTooLargeToPreview, TestArchiveOldPreview keep the rest. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; request-archive-usage (new), request-archive, click-scroll, old-webkit, usage-ledger, usage-ledger-detail, usage-ledger-content, web-page, sse-body pass.
archive: a request the archive kept is opened and downloaded from the Usage page, with its bodies whole (jorben, #447: 请求存档 reachable from usage history, full bodies, download) — the archive's name was only on the Gateway page's recent calls, gone once a call left the list or the gateway restarted, and the archive kept the first 256 KB of each body, the recent list's cut. The usage ledger's row now names the call's archive (archive in usage.jsonl, kept through the Requests page's packed rows), and its details on Usage → Requests show the Request archive panel, Fetch from archive and Download; with the archive on, a request it has no copy of says "Not archived". The bodies are kept whole up to requestArchiveMaxMB in settings.json (32 MB unless set, at most 1024): the response is teed to a temp file as it is sent, the request body written to one when the call ends, so the upload queue holds file handles, not bodies, and one upload at a time reads them; a body past the limit is kept to it and marked cut, with its whole size. What goes upstream is unchanged, the archive stays off unless turned on, headers and bodies are scrubbed as before, and the temp files are removed after the upload or when the queue is full. The page is sent a body to show only when it is 256 KB or less (a longer one is said by its size), an archive over 80 MB is only said to be; Download streams the object whole, to Downloads in the app (archive/export, -2, -3… for a name taken) and as the browser's download in magpie web (archive/file). TestArchiveWholeBodies, TestArchiveLimit, TestRowKeepsArchive and TestArchivePreviewAndDownload fail without the change; TestArchiveOffUsage, TestArchiveTooLargeToPreview, TestArchiveOldPreview keep the rest. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; request-archive-usage (new), request-archive, click-scroll, old-webkit, usage-ledger, usage-ledger-detail, usage-ledger-content, web-page, sse-body pass.
gui: a provider icon always draws — the page's files are served with their own types, not the ones Windows' registry gives, which another program may have changed (an SVG not served as one draws no mask icon), and a mask icon that fails to load becomes the generic one, as a picture of the user's own does. Issue #97
gui: the gateway watch ends with the backend, and TestGatewayTakenOver no longer leaks it — watchGateway looped forever, so each run of the test left its watch (and the gateway it took up) running after the test: under -count the old watches read gatewayWatch while the next run wrote it (3 DATA RACEs in 20 runs), and once a run's cleanup put HOME and MAGPIE_ADDR back an old watch could serve the gateway on the default address under the real HOME, which set served in the next run before its other magpie was gone ("took the gateway while the other had it", 3 of 20 with -race -count=20 on 8e853a63; the #948 review saw 3 of 10). watchGateway now takes a context and an interval and returns when the context ends; startBackend gives it backendCtx, so it also stops with stopServing instead of starting a gateway after the backend has ended. The test runs the watch on its own context and, at cleanup, stops it and the gateway it took up before its environment is restored. gatewayWatch is a const. TestGatewayTakenOver passes 50 of 50 with -race -count=50; go test -race -tags nogui ./internal/gui/ ./internal/tui/ pass; vet, darwin, linux (nogui) and windows builds pass.
test: one list of the agent folders a sandbox clears, so no sandbox misses an agent (yetone, on #522: A shared list of env vars for the other sandboxes would be a welcome follow-up) — every sandbox kept its own list of the variables that point at an agent's folder, and each had drifted: the gateway's cleared 5 of the 32 magpie reads, the provider package's 2, the tui's 10, the library's 20. A sandbox clearing 2 meets whatever Grok, Kimi, Hermes, MiniMax, Qoder or ZCode the developer has installed, which is how #522 came about — Qoder's two were in the library's list and in none of the four that failed. internal/agentenv is now the one list, and the 21 sandboxes that fake an environment with no agent in it clear it, each keeping beside it the variables that are not an agent's: APPDATA and LOCALAPPDATA, which a sandbox sets to a folder of its own rather than clearing, VISUAL and EDITOR. A name in the list is one magpie reads, and TestVarsAreRead walks the sources to say so, which is how two names a sandbox had been clearing turned out to be read by nothing at all — GEMINI_CLI_HOME and OPENCODE_CONFIG in internal/backup's, while OPENCODE_CONFIG_DIR, the one internal/agent reads (agents.go:545), was not in it. Where a sandbox set an agent's folder to a fixture of its own, the clearing now comes first, since a list of 32 wipes what a list of 5 left standing. Not converted: the omp tests' four variables, which are the ones omp's own lookup consults (a Pi fork) rather than every agent's, and the warm-up's Claude Code traffic flags, which are not folders — CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC stays out of the list for that reason too, its one test setting it on purpose. On Windows 11 the whole suite answers as it did before the change: 31 packages pass, and the same 15 tests in internal/library, internal/gateway and internal/agent fail for what they failed for before (symlinks this machine has no privilege for, and TestSuffixModesReachAgentFiles). go vet ./..., go build -tags nogui (windows, GOOS=linux, GOOS=darwin), and go vet -tags nogui for linux and darwin over every package touched pass. (#543)
cli: 新增 magpie webdav,在终端里开启、修改和触发 WebDAV 同步 / add magpie webdav to turn on, change and run WebDAV sync from the terminal (#127)
sync: it can sync by itself less often or only when asked, and the server's setup can be restored over this computer's, undoably (Sun1090, #847: 同步太快了,需要设置个开关控制自动同步,还有恢复按钮) — the gateway's magpie synced every 3 minutes with no way to change it, and a free 坚果云 account is allowed only so many WebDAV requests (600 per 30 minutes, 1 GB up and 3 GB down a month); and there was no way to take the server's setup back over a computer's own, which the next sync would otherwise push up. Settings › Sync now has Sync by itself, the app's segmented control (no <select>): Off, 3 min (the default, as before), 15 min, 30 min, 1 hour; Off syncs only on Sync now, and the sync row says only when asked. The loop reads the setting each minute (no request), so a change takes effect without a restart, and a server limiting requests is still waited for as backoff says. Restore from the server asks first in the app's own dialog, naming the parts replaced; it reads the server's file as it is now, seals this computer's setup (keys and all) into the sync folder as <time>-before-restore (newest 5 kept), brings in each part that differs as a sync brings one changed only there, and leaves the state as such a sync would, so the next sync pushes nothing back. Its notice says what was restored, with Undo, which brings the kept setup back (the next sync then takes it to the others); OK, or a later sync's own notice, drops the undo. The form's Save keeps the setting (SetAuto alone sets it). CLI: magpie webdav|s3 auto off|3|15|30|60, restore, undo; the status says how often it syncs by itself. zh, ja, de strings. TestAutoSync (off: no request by itself; back to 15 min: one sync, then none; Save keeps it; 7 refused) and TestRestore (b's own changes replaced by the server's with no push, the kept copy holds b's provider and key, Undo puts it back and a gets it, nothing differing restores nothing) pass -race; sync-auto-restore.test.cjs (WebKit, en and zh: Off posts -1 and the rows say so, Cancel posts nothing, Restore and Undo post, no click moves the page) fails on the old code. sync-other-kind, sync-spin, s3-sync, webdav-settings-refresh, usage-computers, click-scroll, gui-ja, gui-de and settings-sections pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
gateway: a gateway key can be held to some accounts (#905, LCorleone) — a key could burn any account a provider had: two coding plans of one vendor, and a key given to someone had them both. A key now keeps an optional list of accounts and keys (caller-keys.json "accounts", none = every account, so older files read as before): "<provider>/<account>", an account by its stable id — the login's, set once it is written and kept through renames (logins.json's savedLogin id, written by every write and stood in for by one made of the name until then, so an entry made before stands), a key by its fingerprint — every one a provider has when the list is set, and one gone later (a key rotated, an account signed out) kept as it is, matching nothing, holding the key closer, never wider. The command line and the menu name an account by who is signed in and keep its id. Routing holds a plan to the key's accounts beside its models: a candidate whose account the key may not use is left out — through a routing group the key names no less, for naming a group is not naming its accounts — the plan's order keeping step and its left-out list marking what the key held out (Weighed's held); magpie's own calls for the key — a web search, a picture described, a Codex title — are of the models the user picked, so the models don't hold them, but their spend lands on an account all the same, and the accounts hold them too. When every account or key a model has is one the key may not use, the request is refused with a 403 in the API's own error shape naming the model and the accounts the key may use, logged as a rejected call: after a usage cap's 429 and before an account barred for the model's, itself before the no-member 404. /v1/models and the Anthropic and Gemini lists show the key only the models an account or key it may use serves (a provider's account set not to serve one, #474, is none of the key's); a provider with no account and no key (a local ollama) is one the key names no account of — its models stay listed and its asks pass — while an entry kept after its provider lost its last account or key serves none, holding the key closer. The embeddings, images and videos endpoints hold their candidate to the key's accounts the same way, and counting tokens counts on those alone (the local estimate where it may use none). System One asks its decision model of an account or key the key may use — and sends on that key, of those in use, not the provider's first, which a key held to a later one was spent on; the videos and images keyed paths send on one the key may use too, held to a later key no longer refused for the first alone. Set it in the app's menu the models are picked in — the accounts after the models, an account by who is signed in with their plan in the note, a key by its fingerprint, what the CLI kept that no provider has now kept as "Not signed in now" — or
magpie gateway-key accounts <id> codex/me@example.com openai/<key id> (all takes it off); gateway-key list has an ACCOUNTS column. en/zh/ja/de, the German "{n} accounts" main already had left to it rather than written twice. The images API's fan-out over a subscription's accounts is held to the key's accounts as one: one of the key's whose plan refuses to draw no longer hands the pen to an account it may not use, and the second in the order, when it is the key's, draws what the first may not (drawOnAccounts filters the candidates, the account pinned to with the account header told plainly when the key may not use it). The Routing view's left-out list says an account the key held out for what it is, not that its plan lacks the model, in Chinese, Japanese and German too; the key's badge names its accounts by who is signed in, not by the stable ids it keeps. (#932)
gui: a Command-click on the Mac's menu bar icon is left to the system, which drags the icon with it (#792, BennettChina) — Hidden Bar's users move icons with Command-drag, and magpie's icon opened its panel instead. The tray's click handler now reads the held modifiers ([NSEvent modifierFlags]) and does nothing when Command is down; other systems are unchanged. go vet, builds (darwin with the GUI, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./internal/gui/ under a temp HOME pass; the drag itself wasn't exercised here (it would move icons in the owner's live menu bar).
gui: a Command-click on the Mac's menu bar icon is left to the system, which drags the icon with it (#792, BennettChina) — Hidden Bar's users move icons with Command-drag, and magpie's icon opened its panel instead. The tray's click handler now reads the held modifiers ([NSEvent modifierFlags]) and does nothing when Command is down; other systems are unchanged. go vet, builds (darwin with the GUI, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./internal/gui/ under a temp HOME pass; the drag itself wasn't exercised here (it would move icons in the owner's live menu bar).
codex: Settings decides where Codex's thread-title requests go — Codex's own ChatGPT as before, off, or a magpie model (#705, DuGuaiYA: Codex signed in with ChatGPT sends thread_title / thread_title_reconsideration requests that magpie relays through ChatGPT even when the conversation is on a magpie provider) — a new setting codexTitles ("" = as Codex sends them, the default; "off"; or a model's id such as deepseek/deepseek-v4-flash or a group) is read on /backend-api/codex/responses for a request whose x-openai-subagent or turn metadata thread_source is a title kind. Off answers at once with an empty completed response (Codex then keeps no title, quietly, as its source shows), contacts nothing, and is still recorded: a Routing trace "Titles off" and a usage row of Kind thread_title marked rejected (left out of stats). A model gets the request through serve (so it is recorded as a title request on that provider), and its answer is given back as one Responses message holding {"title":…}, since Codex's text.format schema does not reach Chat or Anthropic upstreams. Settings has a row "Codex thread titles" (zh: Codex 会话标题) with the app's model menu (Codex's own, Off, magpie's models and groups), saved on its own through POST /api/settings/codex-titles, which refuses a model magpie doesn't serve; the page's other saves keep it. TestCodexTitlesSetting (gateway) fails on the old code (off still went to ChatGPT) and checks the ledger kinds, rejected rows and trace; TestTitleJSON; TestCodexTitlesSetting (gui) for the endpoint; codex-titles.test.cjs (en/zh, webkit) fails on the old assets: no select, the picks post, the subtitle follows, no click scrolls. go vet, builds (darwin nogui, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real Codex TUI: no end-to-end run of Codex's title generation against magpie.
gateway, settings, gui: web pages the user lists in Settings › Network may call the gateway from a browser, each request with a gateway key (SkyAerope, #1051)
gui: Settings re-reads the WebDAV setup when redrawn; the off-state description names only what is on (#307)
web: magpie web serves the app's window to a browser, with the gateway beside it — for a computer that can't show the app: no desktop, WSL, a server over SSH, or the CLI-only build, which now carries the page too (what of the app needs Wails is left out of nogui builds). Every request needs this run's key, which the printed link carries and trades for a cookie other sites' pages can't send with a POST; --lan serves it on the network too, with a warning, --addr elsewhere, --no-open leaves the browser shut. In the browser the page opens links and sign-ins itself, names a folder instead of opening it, and leaves out the tray, the Dock and open at login. Tested from a nogui build in a sandbox: no key, a wrong key and a cookieless POST are refused; with the link Agents, Providers and Settings load and a setting saves. Issue #102
dev: a backend restart no longer shows "502 Bad Gateway" — the shell asks a read cut off in flight again, of the backend that comes up, and only a write, which may have landed, still fails (Image #14)
web: magpie web serves the app's window to a browser, with the gateway beside it — for a computer that can't show the app: no desktop, WSL, a server over SSH, or the CLI-only build, which now carries the page too (what of the app needs Wails is left out of nogui builds). Every request needs this run's key, which the printed link carries and trades for a cookie other sites' pages can't send with a POST; --lan serves it on the network too, with a warning, --addr elsewhere, --no-open leaves the browser shut. In the browser the page opens links and sign-ins itself, names a folder instead of opening it, and leaves out the tray, the Dock and open at login. Tested from a nogui build in a sandbox: no key, a wrong key and a cookieless POST are refused; with the link Agents, Providers and Settings load and a setting saves. Issue #102
web: magpie web serves the app's window to a browser, with the gateway beside it — for a computer that can't show the app: no desktop, WSL, a server over SSH, or the CLI-only build, which now carries the page too (what of the app needs Wails is left out of nogui builds). Every request needs this run's key, which the printed link carries and trades for a cookie other sites' pages can't send with a POST; --lan serves it on the network too, with a warning, --addr elsewhere, --no-open leaves the browser shut. In the browser the page opens links and sign-ins itself, names a folder instead of opening it, and leaves out the tray, the Dock and open at login. Tested from a nogui build in a sandbox: no key, a wrong key and a cookieless POST are refused; with the link Agents, Providers and Settings load and a setting saves. Issue #102
gui: TestExplorerWithoutPath is built where explorer is: windows and not nogui — it had no build tag, so go test -tags nogui ./internal/gui, which the project runs, failed to compile on Windows: the test was in while folder_windows.go, which has explorer and fileExists, was left out by !nogui. (#272)
web: magpie web serves the app's window to a browser, with the gateway beside it — for a computer that can't show the app: no desktop, WSL, a server over SSH, or the CLI-only build, which now carries the page too (what of the app needs Wails is left out of nogui builds). Every request needs this run's key, which the printed link carries and trades for a cookie other sites' pages can't send with a POST; --lan serves it on the network too, with a warning, --addr elsewhere, --no-open leaves the browser shut. In the browser the page opens links and sign-ins itself, names a folder instead of opening it, and leaves out the tray, the Dock and open at login. Tested from a nogui build in a sandbox: no key, a wrong key and a cookieless POST are refused; with the link Agents, Providers and Settings load and a setting saves. Issue #102
fx: the state never waits on the dollar-to-yuan rate, a stale one is asked for behind it (#541: placeholders for seconds at start-up) — with CNY chosen, state() and the Settings page asked fx.Get for the rate and, once the one kept was older than its 12 h TTL (every morning's start-up), waited up to 4 s on open.er-api.com before answering. currentFX now takes fx.Soon: the rate known now (this process's, the cache file's, else Fallback, as before), and when that is stale fx.Get behind it, one at a time (a TryLock), with 15 s to answer; the next look shows the new rate. fx.SetRateURL lets another package's test point the rate at a fake endpoint and fx.Settle waits for a fetch under way. TestFXStateDoesNotWait (yesterday's rate cached, an endpoint taking 3 s: the state answers within 1 s with yesterday's, stale, then the new one) fails without the change ("the state waited 3.0s on the rate"). 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.
gui: a routing group in a routing group says what agents are told of it, and where its reasoning is set (#915)
groups: a routing group's own id keeps a model's dots (#968) — a group named GPT 6.1 Sol was group/gpt-6-1-sol, so a harness pricing or tuning by the model's name didn't know it (dbydd). provider.GroupSlug is Slug with the dots kept (a run of them one, none at the ends); SaveGroup, RenameGroup, the backup's import and export, the GUI's save and rename, the CLI's group add and edit, and the Routing editor (groupSlug) derive and check a group's id by it. The ids magpie finds stay Slug's (auto-gpt-6-1-sol), so no id saved before changes, and an agent sending the bare name gpt-6.1-sol reaches a group of that id first (GroupFor). groupid_test (ids, save, rename, bad ids, found ids unchanged) and gateway's TestDottedGroupID (group/gpt-6.1-sol and the bare name served, falling through as the group's order says; -race -count=5) pass and fail without the fix; group-id-dots (hint and Add's body, groupSlug cases) passes in Chromium and WebKit, en and zh, and fails without it. The other group-* GUI tests, agent-model-groups and i18n-keys pass, except group-arrange, whose Chromium cases fail the same on origin/main (mouse.move: Invalid parameters at line 165). go test . provider/gui/gateway/agent/tui, vet, linux/windows builds pass.
gateway, gui, cli: each account or key can have its own limit on requests at once, with a bounded queue that turns requests away with a 429 (#892, margbug01) — a provider's Concurrency applied to every key and account alike, the queue past it had no bound and no wait, and nothing showed what ran or waited. A key or account now may have its own limit over the provider's (Provider.AccountConcurrency, by lowercased account or KeyID; 0 none; unset takes the provider's, as before), counted in one lane across every model, routing group and agent. A provider's Queue size bounds how many wait for each of its lanes (one more is turned away at once) and Queue wait how many seconds one waits (then it leaves the queue); both 0 by default, as before. A request turned away goes on to the next key, account or group member without resting anything, else gets a 429 in its API's own shape (Anthropic rate_limit_error, OpenAI's, Gemini's) with Retry-After: 1, saying the limit and the queue. A slot is given back when the reply is read to the end, fails or the agent goes away; a waiter that goes away leaves the queue, and one granted as it gave up hands the slot on. In a routing group, or among a provider's keys and accounts, a full one gives way once to another with a free slot now, so a group spreads before anyone waits; a fallback provider is never asked for want of a slot, and with all full the request waits for the one routing chose. The Claude subscription path and plugin providers take their slot the same way, around the attempt; a parked Claude Code tool turn has already answered and holds none. GUI: each account and key row has a pill — "1/1 running · 2 queued" while requests are out (GET /api/lanes, read every two seconds and written into the pill in place), else its limit; none set shows on hover only — opening the app's menu (provider's, no limit, 1/2/3/5/10, Other… typed in place), posting provider/accountconcurrency; the editor has Queue size and Queue wait under Concurrency. CLI: magpie provider account-concurrency <id> [account|key [n|off|default]] and magpie provider queue <id> [size [seconds]]. en/zh/ja/de. TestAccountLimitSharedAcrossGroups, TestQueueFullAndWaitedOut, TestLaneReleasedOnFailure and TestFullMemberGivesWay (pass with -race, with TestMaxConcurrency and TestLanesLimitLifted), TestLaneLimitRoutes and TestAccountConcurrencyCmd are new and fail without the change; lane-limit.test.cjs (WebKit, en and zh: a key's busy/queued pill updated in place from /api/lanes, set from the menu, provider's posts null, Other… typed, an account's own limit, none on hover only, queue fields saved and refused out of range, labels on one line, every string in zh/ja/de, no select, no scroll, no left border) fails without the change. key-weight-fit, account-cap, provider-concurrency, click-scroll, account-models and api-key-usage pass in WebKit; the other WebKit failures fail the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
gui: lightweight mode, off by default, lets a closed panel's or window's webview go and makes it again when it is opened (somls on #580: GUI版可否增加轻量模式,关闭面板退出webview,减少资源占用) — closing the tray panel or the main window only hid it, so each kept its page and, on a Mac, its WebKit WebContent process (about 65 MB each at start, 95–100 MB after use) for as long as magpie ran. Settings → General now has Lightweight mode (轻量模式), shown in the app only (not on a browser's page) and kept as this computer's own through a sync or a restored backup (KeepOwn): with it on, a window found closed on two looks in a row 30 s apart (lightStep) is let go 30–60 s after it was closed, so a panel opened again soon after is still the same one at once. Letting go happens on the main thread under a new h.winMu: the window field is set to nil, the panel is unhooked from the tray icon (AttachWindow(nil); left click goes through magpie's own OnClick), and the main window is marked gone so its closing hook, which otherwise cancels a close and hides, lets it close; on the Mac the WKWebView is taken out of the window first (stopLoading, delegates and script handlers cleared, removed from its superview), since Wails' NSWindow close alone left the WebContent process alive for minutes. Opening it again (the tray icon, ShowMain, an import link, the Dock) makes the window anew with the options it was started with (makeMain/makePanel, moved out of Run with their hooks), its text size and title bar or Hyprland name (madeAgain), on Windows shown only once its page has loaded; every other path that reached h.main or h.panel (fit, tint, zoom, hide, glide, the Windows title-bar tint, the Hyprland click watch, the folder picker) now goes through the main thread and does nothing to a window let go; Linux keeps running with no window left (DisableQuitOnLastWindowClosed). Measured in a sandbox build under a temp HOME on other ports: started in the tray with the setting on, two WebContent processes of ~65 MB each; about a minute later both windows were let go and both processes ended (GPU and Networking, ~56 MB, are shared and stay; magpie's own process is unchanged), so the saving is ~130 MB idle and up to ~200 MB after use; the main window and the panel were then opened again with their pages, tinted and at the tray icon, closed, let go a second time, and magpie quit cleanly, with fit/tint/zoom calls every 0.2 s through a release crashing nothing. Windows and Linux build but were not run. TestLightStep and TestLightweightKeptAndOwn are new; lightweight.test.cjs (WebKit, en and zh: the row, off by default, On/Off posted as lightweight true/false, another setting's save keeping it, no scroll, no left-border accent, no row on a browser's page) is new and passes, as do click-scroll, settings-sections and searcher-pick. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
gui: lightweight mode, off by default, lets a closed panel's or window's webview go and makes it again when it is opened (somls on #580: GUI版可否增加轻量模式,关闭面板退出webview,减少资源占用) — closing the tray panel or the main window only hid it, so each kept its page and, on a Mac, its WebKit WebContent process (about 65 MB each at start, 95–100 MB after use) for as long as magpie ran. Settings → General now has Lightweight mode (轻量模式), shown in the app only (not on a browser's page) and kept as this computer's own through a sync or a restored backup (KeepOwn): with it on, a window found closed on two looks in a row 30 s apart (lightStep) is let go 30–60 s after it was closed, so a panel opened again soon after is still the same one at once. Letting go happens on the main thread under a new h.winMu: the window field is set to nil, the panel is unhooked from the tray icon (AttachWindow(nil); left click goes through magpie's own OnClick), and the main window is marked gone so its closing hook, which otherwise cancels a close and hides, lets it close; on the Mac the WKWebView is taken out of the window first (stopLoading, delegates and script handlers cleared, removed from its superview), since Wails' NSWindow close alone left the WebContent process alive for minutes. Opening it again (the tray icon, ShowMain, an import link, the Dock) makes the window anew with the options it was started with (makeMain/makePanel, moved out of Run with their hooks), its text size and title bar or Hyprland name (madeAgain), on Windows shown only once its page has loaded; every other path that reached h.main or h.panel (fit, tint, zoom, hide, glide, the Windows title-bar tint, the Hyprland click watch, the folder picker) now goes through the main thread and does nothing to a window let go; Linux keeps running with no window left (DisableQuitOnLastWindowClosed). Measured in a sandbox build under a temp HOME on other ports: started in the tray with the setting on, two WebContent processes of ~65 MB each; about a minute later both windows were let go and both processes ended (GPU and Networking, ~56 MB, are shared and stay; magpie's own process is unchanged), so the saving is ~130 MB idle and up to ~200 MB after use; the main window and the panel were then opened again with their pages, tinted and at the tray icon, closed, let go a second time, and magpie quit cleanly, with fit/tint/zoom calls every 0.2 s through a release crashing nothing. Windows and Linux build but were not run. TestLightStep and TestLightweightKeptAndOwn are new; lightweight.test.cjs (WebKit, en and zh: the row, off by default, On/Off posted as lightweight true/false, another setting's save keeping it, no scroll, no left-border accent, no row on a browser's page) is new and passes, as do click-scroll, settings-sections and searcher-pick. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
gui: lightweight mode, off by default, lets a closed panel's or window's webview go and makes it again when it is opened (somls on #580: GUI版可否增加轻量模式,关闭面板退出webview,减少资源占用) — closing the tray panel or the main window only hid it, so each kept its page and, on a Mac, its WebKit WebContent process (about 65 MB each at start, 95–100 MB after use) for as long as magpie ran. Settings → General now has Lightweight mode (轻量模式), shown in the app only (not on a browser's page) and kept as this computer's own through a sync or a restored backup (KeepOwn): with it on, a window found closed on two looks in a row 30 s apart (lightStep) is let go 30–60 s after it was closed, so a panel opened again soon after is still the same one at once. Letting go happens on the main thread under a new h.winMu: the window field is set to nil, the panel is unhooked from the tray icon (AttachWindow(nil); left click goes through magpie's own OnClick), and the main window is marked gone so its closing hook, which otherwise cancels a close and hides, lets it close; on the Mac the WKWebView is taken out of the window first (stopLoading, delegates and script handlers cleared, removed from its superview), since Wails' NSWindow close alone left the WebContent process alive for minutes. Opening it again (the tray icon, ShowMain, an import link, the Dock) makes the window anew with the options it was started with (makeMain/makePanel, moved out of Run with their hooks), its text size and title bar or Hyprland name (madeAgain), on Windows shown only once its page has loaded; every other path that reached h.main or h.panel (fit, tint, zoom, hide, glide, the Windows title-bar tint, the Hyprland click watch, the folder picker) now goes through the main thread and does nothing to a window let go; Linux keeps running with no window left (DisableQuitOnLastWindowClosed). Measured in a sandbox build under a temp HOME on other ports: started in the tray with the setting on, two WebContent processes of ~65 MB each; about a minute later both windows were let go and both processes ended (GPU and Networking, ~56 MB, are shared and stay; magpie's own process is unchanged), so the saving is ~130 MB idle and up to ~200 MB after use; the main window and the panel were then opened again with their pages, tinted and at the tray icon, closed, let go a second time, and magpie quit cleanly, with fit/tint/zoom calls every 0.2 s through a release crashing nothing. Windows and Linux build but were not run. TestLightStep and TestLightweightKeptAndOwn are new; lightweight.test.cjs (WebKit, en and zh: the row, off by default, On/Off posted as lightweight true/false, another setting's save keeping it, no scroll, no left-border accent, no row on a browser's page) is new and passes, as do click-scroll, settings-sections and searcher-pick. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
gui: lightweight mode, off by default, lets a closed panel's or window's webview go and makes it again when it is opened (somls on #580: GUI版可否增加轻量模式,关闭面板退出webview,减少资源占用) — closing the tray panel or the main window only hid it, so each kept its page and, on a Mac, its WebKit WebContent process (about 65 MB each at start, 95–100 MB after use) for as long as magpie ran. Settings → General now has Lightweight mode (轻量模式), shown in the app only (not on a browser's page) and kept as this computer's own through a sync or a restored backup (KeepOwn): with it on, a window found closed on two looks in a row 30 s apart (lightStep) is let go 30–60 s after it was closed, so a panel opened again soon after is still the same one at once. Letting go happens on the main thread under a new h.winMu: the window field is set to nil, the panel is unhooked from the tray icon (AttachWindow(nil); left click goes through magpie's own OnClick), and the main window is marked gone so its closing hook, which otherwise cancels a close and hides, lets it close; on the Mac the WKWebView is taken out of the window first (stopLoading, delegates and script handlers cleared, removed from its superview), since Wails' NSWindow close alone left the WebContent process alive for minutes. Opening it again (the tray icon, ShowMain, an import link, the Dock) makes the window anew with the options it was started with (makeMain/makePanel, moved out of Run with their hooks), its text size and title bar or Hyprland name (madeAgain), on Windows shown only once its page has loaded; every other path that reached h.main or h.panel (fit, tint, zoom, hide, glide, the Windows title-bar tint, the Hyprland click watch, the folder picker) now goes through the main thread and does nothing to a window let go; Linux keeps running with no window left (DisableQuitOnLastWindowClosed). Measured in a sandbox build under a temp HOME on other ports: started in the tray with the setting on, two WebContent processes of ~65 MB each; about a minute later both windows were let go and both processes ended (GPU and Networking, ~56 MB, are shared and stay; magpie's own process is unchanged), so the saving is ~130 MB idle and up to ~200 MB after use; the main window and the panel were then opened again with their pages, tinted and at the tray icon, closed, let go a second time, and magpie quit cleanly, with fit/tint/zoom calls every 0.2 s through a release crashing nothing. Windows and Linux build but were not run. TestLightStep and TestLightweightKeptAndOwn are new; lightweight.test.cjs (WebKit, en and zh: the row, off by default, On/Off posted as lightweight true/false, another setting's save keeping it, no scroll, no left-border accent, no row on a browser's page) is new and passes, as do click-scroll, settings-sections and searcher-pick. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
gui: the main window opens as it was last left — maximised again if it was, at its last size otherwise (#995)
gui: lightweight mode, off by default, lets a closed panel's or window's webview go and makes it again when it is opened (somls on #580: GUI版可否增加轻量模式,关闭面板退出webview,减少资源占用) — closing the tray panel or the main window only hid it, so each kept its page and, on a Mac, its WebKit WebContent process (about 65 MB each at start, 95–100 MB after use) for as long as magpie ran. Settings → General now has Lightweight mode (轻量模式), shown in the app only (not on a browser's page) and kept as this computer's own through a sync or a restored backup (KeepOwn): with it on, a window found closed on two looks in a row 30 s apart (lightStep) is let go 30–60 s after it was closed, so a panel opened again soon after is still the same one at once. Letting go happens on the main thread under a new h.winMu: the window field is set to nil, the panel is unhooked from the tray icon (AttachWindow(nil); left click goes through magpie's own OnClick), and the main window is marked gone so its closing hook, which otherwise cancels a close and hides, lets it close; on the Mac the WKWebView is taken out of the window first (stopLoading, delegates and script handlers cleared, removed from its superview), since Wails' NSWindow close alone left the WebContent process alive for minutes. Opening it again (the tray icon, ShowMain, an import link, the Dock) makes the window anew with the options it was started with (makeMain/makePanel, moved out of Run with their hooks), its text size and title bar or Hyprland name (madeAgain), on Windows shown only once its page has loaded; every other path that reached h.main or h.panel (fit, tint, zoom, hide, glide, the Windows title-bar tint, the Hyprland click watch, the folder picker) now goes through the main thread and does nothing to a window let go; Linux keeps running with no window left (DisableQuitOnLastWindowClosed). Measured in a sandbox build under a temp HOME on other ports: started in the tray with the setting on, two WebContent processes of ~65 MB each; about a minute later both windows were let go and both processes ended (GPU and Networking, ~56 MB, are shared and stay; magpie's own process is unchanged), so the saving is ~130 MB idle and up to ~200 MB after use; the main window and the panel were then opened again with their pages, tinted and at the tray icon, closed, let go a second time, and magpie quit cleanly, with fit/tint/zoom calls every 0.2 s through a release crashing nothing. Windows and Linux build but were not run. TestLightStep and TestLightweightKeptAndOwn are new; lightweight.test.cjs (WebKit, en and zh: the row, off by default, On/Off posted as lightweight true/false, another setting's save keeping it, no scroll, no left-border accent, no row on a browser's page) is new and passes, as do click-scroll, settings-sections and searcher-pick. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
factory: the Gateway page says when an older magpie serves the gateway, and a Factory account can be added by its API key (#506, 黄瑞 and aohun: Factory 403 "Factory refused this account the request; check that
droid… sign in to it again" on v0.1.630; aohun: support FACTORY_API_KEY as droid2api does) — the 403's wording quoted in #506 was removed in 5523eb17 (first in v0.1.551; git grep finds it in v0.1.550, not in v0.1.630), so those requests were served by a magpie at v0.1.550 or older still holding :3425 (a magpie serve, a second copy, one that didn't quit): serveGateway leaves the port to any magpie answering GET / and the page said only "running · served by another magpie", so none of the later Factory fixes (droid's opening line on /api/llm/o in v0.1.630 among them) reached their requests. gateway.ServedBy reads the serving magpie's version from its GET / answer (Serving wraps it); the providers state carries it with older when it is below this one's, the Gateway card then says "running · served by magpie <v>, older than this one" with an amber dot and that agents' requests go through it without this version's fixes until it quits, when this one takes over within 15 s (zh too, wrapped, never cut off), and the log says so. A Factory API key (fk-…) is added from the import box (Factory's accounts list "Add an account by API key…", and "Use an API key instead…" on the warning before a sign-in): as droid's om() takes FACTORY_API_KEY, the key is the bearer with droid's headers, never renewed, and sends no X-Factory-Org-Id (droid's active org comes from a sign-in, st() is null for a key); whoami asked with the key says whose it is and its region/host, as droid's yP checks a key ("invalid-api-key" when refused), and a refused key isn't added. factoryFresh hands a key account on as it is (no WorkOS, no whoami for an org) and factoryMendOrg leaves it alone; when Factory has moved to its plugin the key isn't offered, the built-in being what keeps it. No request was made to Factory, no real key or account was tried, and whether Factory serves a key's requests from agents other than droid is not known. TestGatewayServedByAnOlderMagpie (fake magpie answering 0.1.550/0.1.630/0.1.640/no version), TestFactoryAPIKey (fake Factory: import, bearer and headers, no org, no WorkOS, refused key, "exists" again; fails with a 403 without the key handling in factoryFresh) and gateway-older.test.cjs, factory-key.test.cjs (Chromium and WebKit, en and zh) pass; login-import, factory-sub, plugin-moved-signin, gateway-fold pass in WebKit (Chromium's channel launch lacks ICU here). 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.
gui: the Windows tray panel no longer shows a faint close X over its Settings gear — its caption buttons are hidden there (#238 mintonight: a blurry red X at the panel's top-right corner, just above and right of the gear) — Wails v3 makes a frameless window on Windows WS_OVERLAPPEDWINDOW and, for the shadow and round corners, extends DWM's frame into the whole client area; the panel left its close button at the zero value, ButtonEnabled, which sets WS_SYSMENU, so DWM drew the caption's close X into that extended frame at the window's top right, and with the panel translucent (BackgroundTypeTranslucent, WS_EX_NOREDIRECTIONBITMAP, a see-through webview) it showed through the page, faint, over the gear. It was not the page: #winclose is removed everywhere but Linux's window, and is a grey pill in the header row. The panel's options move into panelOptions(goos, theme), unchanged, and on Windows its close, minimise and maximise buttons are ButtonHidden, which takes WS_SYSMENU and the min/max boxes off, so DWM has no caption buttons to draw; DisableFramelessWindowDecorations stays off, so the shadow and Windows 11's round corners stay. The Mac and Linux get the same options as before (their button states left at the zero value), and the main window is not touched. Tested with a new internal/gui/panel_options_test.go (!nogui): on windows the three buttons are hidden and the panel still frameless, translucent, off the taskbar and decorated; on darwin and linux the buttons are as before and URL, backdrop and corner radius unchanged; run on the Mac and compiled for Windows (GOOS=windows go test -c). go build -tags nogui (and GOOS=linux), GOOS=windows go build and go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin timed out once at a load average near 60 and pass alone). Not tried on a real Windows desktop: that DWM stops drawing the X without WS_SYSMENU is from the Win32 custom-frame behaviour and Wails' code, not seen.
plugins: the Plugins page is drawn in parts, Installed at once and npm's figures after (#488: the page showed skeletons for seconds, even on Installed) — the page waited on /api/plugins/market, which answered only once the plugin host, the registry fetch and an npm request for every package (two each, 8s apiece, all at once, kept an hour in memory only) had all come back, so the slowest package decided when anything was drawn. The page now asks for each part on its own, as the Library page does: Installed is drawn from GET /api/plugins as soon as it answers; Discover's cards from the new GET /api/plugins/listings (the registry, with its cached and built-in copies), each with what npm said last; and versions, weekly downloads and an installed plugin's "v… out" fill in from the new GET /api/plugins/npm?names=…, never waited on (a card's button waits, disabled, until npm has said whether it has the package). npm is asked at most six packages at a time and for no more than 8s in all; a package it answers late is what it said last, and its answer is kept for the next time. What npm said is kept on disk (~/.cache/magpie/plugin-npm.json) beside the registry's copy, so a magpie just started shows it before asking, and GET /api/plugins gives each plugin's newest version from it. /api/plugins/market still answers the whole market as before. Once loaded the page shows what it did. TestNPMInfoKeptOnDisk, TestNPMInfoDeadline, TestNPMInfoAtOnce, TestPluginMarketInParts and plugin-progressive.test.cjs (Chromium, WebKit; en, zh; Installed while the market is held pending) fail without the change; plugin-local, plugin-updates, plugin-market, plugin-move, plugin-move-overflow, plugin-release, plugin-git, plugin-key-hint, plugin-plan, list-sort, click-scroll, old-webkit pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
plugins: the folder a plugin lives in is picked, not typed, and its page reads that folder's README (#411)
gui, provider: the request log scrolls sideways from a bar at the window's foot and a request's detail opens with its status, duration and a View routing link (mintonight, #799); a provider has a price rate its costs are counted at (ITea312, #819) — #799: the request table, wider than the window, could only be scrolled sideways from the scrollbar at its very end, below 40+ rows, so it looked like it had none; and the "status detail" the reply pointed to wasn't to be found in the row's detail. A bar (#ledHScroll) now sits at the window's foot while the table is wider than its box, scrolling with it both ways; a row's detail begins with Status (Succeeded for log rows), Duration and Routing: View routing, which opens the request's route. #819: costs were counted only at the official (list or maker) price, or a price set per model with magpie model price; a relay billing 0.8 of it had to have each model priced by hand. The provider editor (key and signed-in alike) has a Price rate field (empty for 1; 0–1000, three decimals), saved as priceRate; EffectivePrice multiplies the list/maker price by it, and a price set for a model stays as set. Usage caches key on the providers file, so the counts follow. zh, ja, de strings. ledger-hscroll.test.cjs (fails on the old code) and provider-price-rate.test.cjs (WebKit, en and zh); usage-ledger, usage-ledger-detail (updated for the new rows), usage-ledger-content, usage-ledger-chart, routing-failed-open, purpose-filter, provider-concurrency and provider-levels-save pass on WebKit. TestPriceRateScalesListPrice, TestPriceRateOK, TestPriceRateOf. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
profiles: a click on a profile shows what it holds, and its Apply applies it (#467 emo172: a saved profile could only be seen by applying it over the setup in use) — a profile's chip applied it at once, and all the page knew of it was a one-line summary of its models. The state now carries each profile's fields by agent, under the agents' names and the fields' labels, in the order magpie lists them, with the Library's servers, skills and instructions each agent gets (not the instructions' text); a value that reads as a key or a token (a field named for one, a known key prefix, a long run of letters and digits) is sent as hidden, never its text. A click on a chip opens its details under its line in the window, and over it in the tray panel with Apply at their foot by the chip, the chip clicked staying where it is; opening them changes nothing, a second click closes them, Apply applies the profile; ↻ and × stay on the chip. In English and Chinese. TestDetails and TestStateProfileDetails fail without the change, as does profile-details.test.cjs (Chromium and WebKit, en and zh, window and panel). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFXInState's TempDir cleanup flakes with or without the change); profile-details, panel-profiles, click-scroll, old-webkit pass.
providers: a custom provider's models can be picked from its vendor's list while it is added, before its first Save (Sun1090 on #578: 添加供应商的时候,希望添加可以获取全模型的按钮或者下拉框?像cc-switch那样,可以选) — the vendor's list, its chips and Refresh were only in a saved provider's editor; the custom provider's add form had a comma-separated Models field and the hint "magpie asks the vendor for its list after saving", so a user adding one had to save it, open it again and pick there, which nothing in the form said could be done sooner. The add form now has Fetch models (获取模型) beside its Models field: it posts the new provider/list with the form as it stands (the URL and key typed, its headers, models URL and proxy; no id), which builds the draft as detect does (from its preset when it has one, then typed) and asks the vendor with the new Provider.List — Fetch's own path (a NoList preset's models, Cline's feed, Kilo's list, fetchOne) without SaveLive, so nothing is kept — answering the models agents chat with, a model's name where it has one. The list shows as chips (a long one with a filter), each ticked one in the Models field and an id typed there ticking its chip, with "{n} of {all} picked", Pick all (Pick those shown under a filter) and Pick none above the chips, so a pick that reflows them moves no button; one typed by hand stays through Pick none. A vendor refusing says why under the field; no URL asks nothing. The Add sends the picks as the provider's models, as the field did. TestProviderListBeforeSave (the list asked with the key typed, an embedding model left out, no provider saved and no list kept, a 401 told, no URL refused) is new; provider-add-models.test.cjs (WebKit, en and zh: nothing asked without a URL, the post carries URL and key and no id, the refusal shown, chips with names, tick and type in sync, filter and Pick those shown / none / all, the Add's models, no click moving the page, no left-border accent) is new and passes; add-sheet, provider-detect, provider-detect-models, provider-typed and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
providers: a model a vendor names its own way is merged with the same model from other providers — by its id as Volcengine Ark spells it, or by the model the user says it is the same as in Names & levels (kyzhouxu, #583: 火山引擎获取到的默认 ds 4.1 flash 模型的名称跟其他供应商的名称不一样,没有自动合并到同一个模型分组) — the groups magpie finds merged a model by its id lowercased, its vendor prefix and an eight-digit date taken off and a version's dot made a dash, so Ark's deepseek-v4-1-flash-260910 (a six-digit YYMMDD snapshot) was a group of its own beside ali-tp's deepseek-v4.1-flash. sameModel now also takes off Ark's YYMMDD (a year from 23 to 39, a month and a day that are one), Vertex's "@YYYYMMDD", reads Fireworks' "p" between digits (deepseek-v4p1) as the dot and "_" as "-"; -flash, -thinking, ":batch", a four-digit release (qwen's -2507, kimi-k2-0905) and a number that is no date (-128000, -261301) stay apart. For an id no rule matches (an Ark endpoint id, a relay's own name), each model in a provider's Names & levels now has a "Same as" (等同于) box, its placeholder the name it is merged by now: the model it is the same as, as any vendor spells it, staged in draft.modelPrefs and made with the provider's Save like the other prefs (Cancel drops it, Restore default empties it, nothing sent or redrawn or scrolled before the Save). It is kept in settings' ModelSameAs by "<provider>/<model>" (renamed with the provider like every Model* map), set by provider.SetModelSame (its own id however spelt keeps nothing; a model the provider doesn't serve, or a name of no model, is refused) and taken by autoGroups, AutoStandIn and the agents' reseat (AutoGroupOf); the agents are told once. TestSameModelVendorSpellings (Ark, Fireworks, Vertex, underscores, and pairs that must stay apart) and TestModelSameAsMergesFoundGroup (c/ep-sol-x joins auto-sol once said, GroupFor("sol") finds it, back out when cleared, refusals) are new; TestProviderSaveModelPrefs covers a Save's same; provider-same-as.test.cjs (WebKit, en and zh: placeholder, staging, unsaved, Restore default, Cancel, one Save carrying exactly the changes, no scroll, Chinese for each string) fails without the change and passes with it; provider-levels-save and provider-detect pass. 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.
provider: Bailian's decision model as a routing preset, and System One for custom providers (#647, Aimer779: Alibaba Cloud Bailian's decision-model-preview speaks TypeSafe's System One at POST <root>/systemone, and magpie only took it from TypeSafe's Jev) — a new preset, Bailian Decision Model (qwen icon, decide-only like TypeSafe Jev), asks decision-model-preview at its plan's root: Workspace · Beijing / Workspace · Singapore (https://{WorkspaceId}.<region>.maas.aliyuncs.com/compatible-mode/v1, the editor asks for the workspace ID and fills it in) or Token Plan · Beijing (token-plan.cn-beijing…, sk-sp- keys). Saving with the workspace still unfilled is refused, both in the editor and in provider.Add. A Bailian host has no /models for it, so its list is the one model, checked by a System One ask. A custom provider can now be a System One API: the CLI's decide=<root> (with models=… named anything, not only jev-*), workspace= and region=/plan= for a preset's plan; the editor's API seg has a fourth option, System One, whose Detect APIs posts a test question to <root>/systemone with the model typed, and a decide-only custom provider passes the "base URL is needed" check. A decide-only provider's first model is what it is asked as (Jev() no longer assumes jev-*), and its models stay routing groups' classifiers, never agents'. TestBailianDecisionPreset, TestCustomSystemOne, TestProviderSystemOne and TestProviderDecidePairs fail on the old code (undefined BailianDecision / DetectDecide, unknown decide=); internal/gui/tests/system-one.test.cjs (webkit, en/zh) covers the workspace, the Singapore and Token Plan plans, TypeSafe's unchanged editor and a custom System One detected and added. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes alone. Not tried against a real Bailian key.
provider, gui: a reset reminder, a notification before a long window renews with much of it unused or before resets expire (thedavidweng, #720)
provider, gui: a reset reminder, a notification before a long window renews with much of it unused or before resets expire (thedavidweng, #720)
provider, gui: a reset reminder, a notification before a long window renews with much of it unused or before resets expire (thedavidweng, #720)
sessions, gui, tui, cli: the Sessions list is as long as the count beside it, and drawn in pages (#1019)
gui: one list of sessions — the Sessions page shows what each session spent, and Usage → Sessions' latest sessions open there (#752, hisiling on v0.1.814: the top bar's 会话 listed every session by agent and folder with resume, delete and the trash but no tokens or cost, while 用量 → 会话's 最近的会话 had folder, model, tokens and cost and resumed too but couldn't delete or group by folder; both opened to the same time, folder, session id and resume command, so the same list was drawn twice with half missing from each). /api/sessions/manage already carried each session's tokens, cost and models (sessions.Managed embeds Session, and ListAgent assembles it priced), the page just never drew them; its rows now end in the same two columns Usage's list has (sessSpent: tokens with in/out/cached under them, ≈cost, "—" with its reason where there is no known price), the sub line names the models and the gateway's route (sessModelsText, shared with Usage), the details gain the per-model breakdown and the Routed lines (sessUsageDetail, which sessionDetail now uses too, so the top-sessions card is as before), and its filter also matches a model. manage now also sends each session's via (usage.Vias since the agent's oldest session, as /api/sessions does), so routing isn't lost; a null models list is read as empty. Usage → Sessions keeps its statistics, charts, top sessions and its latest-sessions list, as that list follows the range, agent, model and folder filters the Sessions page doesn't have: its rows keep title, folder, models, tokens and cost, lose Resume, Open in session terminal and the in-place details, and open on the Sessions page instead (role=link, Enter/Space too, a chevron, title "Open in Sessions" / 在会话页打开): the session's agent is picked, its folder unfolded, the row opened, and once the list is drawn the row is brought into sight — a scroll the reader's click asked for (scrollOnPurpose(e) at the click, then readerScrolls and readerLeaves when the row arrives, as the quota cards' focus does). An All sessions / 全部会话 button by the list's search opens the page on the agent picked there. At 560px and below a Sessions page row drops its tokens column, keeping the cost, so the title keeps room beside Resume, terminal and delete. The other way round (deleting and the trash moved into Usage) was not taken: the Sessions page's per-agent, per-folder tree with picking and the trash is the page made for it, and Usage's list is a drill-down of its filters. No <select>, no left-border accent, no click scrolls on its own, the Agents grip untouched. The GUI has en and zh tables only; both have the new strings. TestSessionsManageCarriesUsage (a Claude session from testdata plus a gateway record: tokens, models and via in sessions/manage) fails on the old handler; sessions-usage-open.test.cjs (WebKit, en and zh) fails on the old assets and passes, and sessions-*, click-scroll, usage-fold and routing-sessions pass; Chromium doesn't launch on this machine. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real session files beyond the fixtures.
sessions, gui: Codex sessions made with another provider are moved to Codex's own from the Sessions page, and back (#887, cjyrainbow) — Codex's history lists only the sessions of the provider it uses now, so sessions made through CC Switch (model_provider "custom") vanished from it once Codex used magpie, though their files were still there and the Sessions page listed them. The Codex tab now reads each session's provider from its session_meta (cached with the rest) and the one Codex in that session's home uses (config.toml's model_provider, or its profile's; "openai" when none), says how many were made with another and which, and tags each. "Move to <provider>…" opens magpie's own dialog listing them, each ticked; Move rewrites only the model_provider string in each session's files (.jsonl and .jsonl.zst, in its own Codex home, so WSL's too, mtime kept) and in Codex's state_*.sqlite threads row: messages, id, title and archive are left as they are. A copy of the files goes to magpie's trash folder (trash/codex-provider, with a move.json) first, a failed rewrite is put back from it, and a session written to in the last minute is left alone and said so. Undo moves each back to the provider it had, byte for byte. Nothing is moved by itself. POST /api/sessions/codex-provider; en/zh/ja/de. TestMoveCodexProvider (listing, bytes, mtime, the DB row and nothing else, backup, byte-identical undo, an active session, a bad id), TestMoveCodexProviderPacked and TestCodexProviderIn fail without the change, as does sessions-codex-provider.test.cjs (WebKit, en and zh: magpie's dialog, Cancel posts nothing, an unticked one stays, Undo, no click moves the page, no left border, translations); sessions-manage, -toolbar, -purge, -folder-delete, -wsl-delete and click-scroll pass. Not tried against a real CC Switch history. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
sessions: a session in magpie's trash can be erased for good, one at a time or the whole trash, after magpie's own dialog (#487: no way to empty the trash but removing its folder by hand) — the Sessions page's Trash only restored, and said magpie never erases what is in it: remove the folder yourself. Each trashed row now has Delete forever and the Trash an Empty trash, with the count of what is in it; both ask in magpie's dialog, as Delete does (never confirm()), naming the sessions, and Cancel sends nothing. They post /api/sessions/purge with the rows' keys, or all for every session the trash lists; each key is checked as Restore checks it (<agent>/<name>, an agent magpie deletes for, no separators, no . or ..), and its folder, and its agent's, must be real folders in <magpie dir>/trash/sessions, not links, with the session's note (session.json) in it; only that folder goes, its note with it, so the trash lists it no more. Nothing is erased by itself: the note under the Trash now says they are kept until erased there, and magpie never erases them by itself. TestPurgeOne, TestPurgeAll, TestPurgeRefusesOutOfTrash, TestSessionsPurgeRoute and sessions-purge.test.cjs (Chromium, WebKit; en, zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFXInState failed once under load, passes alone and in its package); sessions-manage, sessions-toolbar, click-scroll, old-webkit pass.
sessions, gui, tui, cli: the Sessions list is as long as the count beside it, and drawn in pages (#1019)
gateway, settings, gui: web pages the user lists in Settings › Network may call the gateway from a browser, each request with a gateway key (SkyAerope, #1051)
gui: the tray menu is in the page's language (#301: 界面设为中文,托盘菜单仍是英文) — its lines were English in Go, which never read the language setting. Open, Version, Restart to Update (to v) and Quit now follow Settings › Language: en or zh as set, with System the system's (macOS AppleLanguages, Windows' preferred UI language, LANG elsewhere), Chinese for any zh; changing it in Settings relabels the menu at once. TestTrayMenuLabels and TestSettingsLangRelabelsTray fail without the change. go vet, go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
gui: the tray menu is in the page's language (#301: 界面设为中文,托盘菜单仍是英文) — its lines were English in Go, which never read the language setting. Open, Version, Restart to Update (to v) and Quit now follow Settings › Language: en or zh as set, with System the system's (macOS AppleLanguages, Windows' preferred UI language, LANG elsewhere), Chinese for any zh; changing it in Settings relabels the menu at once. TestTrayMenuLabels and TestSettingsLangRelabelsTray fail without the change. go vet, go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
gui: the tray menu is in the page's language (#301: 界面设为中文,托盘菜单仍是英文) — its lines were English in Go, which never read the language setting. Open, Version, Restart to Update (to v) and Quit now follow Settings › Language: en or zh as set, with System the system's (macOS AppleLanguages, Windows' preferred UI language, LANG elsewhere), Chinese for any zh; changing it in Settings relabels the menu at once. TestTrayMenuLabels and TestSettingsLangRelabelsTray fail without the change. go vet, go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
gui: the gateway watch ends with the backend, and TestGatewayTakenOver no longer leaks it — watchGateway looped forever, so each run of the test left its watch (and the gateway it took up) running after the test: under -count the old watches read gatewayWatch while the next run wrote it (3 DATA RACEs in 20 runs), and once a run's cleanup put HOME and MAGPIE_ADDR back an old watch could serve the gateway on the default address under the real HOME, which set served in the next run before its other magpie was gone ("took the gateway while the other had it", 3 of 20 with -race -count=20 on 8e853a63; the #948 review saw 3 of 10). watchGateway now takes a context and an interval and returns when the context ends; startBackend gives it backendCtx, so it also stops with stopServing instead of starting a gateway after the backend has ended. The test runs the watch on its own context and, at cleanup, stops it and the gateway it took up before its environment is restored. gatewayWatch is a const. TestGatewayTakenOver passes 50 of 50 with -race -count=50; go test -race -tags nogui ./internal/gui/ ./internal/tui/ pass; vet, darwin, linux (nogui) and windows builds pass.
gui: lightweight mode, off by default, lets a closed panel's or window's webview go and makes it again when it is opened (somls on #580: GUI版可否增加轻量模式,关闭面板退出webview,减少资源占用) — closing the tray panel or the main window only hid it, so each kept its page and, on a Mac, its WebKit WebContent process (about 65 MB each at start, 95–100 MB after use) for as long as magpie ran. Settings → General now has Lightweight mode (轻量模式), shown in the app only (not on a browser's page) and kept as this computer's own through a sync or a restored backup (KeepOwn): with it on, a window found closed on two looks in a row 30 s apart (lightStep) is let go 30–60 s after it was closed, so a panel opened again soon after is still the same one at once. Letting go happens on the main thread under a new h.winMu: the window field is set to nil, the panel is unhooked from the tray icon (AttachWindow(nil); left click goes through magpie's own OnClick), and the main window is marked gone so its closing hook, which otherwise cancels a close and hides, lets it close; on the Mac the WKWebView is taken out of the window first (stopLoading, delegates and script handlers cleared, removed from its superview), since Wails' NSWindow close alone left the WebContent process alive for minutes. Opening it again (the tray icon, ShowMain, an import link, the Dock) makes the window anew with the options it was started with (makeMain/makePanel, moved out of Run with their hooks), its text size and title bar or Hyprland name (madeAgain), on Windows shown only once its page has loaded; every other path that reached h.main or h.panel (fit, tint, zoom, hide, glide, the Windows title-bar tint, the Hyprland click watch, the folder picker) now goes through the main thread and does nothing to a window let go; Linux keeps running with no window left (DisableQuitOnLastWindowClosed). Measured in a sandbox build under a temp HOME on other ports: started in the tray with the setting on, two WebContent processes of ~65 MB each; about a minute later both windows were let go and both processes ended (GPU and Networking, ~56 MB, are shared and stay; magpie's own process is unchanged), so the saving is ~130 MB idle and up to ~200 MB after use; the main window and the panel were then opened again with their pages, tinted and at the tray icon, closed, let go a second time, and magpie quit cleanly, with fit/tint/zoom calls every 0.2 s through a release crashing nothing. Windows and Linux build but were not run. TestLightStep and TestLightweightKeptAndOwn are new; lightweight.test.cjs (WebKit, en and zh: the row, off by default, On/Off posted as lightweight true/false, another setting's save keeping it, no scroll, no left-border accent, no row on a browser's page) is new and passes, as do click-scroll, settings-sections and searcher-pick. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
gui tests: a test run leaves no gui.test folder in the real ~/Library/WebKit or ~/Library/Caches (waitingsnow) (#1047)
usage: a custom balance's card says when it was read, shows each of its amounts on a line of its own and a percent as a meter (#420 by emo172: 余额卡片显示读取时间、百分比进度条) — a balance read from a provider's own Balance URL showed one line, "Balance $2.00 · Today $2.50 · Used 90%", and nothing said how old it was. magpie now hands the card the amounts the user's expression names, each with its label, and a "%" one's share; the Usage page shows each on a line of its own, the user's label quiet and the first amount the larger, "Balance" only where no label was given, and a percent as the quota meter, amber from 90%. Every card that was read says so, "Updated 3 minutes ago" (3分钟前更新), and one standing in for a reading that failed just now says "As of … — couldn't be read just now" (截至 …,暂时无法获取最新用量), allowance cards too. The menu bar panel's Balances show the same amounts and meter and an amber "As of" (截至), and the tray's tooltip adds "as of …, couldn't be read just now". One amount alone reads as it did, and the balance line, the CLI and the expression syntax are unchanged. No balance URL, path or site is added: /api/usage/token and unlimited_quota are one kind of relay's and stay the user's to write. TestBalanceParts, TestTrayUsageText and balance-parts.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; balance-parts, click-scroll, old-webkit, quota-families, panel-usage, tray-usages, balance-fix, usage-refresh, currency, claude-usage-asked pass.
provider: the hook a landed usage refresh tells is held atomically and told before the refresh is done (#1023) — OnSubscriptionUsage was a plain package variable that the background refresh SubscriptionUsage starts read after closing its pending channel, unsynchronised, while the tray sets it as it starts (watchTrayUsage) and a test swapped it: go test -race ./internal/provider failed in TestSubscriptionUsageServesStale when TestSubscriptionUsageReadingWhileStale's refresh, waited for by its pending, was still reading the hook after it (subscription_usage.go:185 against subscription_usage_test.go:84). OnSubscriptionUsage is now a setter over an atomic.Pointer, so setting it while a refresh is under way is safe in the app too, and the refresh tells it before closing pending, so whoever waits for a refresh has it finished, hook and all, and may change what it reads. The tray's hook only signals a buffered channel and never waits, so telling it first holds no one; the test's hook no longer blocks either. With -race -count=20 the two tests failed on main with the DATA RACE and pass now; go test -race -tags nogui ./internal/provider passes in full; go vet, gofmt -l, darwin, linux and windows (nogui) and windows builds pass.
settings: magpie's own update checks can be turned off, and run every 30 minutes, hour, six hours or day (#472: 更新行为可配置) — the app asked the feed five seconds after start and every six hours, a constant, and downloaded any newer version at once; with releases out most days, a version out just after a check waited up to six hours, and there was no way to stop the checks or the downloads they start (Update button only hides the pill). Settings → About has Automatic updates (on by default) and Check every (30 min, 1 h, 6 h, 24 h; 6 h with nothing set), kept in settings.json as noAutoUpdate and updateEvery, and magpie update auto [on|off] [30m|1h|6h|24h] sets them from the terminal. The updater reads them each minute and asks once the interval since the last check, its own or a click on Check, is up, and never by itself while off; the version row, not asked yet, then says automatic updates are off and offers Check. Check every stays in place, dimmed, while off, so the click doesn't shorten the page under it. TestUpdateDue, TestUpdateAskedAt and TestUpdateAutoCmd fail without the change, as does update-auto.test.cjs (Chromium, WebKit; en, zh). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; update-hide, update-check, whats-new, settings-groups, click-scroll, old-webkit pass.
update, gui: a new version Windows won't start is never left in place — tried before it goes in, and a restart into one that doesn't say it is up puts the running one back; that version isn't downloaded again by the clock, and Settings › About › Version says why (#894, hisiling) — on Windows 11 with Smart App Control on, the update put the new unsigned exe in place of the running one and restarted into it; CodeIntegrity (3033/3077) refused it, magpie never started again and the working version sat in .old. Now, on Windows, the downloaded exe is first started suspended with no window and ended at once (Windows checks a program when its process is created), and one refused stays out: the running exe isn't moved. At the restart the new process is given MAGPIE_STARTED, a file it writes first thing (AwaitPredecessor); the old one waits up to 20s for it and, if the start fails (ERROR_SYSTEM_INTEGRITY_POLICY_VIOLATION 4551, 1260 or any other), the new process quits before saying so, or it says nothing in time (then it is ended), puts the running exe back over the new one (the new one renamed .blocked if it can't go) and keeps running instead of quitting; nothing notes it as updated in the app. The version is noted in the config folder (update-blocked.json, this machine's only, never synced; gone once a version at least as new runs); the clock's checks leave it alone and show why, a Check clicked downloads and tries it once more. Settings › About › Version: for an application control policy, "0.1.x didn't start on this computer: Windows' Smart App Control (or another app control policy) blocked the new version, which isn't signed. magpie stays on 0.1.y. Download it and run it yourself, or turn Smart App Control off and check again", else the reason, with Download and Check; the error in full on hover. en/zh/ja/de. A user who double-clicks the refused exe is refused by Windows before magpie runs; only signing helps there. TestRelaunchRefusedPutsBack (4551 at the start), TestRelaunchSilentPutsBack (no word in time, killed; quit at once), TestRelaunchReportedStays, TestReportStarted, TestBlockedNote and TestUpdateThatWontStart (the restart doesn't quit, the note, the clock not downloading it again, a Check that does) are new and fail without the change; on Windows TestInstallBinaryRefusedStaysOut, TestTrialStart and TestStartCheckedPutsBackForReal (real processes: not a program, quits at once, runs on silent) pass on the Windows 11 test box with the whole internal/update package (Smart App Control itself isn't on there, so the refusal is simulated). update-blocked.test.cjs (Chromium and WebKit, en and zh: the reason and both versions, full error on hover, Download opens the release, Check downloads, the other reason, no select, no scroll, no left border) fails without the change; update-check, update-mirror, update-auto, update-wait and update-download-percent pass in WebKit; click-scroll's request-list case fails now and then the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
update, gui: Settings › About › Download source sets the update mirror, and a download that failed through it offers GitHub (#893, M3chD09) — the mirror updates could come through was only
magpie update mirror in a terminal, and the GUI never said where updates came from or that a failed download was the mirror's. About now has a Download source row: GitHub, or the mirror set (the same UpdateMirror setting the CLI writes); Use a mirror… takes a full https:// address (http, a bare host or a malformed one refused, with the reason in the row and the draft kept), Change edits it, Use GitHub takes it away in one click; Enter saves, Esc cancels, no click scrolls. POST /api/settings/update-mirror sets it; the Settings page's own save keeps it. A download through the mirror that doesn't answer, answers other than 200, or sends a file whose SHA-256 isn't usemagpie.ai's feed's is an update.MirrorError (never installed, the file removed, as before); /api/update's error carries the mirror, and the Version row says the download failed through that mirror and offers Download from GitHub, which takes the mirror away and downloads again. magpie names no mirror or CDN of its own. en/zh/ja/de. TestMirrorFailuresAreTheMirrors (tampered, 404 and closed mirrors each the mirror's, no file kept; a direct failure no mirror's; CheckMirrorURL) and TestUpdateMirrorSetting (set trimmed, http/bare/ftp/empty-host refused, Settings save keeps it, off, an updater failing through a 404 mirror reports it with retry, a new check clears it) are new; update-mirror.test.cjs (WebKit, en and zh: GitHub shown, http refused with the reason, https saved with Enter, Use GitHub posts "", a failed download through the mirror offers GitHub, which posts "" then update/install; no select, no left border, no scroll) fails without the change; update-auto, cli-path, cli-behind, settings-sections and click-scroll pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
gui: the update sheet names the newest release, not the one downloaded hours ago (#910, Moody-Sin), and Names & levels folds where it opened and opens folded (#868, Hu9956)
update: a restart to update waits until the gateway is idle instead of cutting off what the agents have in flight (#577: 正式版更新时保持网关请求与工具链不中断 — a Codex reply streaming through magpie, or a tool chain under way, was cut off by Restart to update) — the app serves the gateway in its own process and a restart to update is install, quit, relaunch, so whatever was in flight ended with it: a stream mid-reply, and a Claude Code turn of the subscription bridge waiting on its caller's tool results, whose Claude Code lives only in this process. The gateway now says what a restart would cut short (Server.Busy): its requests in flight (all but GETs and the bridge's own MCP callbacks), the bridge's runs parked on tool results, and when the last request ended. Restart to update (Settings → Version, the header's Update pill, the tray's item) restarts at once only when nothing is in flight, no turn waits on tools and nothing ended in the last 10 s (a tool chain's gap, the agent running a tool between two requests); otherwise it waits, saying what for: Settings shows "restarts once the gateway is idle · 2 in flight · 1 awaiting tool results" (网关空闲后自动重启 · 2 个请求进行中 · 1 个工具调用待返回) with Restart now and Cancel, the pill reads "Waiting to update" (等待更新) and its next click (not a double click's second half) restarts now, the tray item becomes "Restart Now to Update (3 in flight)" (立即重启以更新(3 个进行中)). The wait asks the feed again once idle, so versions out meanwhile collapse into the newest, downloaded before the restart; it gives up after an hour, saying so, the update then going in at the next restart or quit. A gateway served by another magpie (magpie serve, an older one) is not this one's to wait for. Handing the gateway over to the new version instead (the dev handover,
e0c6892) is not used for releases: it rests on SO_REUSEPORT, which Windows lacks, and the new app would have to start beside the old one (single-instance, the Mac's bundle swapped under it). TestBusy (a stream counts until it ends, a GET never, a parked bridge run until it is answered or ends), TestRestartToUpdateWaitsForTheGateway (fails without the change: the click restarted with 2 requests in flight), TestRestartWhenIdle (not while streaming, parked or in a tool chain's gap; then once; an hour's wait gives up), TestRestartWhenIdleIntoTheNewest (0.1.11 staged, 0.1.12 and 0.1.13 out while waiting: restarted into 0.1.13) and TestTrayRestartNow are new; update-wait.test.cjs (WebKit, en and zh: what is in flight, waiting with Restart now and Cancel, counts following the gateway, the hour's give-up, the pill's wait and restart now, an idle click restarting at once, no click scrolling, no left border) fails without the change and passes with it. update-hide, update-check, sync-update, update-auto and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside; passes on rerun).
update: a restart to update waits until the gateway is idle instead of cutting off what the agents have in flight (#577: 正式版更新时保持网关请求与工具链不中断 — a Codex reply streaming through magpie, or a tool chain under way, was cut off by Restart to update) — the app serves the gateway in its own process and a restart to update is install, quit, relaunch, so whatever was in flight ended with it: a stream mid-reply, and a Claude Code turn of the subscription bridge waiting on its caller's tool results, whose Claude Code lives only in this process. The gateway now says what a restart would cut short (Server.Busy): its requests in flight (all but GETs and the bridge's own MCP callbacks), the bridge's runs parked on tool results, and when the last request ended. Restart to update (Settings → Version, the header's Update pill, the tray's item) restarts at once only when nothing is in flight, no turn waits on tools and nothing ended in the last 10 s (a tool chain's gap, the agent running a tool between two requests); otherwise it waits, saying what for: Settings shows "restarts once the gateway is idle · 2 in flight · 1 awaiting tool results" (网关空闲后自动重启 · 2 个请求进行中 · 1 个工具调用待返回) with Restart now and Cancel, the pill reads "Waiting to update" (等待更新) and its next click (not a double click's second half) restarts now, the tray item becomes "Restart Now to Update (3 in flight)" (立即重启以更新(3 个进行中)). The wait asks the feed again once idle, so versions out meanwhile collapse into the newest, downloaded before the restart; it gives up after an hour, saying so, the update then going in at the next restart or quit. A gateway served by another magpie (magpie serve, an older one) is not this one's to wait for. Handing the gateway over to the new version instead (the dev handover,
e0c6892) is not used for releases: it rests on SO_REUSEPORT, which Windows lacks, and the new app would have to start beside the old one (single-instance, the Mac's bundle swapped under it). TestBusy (a stream counts until it ends, a GET never, a parked bridge run until it is answered or ends), TestRestartToUpdateWaitsForTheGateway (fails without the change: the click restarted with 2 requests in flight), TestRestartWhenIdle (not while streaming, parked or in a tool chain's gap; then once; an hour's wait gives up), TestRestartWhenIdleIntoTheNewest (0.1.11 staged, 0.1.12 and 0.1.13 out while waiting: restarted into 0.1.13) and TestTrayRestartNow are new; update-wait.test.cjs (WebKit, en and zh: what is in flight, waiting with Restart now and Cancel, counts following the gateway, the hour's give-up, the pill's wait and restart now, an idle click restarting at once, no click scrolling, no left border) fails without the change and passes with it. update-hide, update-check, sync-update, update-auto and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside; passes on rerun).
gui: the update sheet names the newest release, not the one downloaded hours ago (#910, Moody-Sin), and Names & levels folds where it opened and opens folded (#868, Hu9956)
upstream: the vendors' own status pages say when their API is down (#971, emo172: an Anthropic or OpenAI outage looked like a sign-in or quota problem) — a provider that calls Anthropic's, OpenAI's or Moonshot's API (by its base URL's host: api.anthropic.com, which the Claude subscription calls too; api.openai.com and chatgpt.com, Codex's; api.moonshot.cn/.ai) carries "API outage" (red), "API degraded" or "API maintenance" beside its name on its Providers row and its Usage card while the vendor's page says so; its title names the parts and the open incidents, and a click opens the incident (or the page), not the row. internal/upstream reads each page's Statuspage-style summary.json (status.claude.com; status.openai.com, incident.io's; status.moonshot.cn), checked by hand on 2026-10-06, and only the API's own parts (Claude API and Claude Code, not claude.ai; Chat Completions, Responses, Codex, Codex CLI, not "Codex in ChatGPT Desktop", which was degraded that day); an incident naming no parts (OpenAI's) counts only while those parts are affected. Pages are read only when the Providers or Usage page asks (GET /api/upstream), at most once in five minutes, never on a request's way; a page that fails, isn't JSON or no longer lists the API's parts is unknown and marks nothing. A relay serving the same models is never marked. DeepSeek's page (Flashduty) has nothing readable and is only known for its link; OpenRouter, xAI, Mistral and z.ai have no page read. The gateway's failure messages, TUI, CLI and tray aren't changed. zh/ja/de added. upstream_test (fake pages from the real summaries: API down, claude.ai down isn't the API, Claude Code degraded, OpenAI's app isn't the API, 503/HTML/changed page unknown, read once in Fresh and Of never waits, hosts; -race -count=5) and gui's TestUpstreamRoute pass; upstream-status (Anthropic down: its provider and the Claude subscription marked, relay/OpenAI/unread Moonshot not, click opens the incident not the row, all well marks nothing; 900 and 440px, the badge whole, the name giving way) passes in Chromium and WebKit, en and zh, and fails 8/8 without the app.js change. i18n-keys, gui-ja, gui-de, click-scroll, account-return, account-models, usage-keys-fold, deprecated-subscriptions and add-sheet pass; go vet, go test ./internal/gui/, linux/windows/nogui builds pass.
usage, gateway, gui, provider, cli: the tests pass when a run crosses midnight, and a usage answer reads the clock once (#1046)
usage, gateway, gui, provider, cli: the tests pass when a run crosses midnight, and a usage answer reads the clock once (#1046)
usage, gateway, gui, provider, cli: the tests pass when a run crosses midnight, and a usage answer reads the clock once (#1046)
web: behind a reverse proxy, magpie web prints MAGPIE_PUBLIC_URL as the page's link (#372) — with MAGPIE_PUBLIC_URL=https://magpie.example.com the link for other machines was http://magpie.example.com:3430/?k=…, its host on the page's port over plain http, which nothing answers. A public URL with no port of its own is now the link, scheme and path kept; one with a port (a container's published ports, http://<host>:3425) still gives that host on the page's port. TestNetworkLinksPublicURL fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
web: MAGPIE_WEB_KEY keeps the page's key across runs — for magpie web kept running as a service, where a new key each start signs every browser out; with it the cookie outlives the browser session too (400 days, Chrome's most). Unset, each run makes its own key in a session cookie, as before. A key under 16 characters is refused. (#109)
portable: a portable magpie on Windows keeps WebView2's profile in data\webview2 and makes no Start-menu shortcut, and an installed one makes the shortcut once and never puts back one the user deleted (#508, suixinex: 便携模式时还是会创建 C:\Users\<username>\AppData\Roaming\magpie.exe\ 文件夹; 开始菜单的快捷方式删除后再打开magpie会重新添加) — the AppData\Roaming\magpie.exe folder is WebView2's user data folder: Wails v3 passes WindowsOptions.WebviewUserDataPath to go-webview2 as its DataPath, and left empty it is %APPDATA%\<exe name>, which portable mode (appdir) never set. The app's Windows options (windowsOptions) now give it appdir.WebView(): data\webview2 when portable, "" (WebView2's default, so the pages' storage and cookies there are kept) when installed. The shortcut came back because shortcut.Ensure ran at every start of the desktop app and wrote Magpie.lnk whenever it was missing or stale, with nothing recording that it had been made. Ensure now does nothing when portable (no shortcut, no App Paths entry); installed, it makes the shortcut on the first start and records that in start-menu-shortcut in magpie's folder (with the exe), keeps one that is still there pointed at the exe (moved exe, update), and makes none once the file is recorded and the shortcut is gone. A user who deleted it under an older magpie isn't given it back on updating either: a missing shortcut with no record but an App Paths\magpie.exe entry already there (every older magpie wrote it at each start) counts as deleted and is recorded as such. The Start menu, the shortcut writer and App Paths are behind function variables, so the decision is tested with fakes on any OS. Checked for other leaks: autostart only refreshes an entry the user turned on (Refresh never enables), the magpie:// link registration is needed for import links and stays, as appdir's doc says, as do temp folders; the update stages beside the exe, caches already follow appdir. Not changed: the Mac's WKWebView and Linux's WebKitGTK keep their own data in the profile (Wails has no option for it), which #508 didn't report. Nothing ran on Windows (the test box isn't reachable): no real WebView2 was started with the data path and no real shortcut was made or deleted. TestEnsureOnce, TestEnsureAfterOlderMagpie, TestEnsurePortable (fake Start menu in a temp folder), TestWebView and TestWindowsOptionsWebViewData fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet ./..., GOOS=windows go test -c for shortcut, gui and appdir, go test -tags nogui -count=1 ./... under a temp HOME pass (TestUsageKeepsTimeToFirstToken and TestUserPath failed once on timing, pass rerun alone; untouched).
site, gui: release notes survive GitHub limiting usemagpie.ai, and a failure to get them says so, apart from a release without any (#661) — /api/notes answered 503 {"error":"no releases"} with Cache-Control public, max-age=300 whenever GitHub's API limited the worker's shared IP, and /api/latest fell back to the release page's redirect with notes "" (kept 300 s too), so the update window had nothing to show. The worker now: never keeps an error (503 is no-store); takes the notes from GitHub's releases feed (releases.atom, not API-limited) when the API says no, turning its HTML back into the markdown the app draws and putting back the <!-- lang:zh --> marker GitHub strips (before the first Chinese heading after an English one); keeps the last whole answer for 30 days in the edge cache and serves it (merged with the feed's newer releases) when GitHub fails; keeps a degraded answer 60 s instead of 300; and sends a GITHUB_TOKEN secret to the API where one is set (none is yet). In the app, /api/whatsnew says error (and the release page's url) when the notes couldn't be had, and Settings' What's new opens a dialog saying "Couldn't load the release notes" with Try again and "Open the release page" (through the app), or "No release notes provided." with the release page and no retry for a version with none, instead of a passing toast; en/zh. Tests that fail on the old code and pass: site/worker.test.mjs (API limited: notes from the feed in either language, briefly kept; all of GitHub down: 503 no-store, then the last whole answer; the token sent), TestWhatsNewTellsFailureFromNone, and whats-new-failed.test.cjs (WebKit, en/zh: failure with retry and link, retry showing the notes, none without retry, no click moves the page). The feed parser was tried on the real releases.atom (v0.1.730–739: English and Chinese split as the API's bodies are). go vet, builds (darwin, GOOS=linux, GOOS=windows), go test -tags nogui -count=1 ./... under a temp HOME, node --test site/ and whats-new.test.cjs pass.
usage, gateway, gui, provider, cli: the tests pass when a run crosses midnight, and a usage answer reads the clock once (#1046)