Tested with TestPiThinkingLeavesTheProvider: a hand-edited contextWindow and another provider stay byte for byte through an unchanged level, a changed level and Default; the level itself changes or clears, and Default again does not rewrite the file. gofmt, go vet -tags nogui ./internal/agent/ and go test -tags nogui ./internal/agent/ pass.
.github/workflows/ui-preview.yml, on pull_request_target or by hand (-f pr=N): detect (internal/gui/assets/, the GUI's non-test Go, or the ui-preview label) puts a "recording" note in the block; record, behind the ui-preview environment's approval since PRs come from forks, builds the PR's magpie (nogui), runs `magpie web` in a fresh home with DeepSeek added and a few requests sent through the gateway, and has DeepSeek plan from the diff and the pages' real outlines what to click and capture (record.mjs, Chromium at 2x, cursor.js drawing the mouse); publish, which never runs the PR's code, puts the files on the ui-previews branch served by Pages and rewrites only the block between <!-- magpie-ui-preview:begin/end --> — the video's poster links to its player page, since GitHub embeds no video it didn't host. Captions say what is being done, never what should come of it. Anything on screen matching the key or the web link's key throws the whole recording away.
Tested locally with run.sh on 033a151's diff in a sandbox home: 3 screenshots and a 16 s mp4 of the Codex model list's 全部隐藏/全部显示, pointer, trail and clicks visible; actionlint passes. Not yet run on GitHub.
- #268: the editor's Duplicate opens Add filled in from the provider ("X copy", a new id, its URLs, models, headers, balance and proxy); AddCopy takes the source's keys, routing, balance token and Zhipu team unless a new key is pasted, the same-key refusal skipped for it. Signed-in accounts can't be copied.
- #263: unticking the account Codex or Claude Code is signed in to pauses it while another account is on (logins.json `paused`): the gateway passes over it and the agent stays signed in, so its remote control keeps working. With no other account on it is used again; switching accounts clears it. The row reads Paused, dimmed; ticking resumes it.
- #266: {key}, {apiKey} or {api_key} in the Balance URL or a header is each key's own (URL-escaped in the URL), so every key shows its own balance; a network error naming the key shows it masked.
Tested with TestAddCopy, TestCodexPausedOwnAccountPassedOver, TestBalanceURLNamesTheKey (each fails without its change) and duplicate-pause.test.cjs in Chromium and WebKit, English and Chinese (6 of 8 fail on the old app.js); balance-fix, add-sheet, click-scroll pass. go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried with real accounts.
A failed check-in always waited wbCheckinRetry, and the loop only looked every 30 minutes. A failure whose request got no reply at all (a net.Error: no name lookup, no connection, no answer in time) is now kept as offline with a count of such tries that day, and waits a minute, doubling each time up to 30; one WorkBuddy answered (a 500, a refused token) still waits 30 minutes. The loop's decision is now wbCheckinLoop.tick, which also looks when a failure comes due. Records written before read as an ordinary failure. Tests: TestWorkBuddyCheckinOfflineRetriesSoon (network down at the first tick after waking, credits claimed by the same loop a minute later; with the network down throughout, tries at minutes 0, 1, 3, 7, 15, 31, 61, 91 — it fails with the 30-minute wait), TestWorkBuddyCheckinRetriesAfterFailure now with an HTTP 500 for the 30-minute case. go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried on a real sleeping Windows machine or account.
tokenFloor read the error from the body magpie had re-encoded for the agent, where Go's JSON encoder had escaped > as >, so the >= and > wordings never matched and the request wasn't retried. The pattern now also takes the escaped sign (a vendor may send it that way too), a trailing = meaning the number is the floor itself; and writeJSON no longer escapes <, > and &, as withFields already didn't, so the agent sees the vendor's words as written — the same JSON either way. Tests: TestTokenFloor gains the escaped and > cases; TestTooFewTokensSaidWithSigns goes through the gateway on /v1/messages (streaming) and /v1/chat/completions with Command Code's wording (asked again at 16), "max_tokens must be > 2" (at 3) and a vendor that sends > itself. Both fail without the change (a 400 after one request). Not tried against Command Code itself.
index.html named no icon, so `magpie web` showed the browser's blank page icon. It now links icons/favicon.svg, magpie's bird drawn dark, or light when the system is dark, as a tab bar would show it. Checked by rendering it in Chromium in light and dark schemes; go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows), go test -tags nogui ./internal/gui/... and mask-emails.test.cjs pass.
On Windows the system's grey title bar sat over the page as a strip of its own, and stayed light over a page set to dark; at the window's first width Claude Code's name was cut to "C." beside a full "Update to …" pill. Now the main window opens with its title bar and background in the page's --bg for the saved theme (Wails' Windows CustomTheme, #f4f4f6 / #1a1a1e), and the page reports the colour it shows (POST /api/window/titlebar) at start and after every change of theme, system or chosen, for Go to paint the caption with DWM and switch its buttons dark or light; elsewhere nothing changes. The agent row is a size container: under 250px the pill shows only its arrow (its words kept as its aria-label), under 150px the version goes, and the name gives way last.
Tested on Windows 11 (sandboxed HOME): with theme light and dark, the bar and the page are one surface, caption buttons in the matching colour, the name whole. cli-update.test.cjs gains a zh subtest at 980/620/540px (fails on the old CSS). The GUI suite passes in Chromium (settings-groups is flaky on HEAD too), go vet, go build (GOOS=windows, GOOS=linux -tags nogui, dev) and go test -tags nogui ./... pass.
ChatGPT Desktop 26.924.22138 is the Codex app renamed (bundle com.openai.codex, codex-cli 0.158.0-alpha.2.1): it reads ~/.codex and honours openai_base_url, model_provider and chatgpt_base_url; the error text comes from chatgpt.com. Codex keeps a thread on the provider it was started on, so an old ChatGPT conversation reopened while magpie is Codex's provider (signed out, allowance used up, or the sign-in set to api) stays on the built-in provider, and magpie removed openai_base_url in that mode, so a magpie model picked in it went to chatgpt.com, which refused it. Provider mode now sets openai_base_url to magpie's gateway too (whose /backend-api/codex serves "provider/model" and passes Codex's own models on), unless the user has one of their own; picking one of Codex's own models or a reset still removes magpie's. Sync adds it to configs an older magpie wrote in provider mode, when the catalog file is magpie's. Verified by driving ChatGPT Desktop's bundled `codex app-server` in a sandbox HOME with a fake ChatGPT auth.json and fake servers: before, a thread started on the built-in provider and reopened with model_provider = "magpie" went to the built-in base URL; now it reaches magpie's /backend-api/codex/responses, and a new thread /v1/responses. Tests: codex_thread_test.go (TestCodexProviderAlsoSetsBaseURL signed out and api, TestCodexProviderKeepsOwnBaseURL, TestCodexSyncAddsBaseURLToProvider; the first and third fail without the change), codex_route_test.go updated for the base URL in provider mode. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried in the real ChatGPT Desktop window or with a real account; a thread already open in a running app keeps the settings it was loaded with, and cloud threads never read the local config.
#257: in a skill's source line the "linked from" / 链接自 label could shrink, and Chinese breaks after any character, so a long path squeezed it into a column of single characters; the line grew tall, and skill rows in a long list have a fixed height with overflow hidden, so the name was clipped and the path cut. The label now stays whole on one line and the path gives way with an ellipsis; long names and descriptions break anywhere rather than widening the row. Icons: each agent's icon was made once and copied onto every row, so if the first copy's picture failed to load every copy stayed empty until a reload. An icon whose picture fails is dropped from that cache, a drawn one asks for its picture again (up to 3 times, growing delays), and the Library's icons are built apart from the Providers page's recycled ones. Problems: the Library only showed a problem as an amber dot on a chip, with nowhere for an agent's instructions, its whole MCP file, a removed server, a project, or an agent hidden from the page, and the failures of applying a profile were never shown although the toast pointed there. GET /api/library now returns every standing problem, and a card above each tab's content lists them (amber dot, no stripe): which agent or project, what failed, the error, with Try again re-running all/sync; it updates in place when a chip is toggled. en and zh. Tested with a new library-items.test.cjs, Chromium and WebKit, en and zh (18/18): long names, links and descriptions inside their rows with the label on one line in light and dark at 980 and 560px, the card's lines and errors, Try again posting all/sync without moving the page, a chip click updating the card with the row in place, Claude Code's icon appearing after its picture fails for the first 300ms; all fail against the old library.js/css. click-scroll, old-webkit and shared-skills pass. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestClaudeSignedOut timed out under load and passes alone). Not tried in the real Wails window; the icon's first failure is simulated, its real cause not reproduced.
Codex sends every MCP tool's full schema inline in `tools` unless the model's catalog entry says supports_search_tool (core/src/tools/spec_plan.rs search_tool_enabled; mcp_tool_exposure.rs Deferred vs Direct); its own gpt-5.5/5.6 entries say so, magpie's generic entries didn't, so with a ChatGPT sign-in's apps a turn began at ~190K tokens. The generic entries now set supports_search_tool: true; base_instructions, the context window and the rest are as they were, and code mode and Responses Lite stay off. The gateway serves the search to models that don't know it: for a model it translates for (Chat, Anthropic…), the client-run tool_search is offered as a function, earlier tool_search_call/tool_search_output become a call and its result ("These tools are now available to call: …"), the tools found are offered after those sent (once, namespaces merged), and a call to it goes back to Codex as the tool_search_call Codex runs, streamed or not. A request relayed to another Responses API (not the ChatGPT backend, which runs it as sent) is rewritten the same way, the found tools' defer_loading dropped, and its stream line by line (toolsearch.go). Measured with codex exec in a sandbox HOME against a fake Responses backend and a fake MCP server with 150 tools: the first request's body went from 307,198 to 23,580 bytes. Tested with toolsearch_test.go (translated, streamed and rendered, end to end through a Chat fake and a relayed Responses fake, merging, the stream split mid-line) and TestCodexCatalogToolSearch (search on, prompt, context window and the other fields unchanged). go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with real Gemini/antigravity calling the search, real ChatGPT apps, or real third-party Responses providers; a non-streamed relayed reply isn't rewritten (Codex always streams).
droid (0.229.0, still the latest) asks /api/cli/whoami about each token it holds and sends the orgId it answers as X-Factory-Org-Id on every request (its auth's Do → _r → Pr/Ar, Ci() spread into xc). Accounts signed in with v0.1.432, or whose org had been dropped after a refusal, kept none, and magpie never asked whoami again after renewing a token, so their requests went with no org and Factory answered its generic 403 {detail:"Forbidden"}. Now an account with no org asks whoami before its request (at most every 10 minutes per account), each renewal asks again, an org whoami refuses is left off and whoami asked without it, and the org, region and whoami's premBaseHostV2 (an org's own host, where droid sends model requests) are kept. whoami goes with droid's three headers only (Authorization, X-Factory-Whoami-Extended, the org). Claude requests carry X-Api-Key: placeholder, as droid's Anthropic client does. A 403 on an account that sent no org asks whoami and retries once; one that stays tells the user to check droid on the same account and model and to sign in again (Provider.Explain, in the gateway's three error paths). Tested with TestFactoryForbiddenWithoutOrg against a fake Factory refusing requests without the org header, the account saved as v0.1.432 did: glm-5.2 succeeds with the exact headers, whoami asked once with droid's headers, a 403 while whoami was down asks again and retries, the enterprise host used, the explanation on 403s only; it fails with the lookup off. The existing Factory tests are updated for the headers and org. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against Factory's real server or tasselx's account.
In a routing group whose first member (a Codex account) had run out of its allowance, a turn went to the next member; when that one failed passingly (busy, a 5xx, a timeout) and the members after it were also spent, the turn ended on the last member's quota error, and Codex, seeing "usage limit reached", stopped taking input until restarted. Now serve() keeps the first failure that is not about allowance, and when every candidate after the current one rests on quota or credit (spentAfter), a passing failure is asked again on the one that may answer, with the same waits as the last candidate gets. If the turn still fails and its last failure was a spent allowance while an earlier member failed otherwise, the agent is told that other failure (its status and error), so it retries instead of thinking the account is out. Tested with codex_quota_group_test.go: a spent Codex moves to the next group member, to Grok with sealed reasoning kept out, Grok busy once is asked again and answers, and Grok failing throughout is told as Grok's error, not Codex's quota; the last two fail before the change. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath timed out under load and passes alone). Not tried with waroy's real accounts or the real Codex app.