Files
…
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).
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.
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.
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)
sync: a backup on a slow line is no longer cut off at 2 minutes; a server that stops moving is (#657, irx999: a 43 MB backup with many skills to a WebDAV server on a 2.4 Mbps VPS, 305 KB/s, needs 2.4 minutes and every sync failed) — a sync had a fixed deadline in all (2 minutes from the gateway's loop and magpie webdav/s3 sync, 90 s from Settings › Sync now), so a large backup on a slow line never got through, however steadily it moved. WebDAV and S3 requests now go through a watchdog instead (stall.go): the backup's GET and PUT fail once nothing has moved either way for 60 s, each byte of the body sent or read putting it back, and after a PUT's body is all handed over the answer gets 60 s on top of as long as the write took (at least 4 minutes), for what still sits in the system's buffers to leave; the other requests (PROPFIND, MKCOL, HEAD, MOVE, DELETE) have 30 s in all. A stall says so ("the server sent and took nothing for 1m0s (PUT)"). Sync now and the CLI have no deadline; the gateway's loop keeps an hour as a backstop; a wait for another magpie's sync stays 30 s. TestSlowTransferIsNotCutShort (20 MB up and down at a steady trickle, over 5 times the watchdog each way), TestStalledTransferFailsPromptly (a server stopping mid-PUT and mid-GET fails within a second) and TestMetadataRequestHasALimit fail on the old code (no watchdog: the stalled ones hang) and pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against a real slow WebDAV server.
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
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.
fx: a rate fetched behind a page for another home sets nothing in this one, and the gui tests ask no rate of the network (follow-up to #541) — fx.Soon asks for a stale rate behind the page; in the gui tests a state read with no rate cached so reached open.er-api.com, and its answer, landing seconds later, was taken as the rate by whichever test ran then (TestFXStateDoesNotWait saw the live 6.71 where its fake endpoint said 7.31). A fetch whose cache path is no longer the one in force now returns without touching the in-memory rate or the failure time, and internal/gui's TestMain points the rate at a closed local port. go test -tags nogui ./internal/gui ./internal/fx pass three runs of four (the fourth, TestFXInState's TempDir cleanup finding .cache not empty, the flake it had before).
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: 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.
…
routing: the groups are put in an order of the user's, as the Agents page's rows are, and /v1/models lists them so (#779)
…
library: a repository's skills are turned on or off for an agent at once from its group's heading (#787, mintonight: 我记得昨天还能直接一次性开关一个作者的所有技能包,怎么今天不行了) — the heading of a repository's group (owner/repo, N skills) showed which agents had its skills as plain icons, which looked like the rows' chips but did nothing, so its skills could only be switched one row at a time (or every skill in the library at once). The icons are now chips, as on a row, with All ahead of them: a chip is lit when its agent has every one of the group's skills, half lit (dashed, the icon dimmed) when it has some, and a click gives that agent every skill of the group or, when lit, takes them all; All gives them to every agent shown, or takes them from every one when all have them. Other groups' skills and agents not clicked keep what they have; a click doesn't fold the group. The page sends the group's skill names to a new library/skills/agents-some, library.SomeSkillsAgents, which is EverySkillAgents for the skills named alone (one write; a name not in the library is refused). zh and ja strings. TestSomeSkillsAgents (on and off for named skills only, the agents' folders follow, an unknown or empty name list refused) and group-skill-chips.test.cjs (WebKit, en and zh: lit/half-lit, a click on a half-lit chip, a lit chip, All on and off, the requests sent, the group not folded) — the latter fails on the old code. gui-ja, list-sort, own-skill, shared-skills, every-skill, library-all, library-toggle-status, library-items, remove-all-skills, import-all-skills, library-row-open-scroll and click-scroll pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and 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: 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.
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.
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.
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: the Providers tab answers at once, the accounts' lists asked for behind it (#541: with 200-odd models the Agents and Providers tabs showed placeholders for ten seconds and more at start-up) — GET /api/providers ran provider.FetchNew(8s) before answering, which asks each signed-in account without its vendor's list yet, one after another, up to 8 s each, under newFetches' lock; at start-up that lock is held by startBackend's own FetchNew(20s), run after the models.dev sync, so the page waited for every account start-up was asking, each up to 20 s, and an account whose list can't be had (cursor-agent hanging, a vendor out of reach) was waited on again a minute later. The page now starts FetchNew in the background (provider.FetchNewBehind, at once unless one it started still runs) and is told fetching (provider.FetchingNew, any FetchNew under way); while it is, the page asks again every 2.5 s, draws the answer only when it changed, never under an open editor, and stops once the lists are in. A sign-in done in magpie still waits for the new account's list (GET /api/providers?wait, #204), as the editor opens on it. TestProvidersAnswerWhileListsComeIn (a cursor-agent taking 4 s over its models: the page answers within 2 s, told fetching, and lists them once in) fails without the change ("the page waited 4.02s"); providers-fetching.test.cjs (WebKit) fails without the page's asking again. The Agents tab's wait (GET /api/state) was not reproduced: with 240 models over 13 agents state() takes ~50 ms here; currentFX can hold it up to 4 s when the CNY rate is stale, left as it is. 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; Chromium's launch fails here, WebKit only.
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.
system one: a decision provider's own models-list URL is asked as given, and its models can each be tested (Discord, ARNO: OpenRouter added as a System One provider with the list https://openrouter.ai/api/v1/models?limit=500&output_modalities=decisions didn't get its models, and a model typed in by hand couldn't be tested from its right-click) — a System One-only provider went to fetchDecide before its ModelsURL was read, so magpie asked …/models, which at OpenRouter lists the 466 chat models, and kept only Jev's ids of those: typesafe/jev-router alone. With a ModelsURL, fetchDecide now asks it as written, query included, and keeps every id in it, since that list is of decision models already (OpenRouter's answers 13: pplx-decider, liquid/d1, cloudflare/clef, respan/span-01, typesafe/jev-1.13 …). Without one nothing changes, Bailian included. Testing a decision model was refused outright (the chip's Test this model off, Test models leaving them out); where the API takes System One's own body (System One, TypeSafe, OpenRouter, Vercel's TypeSafe API) each model is now sent AskSystemOne's yes-or-no question, the one Detect already asks, and the editor offers Test for it (providerJSON.decideTest); Vercel's evaluation models and Workers AI, which wrap it their own way, still say why not. TestDecideModelsURLAndModelTest (a fake OpenRouter: the plain list, the decisions list, a hand-added model through /systemone, an unknown one failing) and TestModelTest's Workers AI case fail on the old code; model-test-why.test.cjs gains a hand-added System One model tested from its chip and Test models, and a Workers AI one still off (WebKit en and zh pass, with model-test-one and model-copy-id; Chromium doesn't launch in this sandbox). OpenRouter's live decisions list returns the 13 models. Not tried with an OpenRouter key: the /systemone question itself. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
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.
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: 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.
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.
…
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: 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).
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).
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.
gui: What's new no longer comes up by itself after "Restart to update", and its dialog has "Don't show again today" (#525: 现在只要升级过,下一次打开窗口就会先弹一屏 What's new……点"重启以更新"之后也一样……今天到现在已经发了 22 个版本……希望改成:只在首次启动时展示更新内容;打开应用、点"重启以更新"之后,默认不弹;弹窗上加一个"今天不再弹出"的勾选) — the dialog came up on the first window of every version newer than last-version, the one the user had just restarted into from magpie included, so with releases many a day it was in the way of almost every open. A restart to update (restartToUpdate, once the new version is installed) now leaves the version it installed in magpie's folder (updated-in-app); that version's start finds it, removes it, and keeps its notes to Settings' What's new row, which still shows everything since the version last run. An update that came some other way (installed on quitting, or by hand) still shows the dialog once, on its first start; a note for another version is no reason to keep quiet, and a build from source leaves it alone. The dialog shown by itself has a "Don't show again today" box (今天不再弹出), posted to the new POST /api/whatsnew/today as it is ticked or unticked, so Close, Escape and a click outside all keep it; the local day it was ticked on is kept in whatsnew-quiet-day, and until the day turns nothing comes up by itself; Settings' notes have no such box. TestWhatsNewQuietAfterRestartToUpdate (a restart to update through restartToUpdate notes the version, its start shows nothing while Settings has the notes, an outside update shows, another version's note doesn't count, quiet today and not the next day, unticked) and TestWhatsNewTodayRoute fail without the change; whats-new.test.cjs (WebKit, en and zh) now ticks, unticks and ticks the box and checks each is posted without moving the page, and that Settings' notes have none, and fails without it. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; whats-new, update-check, update-auto, update-hide pass in WebKit.
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.