Files
yetone ddd4f43144 provider: a routing group can be duplicated, from its menu on the Routing page or with magpie group copy (lc on Discord)
provider.CopyGroup(id, name) saves a copy of the group as it is now: its
models and patterns, routing, stays, rules, classifier, effort, context,
levels, and fast and off members. The copy is deep, so its rules' time
windows aren't shared. Its name is the one given, or "<name> copy", with a
number added past a name in use. Its id comes from the name, numbered past
any id in use, removed ones included. It is listed right after the group
it copies. The copy is a group of the user's and switched on, even when
it copies a found group or a switched-off one. The original is unchanged.

Surfaces:
- GUI: a group row's menu (right-click, or a click on its logos) has
  Duplicate under Move up and Move down. It posts /api/groups/copy with
  the id and t("{name} copy"), so the name is in the reader's language.
  It asks before discarding an editor with unsaved changes. The status
  then says "Duplicated {name}". No click scrolls the page.
- CLI: magpie group copy <id> [name]; duplicate, dup and clone work too.
- zh, zh-TW, ja and de strings. docs/reference.md and the site's start
  page (en, zh, ja) list the command.

Run:
- TestGroupCopy (internal/gui): the copy has the models, pattern,
  routing, rule and off member, is on, and is listed after the original.
  The original stays off and unchanged. A second copy is "Fast copy 2" /
  fast-copy-2. An unknown id is refused.
- TestGroupCopyCLI (root): the default name, a given name, a found
  group's copy as the user's, and the errors.
- group-duplicate.test.cjs: Chromium and WebKit, en/zh/zh-TW/ja/de at
  420px, 10/10. It fails 10/10 on the old routing.js. Siblings
  group-arrange, removed-groups-menu-scroll, group-bulk-remove and
  click-scroll pass 36/36.
- Sandbox CLI (sandbox HOME): group copy and duplicate made "DupTest copy"
  and "DupTest copy 2", listed after DupTest.
- gofmt -l clean. go vet darwin/linux/windows on the root, provider and
  gui packages: ok.
2026-10-10 15:08:14 +08:00
..
providers: a provider's Routing is picked in its editor and made with its Save, and its Stays (会话保持) is beside it (01huadalang on Discord, a WorkBuddy AI subscription with several accounts: 在这里切换会自动保存,没有手动点击就给我保存了,这样切换来看会很卡; 没有看到会话选择) — each click of Smart / In order / In turn / Least used first / Weekly pace posted provider/route, saved, and redrew the providers page with the editor in it, so looking through what each option says lagged and saved a pick only looked at. Routing is now staged in the editor's draft as Names & levels is (b0dc82c4): a click shows the option and its description, says "unsaved" (未保存) beside it, sends and redraws nothing, Cancel drops it, and both editors' Save (a signed-in account's and a key's) carry it. How long a conversation stays with the account or key that answered it (Auto / Session / Within a turn / Off, provider.Affinity, which the gateway already honours for a provider's own accounts) could be set only on the Routing page's "Several accounts or keys" row; it is now in the editor under Routing, staged the same way. The save action takes routing and affinity when sent and keeps them when not: before, every Save of the editor dropped the Stays set on the Routing page, since the editor never sent it and in.Affinity came in empty. routing-save.test.cjs (WebKit and Chromium, en and zh, a subscription with three accounts and a provider with two keys: every option clicked shows its description and unsaved with nothing sent, redrawn or scrolled; Cancel drops them; one Save carries routing "rotate" and affinity "session") fails without the change and passes with it; account-arrange.test.cjs now saves the routing it picks. TestProviderSaveRouting (the Save's picks made, a Save without them keeps them, the Routing page's route and affinity survive the next Save, Smart and Auto sent as "") fails without the change (routing lost). keep-login, provider-levels-save, provider-levels-scroll, click-scroll, editor-short-window, provider-proxy, provider-concurrency, account-proxy, account-models, plugin-move, deprecated-subscriptions, routing-account-order pass in WebKit (redact-rule-row's 640px case fails 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.
2026-10-02 23:46:00 +08:00
2026-10-05 17:50:00 +08:00
providers: Tencent Cloud is one preset, its regions Plan · China, Pay as you go · China and Pay as you go · Global (Discord, Jorben: Tencent Cloud's Token Plan, TokenHub and TokenHub China were three tiles of one vendor) — tencent-token-plan, tencent-tokenhub-cn and tencent-tokenhub become tencent-cloud's regions plan, cn and intl, as Xiaomi MiMo's are, each made as its own preset made it: the plan's /plan endpoints with no Responses, its own models and no catalog; TokenHub's hosts with Responses, models.dev's tencent-tokenhub and its own keys page and docs. A region now carries its Website, Catalog and Models, and a saved provider's region is found by its endpoints (regionOf, by exact URL before path, since TokenHub China and global share their paths): normalize gives it that region's keys page, docs and catalog (a catalog the user picked is kept), and planModels its models. The old ids are aliases, as qianfan-token-plan is of baidu-qianfan: Preset and FromPreset take them, FromPreset builds at their region, normalize rewrites a saved provider's preset to tencent-cloud, and a provider's own id never changes, so its key, models, routing groups, usage and prices stay where they were; magpie://import links by an old id land at its region, and region= now also gives a region's docs and catalog. Imports from other apps match a region's endpoints. The editor's docs link follows the region picked with the segmented control, en and zh ("按量 · 中国", "按量 · 国际"); the add sheet has one Tencent Cloud tile; the site's import docs list tencent-cloud with its regions and import.html still names the old ids. TestTencentCloud (each region, old ids, FromPreset, imports, import links) and TestTencentCloudOldPresetsKeepWorking (a providers.json saved by an older magpie under all three old presets, with a routing group over them, read, re-saved as the editor saves and switched between regions) fail on the old code; TestTencentTokenHubRoutes routes through old ids and tencent-cloud alike. tencent-cloud.test.cjs (WebKit en and zh: one tile, the three regions, no select, the docs and Get a key links following the region, an old id's provider opening at its region and saved under its id) fails on the old app.js; add-sheet, zhipu-team, system-one, strip-scrollbar and click-scroll pass (Chromium doesn't launch in this sandbox). Not tried with a Tencent account. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 19:38:14 +08:00
agent, gui: an agent whose CLI is gone while its settings are left says so, and Install another agent offers it again (evilgaoshu, #843: 卸载了 DeepSeek Harness,但 ~/.dsh 还在,Agents 页面依然把它当作已安装列出来,也不会出现在 Install another agent 里) — an agent was here when its config or folder was, so dsh, uninstalled with ~/.dsh kept, was listed as installed, and Install another agent left out every agent listed. Agent.CLIMissing: its folder or config is here, its CLI isn't, neither on PATH nor where users' tools go (proc.FindTool, as #839's Claude Code is found on Windows); only for an agent that is its CLI and has an install command magpie knows, not one whose folder an app or editor extension keeps too (Codex, Claude Code, Gemini CLI, OpenCode, Cline, Goose), nor a WSL one. Its row stays, settings kept and nothing deleted, with "CLI not found" where its version goes (its tooltip names the folder and points to the install command), and Install another agent lists it, marked "CLI not found" under its name. dsh now has its npm package, @deepseek-ai/dsh, so it is offered with npm install -g @deepseek-ai/dsh, and its version and updates show like the other CLIs'. zh, ja, de strings. TestCLIMissing (dsh with ~/.dsh and no dsh is missing and offered; codex's folder, an agent not here, a WSL one aren't; dsh in ~/.local/bin off PATH isn't) fails on the old code; agent-cli-missing.test.cjs (WebKit, en and zh: the row's note, its height kept, Install another agent lists it with its command) fails on the old code. agent-update-all, agent-layout, agent-start-pick, click-scroll, gui-ja, gui-de and import-row pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 09:25:01 +08:00
agent, gui: Codex left on model_provider = "magpie" with no [model_providers.magpie] loads its config again, and connecting with nothing in magpie says what to do, in the reader's language (Tystem on Discord) — the Codex app said "无法加载 config.toml … Model provider magpie not found", magpie showed Codex as not connected on "deepseek-flash", and its switch answered only "Codex can't be connected to magpie", in English. Codex looks model_provider up in [model_providers.*] as it loads config.toml (codex-rs: "Model provider {id} not found"), so the key without the table keeps it from loading any config, a new thread too, not just an old one. magpie never writes one without the other: it takes model_provider out whenever it steps out as the provider (disconnect, Unwire, a model of Codex's own, Join) and keeps the table so the threads started on magpie still open. That is unchanged, and TestCodexSteppingOutLeavesNoHalfState now covers it. Another tool or a hand edit had taken the table away. Sync, at magpie's start and whenever the catalog changes, now writes the table back whenever the config names magpie as its provider, at the top or in a profile, and has no table, whether Codex is connected or not. Check reports that state too. A config magpie can't parse is still only reported, never written to. A Connect that finds none of magpie's models because no provider or subscription has been added answers agent.NoModelsError, "Add a provider or subscription in magpie first, then connect Codex". /api answers it with code no_models and the agent's name, which app.js says through t() (en/zh/ja/de). docs/reference.md no longer says the table goes with a native model. TestCodexProviderWithoutTable (top and profile: Check says so, Sync writes the table) and TestConnectWithNoModels in internal/agent, TestAgentConnectNoModels in internal/gui, and agent-connect-no-models.test.cjs (WebKit, en/zh/ja/de: the toast in the reader's language, the switch back off, no scroll, no native dialog) are new and fail without the change; the latter's en case passes either way, as the server's message is English. Chromium wouldn't launch on this machine for this test or the existing agent-connect.test.cjs. TestCodexSyncAddsBaseURLToProvider now expects the table added to a hand-made provider that had none. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. In the first full run, internal/gui's TestProvidersAnswerWhileListsComeIn and internal/proc's TestUserPath failed under load; both passed alone and in the second full run.
2026-10-05 21:09:13 +08:00
gui: an agent connected to magpie is never under 未设置, and one not connected goes there whatever its own files say (#726, EZN7L2C3 on the Agents page: Codex, 已接入 with its model on 上次的选择, sat under 未设置 below 收起, while Antigravity CLI and DeepSeek Harness, 未接入 with their switches off, stayed up top — "ones I've turned on go down there; the logic feels chaotic") — arrangeAgents folded what agentUsed called not set up, and agentUsed was "added, or any field with a value (not an effort or ultracode)" from cedc86db/48c496d0, before 「接入」's switch: an agent's fields are read from its own files, so one not connected (wired false) with a model of its own (agy, dsh) counted as set up, and a connected Codex starting on its last choice has no model value and counted as not. Now an agent the switch can connect (connectable) is set up when it is on magpie (onMagpie: wired, added, or drifted — one magpie set up that came apart stays up to be put right), and only then; one with no switch (Cursor, Copilot, an app with no magpie models) goes by what is set on it, as before. Folding starts once one is on magpie while any agent can be connected, so a Cursor on its own "auto" doesn't fold every agent away on a fresh magpie (agentsLead's welcome still says none is connected); with nothing to connect, once any is set, as before. Show on a hidden agent with a switch says it stays under Not set up "until it is connected" (zh 接入前,它会留在「未设置」里). agent-connected-unfolded.test.cjs (the report's rows: Claude Code, OpenCode, Codex on its last choice connected and in view, Cursor in view for its own model, Antigravity, DeepSeek Harness and Kimi Code folded under Not set up/未设置, every row in view switched on and every folded one off; with none connected, nothing folds) fails on the old code (agy and dsh up, codex folded) and passes. Fixtures that are on magpie models but said wired false (agy-launch, claude-direct, model-pick fillers) are wired now, as Agent.Wired would read them; agent-disconnect unrolls the fold for Claude Code, not connected; agent-tweak-fold checks ultracode moves no row with Claude Code connected, and the efforts alone on agents not connected. Webkit: those and show-unset-agent, panel-fold, agent-grip, agent-connect, agent-layout, agent-start-pick, agent-expand-steady, claude-picker-fold, subagent-effort pass; the full webkit suite's other failures (cli-update, routing-time, zcode-site, menu-scroll and others) fail the same on the old code. go vet ./internal/gui and the asset tests pass; the full go test ./internal/gui under a temp HOME was not run here (the sandbox refused the HOME override) — the change is JS only. Not tried in the reporter's app.
2026-10-04 07:27:36 +08:00
gui: an agent connected to magpie is never under 未设置, and one not connected goes there whatever its own files say (#726, EZN7L2C3 on the Agents page: Codex, 已接入 with its model on 上次的选择, sat under 未设置 below 收起, while Antigravity CLI and DeepSeek Harness, 未接入 with their switches off, stayed up top — "ones I've turned on go down there; the logic feels chaotic") — arrangeAgents folded what agentUsed called not set up, and agentUsed was "added, or any field with a value (not an effort or ultracode)" from cedc86db/48c496d0, before 「接入」's switch: an agent's fields are read from its own files, so one not connected (wired false) with a model of its own (agy, dsh) counted as set up, and a connected Codex starting on its last choice has no model value and counted as not. Now an agent the switch can connect (connectable) is set up when it is on magpie (onMagpie: wired, added, or drifted — one magpie set up that came apart stays up to be put right), and only then; one with no switch (Cursor, Copilot, an app with no magpie models) goes by what is set on it, as before. Folding starts once one is on magpie while any agent can be connected, so a Cursor on its own "auto" doesn't fold every agent away on a fresh magpie (agentsLead's welcome still says none is connected); with nothing to connect, once any is set, as before. Show on a hidden agent with a switch says it stays under Not set up "until it is connected" (zh 接入前,它会留在「未设置」里). agent-connected-unfolded.test.cjs (the report's rows: Claude Code, OpenCode, Codex on its last choice connected and in view, Cursor in view for its own model, Antigravity, DeepSeek Harness and Kimi Code folded under Not set up/未设置, every row in view switched on and every folded one off; with none connected, nothing folds) fails on the old code (agy and dsh up, codex folded) and passes. Fixtures that are on magpie models but said wired false (agy-launch, claude-direct, model-pick fillers) are wired now, as Agent.Wired would read them; agent-disconnect unrolls the fold for Claude Code, not connected; agent-tweak-fold checks ultracode moves no row with Claude Code connected, and the efforts alone on agents not connected. Webkit: those and show-unset-agent, panel-fold, agent-grip, agent-connect, agent-layout, agent-start-pick, agent-expand-steady, claude-picker-fold, subagent-effort pass; the full webkit suite's other failures (cli-update, routing-time, zcode-site, menu-scroll and others) fail the same on the old code. go vet ./internal/gui and the asset tests pass; the full go test ./internal/gui under a temp HOME was not run here (the sandbox refused the HOME override) — the change is JS only. Not tried in the reporter's app.
2026-10-04 07:27:36 +08:00
gui: an agent connected to magpie is never under 未设置, and one not connected goes there whatever its own files say (#726, EZN7L2C3 on the Agents page: Codex, 已接入 with its model on 上次的选择, sat under 未设置 below 收起, while Antigravity CLI and DeepSeek Harness, 未接入 with their switches off, stayed up top — "ones I've turned on go down there; the logic feels chaotic") — arrangeAgents folded what agentUsed called not set up, and agentUsed was "added, or any field with a value (not an effort or ultracode)" from cedc86db/48c496d0, before 「接入」's switch: an agent's fields are read from its own files, so one not connected (wired false) with a model of its own (agy, dsh) counted as set up, and a connected Codex starting on its last choice has no model value and counted as not. Now an agent the switch can connect (connectable) is set up when it is on magpie (onMagpie: wired, added, or drifted — one magpie set up that came apart stays up to be put right), and only then; one with no switch (Cursor, Copilot, an app with no magpie models) goes by what is set on it, as before. Folding starts once one is on magpie while any agent can be connected, so a Cursor on its own "auto" doesn't fold every agent away on a fresh magpie (agentsLead's welcome still says none is connected); with nothing to connect, once any is set, as before. Show on a hidden agent with a switch says it stays under Not set up "until it is connected" (zh 接入前,它会留在「未设置」里). agent-connected-unfolded.test.cjs (the report's rows: Claude Code, OpenCode, Codex on its last choice connected and in view, Cursor in view for its own model, Antigravity, DeepSeek Harness and Kimi Code folded under Not set up/未设置, every row in view switched on and every folded one off; with none connected, nothing folds) fails on the old code (agy and dsh up, codex folded) and passes. Fixtures that are on magpie models but said wired false (agy-launch, claude-direct, model-pick fillers) are wired now, as Agent.Wired would read them; agent-disconnect unrolls the fold for Claude Code, not connected; agent-tweak-fold checks ultracode moves no row with Claude Code connected, and the efforts alone on agents not connected. Webkit: those and show-unset-agent, panel-fold, agent-grip, agent-connect, agent-layout, agent-start-pick, agent-expand-steady, claude-picker-fold, subagent-effort pass; the full webkit suite's other failures (cli-update, routing-time, zcode-site, menu-scroll and others) fail the same on the old code. go vet ./internal/gui and the asset tests pass; the full go test ./internal/gui under a temp HOME was not run here (the sandbox refused the HOME override) — the change is JS only. Not tried in the reporter's app.
2026-10-04 07:27:36 +08:00
agent, gui: an agent connected by its config to an address where nothing answers is told, not shown connected (#1013, cjyrainbow: Codex Desktop's WSL environment reads Windows' ~/.codex/config.toml, whose openai_base_url and [model_providers.magpie].base_url the reporter pointed at Windows as WSL sees it, forwarded to 127.0.0.1:3425 by a portproxy of their own; after a reboot the portproxy listened no more and the Agents page still said Codex was connected) — Drift looked only at the config, which was right. Now an agent pointed at an address off loopback (this machine's Codex at an address the user kept, #816, through Agent.reach; every WSL twin under NAT, at Windows as the distro sees it, through Agent.Gateway) has that address tried from here (reach.go: GET /v1/models, any HTTP status an answer, 2 s timeout, in the background, at most every 20 s per address, never waited for), and one that doesn't answer while the gateway answers on loopback is a new drift, "unreachable", with Addr and the advice in Detail: under WSL (or on Windows) magpie with Settings › Share on local network on, or the user's netsh interface portproxy, which may need setting up again after a restart; Windows' firewall allowing the port for the WSL network; on Windows 11, networkingMode=mirrored; elsewhere, the forward to start again or gateway.URL() to point at. An answer not had yet, and a gateway down on loopback too, are no drift. The config is never changed for it, except where the address is one a WSL distro reached Windows at before (distro.Was, the last 4 kept in wsl.json as a re-probe finds Gateway changed) and the distro reaches Windows at another now: Drift's Move names that one, and Reapply moves the config there (Agent.move; Codex's codexMoveGateway rewrites openai_base_url and magpie's own provider's base_url, only where they begin with the old address). An address no distro was seen at is the user's own and is never offered a move. In the GUI the row's line says "{agent} is set up, but nothing answers at the address it reaches magpie by" in red with the advice as its tooltip, and its pill is "How to fix" (an i, not the reapply arrow), which opens the advice in a dialog (too long for the status line; it wraps and can be copied) and sets nothing — or "Use <address>" (the existing "Use {url}" string) when there is a Move, which reapplies; two new t() strings with zh, ja and de. The probe runs from Windows, not from inside WSL, so a firewall keeping WSL out isn't seen; the advice names it. TestKeptAddressNotAnswering (the reporter's case: Codex at 172.28.96.1, unreachable with the address and loopback in the detail; no drift once it answers, none with the gateway down too), TestKeptAddressProbeNeverWaitedFor, TestKeptAddressMovedWithWSL (Move from a Was address, Reapply rewrites both URLs, Check empty and no drift after; a 10.0.0.7 address is told but never moved) and TestWSLAgentAddressNotAnswering (Codex in a NAT distro told, with Share on local network, portproxy and firewall; a mirrored one never probed) fail without the Drift change and pass; TestLoopbackNotProbed, TestReachProbe (httptest: a 401 is an answer, a closed port none) and TestWSLWas pass. TestMain stubs reachProbe so no test dials out. -race -count=10 on them; go test -tags nogui ./internal/... under a temp HOME, go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. agent-unreachable.test.cjs (the window at 560px and the tray panel at 440px: the red line and tooltip, How to fix's dialog posting nothing, Use <address> posting the reapply, no sideways scroll) fails on main and passes on Chromium and WebKit in en, zh, ja and de; agent-connect, agent-layout, agent-field-names, aside-native, gui-de, gui-ja and i18n-keys pass on both. Not tried on a real WSL NAT setup (the Windows test box didn't answer ssh). docs/subsystems/agent-wiring.md says it.
2026-10-06 19:17:55 +08:00
gui: an agent connected to magpie is never under 未设置, and one not connected goes there whatever its own files say (#726, EZN7L2C3 on the Agents page: Codex, 已接入 with its model on 上次的选择, sat under 未设置 below 收起, while Antigravity CLI and DeepSeek Harness, 未接入 with their switches off, stayed up top — "ones I've turned on go down there; the logic feels chaotic") — arrangeAgents folded what agentUsed called not set up, and agentUsed was "added, or any field with a value (not an effort or ultracode)" from cedc86db/48c496d0, before 「接入」's switch: an agent's fields are read from its own files, so one not connected (wired false) with a model of its own (agy, dsh) counted as set up, and a connected Codex starting on its last choice has no model value and counted as not. Now an agent the switch can connect (connectable) is set up when it is on magpie (onMagpie: wired, added, or drifted — one magpie set up that came apart stays up to be put right), and only then; one with no switch (Cursor, Copilot, an app with no magpie models) goes by what is set on it, as before. Folding starts once one is on magpie while any agent can be connected, so a Cursor on its own "auto" doesn't fold every agent away on a fresh magpie (agentsLead's welcome still says none is connected); with nothing to connect, once any is set, as before. Show on a hidden agent with a switch says it stays under Not set up "until it is connected" (zh 接入前,它会留在「未设置」里). agent-connected-unfolded.test.cjs (the report's rows: Claude Code, OpenCode, Codex on its last choice connected and in view, Cursor in view for its own model, Antigravity, DeepSeek Harness and Kimi Code folded under Not set up/未设置, every row in view switched on and every folded one off; with none connected, nothing folds) fails on the old code (agy and dsh up, codex folded) and passes. Fixtures that are on magpie models but said wired false (agy-launch, claude-direct, model-pick fillers) are wired now, as Agent.Wired would read them; agent-disconnect unrolls the fold for Claude Code, not connected; agent-tweak-fold checks ultracode moves no row with Claude Code connected, and the efforts alone on agents not connected. Webkit: those and show-unset-agent, panel-fold, agent-grip, agent-connect, agent-layout, agent-start-pick, agent-expand-steady, claude-picker-fold, subagent-effort pass; the full webkit suite's other failures (cli-update, routing-time, zcode-site, menu-scroll and others) fail the same on the old code. go vet ./internal/gui and the asset tests pass; the full go test ./internal/gui under a temp HOME was not run here (the sandbox refused the HOME override) — the change is JS only. Not tried in the reporter's app.
2026-10-04 07:27:36 +08:00
provider: Azure OpenAI, asked at the user's own resource on its v1 API with the key in api-key, for Codex, Claude Code and chat clients alike, and brought in from other apps instead of left out — Azure OpenAI had no preset, and importing an Azure entry from Alma was skipped with "magpie doesn't support Azure OpenAI yet". A new Azure OpenAI preset (vendor) has no URL of its own: its editor asks for the resource's endpoint in a field above the key, focused first, with https://<resource>.openai.azure.com as the placeholder and a hint on where the portal shows it and that the model ids are the deployments' names (en and zh); adding without one says "Your resource's endpoint is needed" and sends nothing, and Save refuses one without an endpoint too. Whatever is pasted — the bare endpoint, …/openai, …/openai/v1/, a classic deployment URL with its api-version, a *.cognitiveservices.azure.com or *.services.ai.azure.com host, or only the resource's name — is saved as https://<host>/openai/v1 for both chat completions and Responses. magpie uses the v1 GA API rather than the classic /openai/deployments/<name>/…?api-version= paths: the paths match OpenAI's, the body's model is the deployment's name, and no api-version is needed (the reasoning is in internal/provider/azure.go). The key goes in api-key, never as a Bearer, which Azure takes as an Entra ID token; nothing sends Authorization or x-api-key there. Deployments named after GPT/o/codex models go to Responses first, as OpenAI's do, and others to chat completions. Chat bodies ask max_completion_tokens instead of max_tokens and drop thinking/enable_thinking, both in passthrough and in Claude Code's translated requests, and the Test probe asks max_completion_tokens as well. The model list is the resource's deployments (/openai/deployments?api-version=2022-12-01), falling back to /openai/v1/models, with the key in api-key; when neither answers, the error says to type the deployment names in as model ids. Before a list, no catalog models are offered, since a model the resource hasn't deployed would be turned away. An Alma azure entry, and any imported entry at a resource's host, becomes the preset at its v1 base; an Alma azure entry with no endpoint is skipped as "it has no Azure OpenAI endpoint", and one at a non-Azure host (an API Management gateway) keeps its URL and is still signed with api-key. Adds the lobe-icons Azure icon. Tested in Go against fake resources (httptest): TestAzureRoutes checks that Codex /v1/responses, a chat client's /v1/chat/completions and Claude Code's /v1/messages on both a GPT-named and another deployment reach /openai/v1/responses or /openai/v1/chat/completions with api-key set, no Authorization or x-api-key, the deployment name as the model, max_completion_tokens and no thinking; TestAzureBase covers each pasted form and the hosts that aren't resources; TestAzurePresetSaved covers the endpoint required, both URLs on the v1 base, api-key-only headers, Native, the probe's URL and body, and nothing available before a list; TestAzureModels covers deployments first, the v1 fallback and a refused key's message; TestImportAzure covers Alma rows with an endpoint, with a bare resource name and with none, plus a classic URL from another app. azure-endpoint.test.cjs, in Chromium and WebKit, en and zh, checks the field, placeholder, hint and focus, the warning without an endpoint with nothing sent and the page not scrolled, the endpoint sent as chat and responses with the key, and that OpenAI's editor has no such field. add-sheet and old-webkit pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet and go test -tags nogui ./... pass (TestClaudeAccountNamedAsItsLogin failed under load and passes alone). Not tried against a real Azure resource: whether a key still lists deployments with api-version 2022-12-01, whether Azure's Responses API accepts every field Codex sends, Claude models on Azure AI Foundry's Anthropic endpoint (not wired), Entra ID tokens, and API Management custom domains beyond keeping their URL. Not tried in the real Wails window.
2026-09-30 02:17:15 +08:00
agent, gui: Claude Code taken off magpie by Default goes under Not set up and says Default (EZN7L2C3, #834 on v0.1.920) — picking Default and confirming Use default left the row up among the connected ones while it said Not connected, its model read Pick a model and its list had magpie's models alone. Default takes Claude Code's tiers and subagent model out with magpie's endpoint; Apply recorded only the field picked, so those read as changed outside magpie (replaced drift) and the drift kept the row up. Apply now records every field magpie set before that the change moves too. An agent not connected whose list has models of its own (Claude Code's, asked of Anthropic directly) now says what it is on, its Default or its own model, and lists everything as when connected: Default, its own models, magpie's. TestClaudeDefaultLeavesNoDrift (fails on the old code: drift replaced opus; a tier changed outside magpie is still said to be); claude-default-off.test.cjs (WebKit, en and zh: folded under Not set up, switch off, Default shown, Default current with the direct and via-magpie models listed, no scroll; fails on the old code: Pick a model / 选模型). agent-path-same, agent-leave-magpie, claude-picker-fold, claude-direct, agent-start-pick, model-rate, unlisted-models, agent-connect, agent-field-names, pi-default-note, click-scroll, desktop-model-menu, model-pick, favorite-star, agent-disconnect, agent-layout, gui-ja and gui-de pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 06:40:46 +08:00
2026-10-05 12:59:32 +08:00
2026-10-06 14:13:19 +08:00
2026-10-06 14:13:19 +08:00
agent, gateway: Cursor Private Inference's row has an effort, which magpie's gateway asks its requests for (#1003, gjf7) — other agents' rows pick a reasoning effort that magpie writes into their config, but Cursor Private Inference reads only its base URL and key from the environment, and its app offers no Reasoning control for most of magpie's models, so there was no way to set one short of a routing group with a member fixed at a level, which the reporter used as a workaround. Its row now has an effort field like the others': the default (as Cursor asks) first, then every level the models it lists have (cursorLocalEfforts: their levels in MemberEfforts order, low/medium/high where a model reasons with levels unknown, none at all when nothing it lists reasons or it isn't connected). The pick is kept in magpie, settings' agentEfforts by agent (provider.AgentEffort / SetAgentEffort, a level of MemberEfforts or "" for none), and the gateway's candidate loop asks a cursor-local request for it with withFixedEffort, fitted to the model's levels and bounded by a group's, on a model that reasons (the new Provider.Thinks: levels, its list's Reasoning, or models.dev's). Precedence: a group member's fixed effort, then a :effort suffix, then a routing rule's turn pick, then the agent's pick, then the request's own; the pick wins over a level Cursor sent, as Cursor sends its own default as often as a choice, and Default hands the choice back to Cursor. A model that doesn't reason and every other agent's requests go as they came. The Settings page's save carries agentEfforts over as it does fastPicks (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). In the window the field sits under "Settings" rather than "New sessions" for this agent alone, as it applies to every request from then on, not to what a session starts on; no new t() strings (Settings, effort and default are translated in zh, ja and de already). TestAgentEffortOnCursorLocal (Chat and Responses with and without a level of their own, Claude on Messages made to think, OpenCode's request untouched, a non-reasoning model untouched, a member fixed at low kept, the pick taken off) fails without the gateway change and passes; TestCursorLocalEffort covers the options (none with no reasoning model, the union of levels, the low/medium/high fallback, none while disconnected), Apply and a bad level refused; both pass -race (-count=20 gateway, -count=10 agent). cursor-local-effort.test.cjs (the window at 560px and the tray panel at 440px: the field under Settings, the slider's five stops with Default first, a pick posting the field, no sideways scroll) passes on Chromium and WebKit in en, zh, ja and de; claude-effort, subagent-effort, agent-field-names, agent-connect, agent-layout, agent-tweak-fold, agent-models, cursor-local-launch and i18n pass on both. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with the real Cursor Private Inference: whether it sends a reasoning level of its own on models it shows a Reasoning control for is unverified, which the precedence makes moot. docs/subsystems/gateway-routing.md and agent-wiring.md say it.
2026-10-06 17:29:28 +08:00
agent: Cursor Private Inference is connected by its row's switch, as other agents are, with no command to run (mamba on Discord: 这个单独搞个启动命令和别的供应商格格不入啊) — its Open configuration's base URL and key are kept in the state.vscdb regular Cursor shares, and are regular Cursor's own override settings too, so magpie still writes nothing of Cursor's. It sets CURSOR_LOCAL_AGENT_BASE_URL (<gateway>/v1) and CURSOR_LOCAL_AGENT_API_KEY (magpie-cursor-local), which only this build reads, for the user: launchctl setenv on the Mac (apps opened from the Dock or Finder from then on have them), HKCU\Environment plus a WM_SETTINGCHANGE on Windows, ~/.config/environment.d/magpie-cursor-local.conf (from the next login) and systemctl --user set-environment on Linux; switched off, it clears them. magpie notes it in cursor-local.env in its config folder, which the row reads as connected, and sets them again whenever it serves the gateway (launchd forgets them at a restart; the port may have changed). The row has the provider field (magpie) and the connect switch; connecting says to quit the app and open it again, and that a base URL in Open configuration still comes first. The launch command stays as the square beside it, for a start from a shell. TestCursorLocalConnects (connect sets them, a serve sets them again, disconnect clears them, off a serve sets nothing) fails on the old code; cursor-local-launch.test.cjs now checks the switch connects it with no command copied (WebKit, en and zh); agy-launch passes. Not tried with the real app (not installed here), and launchctl/the registry are not touched under tests. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 00:18:22 +08:00
gui: a short window's provider editor scrolls past its models, and a routing group made from a model is added (悠悠哥 on Discord: in a small window the WorkBuddy editor's model list was cut off and the wheel took it no further until the window was maximized; the new group "Hy4 preview" did nothing on 添加, its 推理档位 label at the right and 成员共有/自定 under the labels) — the model chips (.mchips) scroll on their own, 132px high, with overscroll-behavior: contain; in a short window the editor's body shows little more than the list, so the pointer was nearly always over it, and at its ends the held wheel never took the body on to the fields below (the body stopped at 539 of 660px at 800x500). Now the chips are at most 28vh high, and in a window up to 640px tall their ends pass the wheel on to the body; Names & levels is held to 40vh there. The dialog, head and Save already fit any window. A group opened from a model (newGroupWith: the picker's and the provider editor's "Make a routing group of it", the tray panel's ?newgroup=) had no fast in its draft, so Add threw on d.fast.filter and posted nothing, since 10b94023 (unlisted-models.test.cjs has failed since); the editor now fills a draft's members, fast and rules whoever opened it, a throw in Add (or Enter in the name) is said in the status line as "Couldn't save the group: …" (分组没能保存:…), and Add leaves busy (pointer-events: none) once the save is answered, so a refused save can be pressed again. The classifier row's grid cell was left in the grid when it was hidden, so every row after it slipped one cell; it is hidden with its label now. editor-short-window.test.cjs (12) and group-from-model-add.test.cjs (8) fail without the change. Chromium and WebKit, en/zh: editor-short-window, group-from-model-add, unlisted-models, group-member-fast, routing-effort, routing-effort-row, routing-levels, routing-manual, routing-time, routing-compact, settings-groups, agent-model-groups, add-button, add-sheet, library-all, model-test-one, plugin-market, plugin-move, plugin-signin, provider-levels-scroll, saved-signedout-remove, nokey-editor, model-pick, old-webkit pass; click-scroll and plugin-moved-signin timed out under load (avg 239) and pass alone.
2026-10-01 19:38:36 +08:00
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.
2026-10-02 14:58:49 +08:00
gui: a model with a fast mode is sent fast, or not, from its row in the agent's picker (#954, Gyarados4157) — Fast was only a routing group member's (Group.Fast), so a GPT on a ChatGPT account picked straight in Codex or OpenCode could only go fast through a group of one. A model provider.CanFast says has a fast mode (a ChatGPT account's GPT, an OpenAI key's Responses GPT/o-series, Cursor's -fast, Claude Opus on an Anthropic key) now has a bolt beside its star in the agent's picker and in the same picker opened from a provider's row; a click posts /api/agent-fast and switches it for that agent (settings FastPicks, by agent, "<provider>/<model>", moved by a provider rename and kept by the Settings page's save) without changing the pick or closing the picker; a refusal puts it back. The field shows a bolt after a model sent fast. The gateway asks for the vendor's fast mode (service_tier "priority" / speed "fast", as for a group member) on that agent's requests for that entry, every account and key of it, not its fallbacks, another agent's requests or the same model at another provider; the Routing trace marks the try Fast. A routing group is never switched: its members say it. zh/ja/de added. fast-pick (window and 440px panel, en/zh/ja/de, Chromium and WebKit; the bolt only on models with a fast mode, the post, the refusal, the provider row's picker) passes and fails without the fix; TestFastPick (gateway, Chat/Responses/Anthropic in, fails with the gateway hunk off), TestSetFastPick, TestFastPickOptions and the settings-keep test (fails without the carry) pass with -race; i18n-keys, gui-ja, gui-de, favorite-star, agent-models-picker, claude-picker-fold and click-scroll pass; go test ./internal/gui/ ./internal/provider/ ./internal/agent/ ./internal/gateway/ ./internal/settings/ pass.
2026-10-06 11:15:52 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
provider: a decision model has its window and whether it takes images, and the Gateway list shows them with a tooltip saying what it is (ARNO on Discord: decision模型也是需要拉取正确的上下文长度以及是否支持视觉功能的,在网关的模型列表里也应该显示这些信息以及相关的tooltip) — a decision provider's list (OpenRouter's output_modalities=decisions, Vercel's) is now read for its context_length / context_window and its input modalities; a model the list says nothing of (TypeSafe's, OpenCode Zen's, Workers AI's and Bailian's presets) takes its vendor's documented values: Jev 32,000 and text, Clef and Clef Flash 65,536 and images, Bailian's decision-model-preview 65,536 and text. A gateway's Jev among its chat models has them too; its chat models are left as they were. The Gateway list's row of a decision model shows its context and image chips as other models' do, and its tooltip begins Decision model: routing groups ask it, agents never see it (zh, ja, de) in place of Reasoning levels: none known. /v1/models still leaves decision models out: agents can't use them. TestDecideModelsKnowWindowAndImages and gateway-decide-info.test.cjs (WebKit, en and zh) fail on the old code; gateway-model-info passes on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestClientGoneRecordedAsCanceled, TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut flaked under load, pass alone).
2026-10-05 01:20:06 +08:00
gui: a gateway key's menu puts its accounts beside their provider's models, not one block at the end (a follow-up to #932, July, LCorleone) — the menu the key's badge opens listed every provider's models and then all the accounts and keys together after them, so "which models" and "which plan" of one provider sat in two far-apart places, hard to give someone one vendor's plan alone. Each provider's accounts and keys now follow its own models, inside its block — the order the /api/caller-keys/models comment already described — an account by who is signed in with the plan in its note, a key by its fingerprint; a provider with accounts and no model in the list (one kept unlisted, or serving none now) keeps its accounts after the models; what the CLI kept that no provider has now stays behind every live entry, a model gone before an account signed out. Nothing of the picking changes: the same proto menu, the same opts and literalName, the same accountsSet that splits what comes back into "models-key" and "accounts-key", and no new strings to translate. gateway-key-accounts reads the order off the real menu now — relay's two accounts between its models and OpenAI's, the key behind GPT-5 — and pins the dead tail on a key held to a gone model and a signed-out account; its last badge read waits for the row to be drawn with the answers (the wait gateway-key-models already makes): without it a slow engine read the hold one action behind, a flake this sandbox shows on main's own test 5/6 runs on WebKit. gateway-key-accounts passes Chromium and WebKit, en and zh; gateway-key-models, gateway-key-groups, gateway-caller-keys, gateway-connect-keys, gateway-key-limit, i18n-keys and usage-keys-fold pass on Chromium — gateway-key-models' scroll assertion doesn't survive this machine's frozen WebKit build on main either. No Go change (the assets are embedded; no Go test reads their content). docs/reference.md's sentence on the menu follows the new order. (#988)
2026-10-06 16:39:33 +08:00
gateway, gui: a gateway key can be held to routing groups (Magic_zero on Discord) — a key's models menu on the Gateway page listed providers' models only, so a key couldn't be limited to the routing groups an agent was meant to use. The menu now lists the routing groups first, with Every routing group (group/*), and a key may keep group/<id>. A key that names a group may use it with every member in it, the next member too when one fails, but not those members asked for by name, a group it doesn't name, or a fallback outside the group; a group named inside one it doesn't name counts for its own members. A group the key doesn't name still passes only when it may use every member, as before. /v1/models and the Anthropic and Gemini lists show the key its groups. en/zh/ja/de; docs/reference.md. TestGatewayKeyModelsNameAGroup (group/both lists and serves only the group, falls to the next member, members by name and other groups refused; group/* every group; a nested named group; a member's own fallback outside the group refused) fails without the change; TestCallerKeyModelsRoutes lists the groups. gateway-key-groups.test.cjs (WebKit, en and zh: groups first after All models, Every routing group, a group picked sends group/coding, a held key's group ticked, no select, no scroll, no left border) fails without the change; gateway-key-models, gateway-key-limit and click-scroll pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 18:44:27 +08:00
gateway: a gateway key can be held to some models (#882, LCorleone) — a key could reach every model magpie serves. A key now keeps an optional list of models (caller-keys.json "models", none = every model, so older files read as before): <provider>/<model> or <provider>/*, matched against the provider that serves the call, so a bare model name is resolved first and OpenRouter's "anthropic/x" is not Anthropic's. Such a key sees only its models in /v1/models, /v1/models/{id} and the Anthropic (Claude Desktop) and Gemini lists, a routing group only when every member is its; asking for another, by id, bare name or through a group with a member it may not use, is refused before any provider is asked with a 403 in the API's own error shape (permission_error) that names the key and its models, logged as a rejected call like an over-limit key's 429; a fallback it may not use is skipped; embeddings, images and videos it names are held too. magpie's own calls for it (a web search, a picture described, Codex titles with the model Settings name) are marked in their context, not by a User-Agent a client could send, and go on. Set it with the new badge on the key's row in the Gateway page (the app's menu: All models, every model of a provider, each model; sent when the menu closes) or magpie gateway-key models <id> openai/gpt-5 anthropic/* (all takes it off); gateway-key list has a MODELS column. The app's menu, fitting neither below nor above its button, now goes on the roomier side and scrolls instead of running off the window. en/zh/ja/de. TestGatewayKeyModelsHoldAKey (lists, 403s by id, bare name and group, Anthropic shape, fallback skipped, magpie's own call, a free key) fails without the change; TestModelPatterns, TestKeyModelsKept, TestGatewayKeyModelsCLI, TestCallerKeyModelsRoutes pass; gateway-key-models.test.cjs (Chromium and WebKit, en and zh: no native select, menu inside a 420px window, picks sent on close, All models, no scroll, no left border, 560px fits) passes with gateway-connect-keys, gateway-key-limit, gateway-caller-keys and click-scroll. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 16:52:23 +08:00
gui, proc: the Gateway card quits an older magpie serving the gateway and takes it over, or restarts the gateway (leslie_luo on Discord) — the card said an older magpie served the gateway and to quit it, with no way to tell which process that was. With an older magpie on the port it now has "Quit magpie {v} and take over": the process listening on the gateway's port is found (lsof on a Mac, /proc on Linux, Get-NetTCPConnection on Windows), its program path checked to be magpie's (magpie, magpie-*, dial, .exe/.old), asked to quit (SIGTERM, then SIGKILL after 5 s; TerminateProcess on Windows) and this magpie serves the gateway at once instead of within 15 s. Otherwise the card has "Restart gateway" ("Start gateway" when none runs); a magpie of this version or newer gets no button. Anything else on the port is never ended: a program that isn't magpie, one of another user or an administrator, one that won't quit, or a magpie a LaunchAgent/systemd unit/scheduled task starts again is said under the button with its path and pid, in en/zh/ja/de. TestGatewayFixQuitsOnlyAMagpie (copies of the test binary on a port of its own: one named other-server answering as magpie is left running, one named magpie is quit and the gateway is served here, then restarted) and gateway-takeover.test.cjs (WebKit and Chromium, en and zh, no scroll, no left border) fail without the change; TestListeningOn/TestIsMagpie pass on macOS and in a Linux container. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 15:05:31 +08:00
gui: the Gateway page's Connect speaks System One too, with a decision model (ARNO on Discord: gateway添加system one api 有想法吗) — magpie's gateway has served POST /v1/systemone, but Connect showed only OpenAI, Responses, Anthropic and Gemini, so nothing on the page said so or how to ask it. Connect has a System One tab: base …/v1, curl, Python (requests) and Node (fetch) snippets that POST …/v1/systemone with Authorization: Bearer, a state and a choice question, printing the answer's choice and confidence; the note says Clef also reads images ("images": [data URLs]). No SDK speaks it, so it has no Shell tab, and the base URL and key hints say where the request goes and how the key is sent instead of naming env variables. Its model is a decision model only (decidesModel: a decision-only provider's, or a Jev), and the other tabs no longer take one; a click on a decision model in the list switches Connect to System One, a click on any other model back to OpenAI, each tab keeping its own pick (magpie.decideModel). The head says five APIs; Connect's tab strips scroll in themselves on a narrow window, as the region strip does, rather than widening the card. zh and ja strings. gateway-systemone.test.cjs (WebKit, en and zh, light and dark) fails on the old code; gateway-connect-keys, -model-info, -fold, -older, -caller-keys, -key-limit and gui-ja pass on WebKit; Chromium doesn't launch in this sandbox. The snippet's request, sent to the gateway's handler, reached a Workers AI Jev and Clef in their own shapes, and with the owner's real TypeSafe key in a sandbox HOME, jev-latest answered 200 with choice billing, confidence 0.55. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 22:59:23 +08:00
gui, groups: a group's editor sets the context agents are told — its largest model's, its smallest model's, or one typed (Mikan on Discord: 路由组可以支持自定义上下文长短或者支持用户选择按照上下文长度最短的供应商还是上下文长度最長的供应商吗?) — a group told agents its largest member's window (#712), and another could only be set from the CLI. The group editor now has a Context row under Slow to start: Largest model's (as before), Smallest model's, or Custom with a box (200k, 1m, 128000), its hint naming the window agents will be told from the members' own, redrawn as members are added or taken off; a length that isn't one says so and isn't saved. Smallest is kept as smallest (Context -1, ContextSmallest), worked out as the members change, so the agent compacts before any member would turn a conversation away; a conversation too long for one member still goes on to one with room. magpie group set <id> context=smallest|largest|272k does the same. zh, ja, de strings. TestContextSetByUser (smallest, then smallest after a member leaves) fails on the old code; TestApplyGroupPairs covers context=. group-context.test.cjs (WebKit, en and zh): Largest by default with no box, Smallest saves -1 and reopens so, Custom starts at the smallest member's, a bad length shows its error, 300k saves 300000 and reopens, Largest saves 0. group-first-token and group-patterns get a taller window, the editor having grown under the fixed footer; they, group-arrange, fold, member-off, member-fast, rate-limited-sink, routing-compact, manual, levels, steady, time, unlisted-models and settings-groups pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 03:00:34 +08:00
2026-10-05 16:46:59 +08:00
routing: a group's member can be sent in its vendor's fast mode, beside its own reasoning (vs on Discord: a member "gpt-6.1 sol · high · fast") — a member's effort was already its own (#189, "provider/model:effort") but there was no way to ask for speed, so a group could not hold "gpt-6.1-sol at high, priority" next to slower members. A group now lists in fast the members it sends fast, as Members spells them, kept beside Members rather than in their ids so an older version still routes to them, only not fast (checked: HEAD's build on the same providers.json sends the same members at their effort, no tier, and leaves the file as it was). When the gateway sends to such a member it asks in the upstream's own words, only where the model has a fast mode magpie can ask for (provider.CanFast): service_tier "priority" for a ChatGPT account's GPT models and an OpenAI key's GPT/o-series models on its Responses API, Cursor's -fast variant, and speed "fast" with the fast-mode-2026-02-01 beta for Claude Opus 4.8/5/5.5 on an Anthropic key (api.anthropic.com); a relay, Bedrock, a Claude subscription, a model without one, or a provider since moved off those hosts is sent untouched. It is applied in the agent's own API and carried through translation (parseAnthropic now reads speed), so Chat, Responses and Anthropic agents all get it. A translated request for OpenAI's Chat API is still told no tier (TestCursorPluginFast), hence the Responses requirement. The trace marks the try, the order and the group (fast), shown as "fast" beside the reasoning in Requests and the order. Typed, a member is provider/model[:effort]:fast: magpie group add/set models=… takes it, fast=<m1>,… sets them, models-= takes either spelling, and the group lists and shows members as typed with "· fast"; a model with no fast mode, or a group in the group, is refused. The GUI group editor shows a "Standard speed"/"Fast" toggle (标准速度/快速) beside a member's reasoning only for a model that has one, the card reads "· fast" after the reasoning, and fast follows a member's reasoning change and leaves with a removed member; the TUI's group view takes E to step a member's effort and F to toggle fast. Provider renames carry it. TestFastMemberOpenAI, TestFastMemberAnthropic, TestWithFastWire (gateway, per wire format, a fake vendor recording body and anthropic-beta), TestCanFast, TestSaveGroupFast, TestGroupMembersFast, TestGroupMemberEffortAndFast, TestGroupSaveFast and group-member-fast.test.cjs fail without the change. A sandbox magpie (own HOME, gateway on 3966) with providers at http://api.openai.com and http://api.anthropic.com through a local recording proxy sent priority+high to OpenAI and speed fast + the beta to Anthropic from Chat, Responses and Anthropic clients, and nothing extra for Sonnet or a non-fast member. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; group-member-fast, click-scroll, old-webkit, routing-levels, routing-manual, routing-effort, settings-groups pass (Chromium, WebKit, en/zh).
2026-10-01 17:13:34 +08:00
gui, groups: a group's editor sets the context agents are told — its largest model's, its smallest model's, or one typed (Mikan on Discord: 路由组可以支持自定义上下文长短或者支持用户选择按照上下文长度最短的供应商还是上下文长度最長的供应商吗?) — a group told agents its largest member's window (#712), and another could only be set from the CLI. The group editor now has a Context row under Slow to start: Largest model's (as before), Smallest model's, or Custom with a box (200k, 1m, 128000), its hint naming the window agents will be told from the members' own, redrawn as members are added or taken off; a length that isn't one says so and isn't saved. Smallest is kept as smallest (Context -1, ContextSmallest), worked out as the members change, so the agent compacts before any member would turn a conversation away; a conversation too long for one member still goes on to one with room. magpie group set <id> context=smallest|largest|272k does the same. zh, ja, de strings. TestContextSetByUser (smallest, then smallest after a member leaves) fails on the old code; TestApplyGroupPairs covers context=. group-context.test.cjs (WebKit, en and zh): Largest by default with no box, Smallest saves -1 and reopens so, Custom starts at the smallest member's, a bad length shows its error, 300k saves 300000 and reopens, Largest saves 0. group-first-token and group-patterns get a taller window, the editor having grown under the fixed footer; they, group-arrange, fold, member-off, member-fast, rate-limited-sink, routing-compact, manual, levels, steady, time, unlisted-models and settings-groups pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 03:00:34 +08:00
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.
2026-10-04 21:23:31 +08:00
2026-10-05 16:46:59 +08:00
gui: a hidden agent put back to its defaults goes under Not set up at once (Hu9956, #842), and a restart to update shows what changed before it restarts (Hu9956, #844) — #842: an agent hidden by hand stayed under Hidden with its Show (the eye) after its model was set back to Default; only turning its switch off and on moved it, since nothing ever dropped its hiding (the server leaves agentsHidden alone). Now when a hidden agent's settings change (wired, added, drift or a field's value) so that nothing is set on it, its hiding is dropped and saved with the arrangement at once, it goes under Not set up without the eye, and the status line says so; a state answer read before that save, still naming it hidden, doesn't hide it again. One hidden while nothing was set on it, and left alone, stays hidden. #844: the header's Update pill and Settings › About's Restart to update restarted on the click, and what the update brought was only in Settings › About › What's new. Now in the window they open a sheet first: Update to v…, What changed since v<current>, every release after this one up to the new one, newest first (the update's own notes at once, the full list when GET /api/update/notes answers: the site's notes feed for (current, latest], the update's notes when the feed is out of reach), Later and Restart to update; only the latter restarts. A failed swap still restarts again at once; the panel keeps its one-click pill. zh, ja, de strings. hidden-agent-cleared.test.cjs (the groups, the hiding saved once, the late answer, no scroll) and update-notes-ask.test.cjs (the pill and the Settings button ask first, both releases' notes, Later sends nothing, the sheet's button restarts, no scroll, no left border) pass on WebKit in en and zh and fail on the old code; looked at in screenshots. TestUpdateNotesRoute (the releases between, the fallback, none) fails on the old code. update-wait.test.cjs now confirms the sheet; it and update-hide, update-auto, update-check, header-centre, header-zoom-fit, show-unset-agent, fold-show-grip, agent-grip, agent-start-pick, agent-path-same, removed-hide, profile-confirm, agent-update-all, gui-ja, gui-de and click-scroll pass on WebKit (click-scroll's pointer subtests are flaky the same on origin/main). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 11:16:34 +08:00
gui: many keys are added to a provider at once, and a key the gateway rests says why and until when on its row (361 on Discord: 密钥比较多的话,需要批量导入的话就不方便了) — a provider with several keys took them one at a time, each pasted into Add another key's field. Add another key now has Paste several, a box that takes any number of keys, one a line or separated by commas, semicolons or spaces (full-width ones too), quotes trimmed, repeats counted once, and says how many it holds; Add (or Cmd/Ctrl+Enter) posts keys/import, which adds the ones the provider hasn't got in order (the first becomes its key when it has none) and saves once, and the note says N keys added, M already there. Pasting several keys into the one key's field opens the box with them. Each key's row in the editor, when the gateway is passing it over (rate limited, out of credit, refused…), says Resting · 429 rate limited · back at 14:05, from the same rests the Routing page counts; rotation order and failover were there already (routing modes, rests). zh, ja, de strings. TestSplitKeys, TestAddKeys, TestKeysImportAndRest (no raw key in the reply) fail on the old code (don't build); key-import.test.cjs (WebKit, en and zh: the box, its count, the post, the note, a paste opening it, the rest line, no scroll on a click) fails 4 of 4 on the old assets. account-models, account-arrange, account-return 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 (TestDevinSignedInOnlyInMagpie and TestClientGoneRecordedAsCanceled timed out once each under load and pass on their own).
2026-10-05 07:54:16 +08:00
2026-10-05 17:50:00 +08:00
gateway, gui, cli: each account or key can have its own limit on requests at once, with a bounded queue that turns requests away with a 429 (#892, margbug01) — a provider's Concurrency applied to every key and account alike, the queue past it had no bound and no wait, and nothing showed what ran or waited. A key or account now may have its own limit over the provider's (Provider.AccountConcurrency, by lowercased account or KeyID; 0 none; unset takes the provider's, as before), counted in one lane across every model, routing group and agent. A provider's Queue size bounds how many wait for each of its lanes (one more is turned away at once) and Queue wait how many seconds one waits (then it leaves the queue); both 0 by default, as before. A request turned away goes on to the next key, account or group member without resting anything, else gets a 429 in its API's own shape (Anthropic rate_limit_error, OpenAI's, Gemini's) with Retry-After: 1, saying the limit and the queue. A slot is given back when the reply is read to the end, fails or the agent goes away; a waiter that goes away leaves the queue, and one granted as it gave up hands the slot on. In a routing group, or among a provider's keys and accounts, a full one gives way once to another with a free slot now, so a group spreads before anyone waits; a fallback provider is never asked for want of a slot, and with all full the request waits for the one routing chose. The Claude subscription path and plugin providers take their slot the same way, around the attempt; a parked Claude Code tool turn has already answered and holds none. GUI: each account and key row has a pill — "1/1 running · 2 queued" while requests are out (GET /api/lanes, read every two seconds and written into the pill in place), else its limit; none set shows on hover only — opening the app's menu (provider's, no limit, 1/2/3/5/10, Other… typed in place), posting provider/accountconcurrency; the editor has Queue size and Queue wait under Concurrency. CLI: magpie provider account-concurrency <id> [account|key [n|off|default]] and magpie provider queue <id> [size [seconds]]. en/zh/ja/de. TestAccountLimitSharedAcrossGroups, TestQueueFullAndWaitedOut, TestLaneReleasedOnFailure and TestFullMemberGivesWay (pass with -race, with TestMaxConcurrency and TestLanesLimitLifted), TestLaneLimitRoutes and TestAccountConcurrencyCmd are new and fail without the change; lane-limit.test.cjs (WebKit, en and zh: a key's busy/queued pill updated in place from /api/lanes, set from the menu, provider's posts null, Other… typed, an account's own limit, none on hover only, queue fields saved and refused out of range, labels on one line, every string in zh/ja/de, no select, no scroll, no left border) fails without the change. key-weight-fit, account-cap, provider-concurrency, click-scroll, account-models and api-key-usage pass in WebKit; the other WebKit failures fail the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 18:53:16 +08:00
library: a set of instructions added, or given text, while the one in use is empty is the one the agents read, and the page says which set they read (Fate on Discord: a shared set was made and Claude Code ticked, and ~/.claude/CLAUDE.md didn't change) — the agents switched on read only the set in use (the round button), and Fate's was the Default, which was empty, so nothing was written; ticking Claude Code said "it gets the shared instructions once there are some" while a set with text sat beside it. Now a set created, or a set given text, while the one in use is empty becomes the one in use (a set in use with text is left as it is). The Agents head says "They read {set}" (· Empty when it is), an open set not in use has a Switch the agents to this set button (it sends the text typed with it), an agent switched on while the set in use is empty and another has text says it reads the empty one and to switch, and a set added while the one in use is empty says the agents read it. zh/ja/de strings added. magpie still writes its own marked block into CLAUDE.md, keeping the user's text, and takes it out when the agent is switched off. TestInstructionSetTakesAnEmptyOnesPlace (sandbox HOME: CLAUDE.md gets the block after "# Mine", is "# Mine\n" again when off; a set in use with text is kept; emptied, the next set with text takes over) and library-instr-sets.test.cjs (WebKit, en and zh) fail on the old code; the library-*.test.cjs and gui-de pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 01:48:57 +08:00
library: Discover finds an MCP server by its address — a repository, page, endpoint or package pasted in (lc on Discord: https://github.com/crystaldba/postgres-mcp in Library › MCP › 发现 said 没有与 … 匹配的结果) — MarketServers matched featured servers on their words and asked the MCP Registry with the search as typed, and the registry's search is a part of a server's name (checked: "postgres mcp" lists 0, "crystaldba" 0), so a URL found nothing anywhere. A search that reads as an address (address: a scheme, git@, www., host/path, owner/repo or @scope/pkg; words with a space, or a bare word, are searched as before) is normalised (normAddr: lower case, no scheme, user, www., query, fragment, trailing slash or .git; npmjs.com/package/x, pypi.org/project/x, hub.docker.com/r/x and docker.io/x are the package, an image's tag and a package's @version dropped) and matched (addrScore: equal, under it, or a file under one's repository; owner/repo on any host; never a bare host or an owner's page standing for every repository) against each server's addresses: a featured one's page, endpoint and package; a registry one's name, websiteUrl, repository.url, remotes' URLs and packages' identifiers. The registry is asked for the names the address suggests (addrTerms: the repository's or package's name, then its owner's; a site's name for an endpoint, context7 for mcp.context7.com/mcp), and what it lists at the address comes first and alone, marked match; when nothing is, what it lists for the first name is shown, as that search would. Searched against the live registry: github.com/Eszetael/postgres-mcp-hardened.git, ghcr.io/eszetael/postgres-mcp-hardened:0.1.10, github.com/upstash/context7, https://mcp.context7.com/mcp (featured Context7), npmjs.com/package/@playwright/mcp (featured Playwright), github.com/modelcontextprotocol/servers (the reference servers) and an endpoint of ai.getvda's each find theirs. crystaldba/postgres-mcp itself isn't in the registry (no name, repository or package of it in the 124 servers a "postgres" search pages through), so it shows the 12 others called postgres-mcp, and above them "None of these is at {q} — they're found by its name." (以下都不在 {q},是按它的名字找到的。; "Nothing listed is at {q}." 没有列出位于 {q} 的服务器。 when none) with "+ Add it yourself…" (+ 自己添加…): the market's answer carries custom (CustomAt) when the search is an address nothing found is at, and the button opens the existing new-server form filled in — named for the repository (postgres), an endpoint's URL as HTTP, npx -y / uvx for an npm or PyPI page. TestAddress, TestAddrScore and TestMarketFindsByAddress (a fake registry that searches names: by repository found through its owner's name, featured by endpoint, repository, package and a repository folder, registry by endpoint, by name with one to add by hand, words as before) and market-address.test.cjs (webkit, en and zh: the note over the name matches, the form named postgres with no command, an endpoint's URL in it, words that match nothing as before; fails on the old library.js) pass, as does market-have. go vet ./internal/..., builds (-tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./internal/library/... ./internal/gui/... under a temp HOME pass. Not tried in the app.
2026-10-04 07:54:08 +08:00
gateway: a reply says which provider and model answered it, in X-Magpie-Provider and X-Magpie-Model, and Settings › Models › Replies can have its model say magpie's provider/model id too (liuweifeng, #822) — a reply's model was only the vendor's own name (glm-5.3-flash), naming neither the provider nor the group's member, so pi, which counts usage by provider/responseModel, put every member's calls in one bucket. Every reply now carries X-Magpie-Provider (the provider id) and X-Magpie-Model (provider/model of the member that answered, after a failover the one that did), relayed or translated, streamed or not; the body is left as the vendor wrote it by default, since agents read it themselves (Claude Code its features, Codex its catalog). Name the member in replies (memberModel, off by default; zh, ja, de strings), or a request's X-Magpie-Response-Model: member, has the body's model say the member's id as well: Chat's model, Responses' response.model, Anthropic's message.model / model, Gemini's modelVersion, in each event of a stream as it goes and in a JSON body once whole (memberWriter, between the masking and the request log, which keeps the vendor's). Claude Code, Claude Desktop and Codex always get the vendor's name; X-Magpie-Response-Model: vendor asks for it too. The vendor's own name (dated versions included) stays in the usage ledger's Served. With the owner's real DeepSeek key in a sandbox, a group [a dead member, deepseek/deepseek-flash]: the headers said deepseek and deepseek/deepseek-flash on Chat, Messages and Responses, streamed and not; with member asked the body said deepseek/deepseek-flash in all six, with the setting on pi's token got it and Codex's token and a claude-cli User-Agent got deepseek-flash. TestReplyNamesMember (6 paths × streamed or not × default, on, asked, vendor asked, Codex, Claude Code, each after a failover; passes -race) fails on the old code, TestMemberLine; member-model-setting.test.cjs (WebKit, en and zh). settings-sections, settings-groups and searcher-pick pass on WebKit; redact-rule-row fails 2 the same on origin/main. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 02:27:03 +08:00
mimo: a Xiaomi MiMo account (the one MiMo's app signs in to) is signed in inside magpie and its models served through the gateway to any agent, and MiMo Code is an agent magpie configures (#249 thedavidweng) — magpie had neither. The subscription (id mimo-app, shown as Xiaomi MiMo) signs in with Xiaomi's long-poll login (account.xiaomi.com/longPolling/loginUrl, sid mimosgp) and keeps the MiMo server's cookie session (serviceToken, userId) in logins.json at 0600; the passToken is replayed to renew the session after a day or on a 401. Requests go to POST <base>/route/chat/completions on mimo-server-{sgp,ru,in}.xiaomimimo.com/api with X-Mimo-Source mimocode-cli-free and X-Client-Version; mimo-auto is sent as mimo-pro; the models are mimo-pro and mimo-flash. Usage from /user/usage and /user/xiaomi/subscription/self. Several accounts, switched, on/off and forgotten as the other subscriptions. The add list has a Xiaomi MiMo tile (Free · Starter · Plus · Pro · Ultra) that says Xiaomi may treat this as third-party use before anything is posted; en and zh. magpie accounts takes mimo. MiMo Code (agent mimocode, OpenCode-like) is configured in its config dir (MIMOCODE_HOME/config when set), reading mimocode.jsonc, mimocode.json or config.json, whichever is there. Tested with internal/provider/mimo_test.go against fake Xiaomi SSO and MiMo servers in a temp HOME, internal/agent tests for MiMo Code's config, and internal/gui/tests/mimo-sub.test.cjs, Chromium and WebKit, en and zh; they fail without the change. factory-sub, click-scroll, old-webkit, zed-sub and add-sheet pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Xiaomi account: the long-poll's success payload, the SSO cookie chain, whether /route accepts magpie's requests, the UTC+8 times, and whether MiMo Code picks up the config; not tried in the real Wails window.
2026-09-30 07:43:58 +08:00
gui: an agent connected to magpie is never under 未设置, and one not connected goes there whatever its own files say (#726, EZN7L2C3 on the Agents page: Codex, 已接入 with its model on 上次的选择, sat under 未设置 below 收起, while Antigravity CLI and DeepSeek Harness, 未接入 with their switches off, stayed up top — "ones I've turned on go down there; the logic feels chaotic") — arrangeAgents folded what agentUsed called not set up, and agentUsed was "added, or any field with a value (not an effort or ultracode)" from cedc86db/48c496d0, before 「接入」's switch: an agent's fields are read from its own files, so one not connected (wired false) with a model of its own (agy, dsh) counted as set up, and a connected Codex starting on its last choice has no model value and counted as not. Now an agent the switch can connect (connectable) is set up when it is on magpie (onMagpie: wired, added, or drifted — one magpie set up that came apart stays up to be put right), and only then; one with no switch (Cursor, Copilot, an app with no magpie models) goes by what is set on it, as before. Folding starts once one is on magpie while any agent can be connected, so a Cursor on its own "auto" doesn't fold every agent away on a fresh magpie (agentsLead's welcome still says none is connected); with nothing to connect, once any is set, as before. Show on a hidden agent with a switch says it stays under Not set up "until it is connected" (zh 接入前,它会留在「未设置」里). agent-connected-unfolded.test.cjs (the report's rows: Claude Code, OpenCode, Codex on its last choice connected and in view, Cursor in view for its own model, Antigravity, DeepSeek Harness and Kimi Code folded under Not set up/未设置, every row in view switched on and every folded one off; with none connected, nothing folds) fails on the old code (agy and dsh up, codex folded) and passes. Fixtures that are on magpie models but said wired false (agy-launch, claude-direct, model-pick fillers) are wired now, as Agent.Wired would read them; agent-disconnect unrolls the fold for Claude Code, not connected; agent-tweak-fold checks ultracode moves no row with Claude Code connected, and the efforts alone on agents not connected. Webkit: those and show-unset-agent, panel-fold, agent-grip, agent-connect, agent-layout, agent-start-pick, agent-expand-steady, claude-picker-fold, subagent-effort pass; the full webkit suite's other failures (cli-update, routing-time, zcode-site, menu-scroll and others) fail the same on the old code. go vet ./internal/gui and the asset tests pass; the full go test ./internal/gui under a temp HOME was not run here (the sandbox refused the HOME override) — the change is JS only. Not tried in the reporter's app.
2026-10-04 07:27:36 +08:00
gui: a model added by id has its own Add model button, and Built-in icons offers 92 more vendors' icons, found by name (Tom on X: 希望在 provider 的 models 里面增加手动添加模型选项;另外就是 Built-in icons 太少了) — a model the vendor's list leaves out could be added by typing its id in the box under the list and pressing Enter, but the box alone said nothing of that and Tom didn't find it. It now has an Add model button beside it, off until an id is typed, with a title saying what it does (several ids, comma separated); a click picks the typed model as Enter does and a Save sends it. Built-in icons listed only the icons some preset uses, about 60; it now also offers MORE_ICONS, 92 vendors' and platforms' marks from lobehub/icons 1.95.1 (MIT, where magpie's other icons come from): Alibaba Cloud, Bailian, Doubao, Hunyuan, Wenxin, Hugging Face, Perplexity, Cohere, Cerebras, SambaNova, Novita, PPIO, Together's peers, AWS, Vertex AI and the like. A colour icon that draws part of itself in currentColor (black in an <img>, so lost on a dark tile: aws, cerebras, longcat, poe, yi, zeroone, llmapi) is taken in its monochrome version, drawn as a mask in the text colour. The grid, now well over a hundred, scrolls in its own box (196 px) under a Find an icon… box that filters by name. zh strings added. models-add-and-icons.test.cjs (Chromium and WebKit, en and zh): Add model off/on, a click and Enter pick the id, every built-in icon draws (none falls back to the cube, no broken image), find narrows to one, and Save sends the models and the icon picked; it fails on the old code. Looked at in dark mode on WebKit. model-test-why, model-test-one, provider-detect(-models), provider-levels-save, provider-same-as, provider-typed and provider-picks-stay pass on WebKit; model-api-preset (2) and provider-levels-scroll (1) fail the same on origin/main. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./internal/gui/ under a temp HOME pass.
2026-10-04 19:19:57 +08:00
providers: a provider's model list can be set to its free models alone, with Free only beside Select all and Select none (Discord: 是否可以支持只选择免费模型?现在只有"全选"和"全不选"两个按键) — the provider editor's model list (renderModels: keys, custom, signed-in accounts) could pick every model or none, so keeping just the free ones (ClinePass's cline-free/, WorkBuddy's x0.00, OpenCode Zen's, a vendor's foo:free) meant clicking each by hand. A third button, "Free only" / 只选免费, picks exactly the models the list marks free (m.free, or a name that says free, as the FREE badge has it), those the filter shows as Select all does, and unpicks the others, models added by hand among them; it sets the editor's picks as the other two do, so a Save sends them. It is offered only when the list has both free and paid models: with none free it would pick nothing, with all free it is Select all. Same text-action style as the other two, no coloured left border, and a click leaves the page and the editor where they are. That is the only model list with Select all / Select none in app.js (the import's "Select all" is a checkbox of providers, not models). models-free-only.test.cjs (WebKit, and Chromium through a temp copy on plain chromium.launch(), as channel chromium has no ICU data here; en and zh: not offered on an all-paid or all-free list, offered on a mixed one, styled as Select all, the filter's free models alone picked under a filter, exactly the three free ones picked with the editor's body scrolled and nothing moving, a Save posting them, both strings translated) fails without the change. provider-picks-stay, account-models, model-test-one, provider-typed, editor-short-window and click-scroll pass in WebKit and Chromium, except account-models' chromium "Codex account is kept for some models" case, which fails the same on origin/main without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./internal/gui/ (under a temp HOME) pass.
2026-10-02 13:50:37 +08:00
tray: expanding and collapsing on the Agents page keeps what was clicked where it is, and "Show {n} more" unrolls the rest under it (碳碳双键: 这个地方展开 ui 会偶现疯狂抖动,必现拉扯; #199: agent页面展开与收纳会抽搐 甚至跑在窗口外面去; willz: 点击展开的时候,列表不是向下展开 而是向上展开) — the rest unrolled above the button, so in a panel at its tallest with the list scrolled the button slid down and off the foot of the view. What a click holds in place was passed over while anything played on it, a colour easing in included: the button's own hover fade and its label's fade meant the page was held by the list instead, so the button went off-screen for ~300ms and then the page snapped 90px back to it; and a button slid out from under the pointer and back fades its hover again, so the page swung between the two (484 → 563 → 484 → 572 … frame by frame in Chromium). Held elements were also put back a frame late, from requestAnimationFrame, so what was clicked trembled by a frame's growth. Now only a play that moves an element (transform, top, margin …) passes it over, the hold is put back as soon as a growing part is laid out (a ResizeObserver, before paint), and the button comes before what it unrolls: the rest come in under it, the rows above stay with it, and in a view with no room below the view goes down with them (scrollOnPurpose with the click), led by the fold's foot frame by frame, never past the button's top; Show less rolls them up under the button, which stays put. Tested in Playwright Chromium and WebKit with a stub panel (13 agents, Codex open, list scrolled to its end) and the window: new internal/gui/tests/panel-fold.test.cjs checks each painted frame of Show more (the view only goes down, the row above keeps with the button, the rest below it, then in sight), Show less and a row opened and closed (what was clicked within 2px); it fails on main (button moved 99px, the swinging in Chromium). All node tests and go test -tags nogui ./internal/gui/... pass; click-scroll's "the view must be at its end" also flakes on main under load
2026-09-29 16:40:03 +08:00
2026-10-04 11:28:50 +08:00
library: skills are picked to turn on or off together, and a part of a list (lark-*) has its own chips (#791, mintonight: 资源库中local中的技能,能不能支持多选分组,或者支持多选来开启或关闭 … lark-* 我希望把他们归到一起统一来管理) — the skills on this computer are one list with no heading when they're all from there, so #787's group chips never showed for them, and Feishu's lark-* skills were turned on or off one row at a time. A part of a big list (its skills' shared name start, or their folder) now has chips on its heading, as a repository's heading does: lit when an agent has them all, half lit when it has some, a click gives it every one or takes them all, All does it for every agent; a click doesn't fold the part. Select, beside Turn all on, puts a box on every skill's row and on every heading (ticked when all under it are picked, half with some), a row's click picks it instead of opening its SKILL.md, and a bar that stays in view under the list says how many are picked, has a box for every skill the filter shows ("All 4 that match"), the same chips for the picked ones, Clear and Done; the picks stay through a change and Done takes the boxes away. A pick doesn't scroll the page. zh and ja strings added. pick-skills.test.cjs (WebKit, en and zh): the lark part's Codex chip posts the four lark skills, rows and headings pick, the bar's chips post the picked ones, the filter's All picks what it shows, Done ends it; it fails on the old code. Looked at in light and dark. group-skill-chips, every-skill, library-all, library-items, library-toggle-status, library-row-open-scroll, own-skill, remove-all-skills, shared-skills, import-all-skills, library-own-folder and library-loading pass on WebKit; Chromium doesn't launch in this sandbox. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./internal/gui/ under a temp HOME pass.
2026-10-04 22:52:01 +08:00
plugins: Check for updates asks npm now for each installed plugin's newest version, and each row says what it found (the owner: 插件的检查更新按钮还是得做) — the community's plugins update by themselves a little after magpie starts and every 6 hours, and the page knew of anyone else's new version only from what npm said within the hour (Info's cache), so a reader who heard a plugin had a new version had no way to have magpie look. The foot of Installed (beside Update all) now has Check for updates, shown when a plugin is npm's (a folder's or a git one's has no versions there; a git one keeps its own Update, which fetches it again): it says Checking… with a spinner while POST /api/plugins/check asks npm, a few at a time, past the hour's cache (plugin.CheckNow, through npmAsk, which is npmInfo telling why npm said nothing), and nothing is installed. Each row then says it: "v0.0.9 out" titled "v0.0.7 installed, v0.0.9 on npm" with its Update (the existing upgrade path, Upgrade → Add), "Up to date", or an amber "Couldn't check" chip with a dot, titled why — npm not reached (check your connection or proxy), npm turning requests away (429, try again in a few minutes), npm answering with an error, or npm not having the package (fetchJSON's answers other than 200/404 are now a statusError, so a 429 is told apart). The status line says how many have updates, or that every plugin is up to date, and how many couldn't be checked and why; npm not reached at all is "Couldn't check for updates: npm couldn't be reached — check your connection or proxy". A community plugin's Update is titled that magpie would update it by itself within a few hours and Update does it now. What npm said is kept as Info keeps it, so the rows and the market read it, and an update found for the reader (someone else's, or pinned) waits as the background look's do, putting the dot on Plugins; one npm didn't answer for keeps what waited before. Every new string has its Chinese; the click never scrolls the page. TestCheckNow (an httptest npm registry: the community's and someone else's updates found past an hour-old cache saying otherwise, up to date, 404 missing, 429 limited, 500 registry, git and folder not asked, nothing installed, the cache and the waiting list updated) and TestCheckNowOffline (npm not reached: offline, what waited still waits) are new, as is TestPluginsCheckRoute; plugin-check-updates.test.cjs (WebKit and Chromium, en and zh: Checking… while npm is asked, the rows' out / up to date / couldn't check chips and titles, the status line, Update after the check, offline said plainly, the view's scroll unmoved) fails without the change and passes with it. plugin-updates, plugin-market, plugin-progressive, plugin-git, plugin-local and click-scroll pass in WebKit and Chromium. 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.
2026-10-02 21:58:22 +08:00
plugin, provider: a plugin can press its vendor's daily check-in itself (auth.checkin), and magpie runs it as its own check-ins (Lemon on Discord #feedback: provider 每日签到的方法能放到插件里面去做吗; ARNO earlier asked for Qoder's) — the check-ins magpie had (WorkBuddy, Trae CN, MiniMax Code, Qoder) are each written in magpie against the vendor's endpoint, even for a subscription that moved to its plugin, so a plugin author had no way to give their provider one. A plugin's auth hook may now have checkin(getAuth, provider) beside usage: the host lists the provider with checkin: true (plugin.Provider.Checkin) and answers checkin({provider, account}) (plugin.AccountCheckin), refreshing the account as usage does, with {outcome, credit, streak, message}; outcome is claimed, done, ineligible, inactive, captcha or failed, and a throw or an outcome outside these is failed with the reason. internal/provider/plugin_checkin.go runs these through checkin.go's checkiner, as the built-ins are: each signed-in account in use once a Beijing day (first look 2 minutes after start, then every 30), a failure asked again after wbCheckinRetry, every other outcome settled for the day, results kept in plugin-checkin.json by provider|account; a failure is recorded as failed and resets nothing. captcha is new (CheckinCaptcha): the vendor wants a captcha, which magpie never solves, so it is said as "check in in its own app", not as a failure, and not asked again that day. The results mark the account's Usage card (checkinBy "plugin:<provider>", WithCheckins), so the card's check-in row, its Auto check-in and Check in now work for it (POST /api/settings/plugin-checkin {provider, on}, POST /api/usage/plugin-checkin {provider}); Settings → Usage gains a Plugins tab, shown while such a plugin is signed in, a Daily check-in row per provider with Off/On and how each account went; magpie accounts checkin and c on the TUI's Usage page include them (the TUI's "no account" line names check-in plugins). The switch (settings.PluginCheckins, by provider) is off by default; for WorkBuddy, Trae CN, MiniMax Code and Qoder it follows that vendor's existing switch until set on its own, and magpie's own check-in through the plugin's fetch leaves a provider whose plugin checks in to it (pluginChecksIn), so no account is checked in twice. The Settings page's save carries PluginCheckins over (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). No plugin declares the hook yet; Qoder's check-in stays magpie's own here. TestPluginCheckinRunsAsTheBuiltinsDo (the fake plugin with FAKE_CHECKIN, five accounts answering claimed, done, captcha, a throw and an unknown outcome: off presses nothing; on, each outcome kept with its vendor; a minute on nothing pressed again; past the retry only the two failures; the next day all five; PluginCheckins and the cards marked; another provider's press asks none) and TestPluginCheckinTakesOverTheVendors (a qoder plugin without the hook leaves magpie's Qoder check-in its account, with it none; PluginCheckinOn's fallbacks) pass -race -count=10, and the latter fails with pluginChecksIn stubbed false; TestTUIChecksPluginsIn passes -race -count=20. plugin-checkin.test.cjs (the card's row with the captcha said, Auto check-in posting {provider, on}, no page move; the Settings Plugins tab at 440px with the row fitting, no sideways scroll, On posting) passes on Chromium and WebKit in en, zh, ja and de; usage-checkin, i18n-keys, gui-ja, gui-de and settings-* pass. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with a real plugin or account: no published plugin has the hook yet. docs/subsystems/provider-plugins.md (the hook's contract) and docs/reference.md say it.
2026-10-06 18:40:45 +08:00
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.
2026-10-02 08:20:32 +08:00
plugin, gui: a plugin's account whose model list failed says why, rather than listing Auto alone (gnayiab on X) — with magpie in WSL, Cursor's plugin account offered only Auto: the plugin's models hook couldn't get the account's list and magpie, with no list kept from before, showed the plugin's defaults with nothing saying so, its editor even calling them "vendor list", and Refresh answered "1 models". The host now keeps why a models hook fell back (what it threw, else how its last fetch failed) for the provider and each account; plugin.Cached carries it with the accounts it reads again from the sign-ins; provider.ListError gives it for a plugin's account. The editor says "<provider> couldn't list its models: <why>" under the models (amber, no left border) and no longer calls the defaults the vendor's list, and Refresh fails with it. A list kept from before, or one that comes in, says nothing. en/zh/ja/de. Tried with the real Cursor plugin (0.1.9) in a sandbox: a key Cursor refuses shows "Cursor couldn't list its models: Cursor didn't take this API key" and Refresh says it; the owner's real account lists 72 models with nothing said, and Claude Code -p through the gateway ran a Bash tool on cursor/auto, composer-2.5 and claude-opus-5-thinking. The plugin itself, in WSL (magpie-desktop), listed 254 models and streamed chats with tools. TestFallenBackListSaysWhy (hook returns its defaults, throws, or hands back its own) fails without the change; plugin-list-error.test.cjs (Chromium and WebKit, en and zh) too; TestFallenBackListKeepsTheLastOne, account-models, plugin-signin and click-scroll pass. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 17:39:29 +08:00
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.
2026-10-02 08:20:32 +08:00
gui: built-in subscriptions that have a community plugin are marked Deprecated, with the reason — some subscriptions are reached in ways their vendors' terms may not allow, so to keep magpie itself from being banned they are being decoupled from magpie and run by community plugins instead. The provider list's quiet "can run on community plugins" line is now a deprecation notice (an amber card with the plugin mark: the deprecated signed-in subscriptions, the reason, that moving keeps accounts, models and agents, and Review in Plugins — or Review the move for one — beside Not now, which hides it until another deprecated subscription signs in). Their rows, their Add tiles (signed in or not; /api/providers' new movable lists every built-in with a plugin, from provider.MovableIDs) and the editor's Runs on field carry an outlined Deprecated badge whose tooltip gives the reason; the Add sheet's Subscriptions end with a line saying why, with Browse plugins. A subscription already on its plugin, or one with no plugin, carries none. deprecated-subscriptions.test.cjs (Chromium and WebKit, en and zh: the badges, their reason, none on a moved or keyed provider, the notice, Not now staying hidden), plugin-move and plugin-movable-review updated to the notice. add-button, add-sheet, mobile-web, old-webkit, plugin-move-overflow, plugin-moved-signin, plugin-market, plugin-signin, plugin-plan, provider-search, provider-picks-stay, providers-off-fold pass (click-scroll's 5 failures fail on main as well). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gui/ pass.
2026-10-02 11:39:05 +08:00
gui, sessions: a moved subscription's other sign-in ways are a link away, ZCode's app's own account among them (Jinyu), and a session in a WSL distro is deleted as this computer's are (TJHHHH) — Jinyu asked to add a ZCode account from the one the ZCode app is signed in to rather than signing in again. The ZCode plugin (0.1.8) already does that (its "ZCode app's sign-in" reads ~/.zcode/v2/credentials.json), but a moved subscription's Add went straight to the plugin's first way (#777: one click), so it couldn't be reached. Now ZCode's site step shows its other ways beside the sites (the app's account, the GLM Coding Plan API key), and while a moved subscription's sign-in waits, Other ways to sign in… (when it has ways other than the one under way and Use an API key instead) cancels it and shows the plugin's ways; the owner's suggestion on #777 (suyu-ops). Factory, whose two ways are its device code and its key, still goes straight through with no extra link. No plugin release or mover change. With a sandbox copy of the owner's real ~/.zcode the plugin's way signed in (account, key, site zai). TJHHHH asked to delete sessions listed from WSL: they were resumable only, the page saying to delete them in WSL. Now they have their box and are deleted the same way: moved to magpie's trash over \\wsl.localhost (copied and removed across the device boundary), Claude Code's file history from the distro's ~/.claude, never this computer's; dropped from the distro's listing at once, which is listed again next; Restore puts the files back in the distro and lists it again. zh, ja, de strings; the WSL note's strings are gone. TestWSLSessionDeleted fails on the old code. On the Windows box against a sandbox folder in a running WSL2 distro (magpie-desktop), Delete moved the session, its subagents and its file history out of the distro into a %APPDATA% trash, and Restore put them back and listed it again (removed afterwards). plugin-other-ways.test.cjs (ZCode's ways at the site step and from the waiting box, Factory's one click) and sessions-wsl-delete.test.cjs (the WSL row's box, Delete in magpie's dialog posts its id, no note) fail on the old code and pass on WebKit in en and zh. sessions-folder-delete, sessions-manage, checkbox-look, plugin-moved-signin, gui-ja, gui-de 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.
2026-10-05 11:26:08 +08:00
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.
2026-10-02 08:20:32 +08:00
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.
2026-10-02 08:20:32 +08:00
gui: the Add sheet shows a new user only magpie's own subscriptions; a deprecated one is installed from Plugins (the owner: 对于新用户来说,这里应该只显示内置的 provider 啊,更多不是要点击那个链接跳转到插件页面让用户去主动安装吗?) — a fresh magpie listed all 18 subscription tiles, the deprecated built-ins (Cursor, Kiro, Zed, Qoder…) among them, each offering its plugin when clicked. A deprecated built-in with no account in magpie (unusedSub) is now left out of the Subscriptions section, which for a new user is Claude, ChatGPT, Copilot, Gemini CLI and Antigravity, then More in Plugins; a search naming a left-out one shows More in Plugins, which opens the Plugins page on that search, where its plugin is installed. One with an account stays, badged Deprecated; one moved to its plugin stays as before; one whose plugin is installed shows the plugin's tile (replacedSub). The tile's plugin offer (Install and sign in / Use the built-in), unreachable now, is removed with its CSS and zh strings. AGENTS.md says so. deprecated-subscriptions.test.cjs now checks Zed (no account) isn't in the sheet, a search for it shows More in Plugins and opens Plugins on "zed" with Zed's card, and nothing signs in or installs by itself; plugin-replaced.test.cjs expects no tile for an unused deprecated built-in and one for a signed-in one. Both pass in Chromium and WebKit (en/zh), as do add-sheet, provider-search, qoder-cn-tile, mobile-web, deprecation-notice, click-scroll, plugin-move-overflow, plugin-movable-review, plugin-move-unserved, plugin-progressive and plugin-check-updates; plugin-move's editor-scroll check in Chromium flakes the same on origin/main.
2026-10-03 00:52:41 +08:00
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.
2026-10-02 08:20:32 +08:00
library: a project gets its own MCP servers, agent by agent (#500) — the library's servers could only be given to agents user-wide, while its skills could already go into a project. The MCP tab now lists the same projects as Skills under "In projects" (add one by its folder, or the folder picker in the app), and a project's card opens to each server with chips for the agents that read a project's own servers: Claude Code .mcp.json, Codex .codex/config.toml, Gemini CLI .gemini/settings.json, Cursor .cursor/mcp.json, OpenCode opencode.json (opencode.jsonc when that is the project's file), each as its docs give it. Other agents have no chip and are named under the card; the project files of Copilot, Crush, Droid, Kimi and the rest weren't confirmed, so none is written. The server is written with the same encoders and edit helpers as the user-wide files, so the rest of a file (its comments, other keys, the team's own servers) is kept; magpie records what it wrote in the project (Project.Servers, Wrote, MadeFiles) and only ever takes that away again, a same-named server that isn't magpie's is left as it is and named as a problem, as is an SSE server for Codex. A file magpie made is listed in the project's .gitignore between magpie's lines (a server's env and headers can hold keys) and removed with its folders once nothing of magpie's is left in it, unless the user wrote something else into it; a file that was there already is never ignored or removed. Renaming a library server renames it in its projects, removing it takes it out of them, and removing a project takes out its servers as well as its skills. A project whose MCP file is an agent's user-wide file (the home folder) is refused, as for skills. The chips of a project row (servers and skills) now light at once and send one write at a time, the last click winning: two quick clicks posted the first's stale list before. Claude Code still asks before it starts a project's servers and Codex reads .codex/config.toml only in a trusted project; the card says so. library-all.test.cjs gets a 800px-high window: the MCP tab's new "In projects" made it overflow 700 by a pixel, and Chromium's scroll anchoring then moved it when the problems card came in above. TestProjectServers (merge into a user's .mcp.json keeping its comment, key and server; Codex TOML; .gitignore; rename; removal down to the made folders; RemoveProject keeping the user's own), TestProjectServerNotOurs, TestProjectServerFileKept, TestProjectServerChecks and TestProjectServerOpenCodeJSONC run in a temp HOME; project-servers.test.cjs (en, zh; chromium and webkit) checks the chips, the greyed Codex chip for SSE, two clicks before magpie answers, and that nothing scrolls. No real agent was started on a written project. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; the library's Playwright tests pass in webkit, and in chromium except list-sort, shared-skills and import-all-skills, which fail the same way at HEAD.
2026-10-02 12:37:07 +08:00
2026-10-02 11:31:11 +08:00
providers: each provider, a signed-in account like Codex's among them, goes through its own proxy — the one in Settings, none, or its own address (#237 xosi: 目前似乎是全局设置代理的,codex和国模供应商无法单独区分设置代理) — magpie had one proxy, Settings' (or the environment's, or the system's), for every request it made, so Codex, which needs a proxy from China, and a vendor at home, which is slower or refused through one, couldn't both be right. A provider now has a proxy, "" (following the global one, as before and the default), "direct" (none, whatever Settings says) or an http://, https:// or socks5:// address (host:port meaning http), kept as proxy in providers.json only when set, and for an account provider in its picks entry beside its models. It is checked on save by the same rule Settings' proxy is (settings.CheckProxy, now shared), so ftp:// and the like are refused. netproxy.With puts a provider's choice on a request's context and netproxy.Func honours it; netproxy.Dispatch sends such a request through a clone of the transport kept for that proxy alone (at most 32, cleared past that), since a transport shares an HTTP/2 connection by host whatever proxy dialled it; loopback is never proxied. The gateway's client and http.DefaultClient go through Dispatch, and every request made on a provider's behalf carries its choice: the gateway's turns (attempt, forward, draw, decide, the Codex backend), and the model lists, balances, plan and subscription usage, tests, sign-in, Codex's warm-up and reset, while the Claude subscription's and warm-up's CLI runs get the account's proxy in their *_PROXY (netproxy.EnvWith). The provider editor, a key provider's and a signed-in account's alike, has a Proxy row after Fallback: Global proxy | Direct | Custom (全局代理 | 直连 | 自定义), the address field beside the options only for Custom, its room kept otherwise so a pick never changes the centred dialog's height and moves nothing, and a line under it saying what the pick does; Save posts proxy ("" , "direct" or the trimmed address), a save without it keeps the old one, and Custom with no address is refused before anything is posted ("Proxy: type its address, like http://127.0.0.1:7890"). No border stripes; en and zh strings together. Tested with a new internal/gateway/proxy_test.go in a sandbox HOME: with a global proxy set, provider A's turns and model list go through its own fake proxy, B's (direct) straight to the vendor, C's (following) through the global one, none through another, and A set back to following goes through the global one; ftp:// is refused (fails with With neutered). netproxy_test.go's TestWith covers Func, Dispatch's per-proxy transports and EnvWith. A new internal/gui/tests/provider-proxy.test.cjs in Chromium and WebKit, en and zh: a key provider opens on Global proxy with no address, Custom unfilled is refused and nothing posted, a padded socks5 address saved trimmed, Direct saved as "direct" and Global as ""; Codex's account opens on Custom with its address and saves Direct with its models; every pick leaves the option within 1px; no left border. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./..., provider-proxy, click-scroll, old-webkit, balance-fix and add-sheet pass (click-scroll's WebKit case failed once alongside the others and passes alone, twice). Not tried against a real proxy or the real Wails window.
2026-09-30 02:15:50 +08:00
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.
2026-10-02 17:31:54 +08:00
2026-10-01 17:13:34 +08:00
gui: a subscription with several accounts shows one of them on Usage and in the tray panel, the others behind "Show N more accounts" (whqtian on Discord: 我这个workbuddy只有4个账号,其实大多数情况下,我不需要显示全部的账号信息,你只要显示一个账号信息即可,其他的可以点击展开再看) — every provider's card listed each account's meters, credits and curve one under another, so four WorkBuddy accounts took the page. Now a card with several accounts shows the one that answered through the gateway last (lastServedAt), else the first, with "Show 3 more accounts" / 展开其余 3 个账号 under it; opened, all are shown in their order with "Show fewer accounts" / 收起其余账号. Opened is remembered by provider (localStorage magpie.usageOpen), for the window and the tray panel alike, each following the other as it is turned; a provider with one account has no button. The folded accounts stay in the page, hidden, so Hide accounts masks them as before; the line between accounts is drawn only after one in sight. The click keeps the page where it is (backToReader). usage-fold.test.cjs (WebKit, en and zh: 4 accounts show the last-served one and the button, opening shows all without moving the page, remembered after a reload, one account has no button, masking covers them, the panel folds the same and the window follows it) fails on the old code and passes; quota-families and quota-pools open the panel's second Antigravity account first. account-mask, quota-curve, click-scroll, mask-emails, credit-days, balance-parts, usage-arrange, panel-usage, tray-usages and usage-accounts pass; the GUI tests failing here fail the same on origin/main. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 16:50:31 +08:00
gui: a subscription with several accounts shows one of them on Usage and in the tray panel, the others behind "Show N more accounts" (whqtian on Discord: 我这个workbuddy只有4个账号,其实大多数情况下,我不需要显示全部的账号信息,你只要显示一个账号信息即可,其他的可以点击展开再看) — every provider's card listed each account's meters, credits and curve one under another, so four WorkBuddy accounts took the page. Now a card with several accounts shows the one that answered through the gateway last (lastServedAt), else the first, with "Show 3 more accounts" / 展开其余 3 个账号 under it; opened, all are shown in their order with "Show fewer accounts" / 收起其余账号. Opened is remembered by provider (localStorage magpie.usageOpen), for the window and the tray panel alike, each following the other as it is turned; a provider with one account has no button. The folded accounts stay in the page, hidden, so Hide accounts masks them as before; the line between accounts is drawn only after one in sight. The click keeps the page where it is (backToReader). usage-fold.test.cjs (WebKit, en and zh: 4 accounts show the last-served one and the button, opening shows all without moving the page, remembered after a reload, one account has no button, masking covers them, the panel folds the same and the window follows it) fails on the old code and passes; quota-families and quota-pools open the panel's second Antigravity account first. account-mask, quota-curve, click-scroll, mask-emails, credit-days, balance-parts, usage-arrange, panel-usage, tray-usages and usage-accounts pass; the GUI tests failing here fail the same on origin/main. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 16:50:31 +08:00
quota: each allowance says when it was read, a cached one keeps its own time, and magpie quota history tells the windows' readings over time (#802, margbug01) — a subscription's card had no time unless its reading had failed, and a Claude account's allowance, which is what Claude Code last told (kept until it answers again or runs /usage), was shown on every refresh as if just read and written into the history again at each one's time. A reading now carries ReadAt: the time it was made (readNow, for every subscription and account read), and a Claude account's the time Claude Code told it (claudeReadAt). The usage card says "Updated 40 minutes ago" from it, a reading standing in for a failed one still says "As of …" in amber, a failed one says neither. The history (#651) keeps a point at its ReadAt, so a reading shown again is no new point. magpie quota and magpie accounts add "read 40m ago" to a line read more than 2 minutes before, and --json and GET /v1/magpie/quotas carry readAt. magpie quota history [<provider|account>…] [--days N] [--json] tells each account's windows over time (UTC, kept 45 days, a point only where it changed, the runs of a window apart by their reset), the last eight a window in text with ↻ where it started again, every one with --json, as GET /v1/magpie/quotas/history answers. On the card, hovering a window's key under the curve lists its latest readings with their times; no control is added to the card (the curves are kept uncluttered). Strings in zh, ja, de. Request-log correlation between readings isn't attempted. TestClaudeKeptReadingKeepsItsTime (a kept Claude reading shown three times ten minutes apart is one point at its own time; fails on the old code), TestQuotaHistoryCmd, and quota-read-at.test.cjs (WebKit, en and zh; fails on the old code). quota history run against a copy of the owner's real quota-history.json lists their Claude, Codex, Command Code, Copilot, Cursor and Devin windows. quota-curve, quota-pools, quota-families, quota-credits, quota-count-wrap, claude-usage-asked, panel-usage and balance-* pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn, TestUserPath, TestClaudeSignedOut and TestCursorSignInJoinsAWrappedLink failed under load and pass alone).
2026-10-05 00:14:25 +08:00
usage, gateway, gui: a reply that reasoned is timed by its answer, from its first text, not by all its output from its first token (tony on Discord #general: Usage › Requests read "gpt-6.1-sol 163 token/秒 · 8,036 个请求 · 首字 14 秒" and asked whether subagents were added up) — speed was every output token over the time after the first content (text, reasoning or a tool call). Output counts the reasoning, and a model whose reasoning is hidden (OpenAI's encrypted reasoning, a summary sent when it is done, Claude's omitted thinking) writes it before the stream shows anything, inside the TTFT; so a 2,000-token think and a 200-token answer over the 1.5 s after the first token read 2,200 in 1.5 s, 1,467 tok/s. Concurrency was not the cause: the aggregate is Σ tokens over Σ per-request windows, so subagents running at once never add up. In the owner's own usage.jsonl codex/gpt-6-luna turns read up to 600 tok/s (out 496, reasoning 224, 11,055 ms, ttft 10,228) and its aggregate 133; it is 80 now. usage.DecodeOf is the one rule: a reply with reasoning counts out − reasoning over ms − firstText, one that reasoned and wrote only tool calls tells no speed, one without reasoning is timed from its TTFT as before; DecodeWindow's burst cut (#731) still applies. Record.Decode feeds summarize, the ledger's Totals and its timeline Parts; a route keeps its reply's reasoning (Route.Reasoning, from gateway.go and codex_backend.go), and routing.js's decodeOf/speedOf times its rows, the story and a session's average by it (an older route takes its served usage's reasoning); app.js's ledDecode does the Usage page's rows, whose tooltip says the reasoning left out, and the Output speed help says the rule, in en, zh, ja and de. History is read the same way, as it keeps the reasoning and the first text. Routing never ranks by speed. TestReasoningReplyTimedByItsAnswer (the 2200/2000 turn, a summary 10 ms before the text, a concurrent subagent, tool calls only, reasoning over output, no reasoning, not streamed: 1,200 tokens in 11 s through summarize, addRow, LedgerSeries and Decode) fails without the change and passes -race -count=10; TestUsageKeepsTimeToFirstToken now has its paced replies report reasoning and checks the route keeps it. reasoning-speed.test.cjs (Routing rows 133, 100, none and 100, the story never 1,467, the session 105; the Usage rows, tooltip and KPI the same; no sideways scroll) passes on Chromium and WebKit in en, zh, ja and de, and fails against the old assets (1467 tok/s); routing-request-metrics, routing-burst-speed, usage-speed, usage-ledger-detail, i18n-keys, gui-ja, gui-de, ledger-hscroll and click-scroll pass on both. go test -race -tags nogui ./internal/... under a temp HOME passes but internal/proc's TestUserPath ("the login shell's PATH never came"), which failed under the full run's load, passes 3/3 alone and is not touched here; go vet, gofmt -l, linux and windows (nogui) builds pass. Not tried with tony's data. docs/reference.md says how output speed is counted.
2026-10-06 19:47:56 +08:00
gui: the header's refresh reads again what the page shown draws, and no longer looks for a newer magpie (evilgaoshu, #844: 顶部刷新按钮按直觉应该是「刷新当前页面」,检查更新是低频操作,不必占用顶部一个常驻按钮) — the round ↻ refreshed the model lists and checked for an update whatever page was shown. Now on Agents it looks for the agents on this computer again: POST /api/agents/rescan (agent.Rescan forgets the answers kept a while because asking was slow: Cursor Private Inference's apps folder, WSL's distro list and failed probes, Alma's server) and draws the list, Install another agent and the CLI versions afresh, so one just installed or removed, or changed outside magpie, shows at once. On Usage it reads the page now, the allowances too (as its Refresh now); in the panel, Agents rescans, Allowances reads the allowances and Usage the requests; on Providers and every other page it refreshes the model lists as before. Its tooltip says what it reads on the page shown (zh, ja, de strings). A newer magpie is looked for by magpie's own timer and Settings › About's Check, as before. TestRescanLooksAgain (an app installed after magpie looked is found only after the half minute without it) and refresh-page.test.cjs (WebKit, en and zh, window and panel: tooltip per page, Agents rescans and a new agent shows and leaves Install another agent, Usage reads the allowances afresh, Providers syncs, never an update check, no click moves the page) fail on the old code; sync-update.test.cjs, which asserted the old update check, is replaced by it. sync-spin, update-hide, update-check, click-scroll, gui-ja, gui-de, usage-card-refresh and agent-update-all pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 09:26:30 +08:00
gui: Routing › Requests' day bar shows the days that fit and keeps the rest under its last pill, "Earlier days" (#961, emo172: Windows 200%, text size 150%, the bar cut off at the right and the older days out of reach) — the bar scrolled with its scrollbar hidden, so with the 30 days the gateway keeps the later pills were cut at its edge and a mouse wheel (vertical) couldn't scroll it. fitDays hides the oldest pills until the bar fits (on each redraw and on resize), and the last pill opens the app menu (openProtoMenu) listing each hidden day with its count; the day looked at stays in sight: picked from the menu, the pill is named by it and pressed. A pick in an app menu (in body, outside any view) now counts as a click on the pill that opened it for the click hold in app.js, so what the day brings in above the bar doesn't push it down (~138px before). zh/ja/de added. routing-days-fit (30 days at 1100, 733 = 1100 at 150%, and 440px: nothing cut, the menu holds the rest, the oldest picked is asked for, names the pill, the bar held; 3 days no pill; narrowed then widened) passes in Chromium and WebKit, en/zh/ja/de, and fails without the fix (10/12; with only app.js reverted, 8/12 on the bar moving). click-scroll, menu-scroll, proto-menu-overflow, purpose-filter, routing-purpose-state, i18n-keys, gui-ja, gui-de and the other tests opening an app menu pass, except sessions-talk's WebKit omp case, which fails the same on origin/main (184 vs 179), and gateway-key-accounts, which failed 2 of ~26 runs while other test runs loaded the machine (badge read before its redraw) and then passed 25/25 (origin/main 20/20).
2026-10-06 11:49:25 +08:00
gateway, provider: the Routing page says what a lone key or account has left, and a key bought as a plan (Kimi Code, GLM Coding Plan, MiniMax, OpenCode Go, Command Code) is weighed by its plan's windows (#1016, gphysign: Kimi Code (China) with one key on, Smart routing; the card said only "Kimi Code (China) 只启用了一个 Key,无需选择。" while /v1/magpie/quotas had its 5 hours 49 used, 51 left and its 7 days 45 used) — two gaps. weighRouted returned before reading anyone's allowance when there was one candidate, so a lone key or a lone subscription account was never known in the trace, and the Routing page, which shows a bar and "{n} used · renews in {d}" for a known row, had nothing to show; every single-account subscription said "what's left not known yet" the same way. And KeyAllowance read only a sub2api key's limits, so a key whose plan the Usage page reads (planQuotaSourceOf: Kimi Code, GLM Coding Plan and Z.ai, MiniMax Coding Plan, OpenCode Go, Command Code) was never known to routing, with one key or several. weighRouted now reads what each candidate has left before returning a lone one (one alone is still weighed against nobody). readsKeyWindows takes a plan key too; keyWindows reads it through planKeyWindows (planWindows, with Zhipu's team quota behind a key that has none of its own, as PlanQuotas does), and PlanQuotas' own read of a card is noted for routing at once (noteKeyAllowance), so the Usage page and routing never ask twice; the card kept on disk seeds it after a restart (keyCardTag keys a plan card as plan:, a sub2api one as balance:). A key at such a vendor that turns out to be pay-as-you-go, with no windows, is asked again every 30 minutes (keyNoWindowsAge), not every minute it is used. So, beyond the issue: several plan keys on one provider are now weighed by their windows under Smart, usage and pace, and a plan key the vendor refuses as out of quota rests until its window renews and comes back once read short of full, as a sub2api key does. affine no longer says a lone key or account all but used up "moved" its conversation: there is nobody to move to (at > 0). The GUI needed no change. TestLoneOneTellsWhatItHasLeft (a lone Kimi key known at 49% with both renews, the week first; a lone Codex account known; a plain DeepSeek key not), TestLoneSpentKeyKeepsItsConversation (fails with the at > 0 guard taken out), TestPlanKeyAllowance (the windows read behind the request, asked once; DeepSeek and Moonshot keys never asked), TestPlanKeyAllowanceFromItsCard (PlanQuotas' read known at once without a second ask; after a restart with the vendor down, the card on disk stands in and a failed read keeps it) and TestPayAsYouGoKeyAskedSeldom (a GLM pay-as-you-go key asked once, not two minutes on, again 31 minutes on) all fail without the change and pass -race -count=20 (provider) / -count=10 (gateway). routing-key-windows.test.cjs gains a lone Kimi Code key at 1100px and 440px (the one-key story, the row's bar at 49%, its provider row "49% used · renews in …" fitting, no sideways scroll) and passes in full on Chromium and WebKit in en, zh, ja and de; routing-quota-left and panel-routing pass. go test -tags nogui ./internal/..., go test -race on ./internal/provider and ./internal/gateway in full, go vet, gofmt -l, darwin, linux and windows (nogui) builds pass. Not tried with a real Kimi Code key. StepFun's Step Plan is read through a sign-in, not the key, so it isn't one of these. docs/subsystems/gateway-routing.md, providers-accounts.md and docs/reference.md say it.
2026-10-06 20:25:04 +08:00
routing: requests coming in leave the routing groups where they are on the screen, a group being edited too (Jerell.OvO on Discord #general: on the window's Routing page the request log refreshes live and each refresh scrolls the page up, so the routing groups further down can't be read or edited; a switch to pause it or moving the log to another tab was suggested) — every request redraws what sits above the groups (the stage gains or loses agent and account rows, the story a line, the requests and accounts lists theirs), and app.js's backToReader kept the view's scrollTop as a fixed number, putting it back after any scroll the reader didn't make: WebKit has no scroll anchoring to make up for it and in Chromium backToReader undid the browser's own, so the groups slid up under the reader as the parts above shrank and down as they grew. In WebKit the accounts list, emptied and refilled every second for its countdowns, also made the view scroll on its own. Now a view can name a part of itself to keep in place (keepInView): its position on the screen is taken when the reader leaves it (their own scroll, or a click held), and backToReader follows it wherever the parts above take it, from a ResizeObserver so before paint, with the browser's own anchoring off for that view so the two don't fight; a view at its top stays at its top. The Routing page keeps the groups in place while they're in the upper half of the view or a field in them has the focus, else the lists, and with neither (the stage in sight) nothing moves. The accounts list is patched, keeping each row that's unchanged (rows with a button are always replaced, so their handler is the new one), not emptied and refilled. No live-update switch was needed and no new strings; no border stripe. Tested with a new internal/gui/tests/routing-steady.test.cjs in Chromium and WebKit: with the view wheeled down to the groups, one open in its editor and text typed into its name, six requests streamed in from the trace (new agents and accounts, failures and retries): the part above the groups really changed (more than 50 px), the section's head, the editor and the last group stayed within 1.5 px on the screen on every layout and scroll seen (WebKit scrolls by whole pixels while the parts above are laid out in fractions), the view's scrollTop moved by just what grew above them, and the field kept its focus and its text and went on taking typing. On origin/main it fails in both engines (the head moves 233 → 171 px). go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (126 tests) pass. Not tried in the real window with a live gateway.
2026-09-29 23:52:30 +08:00
gateway, gui: a request an aggregator passed on says which provider answered it behind it, "Upstream: DeepInfra" (leslie_luo on Discord: 像 CPA 那样指出实际供应商) — through OpenRouter a request was only "OpenRouter · deepseek/deepseek-chat", with no word of the provider (DeepInfra, StreamLake, Novita…) OpenRouter had sent it on to. OpenRouter names it in its replies, as tried with the owner's real key: Chat's body and every chunk of its stream have a top-level "provider", Messages' body has one and its stream's message_start a message.provider; its Responses replies and every reply's headers name none. The gateway now reads it (upstreamOf) from a body relayed as it came and from each event of one it translated, Chat, Messages or Responses, streamed or not, and keeps it on the request's try and route (upstream) and in the usage ledger (Record.Upstream; the CSV export's last column, searched as the rest). The Routing page's Requests list says Upstream: DeepInfra beside where it went, giving way after the provider's name when short of room, with its story saying the aggregator passed it on; a request's details on the Usage page have Upstream provider. A reply naming none says nothing. zh, ja, de strings. ClinePass and Cline weren't tried (no account); Vercel's gateway documents no such field in its replies, and a vendor whose reply has a top-level "provider" string is read the same way. In a sandbox magpie with the owner's real OpenRouter key: Chat, Messages and Responses (translated to Chat), streamed and not, all six said StreamLake in the trace, the ledger and on the Routing page. TestUpstreamRecorded (6 paths × streamed or not: the route, its try, Recent and the ledger) fails on the old code, TestUpstreamOf; routing-upstream.test.cjs (Chromium and WebKit, en and zh: the tag inside its row, the story, the Usage details, a click moves nothing, no left-border accent) fails on the old assets. ledger-hscroll, request-archive-usage, usage-ledger and usage-ledger-detail pass on WebKit; routing-served fails the same on origin/main (its swap wording). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass but for TestTranslatedStreamKeepsClientAlive, which passes alone 5 times here and on origin/main.
2026-10-05 14:08:08 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
sessions, gui: Codex sessions made with another provider are moved to Codex's own from the Sessions page, and back (#887, cjyrainbow) — Codex's history lists only the sessions of the provider it uses now, so sessions made through CC Switch (model_provider "custom") vanished from it once Codex used magpie, though their files were still there and the Sessions page listed them. The Codex tab now reads each session's provider from its session_meta (cached with the rest) and the one Codex in that session's home uses (config.toml's model_provider, or its profile's; "openai" when none), says how many were made with another and which, and tags each. "Move to <provider>…" opens magpie's own dialog listing them, each ticked; Move rewrites only the model_provider string in each session's files (.jsonl and .jsonl.zst, in its own Codex home, so WSL's too, mtime kept) and in Codex's state_*.sqlite threads row: messages, id, title and archive are left as they are. A copy of the files goes to magpie's trash folder (trash/codex-provider, with a move.json) first, a failed rewrite is put back from it, and a session written to in the last minute is left alone and said so. Undo moves each back to the provider it had, byte for byte. Nothing is moved by itself. POST /api/sessions/codex-provider; en/zh/ja/de. TestMoveCodexProvider (listing, bytes, mtime, the DB row and nothing else, backup, byte-identical undo, an active session, a bad id), TestMoveCodexProviderPacked and TestCodexProviderIn fail without the change, as does sessions-codex-provider.test.cjs (WebKit, en and zh: magpie's dialog, Cancel posts nothing, an unticked one stays, Undo, no click moves the page, no left border, translations); sessions-manage, -toolbar, -purge, -folder-delete, -wsl-delete and click-scroll pass. Not tried against a real CC Switch history. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 17:26:19 +08:00
sessions: each project folder's row starts with a box, so several folders are picked and deleted together with the bar's Delete, the count still in sessions (mintonight, on #527: 现在这样一次只能删一个项目…在项目文件夹那一行的字首加一个复选框…多选同时删除多个项目,并且还能复用已选 x 项那一行右侧的删除按钮,这个数量 x 的计数逻辑还是按会话数量来,不要把文件夹这个复选框当做一个数) — #527 gave each folder's row a Delete all, which deleted one folder at a time beside the box-and-bar way sessions are deleted. Each folder's row now starts with a box, as each session's row does, where magpie can delete the agent's sessions: ticked, it picks every session of the folder shown, folded or not, and the bar says "{n} selected" (已选 {n} 个) counting those sessions, the folder not one of them; unticked, it lets them go; a session's own box ticked or not part-ticks or ticks its folder's at once; under a filter it picks the ones the filter shows, as the bar's select-all does. Folders and single sessions picked together go with the bar's Delete, which asks in magpie's own dialog as before: a pick that is one whole folder is headed "Delete all {n} sessions in {folder}?" with its path, as Delete all's was; several whole folders "Delete all {n} sessions in {k} folders?" (删除 {k} 个文件夹下的全部 {n} 个会话?); anything else "Delete {n} sessions?". They go to magpie's trash through sessions/delete as before, and one written to in the last minute is left and said so. Delete all is gone from the folder's row: it did what the box and the bar's Delete do, two ways for one thing (the lesson of #526's Close). The box's label is "Select every session in {folder}" (选中 {folder} 下的全部会话); the box doesn't fold the folder or move the page; no left-border accent. sessions-folder-delete.test.cjs now covers the boxes (first in their row, none for OpenCode), a folded folder picked and asked about by name, Cancel posting nothing, two folders and a mixed pick, the part-tick, a filtered pick, the whole of app with its running session refused; it fails without the change (no folder box). sessions-manage, sessions-purge, click-scroll, sessions-toolbar, session-terminal, sessions-bar-names and sessions-overview pass in WebKit.
2026-10-02 20:22:00 +08:00
sessions: a session's conversation can be read in magpie, and a Pi session carried on in oh-my-pi (sxwedo, #845) — the Sessions page could resume a session only in the agent that wrote it, and showed nothing of what was said. A session's details now have Show conversation: magpie reads the agent's own session file (Claude Code, Qoder, Qoder CN, Codex, Pi, oh-my-pi; found by agent and id, never a path from the page; read only, no copy kept, 400 KB at most with "There was more than is shown here") and lists the user's words, the model's, each tool call by name and its result, thinking and summaries folded. For Pi and oh-my-pi it follows the branch the session is on (parentId from the last entry), not the turns left on an abandoned one. A Pi session also has Continue in, the app's menu (no <select>): oh-my-pi's omp --fork <file> in the session's folder, copied or opened in the session terminal (the server makes the command from the session it finds; the page sends agent, id, in), and the details say it. omp --fork loads the Pi file, migrates it and writes a new oh-my-pi session, leaving Pi's untouched; with the owner's Pi sessions copied into a sandbox HOME, oh-my-pi's own SessionManager forked them whole (4→5 and 46→47 messages, its system line added; text, thinking and tool calls kept) and the source files' sha256 didn't change. Only that pair is offered: Pi can't read an oh-my-pi file (it wants the session header first, oh-my-pi writes a title line first), oh-my-pi takes Claude Code and Codex only from its own import picker, not a given session, and the other agents' formats differ, so carrying them on would mean converting them, which isn't done. zh, ja, de strings. TestTranscriptClaude, TestTranscriptCodex, TestTranscriptPiBranch, TestTranscriptOmp, TestTranscriptUnsupported, TestTranscriptCut, TestCarryPiToOmp; sessions-talk.test.cjs (WebKit, en and zh) fails on the old code. sessions-*, click-scroll, gui-ja and gui-de pass on WebKit; session-terminal and session-identity fail the same on origin/main. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 10:08:35 +08:00
2026-10-04 11:28:50 +08:00
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.
2026-10-04 14:14:43 +08:00
gui, sessions: a moved subscription's other sign-in ways are a link away, ZCode's app's own account among them (Jinyu), and a session in a WSL distro is deleted as this computer's are (TJHHHH) — Jinyu asked to add a ZCode account from the one the ZCode app is signed in to rather than signing in again. The ZCode plugin (0.1.8) already does that (its "ZCode app's sign-in" reads ~/.zcode/v2/credentials.json), but a moved subscription's Add went straight to the plugin's first way (#777: one click), so it couldn't be reached. Now ZCode's site step shows its other ways beside the sites (the app's account, the GLM Coding Plan API key), and while a moved subscription's sign-in waits, Other ways to sign in… (when it has ways other than the one under way and Use an API key instead) cancels it and shows the plugin's ways; the owner's suggestion on #777 (suyu-ops). Factory, whose two ways are its device code and its key, still goes straight through with no extra link. No plugin release or mover change. With a sandbox copy of the owner's real ~/.zcode the plugin's way signed in (account, key, site zai). TJHHHH asked to delete sessions listed from WSL: they were resumable only, the page saying to delete them in WSL. Now they have their box and are deleted the same way: moved to magpie's trash over \\wsl.localhost (copied and removed across the device boundary), Claude Code's file history from the distro's ~/.claude, never this computer's; dropped from the distro's listing at once, which is listed again next; Restore puts the files back in the distro and lists it again. zh, ja, de strings; the WSL note's strings are gone. TestWSLSessionDeleted fails on the old code. On the Windows box against a sandbox folder in a running WSL2 distro (magpie-desktop), Delete moved the session, its subagents and its file history out of the distro into a %APPDATA% trash, and Restore put them back and listed it again (removed afterwards). plugin-other-ways.test.cjs (ZCode's ways at the site step and from the waiting box, Factory's one click) and sessions-wsl-delete.test.cjs (the WSL row's box, Delete in magpie's dialog posts its id, no note) fail on the old code and pass on WebKit in en and zh. sessions-folder-delete, sessions-manage, checkbox-look, plugin-moved-signin, gui-ja, gui-de 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.
2026-10-05 11:26:08 +08:00
provider: Trae CN's daily check-in (每日签到) is pressed once a Beijing day for each Trae CN account, as WorkBuddy's is (#694, Hu9956: TRAE cn也有每天签到送100积分活动,这个会考虑支持吗) — Trae CN is served only by its community plugin (trae-cn, no built-in, no mover), so magpie asks Trae through the plugin's fetch, which from trae 0.1.4 (magpie-community/plugins, branch trae-checkin) sends api.trae.cn's /trae/api/ pages as the account (Cloud-IDE-JWT renewed when near its end, its device id): POST /trae/api/v2/ug/checkin_credits/status with {}, and /claim with {} only while enable is set and checked_in isn't. These are the endpoints autumnsentiment/Trae2api-cn (the relay the plugin's protocol came from) and qilimixingkong/trae-checkin both call; an unauthenticated POST to /status answers 200 {code:1001, enable:false, checked_in:false} and a made-up sibling path 404s. Answers: not enabled → inactive, checked_in → done with its credits, a claim → claimed +credits(+extra_credits), 9095 (this device is in today) → not eligible for the day (no other device id is sent to get round it, as qilimixingkong's does), 1001 or the plugin's X-Magpie-Sign-In: expired → failed, sign in again; 9074 and anything else → failed and tried again; a plugin before 0.1.4 (400, not JSON) → failed, update it. The day loop and its file are WorkBuddy's, made generic: checkin.go's checkiner (accounts as checkinAcct{key, do}) holds checkinNow, which wbCheckiner now wraps; keepCheckedIn and wbCheckinLoop.tick take a checkinRunner and a log label; withCheckins goes through markCheckins over checkinCards. Trae's is kept in trae-checkin.json by trae-cn|uid; settings.TraeCheckin (traeCheckin) is its own switch; the gateway runs KeepTraeCheckedIn beside KeepWorkBuddyCheckedIn; WithCheckins marks Trae CN cards too, with SubscriptionQuota.CheckinBy and WorkBuddyCheckin.By "trae" (never kept), so the Usage card's row uses POST /api/settings/trae-checkin and /api/usage/trae-checkin and its first Trae card holds Trae's own Auto check-in and Check in now; Settings' Usage part gains a Trae CN tab (traeList) shown while an account is signed in or the switch is on; magpie accounts checkin and the TUI's c check Trae CN accounts in too (the footer says daily check-in). Tests: trae_checkin_test.go (once a day: status then claim, nothing sent again that day, done the next; inactive, 9095 held for the day, 9074 retried, expired, an old plugin; Trae cards marked, another plugin's not), TestTUIChecksTraeIn, usage-checkin.test.cjs's Trae CN card (its own switch and press, WorkBuddy's untouched) and settings-groups.test.cjs's Trae CN tab; the WorkBuddy check-in tests pass unchanged. go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass; the GUI tests pass on WebKit (Playwright's Chromium here fails to start, icudtl.dat missing, for the old tests too). Not tried with a real Trae CN account: none is signed in to the owner's magpie (no trae-cn in plugin-auth.json), so the authenticated status and claim answers' fields are as both open-source clients read them, not seen here.
2026-10-04 03:54:23 +08:00
gui: a waiting sign-in has one button that puts it away, Cancel, and no Close beside Open again doing the same (mintonight, on #526: 那你这样改「取消」和「关闭」功能不是重复了吗,只保留一个就行了) — the fix for #526 had two causes to answer: the box's Cancel was pushed out of the account list by Qoder's long device link, and the reporter asked for a Close beside Open again. The first was fixed in the layout (the list's column is minmax(0, 1fr), the link cut short), which brought Cancel back in sight; the Close added beside Open again then did exactly what Cancel does (cancelSignIn: signin/<id>/cancel posted, the polling stopped, Add another back), two buttons for one thing in the same box. Close is gone; Cancel stays, as it is the button every other state of the sign-in box has (failed, installing, paste a code, the risk notice), at the box's right, and now carries Close's title, "Stop waiting for this sign-in" (不再等待这次登录). Open again stands alone in its row, with the import and key ways beside it where a subscription has them. The editor's Cancel, Save and Remove still put a waiting sign-in away. signin-close.test.cjs now checks that Open again stands alone, that no Close is in the box and that the one Cancel, titled so, posts the cancel, stops the polling and brings Add another back without moving the page; it fails without the change (Close still beside Open again). signin-link, signin-again, plugin-signin, routing-signin and signin-closes-sheet pass in WebKit (signin-callback's one case launches Chromium alone, which can't start here without its ICU data).
2026-10-02 20:22:00 +08:00
providers, sign-in, tray, plugins: four of StringKe's reports on Discord — (1) Removing a provider left its editor open and listed, and a second Remove said there was no such provider: the removal had been made, but moving the agents off its models failed on one agent whose config couldn't be written, and Reseat returned that as the change's error; now such an agent is a move with its error ("still on X, not moved: …", a warning in the panel, a line in the CLI and TUI), the others are moved, and the panel, told a delete failed, re-reads the list and closes the editor when the provider is gone. (2) The sign-in waiting sheet shows the sign-in link itself, selected whole on a click, with a copy button, for a browser other than the one that opens. (3) A subscription with several accounts offers "Account in use" among the menu bar's usages, following the account the gateway goes to first. (4) The Antigravity plugin's sign-in never started: magpie called every plugin's authorize with an inputs object, the way OpenCode's CLI does, and opencode-antigravity-auth then asked its Project ID on the terminal — magpie's stdin — until the minute ran out; inputs now go only to a method with prompts, as OpenCode's TUI passes them (checked against the real package under Bun in a sandbox: the Google sign-in URL comes back at once). TestReseatUnmovable, TestTrayInUse, TestInUseOf, TestAuthorizeNoPrompts, provider-remove.test.cjs, signin-link.test.cjs and tray-inuse.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, tray-usages, plugin-signin, signin-callback, factory-sub pass.
2026-09-30 20:45:23 +08:00
sync: it can sync by itself less often or only when asked, and the server's setup can be restored over this computer's, undoably (Sun1090, #847: 同步太快了,需要设置个开关控制自动同步,还有恢复按钮) — the gateway's magpie synced every 3 minutes with no way to change it, and a free 坚果云 account is allowed only so many WebDAV requests (600 per 30 minutes, 1 GB up and 3 GB down a month); and there was no way to take the server's setup back over a computer's own, which the next sync would otherwise push up. Settings › Sync now has Sync by itself, the app's segmented control (no <select>): Off, 3 min (the default, as before), 15 min, 30 min, 1 hour; Off syncs only on Sync now, and the sync row says only when asked. The loop reads the setting each minute (no request), so a change takes effect without a restart, and a server limiting requests is still waited for as backoff says. Restore from the server asks first in the app's own dialog, naming the parts replaced; it reads the server's file as it is now, seals this computer's setup (keys and all) into the sync folder as <time>-before-restore (newest 5 kept), brings in each part that differs as a sync brings one changed only there, and leaves the state as such a sync would, so the next sync pushes nothing back. Its notice says what was restored, with Undo, which brings the kept setup back (the next sync then takes it to the others); OK, or a later sync's own notice, drops the undo. The form's Save keeps the setting (SetAuto alone sets it). CLI: magpie webdav|s3 auto off|3|15|30|60, restore, undo; the status says how often it syncs by itself. zh, ja, de strings. TestAutoSync (off: no request by itself; back to 15 min: one sync, then none; Save keeps it; 7 refused) and TestRestore (b's own changes replaced by the server's with no push, the kept copy holds b's provider and key, Undo puts it back and a gets it, nothing differing restores nothing) pass -race; sync-auto-restore.test.cjs (WebKit, en and zh: Off posts -1 and the rows say so, Cancel posts nothing, Restore and Undo post, no click moves the page) fails on the old code. sync-other-kind, sync-spin, s3-sync, webdav-settings-refresh, usage-computers, click-scroll, gui-ja, gui-de and settings-sections pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 10:36:46 +08:00
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.
2026-10-03 15:21:15 +08:00
providers: Tencent Cloud is one preset, its regions Plan · China, Pay as you go · China and Pay as you go · Global (Discord, Jorben: Tencent Cloud's Token Plan, TokenHub and TokenHub China were three tiles of one vendor) — tencent-token-plan, tencent-tokenhub-cn and tencent-tokenhub become tencent-cloud's regions plan, cn and intl, as Xiaomi MiMo's are, each made as its own preset made it: the plan's /plan endpoints with no Responses, its own models and no catalog; TokenHub's hosts with Responses, models.dev's tencent-tokenhub and its own keys page and docs. A region now carries its Website, Catalog and Models, and a saved provider's region is found by its endpoints (regionOf, by exact URL before path, since TokenHub China and global share their paths): normalize gives it that region's keys page, docs and catalog (a catalog the user picked is kept), and planModels its models. The old ids are aliases, as qianfan-token-plan is of baidu-qianfan: Preset and FromPreset take them, FromPreset builds at their region, normalize rewrites a saved provider's preset to tencent-cloud, and a provider's own id never changes, so its key, models, routing groups, usage and prices stay where they were; magpie://import links by an old id land at its region, and region= now also gives a region's docs and catalog. Imports from other apps match a region's endpoints. The editor's docs link follows the region picked with the segmented control, en and zh ("按量 · 中国", "按量 · 国际"); the add sheet has one Tencent Cloud tile; the site's import docs list tencent-cloud with its regions and import.html still names the old ids. TestTencentCloud (each region, old ids, FromPreset, imports, import links) and TestTencentCloudOldPresetsKeepWorking (a providers.json saved by an older magpie under all three old presets, with a routing group over them, read, re-saved as the editor saves and switched between regions) fail on the old code; TestTencentTokenHubRoutes routes through old ids and tencent-cloud alike. tencent-cloud.test.cjs (WebKit en and zh: one tile, the three regions, no select, the docs and Get a key links following the region, an old id's provider opening at its region and saved under its id) fails on the old app.js; add-sheet, zhipu-team, system-one, strip-scrollbar and click-scroll pass (Chromium doesn't launch in this sandbox). Not tried with a Tencent account. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 19:38:14 +08:00
update, gui: a new version Windows won't start is never left in place — tried before it goes in, and a restart into one that doesn't say it is up puts the running one back; that version isn't downloaded again by the clock, and Settings › About › Version says why (#894, hisiling) — on Windows 11 with Smart App Control on, the update put the new unsigned exe in place of the running one and restarted into it; CodeIntegrity (3033/3077) refused it, magpie never started again and the working version sat in .old. Now, on Windows, the downloaded exe is first started suspended with no window and ended at once (Windows checks a program when its process is created), and one refused stays out: the running exe isn't moved. At the restart the new process is given MAGPIE_STARTED, a file it writes first thing (AwaitPredecessor); the old one waits up to 20s for it and, if the start fails (ERROR_SYSTEM_INTEGRITY_POLICY_VIOLATION 4551, 1260 or any other), the new process quits before saying so, or it says nothing in time (then it is ended), puts the running exe back over the new one (the new one renamed .blocked if it can't go) and keeps running instead of quitting; nothing notes it as updated in the app. The version is noted in the config folder (update-blocked.json, this machine's only, never synced; gone once a version at least as new runs); the clock's checks leave it alone and show why, a Check clicked downloads and tries it once more. Settings › About › Version: for an application control policy, "0.1.x didn't start on this computer: Windows' Smart App Control (or another app control policy) blocked the new version, which isn't signed. magpie stays on 0.1.y. Download it and run it yourself, or turn Smart App Control off and check again", else the reason, with Download and Check; the error in full on hover. en/zh/ja/de. A user who double-clicks the refused exe is refused by Windows before magpie runs; only signing helps there. TestRelaunchRefusedPutsBack (4551 at the start), TestRelaunchSilentPutsBack (no word in time, killed; quit at once), TestRelaunchReportedStays, TestReportStarted, TestBlockedNote and TestUpdateThatWontStart (the restart doesn't quit, the note, the clock not downloading it again, a Check that does) are new and fail without the change; on Windows TestInstallBinaryRefusedStaysOut, TestTrialStart and TestStartCheckedPutsBackForReal (real processes: not a program, quits at once, runs on silent) pass on the Windows 11 test box with the whole internal/update package (Smart App Control itself isn't on there, so the refusal is simulated). update-blocked.test.cjs (Chromium and WebKit, en and zh: the reason and both versions, full error on hover, Download opens the release, Check downloads, the other reason, no select, no scroll, no left border) fails without the change; update-check, update-mirror, update-auto, update-wait and update-download-percent pass in WebKit; click-scroll's request-list case fails now and then the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 19:43:31 +08:00
update: on Windows, an exe something holds open for a moment still goes aside for the new version, and one held for good says why in full, with the release page a click away (freecss on Discord: "0.1.628 已下载 · couldn't move magpie-windows-amd64.exe aside to put the new version in: rename C:\Us…") — InstallBinary renamed the running exe to .old once and gave up on the first error, which Go prints as "rename <exe> <exe>.old: <reason>", two full paths ahead of the reason, and the version row cut it off on one line, so what failed couldn't be read. A leftover .old was already handled (RemoveOld, then .old-2…, TestInstallBinaryBesideARunningOld), and Windows renames a running image (it is mapped with FILE_SHARE_DELETE), so the likely cause is another program holding the exe open without delete sharing — an antivirus scan, OneDrive or a similar sync client — or a folder that refuses the change (access denied); which one isn't known until the full message is read. moveAside now tries again for about 3 s (0.1, 0.2, 0.4, 0.8, 1.5 s apart), clearing a read-only attribute after the first failure, with a fresh .old name each try; a missing exe isn't retried. When it still can't, the message is the reason without the paths and what to do: ERROR_SHARING_VIOLATION/ERROR_LOCK_VIOLATION says another program has it open (often an antivirus or OneDrive), access denied names the folder Windows won't let magpie change, anything else says to download the new version; the download is kept for another try, as before. The version row wraps a failed install's or download's reason (.sub.wraps) with the whole text as its tooltip, and a ready-but-failed install offers Download, which opens the release page. The Windows test box wasn't reachable from this session (ssh key refused), so nothing ran on Windows; TestInstallBinaryHeldOpen (windows-only: an exe held open without FILE_SHARE_DELETE for 0.5 s still installs; held for good, the error says another program has it open and the download is kept) was cross-compiled with GOOS=windows go test -c but not run. TestMoveAsideTriesAgain and TestMoveAsideSaysWhy (portable, with a fake rename) and the new update-check.test.cjs case (webkit, en and zh: the reason is not cut off, its tooltip is the full text, Download opens the release page, a later state is one line again) fail without the change; Chromium couldn't launch here (its Playwright cache has no ICU data). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet, go test -tags nogui ./... pass (TestFXInState failed once on a TempDir cleanup race, passes on the rerun; untouched).
2026-10-02 13:26:54 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
update, gui: Settings › About › Download source sets the update mirror, and a download that failed through it offers GitHub (#893, M3chD09) — the mirror updates could come through was only magpie update mirror in a terminal, and the GUI never said where updates came from or that a failed download was the mirror's. About now has a Download source row: GitHub, or the mirror set (the same UpdateMirror setting the CLI writes); Use a mirror… takes a full https:// address (http, a bare host or a malformed one refused, with the reason in the row and the draft kept), Change edits it, Use GitHub takes it away in one click; Enter saves, Esc cancels, no click scrolls. POST /api/settings/update-mirror sets it; the Settings page's own save keeps it. A download through the mirror that doesn't answer, answers other than 200, or sends a file whose SHA-256 isn't usemagpie.ai's feed's is an update.MirrorError (never installed, the file removed, as before); /api/update's error carries the mirror, and the Version row says the download failed through that mirror and offers Download from GitHub, which takes the mirror away and downloads again. magpie names no mirror or CDN of its own. en/zh/ja/de. TestMirrorFailuresAreTheMirrors (tampered, 404 and closed mirrors each the mirror's, no file kept; a direct failure no mirror's; CheckMirrorURL) and TestUpdateMirrorSetting (set trimmed, http/bare/ftp/empty-host refused, Settings save keeps it, off, an updater failing through a 404 mirror reports it with retry, a new check clears it) are new; update-mirror.test.cjs (WebKit, en and zh: GitHub shown, http refused with the reason, https saved with Enter, Use GitHub posts "", a failed download through the mirror offers GitHub, which posts "" then update/install; no select, no left border, no scroll) fails without the change; update-auto, cli-path, cli-behind, settings-sections and click-scroll pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 18:49:04 +08:00
gui: a hidden agent put back to its defaults goes under Not set up at once (Hu9956, #842), and a restart to update shows what changed before it restarts (Hu9956, #844) — #842: an agent hidden by hand stayed under Hidden with its Show (the eye) after its model was set back to Default; only turning its switch off and on moved it, since nothing ever dropped its hiding (the server leaves agentsHidden alone). Now when a hidden agent's settings change (wired, added, drift or a field's value) so that nothing is set on it, its hiding is dropped and saved with the arrangement at once, it goes under Not set up without the eye, and the status line says so; a state answer read before that save, still naming it hidden, doesn't hide it again. One hidden while nothing was set on it, and left alone, stays hidden. #844: the header's Update pill and Settings › About's Restart to update restarted on the click, and what the update brought was only in Settings › About › What's new. Now in the window they open a sheet first: Update to v…, What changed since v<current>, every release after this one up to the new one, newest first (the update's own notes at once, the full list when GET /api/update/notes answers: the site's notes feed for (current, latest], the update's notes when the feed is out of reach), Later and Restart to update; only the latter restarts. A failed swap still restarts again at once; the panel keeps its one-click pill. zh, ja, de strings. hidden-agent-cleared.test.cjs (the groups, the hiding saved once, the late answer, no scroll) and update-notes-ask.test.cjs (the pill and the Settings button ask first, both releases' notes, Later sends nothing, the sheet's button restarts, no scroll, no left border) pass on WebKit in en and zh and fail on the old code; looked at in screenshots. TestUpdateNotesRoute (the releases between, the fallback, none) fails on the old code. update-wait.test.cjs now confirms the sheet; it and update-hide, update-auto, update-check, header-centre, header-zoom-fit, show-unset-agent, fold-show-grip, agent-grip, agent-start-pick, agent-path-same, removed-hide, profile-confirm, agent-update-all, gui-ja, gui-de and click-scroll pass on WebKit (click-scroll's pointer subtests are flaky the same on origin/main). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 11:16:34 +08:00
upstream: the vendors' own status pages say when their API is down (#971, emo172: an Anthropic or OpenAI outage looked like a sign-in or quota problem) — a provider that calls Anthropic's, OpenAI's or Moonshot's API (by its base URL's host: api.anthropic.com, which the Claude subscription calls too; api.openai.com and chatgpt.com, Codex's; api.moonshot.cn/.ai) carries "API outage" (red), "API degraded" or "API maintenance" beside its name on its Providers row and its Usage card while the vendor's page says so; its title names the parts and the open incidents, and a click opens the incident (or the page), not the row. internal/upstream reads each page's Statuspage-style summary.json (status.claude.com; status.openai.com, incident.io's; status.moonshot.cn), checked by hand on 2026-10-06, and only the API's own parts (Claude API and Claude Code, not claude.ai; Chat Completions, Responses, Codex, Codex CLI, not "Codex in ChatGPT Desktop", which was degraded that day); an incident naming no parts (OpenAI's) counts only while those parts are affected. Pages are read only when the Providers or Usage page asks (GET /api/upstream), at most once in five minutes, never on a request's way; a page that fails, isn't JSON or no longer lists the API's parts is unknown and marks nothing. A relay serving the same models is never marked. DeepSeek's page (Flashduty) has nothing readable and is only known for its link; OpenRouter, xAI, Mistral and z.ai have no page read. The gateway's failure messages, TUI, CLI and tray aren't changed. zh/ja/de added. upstream_test (fake pages from the real summaries: API down, claude.ai down isn't the API, Claude Code degraded, OpenAI's app isn't the API, 503/HTML/changed page unknown, read once in Fresh and Of never waits, hosts; -race -count=5) and gui's TestUpstreamRoute pass; upstream-status (Anthropic down: its provider and the Claude subscription marked, relay/OpenAI/unread Moonshot not, click opens the incident not the row, all well marks nothing; 900 and 440px, the badge whole, the name giving way) passes in Chromium and WebKit, en and zh, and fails 8/8 without the app.js change. i18n-keys, gui-ja, gui-de, click-scroll, account-return, account-models, usage-keys-fold, deprecated-subscriptions and add-sheet pass; go vet, go test ./internal/gui/, linux/windows/nogui builds pass.
2026-10-06 12:36:05 +08:00
usage: each subscription account's tokens and cost, by the account that answered (#557: 希望用量账本能按订阅账号统计,每个 Codex / Claude 账号各用了多少 token、多少钱) — a call's record named the upstream key it went out on (ProviderKeyID/Name, #500) but never the subscription account, so with two Codex accounts signed in, or a Claude account that took over after a failover, the ledger could say what Codex cost and not what each account did. A record now carries providerAccount, the account that answered as the Routing trace names it (Account.User: its email or login, never a token), set by the gateway at the try that went out (the candidate loop's last try, so a failover's answer is the account that took over and the refused try stays the first account's; an X-Magpie-Account pin is the pinned one), and on the decide, draw and video paths too. Record.Account() gives it, or for a record written before the field the account its host named (Where() has long written "chatgpt.com as dee@example.com" for an account), and otherwise "" — shown as "account not recorded", never inferred from today's sign-in. The summary groups calls by provider and account (Summary.Accounts, the same email on Codex and on Claude being two accounts), priced as every other group; the ledger's Filter has Account, the search finds a call by its account, the CSV has a provider_account column beside provider_key_name (the provider's identity columns together, the caller's still last), and the Requests page's packed rows keep it and offer the period's accounts. magpie usage prints an accounts table after the provider keys, and magpie usage --csv --account <name> keeps one account's calls. The app's Usage page lists Accounts on the Overview (tokens and cost per account, the provider under its name, an unrecorded one in the faint colour, no stripe down a row's side) and has an Account filter on Requests, each account noting the providers it answered for, which asks for and exports that account's calls without moving the page. OTLP export is an allowlist and does not carry the account. TestUsageNamesPinnedAccount (both pinned accounts on magpie's endpoint and on Codex's own, the trace's Who and the record agreeing, no token in usage.jsonl) and TestUsageNamesAccountThatTookOver (Ordered routing, the first account refusing: the answer recorded as the spare, the refusal as the first) fail without the gateway change; TestSummaryByAccount, TestLedgerByAccount and TestRequestPageByAccount cover the grouping, the host fallback, "not recorded", keyed providers kept out, the filter, the search, the CSV column and the packed rows; TestLedger and TestLedgerWithSessionLogCalls take the new column. usage-accounts.test.cjs (WebKit and Chromium, en and zh: the Overview's accounts and costs, the Account picker's notes, its request and export, All accounts clearing it, scrollTop unchanged) fails without the asset change; api-key-usage, click-scroll, panel-usage, request-archive-usage, tray-usages, claude-usage-asked and every usage-* test pass in WebKit. 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 failed once on its TempDir cleanup, passes alone).
2026-10-02 20:15:33 +08:00
gui: a Usage card has a refresh of its own, left of its "Updated" time, that reads that card alone (Hu9956, #840: 在更新时间左侧加一个刷新按钮,鼠标悬停在套餐卡片上时显示,可以单独刷新某个套餐的用量,不用刷新全部) — only the page's Refresh read the allowances again, every card at once. Each card now has a ↻ that opens left of "Updated 3 min ago" (or of a failed reading's reason, or under a balance) as the card is hovered; at rest the line starts where the card's others do. A click POSTs /api/usage/quotas/refresh?provider=&user=: provider.RefreshUsage reads that subscription account, plan key or balance key again (a Claude account by running Claude Code's own /usage, as the page's Refresh does) and puts it in place in the caches, the other cards not asked and kept as they were read; the button turns while it reads. zh, ja, de strings. TestRefreshOneCard (two keys of one relay and another provider: a refresh asks that key alone, the others keep their figures) and the plugin case in TestPluginUsage (one account of three read) fail on the old code; usage-card-refresh.test.cjs (WebKit, en and zh: aligned at rest, left of the time when hovered, a click reads that card alone, turns meanwhile, doesn't scroll; a failed card's own button) fails on the old code. usage-quota-head, usage-checkin, click-scroll, quota-read-at, balance-parts, claude-usage-asked, gui-ja and gui-de pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 08:59:47 +08:00
usage: with WebDAV or S3 sync, each computer shares its usage and sees the others', the total and each computer's part (#542) — sync carried keys, agents and the library, and each computer's Usage page counted only the calls made on it. Sync now has a Usage tick (off unless picked, usage=yes in magpie webdav/magpie s3): a computer with it on puts its own calls up as one file a day, magpie/usage/<id>-<day>.magpie-usage beside the bundle (under the prefix's usage/ on S3), each a gzipped list of times, agents, providers, models, tokens, costs and statuses, sealed with the sync passphrase (backup.SealData, the key cached per passphrase so a sync derives it once); no request body, session, archive name or key goes, a call another computer passed on over the LAN (Via) isn't put up again, and an error is cut to 300 characters. The id is a random one kept in usage-computer.json, which no backup carries, with the host name beside it. A computer only ever writes and deletes its own names, so none overwrites another's; the files are not in the bundle and its hash, so sharing usage never makes a sync or a conflict of the bundle; the first sync puts up the last 90 days, after that only yesterday and today, at most every 15 minutes (a Save or Sync now at once), and a day past 90 is deleted by the computer it is of. The others' files, listed (PROPFIND Depth 1; ListObjectsV2) and fetched only when changed, are kept in usage-others and read with the ledger, as Record.Computer; a failure is said in the sync row ("Couldn't share usage") and never fails the sync. The Requests page and the panel's Usage tab get a Computer pick (this computer, other computers when more than one, each by name) sending computer=this|others|<id>; a row from another computer says "· on <name>" beside its agent, and its details name the computer and say what was said stays there, nothing asked of this computer's session files for it. With no other computer's usage there is no pick and nothing new on either page, and an older magpie never lists or reads usage/. TestUsageSharedBetweenComputers (computers a and b over a fake WebDAV under httptest: each sees 4 calls, the totals and its own and the other's part, nothing counted twice over repeated syncs, the bundle not put again for usage, a new call reaching the other, a day past the window deleted), TestUsageSharedThroughS3 (the same through a fake S3, signed list and delete, a prefix with a space and a plus) and TestUsageOffAsksNothing are new and don't build without the change; usage-computers.test.cjs (WebKit, en and zh: no pick without other computers, the pick and computer=this, another computer's row and details with no /content asked, the panel's pick, the sync form's tick posting usage: true, no click moving the page) fails without it on all but the no-other-computers case. s3-sync, sync-other-kind, panel-usage, request-archive-usage, click-scroll and usage-ledger-chart pass in WebKit. 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 failed once on its TempDir cleanup, passes alone). Turning the Usage tick off, or sync off, lets the other computers' usage kept here go (DropAllShared), so the Usage page counts this computer's alone again: TestUsageSharedBetweenComputers checks both, and fails without it.
2026-10-02 17:47:36 +08:00
usage test: usage-ledger-detail expects each gateway row's protocol badge and the details' Protocol (red on main since c5b57b90) — c5b57b90 (蓝猫 on Discord) tagged every gateway row of Usage › Requests with the protocol it was sent upstream in (a "src proto" badge: "Responses", "Anthropic → Chat") and added Protocol to a request's details after Endpoint. The test's "only the session file's rows are marked" read any .src, so it found the protocol badges (['Responses', 'Anthropic → Chat', …] against ['', '', …]) and failed 6/6 in Chromium and WebKit; that hid two more stale checks behind it: the details' labels and value positions without Protocol, and "the click moved the page" (50 vs 30): a row with badges is two lines tall (51px, was 32), so the first row was no longer wholly in sight at the wheel's 30px and reader.click's own wheel brought it there before clicking. The test now reads the local-session badge (.src.local) and the protocol badges (.src.proto) apart, expects Protocol (协议) in both details with its value (Responses, Anthropic → Chat), and takes the page's scroll after reader.inView of the first row, so a scroll by the app is still caught. No app change: c5b57b90~1 passes the old test 6/6, and with the row badge turned off the page doesn't move. usage-ledger-detail passes in Chromium and WebKit, en and zh; ledger-*, usage-* and click-scroll pass (257 cases), except usage-sessions-agents-fit's WebKit cases, which time out alone too, on origin/main's app (not in this commit).
2026-10-06 13:32:36 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
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.
2026-10-03 17:12:48 +08:00
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.
2026-10-02 15:48:17 +08:00
gui: code text on Windows is Cascadia Mono / Consolas with Chinese in YaHei UI, not SimSun (#690) — on a Chinese Windows 11 the Library's MCP tags (远程 · 需登录), the routing page's model chips (gpt-6.1-sol) and other small code text drew thin, unhinted and not round: the tags take --code, whose stack (ui-monospace, SF Mono, Menlo, Consolas, monospace) has no Chinese face, so their Chinese fell to the generic monospace, SimSun on a Chinese Windows; and routing.css's model chips and step lines, the sign-in sheet's link and code, the import textarea, the headers editor and What's new named stacks of their own (ui-monospace, SFMono-Regular, Menlo, monospace, or var(--mono, …) with no --mono anywhere) with no Windows face at all, so all their text was SimSun. Now every monospace text takes var(--code), and on Windows (html.win, as --font already is) --code is "Cascadia Mono", Consolas, "Microsoft YaHei UI", "Microsoft YaHei", monospace; the Mac's and Linux's stack is unchanged (the Mac's ui-monospace is SF Mono either way). win-fonts.test.cjs (no sheet names a monospace stack of its own; the badge, the id, the model chip and the sign-in link take the Mac's stack off Windows and Cascadia Mono / Consolas / YaHei UI with html.win) fails on the old CSS and passes in webkit and chromium; the routing, library, omarchy and signin-link GUI tests pass as before but routing-compact, routing-manual and routing-time, which fail the same on main. Not seen on a real Windows machine; the light grey of the ids (--faint) is unchanged.
2026-10-03 22:23:17 +08:00
zed: a Zed subscription (Pro, Student, Business) signed in from magpie, several accounts, served through Zed's own cloud (yosocoli5 on X: 买了 Zed 订阅,可以加进供应商吗 — yetone: 当然可以!) — magpie had no way to use Zed's hosted models. Zed is now a subscription in the add sheet (Zed's own logo from zed-industries/zed, lobe-icons has none), signed in as the Zed editor signs in (new internal/zed, the protocol): an RSA key is made, the browser opens zed.dev/native_app_signin with its PKCS#1 public half, magpie's loopback port and a system id, and comes back with user_id and the access token encrypted to the key (OAEP SHA-256, PKCS#1 v1.5 also read), answered with a redirect to zed.dev's succeeded page. The account is asked of /client/users/me (signed "<uid> <token>" with x-zed-system-id), kept with its GitHub login, its default organization and plan; an organization with hosted models turned off is refused. Several accounts can be added and switched like the other subscriptions. Requests get an LLM token from /client/llm_tokens (kept in memory, minted again when Zed answers 401 or x-zed-expired-token/x-zed-outdated-token, once; a refused pair marks the account lapsed, "sign in again"). The model list is Zed's /models for the account (disabled and unknown providers left out; context, output, images and effort levels kept). The gateway asks cloud.zed.dev/completions in Zed's envelope {provider, model, provider_request}, the request written in the model's own provider's API as Zed writes it — Anthropic Messages without stream, OpenAI Responses, xAI chat completions with max_completion_tokens, Gemini's request with models/<id> — and reads the NDJSON reply's events with that API's decoder: a failed status is an error with its status (upstream_http_N, rate limit 429, overloaded 529, payment 402), a reply without stream_ended is an error, not a shorter answer, a 402 says the plan doesn't include hosted models or its allowance is used up. relayQoder's status-keeping relay is now relayStatus, shared by Qoder and Zed. Usage shows the plan and its period's end; Zed reports no usage counter, so no windows. Signing in says first, as Antigravity's does, that Zed serves these models to its own editor and may treat magpie's use as third-party use (en and zh); the import box now uses a subscription's own risk note too. Tested in Go against fake zed.dev/cloud.zed.dev servers, no real account: internal/zed TestSignInKey, TestParseModels, TestReadLines; provider TestZedSignIn (the whole sign-in through the loopback callback, what is kept, the account and its models, the token kept and renewed, the quota's plan and end, a refused pair lapsing) and TestZedFreePlanQuota; gateway TestServeZedProviders (each of the four providers, streaming and not), TestServeZedFailures (stale token retried once, 402, upstream 529 with retry_after, a failed 429, cut short → 502), TestZedRequestShapes. New zed-sub.test.cjs in Chromium and WebKit, en and zh: the Zed row with its plans and logo, the risk note in Zed's words with nothing asked, then Sign in anyway asking for zed. add-sheet, add-button, old-webkit, signin-callback and own-signin-remove pass. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Zed account: the sign-in page's real callback, the LLM token and completions endpoints' acceptance of magpie's headers, and each provider's real event shapes are from Zed's source, not seen on the wire.
2026-09-30 03:19:19 +08:00
providers: a Zhipu or Z.ai key on a team's GLM Coding Plan can be given the team's organization and project IDs, and the Usage page then shows the team's 5-hour and weekly windows (#236 EZN7L2C3: the IDs are in the BigModel console; CC Switch asks the team quota with them) — a key pasted alone was asked for the team's quota (type=2) without the Bigmodel-Organization / Bigmodel-Project headers, which magpie can't find from a key, so a team key showed nothing. providers.json keeps zhipuTeam {org, project} for those key providers (trimmed; both empty removes it; a save without it keeps it, {} clears it; carried through from the Zhipu and Z.ai presets). The team query sends the typed IDs, else those of a ZCode team login holding the key, and is now asked at the key's own host (open.bigmodel.cn, as CC Switch asks it) first and bigmodel.cn (where ZCode asks it) only when that gives no windows. It still runs only when the key's personal quota fails or has no windows. The provider editor shows Team org ID / Team project ID (团队组织 ID / 团队项目 ID), optional, for Zhipu and Z.ai key providers alone, adding and editing; one without the other is refused before anything is posted. Tested with internal/provider/zhipu_team_key_test.go against a fake open.bigmodel.cn / bigmodel.cn in a temp HOME: the IDs kept trimmed, the team asked with type=2, the key and both headers at open.bigmodel.cn and read as 5 hours 42% and weekly 10%, a key without them asked with no team headers at both hosts and showing nothing, a pay-as-you-go key with blank IDs no card, Z.ai taking the fields and DeepSeek not. internal/gui/tests/zhipu-team.test.cjs, Chromium and WebKit, en and zh: the fields for a saved Zhipu provider and from the add sheet for Z.ai, none for DeepSeek or a relay, trimmed values posted, one alone refused, both empty posting {}, nothing scrolled on a click. Both fail without the change. click-scroll, old-webkit, provider-proxy and account-proxy pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real team key or in the real Wails window; the team's reset counts are not read for a key (the reset list is asked only with a ZCode sign-in token).
2026-09-30 05:49:19 +08:00

Dropdown browser regression

Installed desktop fonts

fonts.test.cjs exercises the interface/code font controls with installed family/style fixtures: independent traits, literal names, search and keyboard selection, rapid queued saves and rollback, reload, unavailable faces, discovery failure and refresh, empty collections, platform fallback, Omarchy precedence and browser-mode isolation. It checks narrow windows and no scroll on click in Chromium and WebKit, in English and Chinese. ARTIFACT_DIR retains screenshots; MAGPIE_FONT_ASSETS can point to an older asset directory to verify that the test fails on the missing picker. font-cascade.test.cjs checks that a reset restores the original weights under bold parents, while explicit component emphasis survives a choice.

node --test internal/gui/tests/fonts.test.cjs internal/gui/tests/font-cascade.test.cjs internal/gui/tests/win-fonts.test.cjs
go test -v ./internal/fonts
go test -tags nogui ./internal/settings ./internal/gui -run '^TestFont'

The native test reads the operating system's actual installed font metadata. Run go test -v ./internal/fonts without nogui on each desktop platform. Browser fixtures never read or write user settings, install fonts or contact a live gateway.

Gateway Caller Keys

gateway-caller-keys.test.cjs checks the named caller-key list on the Gateway page, including creation, copying, renaming, disabling, enabling, rotation and deletion. It verifies key-level usage overview, request filtering and CSV export in Chinese and English on Chromium and WebKit. The fixtures do not access local credentials. Gateway keys appear only while sharing is on, with one create entry point and confirmation before rotation/removal (Cancel and Escape send no mutation). Creation, renaming and confirmation focus their controls without scrolling. Gateway is the only key-management page; Settings controls sharing and shows addresses. The test checks that toggling sharing retains the key, and that a removed default key is recreated as Magpie and appears in Gateway without a reload.

gateway-connect-keys.test.cjs selects loopback and LAN addresses and enabled caller keys for Shell, curl, Python and Node examples across all four APIs. It checks rotation, disabling, removal, stale list responses, empty states, literal custom names and narrow layouts in Chinese and English on both engines. Sharing off keeps the original API-key field and hides the gateway-key picker; sharing on names the arbitrary local option separately from the Magpie key.

gateway-takeover.test.cjs checks the Gateway card's button: with an older magpie serving the gateway it reads "Quit magpie {v} and take over" and posts /api/gateway/take-over; once this magpie serves it is "Restart gateway" and posts /api/gateway/restart; a magpie of the same version gets no button. A refusal (a program that isn't magpie on the port) is said under it with the program's path and pid; a container's forwarder on the port (OrbStack Helper) is said as a container to stop, with docker stop, and in dark mode the button is bordered so it reads as one. No click scrolls, nothing has a left border, in Chinese and English.

gateway-key-limit.test.cjs checks a gateway key's own limit (#585): each limited key's badge and line (used, limit, left, reset, requests in flight, calls without a price), a spent key's "refused until", and the limit editor under a key's row. Window, tokens, cost and cache reads are staged, marked unsaved and sent only by Save; Cancel drops them, a bad amount disables Save, and No limit + Save sends null. No click scrolls the page, nothing has a left border, and the open editor fits a 560px window, in Chinese and English.

gateway-key-models.test.cjs checks the models a gateway key may use (#882): the badge on each key's row ("All models", or how many), the app's own menu (no native select) that stays inside a short window, picks sent only when it closes, and "All models" taking the restriction off. No click scrolls the page, nothing has a left border, and the row fits a 560px window, in Chinese and English.

api-key-usage.test.cjs exercises the existing provider key list: adding, enabling and disabling, choosing the first key, renaming and removing. The Usage page's Gateway key rows, request filter and CSV exports identify client keys, not those provider credentials. A client using different upstream keys stays grouped together; renaming an upstream key does not rename the client. It also checks historical records, Chinese and English, and the narrow window in Chromium and WebKit. The API fixtures use test keys and never read local user configuration. The existing usage-ledger.test.cjs also checks request-route and caller-key filters together, CSV export, and restoring the previous caller filter when the request filter is cleared. A caller absent from the period's options clears the filter and reloads the unfiltered rows from the first page.

api-key-theme.test.cjs checks Gateway's enabled and disabled keys, creation and renaming inputs, rotation and key picker, plus Settings' LAN address controls against the global palette. Light and Dark override the OS; System follows live OS palette changes. The settings theme picker is also switched and reloaded in Chromium and WebKit. No separate colours are defined for gateway keys.

node --test --test-concurrency=1 internal/gui/tests/gateway-caller-keys.test.cjs internal/gui/tests/gateway-connect-keys.test.cjs internal/gui/tests/gateway-key-limit.test.cjs internal/gui/tests/gateway-key-models.test.cjs internal/gui/tests/api-key-usage.test.cjs internal/gui/tests/api-key-theme.test.cjs

Other Browser Regressions

purpose-filter.test.cjs checks Usage and Routing purpose filters (#742) in English and Chinese on Chromium and WebKit. It covers title aliases, literal unknown names, unmarked records, pagination and totals, combined failure filters and CSV export, route navigation, session grouping, historical days and the narrow layout. Routing also covers keyboard dismissal and clearing the purpose while keeping the selected day and session grouping. Checkbox selection also covers combining purposes, immediate updates without closing or scrolling, keyboard toggling, restoring all purposes and returning focus to the purpose button when All purposes closes the menu, and retaining the union through history, session grouping and matching route navigation, in English, Simplified/Traditional Chinese, Japanese and German at narrow widths. It uses isolated API fixtures. Run with node --test internal/gui/tests/purpose-filter.test.cjs and the Playwright environment described below.

routing-purpose-state.test.cjs covers #771's follow-ups: selected-purpose counts (including broken-off 200s and failures without an HTTP status, while excluding informational notes on successful 200s), the failed-request link, the latest matching story through live updates and returning to the page, preserving a picked request, an empty historical selection, and both ways to clear the compact purpose control. Unfiltered live counters retain gateway lifetime totals; scoped counters describe the completed requests kept in the selected list. Run with node --test internal/gui/tests/routing-purpose-state.test.cjs.

Request-kind aliases and English labels live in internal/usage/purpose.go. After changing them, run go generate ./internal/usage to update assets/purposes.js; TestPurposeAssetMatchesUsage rejects a stale asset.

routing-sealed-task.test.cjs checks that routing explains why an encrypted subagent task excludes non-ChatGPT providers and why the parent account goes first (#619). It also checks plain tasks, old traces, direct model requests, short-window allowance wording and rules with no eligible candidate, in English and Chinese on Chromium and WebKit:

node --test internal/gui/tests/routing-sealed-task.test.cjs

routing-sessions.test.cjs checks the Routing request list in Chromium and WebKit, English and Chinese: the list defaults to By request and remembers the grouping choice across reloads; sessions are separated by agent and ID across model changes, known and partial costs are summed, zero partial estimates retain their amount and +, single-request headings use the singular, zero prices stay known, title helpers merge only into their explicit parent (even arriving first), unnamed Codex memory-only groups explain their background purpose while keeping separate IDs, costs and folds; named and mixed chat groups keep their titles, late chat names and renames update while preserving folds and ID tooltips, missing IDs and old unpriced requests remain visible, folded sessions stay folded through live updates, costs follow the currency choice, and the original time-ordered list, history selection and routing story still work. It also checks the request list at 1440 and 560 pixels. Run with node --test internal/gui/tests/routing-sessions.test.cjs and the same Playwright environment described below. APIs are isolated fixtures.

session-terminal.test.cjs checks the macOS Settings choice for installed .command handlers in English and Chinese. The system default appears once and is selected at first. It selects Ghostty, changes the theme, then returns to the system default, reloading to verify both saved choices. The API is faked and no terminal app is launched.

menu-scroll.test.cjs loads the real HTML, CSS and JavaScript with isolated API fixtures. It checks session folder/model filters and the main model picker in Chromium and WebKit, including wheel, scrollbar track/thumb, keyboard selection and dismissal. It does not start the backend or read local user configuration.

provider-levels-scroll.test.cjs keeps the provider editor's scrollable model list in place after saving a reasoning level farther down the list.

routing-save.test.cjs checks that a provider's Routing and Stays, in its editor, are staged and made with the editor's Save: each option clicked shows what it does and "unsaved", with nothing sent, redrawn or scrolled; Cancel drops them; one Save carries routing and affinity. For a subscription with several accounts and a provider with several keys, in English and Chinese, Chromium and WebKit.

click-scroll.test.cjs holds the rule that a click never moves the page (see "where the reader is" in app.js): on the Routing page, Live and a day picked in turn with the list scrolled to its end; a click whose handler sets scrollTop, grows the page above, redraws itself, or scrolls from a timer or a shared helper; a control under what it unrolls (data-unrolls) going down with it; a click that asks to go somewhere with scrollOnPurpose(e); the room kept at the foot going as the reader scrolls back; and the wheel.

routing-days-fit.test.cjs checks Routing › Requests' day bar with the 30 days the gateway keeps (#961): at 1100px, 733px (1100 at 150% text size) and 440px no pill is cut off at its edge and the days that don't fit are under its last pill, whose menu lists each with its count; the oldest picked from it is asked for and names the pill, with the bar held where it was; three days need no such pill; a window narrowed then widened fits again. Chromium and WebKit, in English, Chinese, Japanese and German.

agent-disconnect-preview.test.cjs checks the disconnect confirmation's file previews fill the body instead of the editor's label column. omp and Codex previews, restored values, expanding hidden lines, scrolling, Cancel, and Codex's separate note are covered at 890×800 and 560×420, including a scaled viewport for 150% text size, in English and Chinese on Chromium and WebKit. The test uses the real assets with isolated API fixtures and never changes user config. Set ARTIFACT_DIR to retain screenshots.

node --test internal/gui/tests/agent-disconnect-preview.test.cjs

agent-unreachable.test.cjs checks an agent whose config is right but whose address doesn't answer (#1013): its line says so in red with the advice as its tooltip, its pill is "How to fix", which opens the advice in a dialog and sets nothing, and when WSL reaches Windows at another address now the pill is "Use

" and posts the reapply. The window at 560px and the tray panel at 440px, in English, Chinese, Japanese and German on Chromium and WebKit.

node --test internal/gui/tests/agent-unreachable.test.cjs

agent-layout.test.cjs keeps the main window's agent names readable at 520, 560 and 600 CSS pixels, while the model and effort controls stay inside their rows. At 601, the default 660 and 960 pixels, controls remain aligned beside the names. English and Chinese, Chromium and WebKit.

signin-callback.test.cjs checks a sign-in's pasted callback (the field a sign-in with pasteCallback shows) in a narrow Chinese dark window: invalid input remains editable, retry reaches the callback route, and a pending or accepted submission cannot be submitted twice.

signin-paste-plugin.test.cjs checks a plugin's browser sign-in that comes back to a port on magpie's machine: the plugin's words, the pasted-address field posted to the sign-in, and the plugin's API key way, in a 360px window. English and Chinese, Chromium and WebKit.

plugin-updates.test.cjs checks the dot on Plugins while a plugin's update waits for the reader, gone once it's updated, and the "Auto-updated" chip on a plugin magpie updated by itself.

plugin-signin.test.cjs checks a provider an OpenCode plugin signs in to: the add sheet lists it under "From plugins"; its sign-in asks the way, the method's questions (a pick, then a text the plugin checks), then takes the code the browser page shows; an API-key way opens the account. English and Chinese, Chromium and WebKit, with the API faked.

plugin-move-overflow.test.cjs checks a Plugins card that offers a built-in's accounts: the button is one word beside the name, as Install is ("Move", its title saying whose accounts and how many), on one line, and it stays inside the card and the plugin's name and "magpie community" aren't cut, in a three-, two- and one-column window. English and Chinese, Chromium and WebKit, with the API faked.

deprecation-notice.test.cjs checks the deprecation notice over the Providers list is as calm as the list's rows: the list's own background and border, an amber dot and no badge, icon tile or filled button, the line naming Devin and WorkBuddy AI with why under it (no space after a Chinese full stop), and Not now and Review in Plugins side by side on its right, each on one line; on a 390px phone they go under the text at its end with no sideways scroll. English and Chinese, light and dark, Chromium and WebKit.

plugin-movable-review.test.cjs checks the line over the Providers list naming several built-in subscriptions a community plugin can run: its Take a look opens the Plugins tab, whose cards offer each one's Move, not the first one's editor. English and Chinese, Chromium and WebKit, with the API faked.

plugin-key-hint.test.cjs checks a plugin's API-key way: its label titles the key's field and its placeholder is the field's hint, while a way labelled only "API key" keeps " API key"; the provider's own icon shows on its row. English and Chinese, Chromium and WebKit, with the API faked.

plugin-market.test.cjs checks the Plugins tab: Discover lists the suggested plugins in their two sections, a card installs its plugin and then offers its sign-in, which opens in the Providers add sheet; a search filters the list at once and adds what npm has; a card opens the plugin's page with its README (no pictures, links opened outside); Installed shows why one didn't load, updates one and removes one. The add sheet's "More in Plugins" row (the providers list has no button of its own) and "look for a plugin" link lead there. English and Chinese, Chromium and WebKit, with the API faked.

plugin-git.test.cjs checks a plugin installed from a git repository: the market's field says it takes a GitHub repo, and Install sends github:owner/repo as typed; Installed names it by the package its repository holds, its Update fetches the repository again, and its page shows the README it came with, no npm link and a Source link to the repository. No click scrolls. English and Chinese, Chromium and WebKit, with the API faked:

node --test --test-concurrency=1 internal/gui/tests/plugin-git.test.cjs

panel-fold.test.cjs expands and collapses on the Agents page with the list scrolled to its end, in the tray panel (one agent open) and in the window: "Show {n} more" unrolls the rest under the button, the view going down with them and never back up; "Show less", and a row opened and closed, leave what was clicked where it is on every frame.

rollup-short.test.cjs folds the Agents page's list in a window taller than the list rolled up (#355): 24 agents, 5 in use and 19 folded, 4 of them hidden. "Show {n} more" takes the button up the view; "Show less" leaves it where it is on every frame painted, the room kept at the view's foot measured from where the content ends rather than from scrollHeight (never less than the view is tall), and the room goes once the reader scrolls back to the top. With Reduce motion and without, Chromium and WebKit, English and Chinese.

rail-tip.test.cjs hovers the model picker's rail in Chromium and WebKit, English light and Chinese dark: an icon's name shows to its right, level with it and over no other icon (the browser's tooltip put Devin's name on ZCode's Z), moves at once to the next icon, and goes on a click, on leaving, and when Esc closes the picker; a rail icon focused from the keyboard is named too.

agent-models-all-hidden.test.cjs takes every model out of DeepSeek Harness's lists with "Hide all" (#356): the line under its name reads "Showing 0 / 3 models" (显示 0 / 3 个模型), stays once the list is closed and the agents are drawn again from the state, and opens the list again, where "Show all" puts them back; no click moves the page. Chromium and WebKit, English and Chinese.

agent-models-picker.test.cjs takes two of Claude Code · WSL Ubuntu's three models out in its model list, then clicks its model picker, which closes the list and opens on the one left at once, while the state that follows is still on its way (#927). Chromium and WebKit, English and Chinese.

agent-models.test.cjs opens Codex's model list from the line under its name, in Chromium and WebKit, English and Chinese: the line reads "Showing 8 / 31 models" under the name, opening it moves nothing and puts it on the screen whole, routing groups come first and OpenRouter's 24 start folded, the model Codex is set to can't be taken out, a click takes one out at once with the whole list sent and the count under the name following, a search opens a folded group, "Shown" keeps one just turned off till the view changes, a group's "Show all" comes on hover, "Hide all" at the foot sends every one but the model in use, "Show all" there sends none, and Esc or a click elsewhere closes it.

omp-roles.test.cjs draws omp's subagents, smol and slow roles in Chromium and WebKit, English and Chinese: each is a square after the model's picker, not a picker of its own, as they follow the model while unset; unset it is named "same as model" (「同主模型」), its picker opens on "Same as model" with the model beside it, and picked it names the model it is on. OpenCode's small model, which doesn't follow the model, stays a picker.

subagent-effort.test.cjs draws Codex's subagent effort (#469) in Chromium and WebKit, English and Chinese: a square beside the subagents' one, not a third picker; unset, its label says what that means (the session's effort, or the subagent model's own default; 「跟随当前会话的推理强度…」), a click opens the effort slider without scrolling the page, Default its first stop, and a level picked is posted for subagent_effort alone and lights the square. Claude Code's subagents (#468) get no square until it runs through magpie (nothing to pick), and one that says "same as model" once it does.

codex-subagents-lead.test.cjs draws Codex's subagents square (willz on Discord) in Chromium and WebKit, in English, Chinese, Traditional Chinese, Japanese and German, at 1100px and 560px. Codex's lead may name a model in spawn_agent, and that overrides default_subagent_model. So the unset square reads "the lead's pick, else same as model" (「由主代理自选,未指定时同主模型」) and its title says how to make every subagent use one model. This holds on the local row and on a WSL row. The picker opens on "Lead's pick", with the model and "unless the lead names another" beside it. Picking a model posts subagent, and the square still carries the hint. Clicking the first entry unsets it again. A click doesn't scroll the page. Claude Code's subagents still say "same as model".

tier-effort.test.cjs draws Claude Code's per-tier effort (#536) in Chromium and WebKit, English and Chinese: no tier's effort is a field of its own; the tiers' square lists each tier's model and then its effort (the bars, and "the effort Claude Code asks for" / 「跟随 Claude Code 请求的推理强度」 unset); an effort entry opens the effort slider without scrolling the page, Default its first stop, a level picked is posted for that tier's <tier>_effort alone and named in the square's title, and the session's effort stays. Its subagents' effort is a square that says what unset means for Claude Code, and a Claude Code not through magpie has no levels, no square and no effort entries.

gateway-fold.test.cjs folds Connect on the Gateway page with the view scrolled: its fields hide, the head keeps the base URL and a copy button, the head stays where it was, and the fold is remembered across a reload.

sse-body.test.cjs opens streamed replies in the Gateway page's recent calls (Anthropic Messages, Chat Completions with [DONE], OpenAI Responses cut before it completed): the response body reads as each event's data pretty-printed under its name, as the reply put together (text, reasoning, tool calls with their arguments as objects, how it stopped, usage) or as it came, the pick remembered. A 450-event stream draws 200 at a time from a button under the box that stays where it is, the page too; a body that isn't a stream is as before. English and Chinese, Chromium and WebKit.

json-tree.test.cjs opens a JSON request and response in the Gateway page's recent calls: keys, strings, numbers and keywords each have their colour, the body's text is still the pretty-printed JSON, an object or array folds to {…} with its count and unfolds from its arrow or that summary with the page kept where it is, a fold outlives the list being drawn again, a value's copy button (shown on hovering its line) copies a string's text or an object's JSON, a click on a key copies its name, and a stream's events and its reply fold the same way. English, Chinese, Japanese and German, 1000px and 440px, Chromium and WebKit.

routing-kind.test.cjs lists calls Codex makes for itself (a guardian review, a title, memories, a turn on Luna Reserve, a kind it does not know yet): each has a grey tag by its model in the Requests list, in English and Chinese, the model keeping its room first, and the request's story says what it was.

routing-log-icons.test.cjs opens a request that failed on a Zhipu account and was answered by an OpenAI key: the story's why line and each try's line lead with the provider's logo before the account or key they name, 14px and on the name's line, and what the vendor said word for word has none. English and Chinese, Chromium and WebKit.

routing-side-calls.test.cjs lists what a DeepSeek chat in Codex sends besides its turns (#314): the new chat's title, which Codex asks of its own Luna on a hidden thread (known by its turn metadata), and the web searches magpie runs for DeepSeek on the model it searches with. Each has its grey tag (Title / 标题, Web search / 联网搜索), a search's story names the agent and model it was for, and picking a request moves nothing. Chromium and WebKit, English and Chinese.

routing-effort-row.test.cjs lists live requests sent at high reasoning, one under way, one with two tries, in Chromium and WebKit, English and Chinese, at 1440, 1000 and 480px: in every row the "· high" is shown whole and no run of text is drawn over another (#273: in two columns of ~470px it sat on the time taken).

routing-effort-change.test.cjs lists live requests whose reasoning was changed on the way ("· medium → low", "· medium → xhigh", one under way, one picked for the turn) beside "15 s · TTFT 14 s · 6.9k tokens", with one further down whose numbers run long (three tries, minutes, a million tokens), in Chromium and WebKit, English and Chinese, in the dark theme, at widths from 1440 down to 400px, and again with the styles as Safari 15 reads them (no subgrid, no container queries). In every row the reasoning stays within where it went and no run of text is drawn over another (#435, azir12345: 文字重叠 — the numbers' column, as wide as the longest numbers, left the reasoning next to no room and it ran over them); where it went and the level asked for are cut first, and the level sent is always whole.

routing-levels.test.cjs edits a group's reasoning levels in Chromium and WebKit, English and Chinese (#295: a member with low/high/max took medium and xhigh from the rest). "Its models' shared" names those levels; "Named" shows a toggle per level, starting from the shared ones, and toggling them moves nothing; saved, they go lowest first and the group's family stays. A group with its own opens on them, none picked is refused, and back to shared saves none.

group-member-fast.test.cjs sends a group's member in its vendor's fast mode, in Chromium and WebKit, English and Chinese (vs on Discord: "gpt-6.1 sol · high · fast"). The group's card reads "· fast" after the member's reasoning; in the editor a model with a fast mode shows a "Standard speed"/"Fast" toggle beside its reasoning and one without none, toggling moves nothing on the page, a member whose reasoning changes stays fast under its new id, one taken out leaves it, and the save sends the members sent fast as fast.

group-found-off.test.cjs turns the groups magpie finds on its own off and on again, in Chromium and WebKit, English and Chinese (蓝猫 on Discord: "路由分组会自动创建, 可以关闭掉吗"). A switch right under the routing groups' head says what found groups are; clicked off it posts groups/found {on:false}, the found group leaves the list while the user's stays, the hint says only theirs are served, and the status line says which agent was moved to the model from one provider; clicked on, the found group is back. Neither click moves the page, the switch has no left-border accent, and every string has its Chinese.

group-id-dots.test.cjs checks that a routing group's id keeps the dots of a model's name (#968): a new group named "GPT 6.1 Sol" is said and sent as group/gpt-6.1-sol, and groupSlug folds a run of dots and trims them at the ends as provider.GroupSlug does, in English and Chinese, Chromium and WebKit.

group-new-head.test.cjs checks that a routing group of any models can be made from the top of the Routing page, in Chromium and WebKit, English and Chinese (mintonight, #944: the groups' New group sat below the requests, so only found groups were used). The page's head has New group with a plus and a tooltip saying what a group is, in sight while the groups are below the fold; its click opens an empty group's editor, brings it into view and focuses its name. The groups' own New group has the plus too. No left-border accent.

name-suffix-where-named.test.cjs checks the setting that says whether a provider's name or "· routing group" follows a name in the agents' lists (#868) where names are given, in Chromium and WebKit, English and Chinese. The provider editor's Names & levels has its three choices and says what the lists show. For the groups it is one setting for all of them, so it sits over the groups on Routing (PAMI on Discord: in one group's editor it read as that group's): a group's editor has no choices, only what its group will be called and that it is set above; a pick there posts settings/plain-names once and the open editor's line follows it with its unsaved name. No click scrolls, nothing native, no left border, and every string has its Chinese, Japanese and German.

routing-manual.test.cjs routes a group by hand in Chromium and WebKit, English and Chinese (#317: pick the model, as CC Switch picks a provider). A manual group's card lists its models, the one every request goes to marked, and its rules tag says they wait; the reader wheels down to it and clicks another, which saves the group with that pick and its other fields as they were, the editor staying shut and nothing on the page moving, and clicking the one picked does nothing. The editor offers Manual with its hint and keeps the pick on save.

routing-served.test.cjs lists a request whose vendor's reply names another model than the one asked for (gpt-6-sol served as gpt-6-luna), one answered under the model's dated name and ones naming none: only the first is marked "served gpt-6-luna" by its answer in the Requests list and "requested gpt-6-sol · served gpt-6-luna" in its story, in English and Chinese, and the click that picks it leaves the page where it is.

remote-group-served.test.cjs lists a request sent to a remote magpie for one of its routing groups (group/auto-deepseek-v4-1-flash answered by deepseek/deepseek-v4.1-flash) beside a real swap: the group's member is shown plain, not amber, in the Routing page's Requests list, with a title (and a line in its story) saying the remote magpie's group routed the request there, and muted with the same title in the Usage page's Requests, while the swap stays amber; Chromium and WebKit, English and Chinese, and the click that picks it leaves the page where it is.

routing-reroute-title.test.cjs lists a request the plan's account rate limited and a relay took, and one answered on its first try: the rerouted one's title names the relay and its model, as its row does, then the account it was rerouted from, in the window's Requests list and the tray panel's Routing tab alike; the other keeps "agent · model → provider". English and Chinese, Chromium and WebKit (#337).

plain-names.test.cjs checks the Settings page's "Provider in model names" row (#335): on by default, Off posts settings/plain-names with mode: "off" on its own and lights Off, On posts it back, and neither click scrolls the page. English and Chinese, Chromium and WebKit.

model-suffix-own.test.cjs checks that row's third way (#92): "Not on names I set" (自定义名称不带供应商) between Off and On, lit by plainOwnNames; On and then it post settings/plain-names with mode: "on" and mode: "own", neither click scrolls the page, the hint isn't cut short and the three fit the row at 900px and 560px. English and Chinese, Chromium and WebKit.

routing-steady.test.cjs scrolls the Routing page down to its routing groups, opens one in its editor and types into its name, then streams six requests in from the trace (new agents and accounts on the stage, failures and retries in the story, the lists growing): the groups and the editor stay where they are on the screen on every layout and scroll, the view's scrollTop moving by just what grew above them, and the field keeps its focus and what was typed.

routing-flood.test.cjs takes the Routing page out of sight three ways (#302): the window hidden, the window covered and drawing no frames without saying it is hidden, and another tab open. Meanwhile 30 requests come and are answered, and 2 are still under way. When the page is seen again, at most two magpies per request still under way fly at once, those requests do fly, and all 33 are listed. It runs in English and Chinese. Before the fix, 39 to 62 birds flew at once.

routing-busy.test.cjs streams requests into the Routing page, one trace update each (#308). The row of a request that hasn't changed stays the same element, and a click on it still picks it. With another tab open, the list isn't touched, and coming back lists every request that came meanwhile. It runs in English and Chinese.

routing-weight-chip.test.cjs checks the stage's header for a provider routed By weight (#841): the MODES table had no weight entry, so the chip and the mode paragraph under it fell back to Smart's. A trace over two weighted keys now has the chip read By weight and the mode paragraph say what the provider editor's routing option says (the words already spoken in each language), with the story telling whose share went first, while keys under Smart keep Smart's chip and the keys' own mode line. It runs in English, Chinese, Japanese and German on Chromium and WebKit.

balance-fix.test.cjs opens a custom provider whose balance token sits beside new-api's /api/usage/token (and a new one with a token and no Balance URL): the editor says so, one click moves it to /api/user/self with the quota as its field, the New-Api-User header is asked for until it is typed, Check balance asks as the form has it and says the fix plainly, and the Usage page's card does too. It also holds the card's own words for a ZCode account with nothing left to spend (#1001): a Start Plan that has ended is said as that, while one that couldn't be read stays "Allowance unavailable" — the two are not the same thing.

balance-field-default.test.cjs leaves a custom provider's Balance field empty (#881): its placeholder is the field magpie reads from a Balance URL whose reply it knows (new-api's /api/usage/token and /api/user/self, a sub2api panel's /api/v1/user/profile, OpenAI's credit_grants), following the URL as it is typed; any other URL keeps the example.

cli-update.test.cjs draws the agents' CLI versions on the Agents page (#202) from a faked API: the version after each name, "Update to x.y.z" only where magpie knows how the CLI was installed, the rows' height kept, an update clicked with the view scrolled (busy, then the new version, the page left where it was), a failed one giving its reason and the pill back, and the words in Chinese. Nothing is installed.

panel-profiles.test.cjs saves a profile in the tray panel with the list scrolled to its end: "+ Save current" opens the name field beside it, the list not moving, field and button in sight, one focus ring; the button then reads Save and a click on it saves, as Enter does; an empty name keeps the field; Escape closes it; in English and Chinese.

profile-details.test.cjs (#467) clicks a saved profile's chip, in the window and in the tray panel with its list scrolled to its end: the details open, applying nothing and leaving the chip where it was on the screen; they list each agent by name, its fields under their labels (an empty one as the agent's default, one that reads as a key as dots, all four of Claude Code's tiers, one left empty as following the main model and which that is, #480) and what the Library gives it; a second click closes them; their Apply applies the profile, once; ↻ and × stay on the chip. In Chromium and WebKit, English and Chinese:

profile-confirm.test.cjs (#478), in the window and in the tray panel: a chip's ↻ and × change nothing on a first click, reading "Overwrite?" and "Delete?" instead, and act on a second; the question goes on Escape or when left alone. The details' × closes them, the chip staying where it is; Escape closes the details, then the panel's list, and only then hides the panel; the list closed with the details open opens again on the chips alone; Apply closes the details, the tray's list opening again on the chips, and their × closes them after it (#489). In Chromium and WebKit, English and Chinese:

node --test --test-concurrency=1 internal/gui/tests/profile-details.test.cjs internal/gui/tests/panel-profiles.test.cjs internal/gui/tests/profile-confirm.test.cjs

panel-routing.test.cjs opens the tray panel's Routing tab: the gateway's latest requests from a faked trace, newest first, each with its agent, the model asked for, the provider and account it went to, the model that answered ("served gpt-6-luna" on the one a vendor answered with another, not on a dated name), a failure's status, and the time; today's calls and tokens over them; the allowances' tab named Allowances. A click, with the panel scrolled, asks for the window's Routing page on that request and moves nothing; the window opened so has that request picked. In English and Chinese.

login-import.test.cjs brings ChatGPT accounts in from CLIProxyAPI's auth files: offered from the accounts list and from a browser sign-in under way, the box says the refresh spends the file's sign-in, what is pasted is posted as it is, and each account's outcome is listed, in English and Chinese.

agy-launch.test.cjs gives Antigravity CLI's row the square that copies the command starting agy on magpie: the command in its tooltip, a click copying it with the page left where it was, none on other agents, in the tray panel's opened row too, in English and Chinese.

currency.test.cjs shows a cost in dollars by default and in yuan, at magpie's cached exchange rate, once the Settings page's Currency row picks cny (#212): the Usage page's total converts, the row's tooltip carries the rate, picking it with the settings list scrolled well down moves nothing, and the choice survives a reload — in English and Chinese.

context-window.test.cjs opens Usage's Context tab: each agent's score, its grade and tags, its sessions counted one or many, the score bars toned by how much of them there is; a refresh with the same answer keeps the pane; a session opened draws its latest request's window in 400 cells without moving the page, a cell hovered names its part and what is largest in it, and the Tools tab of the contents lists only tools; nothing scrolls sideways — in Chromium and WebKit, in English, Chinese, Japanese and German, at 1100px and 420px. With the state answering after the history, the cards drawn by the agents' ids take their names once it is in, though the history is the same.

tray-usages.test.cjs picks several of the Usage page's cards for the menu bar in Settings: the menu keeps open as Codex and Claude Code are ticked beside Copilot, posts nothing till it closes and then the three once, in the menu's order; the pill reads "3 subscriptions"; a menu opened and closed unchanged posts nothing; a save of another setting keeps them; Off unticks them all, closing the menu and the refresh row; no row has a left-border stripe and no click moves the page; in English and Chinese.

tray-logos.test.cjs turns the menu bar's logos off and on in Settings: on a Mac with a card in the menu bar the Menu bar logos row shows On by default; Off posts trayNoLogos: true, a save of another setting keeps it, On posts it back; the row has no left-border stripe, no click moves the page, and there is no row with no card picked nor off a Mac; in English and Chinese.

tray-bird.test.cjs turns the menu bar's magpie icon off and on in Settings: on a Mac with a card in the menu bar the Menu bar magpie icon row, under the logos row, shows On by default; Off posts trayNoBird: true, a save of another setting keeps it, On posts it back; no click moves the page, and there is no row with no card picked nor off a Mac; in English and Chinese.

tray-cell-click.test.cjs checks menu-bar quota navigation in the panel and main window: selecting Allowances/Overview, scrolling to and highlighting the matching account or balance, and keeping a click pending while quotas load. It checks that the panel loads Wails so native ExecJS can run. A focus-triggered redraw preserves the remaining highlight animation in both panel and main window. It expands and remembers folded target accounts, disables panel-header dragging, and cancels navigation after five seconds or a purposeful user scroll. It runs in English and Chinese on Chromium and WebKit, with a mocked API. Run with node --test internal/gui/tests/tray-cell-click.test.cjs.

panel-arrange.test.cjs opens the tray panel's Allowances tab at 440px with a subscription hidden in the settings: it has no card and the foot says one is hidden. Arrange keeps the page where the reader scrolled it and lists a row a subscription in the shared order, with the note that hiding is the panel's only; the grip by a logo shows on hover only, and no row has a left stripe or runs off sideways. Show and Hide post panelHidden without moving the page, a failed save is put back, Alt+arrow and a drag by the logo post the order, and Done draws the cards in it. A menu bar cell for a hidden subscription still opens its card until the panel is put away, and the Usage page keeps the hidden card in the same order. It runs in English, Chinese, Japanese and German on Chromium and WebKit, with a mocked API.

TestTrayCellClickReleasedPanel (darwin, cgo, GUI) runs a separate AppKit process with an isolated config and a minimal page: a quota click recreates the released panel, queued ExecJS selects the account once Wails is ready, the next click closes it, and another release can be reopened and focused. It requires a macOS graphical session and does not start the magpie backend or create a status item. Run with go test ./internal/gui -run '^TestTrayCellClickReleasedPanel$' -count=1.

text-size.test.cjs checks the Settings page's Text size row (100, 110, 125 and 150%): a pick posts to /api/settings/text-size without scrolling the page, stays picked after a reload (boot.js carries it, and the page's --zoom has it before the first paint), and a save of the other settings leaves it be; Ctrl/Cmd + = and − step through the sizes and 0 goes back to 100%, in the window and the tray panel, the other modifier doing nothing. The zoom is the webview's, so here it is as a browser zooms: the smallest window at 150% (840×630 points) is 560×420 CSS pixels at a device scale of 1.5, and no view (nor the panel at 150%) runs off to the side, the Mac header still 50 points tall for the traffic lights. At 150% in a 1440×900 window scrolled down, a click on 100% (the window then 1440×900 CSS pixels) leaves no .view-room and no blank under the page, nor scrolls it beyond the browser's own fit (#911). English and Chinese, light and dark; ARTIFACT_DIR gets screenshots at 100% and 150%.

settings-groups.test.cjs puts the Settings page's warm-ups and check-in under a tab per service (#124): Codex, Claude Code and WorkBuddy tabs where a heading would be, in the Usage part after its list, Codex's picked to begin with and each showing its rows alone, named without the service; the last warm-up and today's check-in still on the short lines, no left-border stripe; a daily warm-up's time field only while it is on; each control posting the setting it did and a tab posting nothing, with the page scrolled and left where it was; the arrows, Home and End along the tabs; a tab picked at the page's very end (a shorter card, then a taller one) leaving the tabs where they were; the tab remembered across a reload, Codex's shown when the remembered WorkBuddy one is gone; the WorkBuddy tab only with an account signed in; in English and Chinese.

settings-sections.test.cjs checks the Settings page in parts (#471): a tab list at its top — General, Usage, Network and sharing, Models, Privacy, Observability, Sync and backup, About — each tab showing its part's rows alone, posting nothing and leaving the page and the tab where they were; a part's controls still posting what they did; one tab in the Tab order and the arrows, Home and End along them; the part kept in the address (?view=settings&tab=…) across a reload, out of it on another view, and remembered for Settings opened again without one; a link to a part opening on it. The tabs fit, on a line more where they must, in the 560×420 window at 100, 110 and 125% and in magpie web on a phone, nothing running off to the side. Chromium and WebKit, English and Chinese; ARTIFACT_DIR gets a screenshot of each size:

node --test --test-concurrency=1 internal/gui/tests/settings-sections.test.cjs internal/gui/tests/settings-groups.test.cjs

segs-press-redraw.test.cjs presses a Settings option, redraws the page (renderSettings(), as a save's answer does) while the button is held and lets go on the same spot: Lightweight mode → On is posted once and shown on. Dragged off to the other option, or to the same option of another control, nothing is posted, and a plain click is posted once, not twice. It then presses every option control on every Settings tab (49 in English, the warm-up, check-in, sync and local network ones included) the same way and checks that the control drawn in its place is clicked once on the same option. Without the fix no click comes at all. A tap through the redraw is posted once, not twice (Chromium only: Playwright can't hold a tap in WebKit). A list of rows, each with its control, drawn again with a row gone, come in or moved above the pressed one picks nothing, whether its rows have data-* or not, and picks the pressed option when drawn again the same. Chromium and WebKit, English, Chinese, Japanese and German, 900 and 440px wide:

node --test internal/gui/tests/segs-press-redraw.test.cjs

update-check.test.cjs checks the version row: the button stays, dimmed, through a check, a second click asks nothing, and the answer puts it back. A read still out cannot draw "checking" over that answer. A row drawn again while the check runs keeps the button and catches up when the check answers. A failed check gives the button back, and a check already under way shows the button without starting another. The page stays where it was. English and Chinese.

update-wait.test.cjs checks a restart to update with the gateway busy (#577): Settings → Version says what is in flight (requests, Claude Code turns awaiting tool results), Restart to update then waits with "Restart now" and "Cancel", the counts follow the gateway, a wait that ran out says so, and with nothing in flight the click restarts at once. The header's Update pill waits on its click and restarts at once on the next (not on a double click's second half). No click moves the page, no coloured left border. English and Chinese.

refresh-page.test.cjs checks the header's refresh reads again what the page shown draws (#844): on Agents it looks for the agents again (one just installed shows, and leaves Install another agent), on Usage it reads the allowances afresh, on Providers it refreshes the model lists. It never looks for a newer magpie (Settings › About's Check does), its tooltip says what it reads, and no click moves the page. Window and panel, English and Chinese.

update-hide.test.cjs keeps the header's Update pill away when asked (Wang Hsiaohi on Discord): its ×, shown with the pointer over the pill, hides it for that version, through a reload, until a newer one is out; a right-click does the same. Settings → Update button says which version it is hidden for, with Show again, and turns it off for good; the version row still offers the restart. No click moves the page. Panel and window, English and Chinese.

update-download-percent.test.cjs checks the Update pill while a failed update downloads again (inaction on Discord): with the size known and nothing in yet it says 0%, with no size "Downloading…", then the percent — never NaN, which it said while the answer left done out. The click doesn't move the page. English and Chinese.

update-auto.test.cjs checks Settings → Automatic updates (#472): on, every 6 h, with nothing set; Check every saves 30 min, 1 h, 6 h or 24 h; Off is saved and kept through a reload, the interval staying in place, dimmed, for when it is on again; the version row, not asked yet, says automatic updates are off and its Check asks the backend. No click moves the page. English and Chinese, Chromium and WebKit.

add-sheet.test.cjs opens the Providers page's add sheet as quiet rows: Subscriptions, Vendors, Relays and On this machine, each with its word on what it is; rows with no border, no second line and no overflow, three to a line at 900px; an added provider not faded but marked by a small green dot after its name, a grey count only past one account (the Claude subscription's 2); short names with the full name, plans and host in the title; a vendor's global and China presets one row with no tag, the regions' hosts in its title, its editor picking the region with the key typed kept; the custom provider a line at the foot, gone while searching; the dialog growing out of the row clicked on a spring and folding back into it on close, the focus back on the row it came from; Duplicate and Add another opening a new editor with no sheet, cancelled back to the list; the search's Escape emptying it and never hiding the tray; in English and Chinese.

own-signin-remove.test.cjs opens the Kiro subscription's accounts with kiro-cli's own sign-in behind one of magpie's, then alone and first: its Remove is there both times, its title saying magpie only hides it and it shows again when Kiro signs in anew, and it posts login/forget; magpie's own account in use first has no Remove; in English and Chinese.

saved-signedout-remove.test.cjs shows Claude Code signed out with two accounts saved in magpie: the Providers line naming them has Remove, whose dialog lists them; Cancel and Escape post nothing, Remove posts login/forget once per account and the line goes; in English and Chinese.

codex-signout.test.cjs opens Codex with one account (mamba on Discord: it couldn't be removed while Codex was signed in to it): its row has Sign out, asked in the app's own dialog; Cancel and Escape post nothing and go back to the editor, Sign out posts login/forget and says so. With another account saved the one in use has no Sign out but Remove, which posts login/forget (vincentzhang1_55530 on Discord: magpie signs Codex in to another account first), and a signed_in refusal is said in the reader's language. No select, no left border, the page doesn't move; in English and Chinese.

nokey-editor.test.cjs opens two local Ollama providers saved with no key, one from the preset and one custom, as /api/providers gives them: a click on each row opens its editor (the key box saying one is optional) and nothing throws, where the row only toggled. A plugin's provider (base plugin://codearts) has no link in its editor's head, where it said "codearts ↗" and opened https://codearts (Lemon on Discord), while the Ollamas keep theirs; in Chromium and WebKit, English and Chinese.

editor-short-window.test.cjs opens a provider's editor with 40 models in a short window (800x500, 600x420, 420x340; 悠悠哥 on Discord: a small window's WorkBuddy editor stopped part way down its models): the dialog fits the window with its head and Save in sight and uncovered, and the wheel held over the model chips runs them to the last one and then takes the editor's body on to its end, where the chips' held ends stopped it; the page never moves. In Chromium and WebKit, English and Chinese.

group-from-model-add.test.cjs opens the Routing page on a new group made from a model (?newgroup=, as "Make a routing group of it" does; 悠悠哥 on Discord: "Hy4 preview" did nothing on Add in a small window). Each label sits beside its field, where a hidden classifier's empty cell put Levels' label at the right and its choices under the labels; the wheel reaches Add, uncovered, and Add posts the group, where its draft had no fast members and Add threw. A save the server refuses shows its error in sight, the editor stays, and a second Add is posted, where the button stayed busy. At 600x420 and 1000x700, in Chromium and WebKit, English and Chinese.

group-pick-filter.test.cjs covers a group's Add a model picker and the filter over the routing groups (#907; PAMI on Discord). A model in the group with its reasoning fixed (a/two:max) is ticked, and a click takes that member out instead of adding it a second time. A model that is in other groups names them, and the whole note shows on hover. The filter keeps the groups whose name, id or models match every word typed, and the keyboard stays in it. When nothing matches it says so, and Escape clears it. The page never moves, and there is no select or left border. In Chromium and WebKit, English and Chinese.

old-webkit.test.cjs holds the page to Safari 15.0, the WebKit macOS 12 can have (#220: a regex lookbehind in app.js, a syntax error before Safari 16.4, left the panel with its headings and nothing working). Every script in assets/ is parsed (with the Babel parser Playwright bundles) and may have no regex lookbehind, v flag or modifiers, class static block, decorator, import attributes or using, in a literal or new RegExp("…"); no built-in newer than 15.0 (.at, findLast, structuredClone, Object.hasOwn, toSorted, Set methods, Object.groupBy, Promise.withResolvers …) unless compat.js, loaded before the rest, fills it in; the styles no :has(), no :focus-visible in a list with other selectors, no nesting or dvh units, and container queries, subgrid columns and a color-mix() custom property only with an @supports not (…) fallback in the same file. Then, in WebKit with those built-ins deleted, the panel's tabs and the window's pages are drawn with no page error. It cannot run an old engine, so syntax is judged by the parse, not by running it.

add-button.test.cjs keeps the Providers page's "Add provider" at the view's foot over a long list, at the top and at the end alike; one click with the list scrolled to its end opens the sheet under the list and takes the view down to it (in WebKit too); the button steps aside while the sheet's head is in sight and, scrolled back up, takes the view to the sheet again; a dialog opened and closed over the page keeps every logo it drew rather than making them afresh; and the sheet closed by its Close, twice over, or by Escape leaves no blank under the list, the focus back on "Add provider" (#433 made it a dialog over the list for that blank; it unrolls under the list again, the editor being the only layer over the page); in English and Chinese.

usage-ledger.test.cjs opens the Usage page's Requests, a row per request from a faked /api/usage/requests: the columns, the model asked for, the provider and account, the model sent, "gpt-6-luna" amber by the one a vendor answered with another model and plain on a dated name, a failure's red dot and 429 and its agent, on another computer whose magpie passed it on, as "Codex · via office-mac", the totals over them. Older and Newer page through 130 requests with the pager held where it was; the agent, Failed and search filters and the period ask the server again from the first page; Export CSV posts the filters shown with no page and says where the file went. At the window's narrowest (560) the table scrolls in its box and no tab scrolls the page sideways; no left-border stripe; in English and Chinese, light and dark.

usage-ledger.test.cjs also follows a request from Usage to its routing story and back to every usage row sharing that route ID. It checks archived routes with no live gateway, cleared history, rows recorded before IDs were kept, clearing the route filter and exporting it, in Chromium and WebKit, English and Chinese.

sessions-overview.test.cjs checks the Usage page's Sessions overview: six figures (sessions with their median and p90, tokens, cost, cache read, active time, projects with the top one's share), the range as bars up to 100 days and a calendar past them, counted in tokens, cost, sessions or active time (active time off while a model is picked), the week by hour in the local time zone, projects and models as bars that filter without moving the page, and the top sessions opened in place from their files; nothing overflowing at 1100 or 560 wide; in English and Chinese. It also checks the heatmap by messages and output tokens, the session shape by messages, minutes and autonomy (remembered, the page left where it was), tool use (the top tools, their kinds and weeks) and the top skills with their last use, agents and projects.

sessions-bar-names.test.cjs checks the Sessions overview's bar rows (projects, models, tools and skills: a name, a bar, figures): at 1600 wide every long name is shown whole, each card's bars start at one place and are at least 5em, at 420 wide the long names are shortened while no bar is under 5em and nothing runs past its card, and wide again the names are whole; in Chromium and WebKit, English and Chinese.

usage-sessions-agents-fit.test.cjs checks Usage › Sessions with 15 agents (#929): at 860 and 560 wide the page, its main and the Usage view never scroll sideways; the agent strip fits the window and scrolls in itself, and the last agent, clicked at the strip's end, stays in view as the strip is drawn again; in Chromium and WebKit, English and Chinese.

usage-unpriced-names.test.cjs checks Usage › Sessions where an agent recorded tokens under no model name (tony on Discord: the cost's note read "未计入:,没有已知价格"): the cost header's note, a session row's cost note and its models name it "a model with no name", with no blank where a name goes; in Chromium and WebKit, English and Chinese.

usage-keys-fold.test.cjs checks the Usage page's card of a provider with many keys, each its balance (361 on Discord: an OpenRouter card listed every key's): the first three are in sight, the rest behind "Show N more keys" at its foot, the keys' sum in its head; the button opens them in place and folds them again without moving the page, and is remembered across a reload; DeepSeek's keys fold too, four keys aren't folded, a subscription's accounts aren't keys, an unread key leaves the sum out, and at 440px nothing is cut; in Chromium and WebKit, English and Chinese.

upstream-status.test.cjs checks the vendors' status pages on the Providers and Usage pages (#971): with Anthropic's page saying its API is down, the Anthropic provider's row and the Claude subscription's row and usage card carry "API outage" beside the name, its title naming the part and the incident; a click opens the incident and not the row; a relay serving the same models, OpenAI (its page all well) and Moonshot (its page unread) carry nothing, and nothing does once the page is well again; at 440px the badge is whole (the name gives way) and nothing spills; in Chromium and WebKit, English and Chinese.

usage-accounts-fold.test.cjs checks the Usage page's card of a subscription with many accounts (Linx on X: twenty-odd on one plugin made a very long card): of 22 WorkBuddy AI accounts the first five are in sight, the rest behind "Show 17 more accounts" at its foot; of nine Trae accounts the one in full and the one with the check-in switch stay in sight too; eight aren't folded. The button opens them in place without moving the page, is remembered across a reload and folds them again from the foot (the page resting at its end, the button in sight); at 440px nothing is cut, folded or open; in Chromium and WebKit, in English, Chinese, Japanese and German.

sessions-calendar-fill.test.cjs checks that the Sessions overview's activity calendar fills its card: 118 days (17 weeks) at 1400 wide take over 90% of it, the weeks before the range coming in as empty cells without tooltips, the cells square and the month names over their weeks; at 1000 and 560 wide the weeks before are fewer and nothing runs past the card, narrower still there are none and the cells shrink, and wide again they come back; in Chromium and WebKit, English and Chinese.

usage-ledger-detail.test.cjs opens the Requests tab's rows: a failure the gateway logged shows its status and the vendor's error type in the row and, opened, what the vendor said with the request's id and endpoint; a call read from an agent's session file carries a "session log" mark, says "Succeeded" or the error that ended it, and says in its details that the file records no status; its models are the one asked for, the one sent as asked and the one that answered, its effort is there, and its time is about (≈) what the file's stamps tell. A click opens and closes a row without moving the page, Enter and Space do the same, text selected in the details is not a click, and the rows stay open when the list is asked for again; in English and Chinese.

usage-ledger-chart.test.cjs opens the Requests tab on what its requests add up to: a strip of four totals (tokens, requests, cost, the cache hit rate), then the trend of one metric — tokens, cost or requests — as columns by the hour, each told apart by provider, agent or model and standing on the bottom line in its hour's slot, beside a ranking of the same that is the chart's legend. The pointer over a column shows what each had of it, over a ranked one the others fade; a click on a provider or an agent in the ranking, or the provider picker beside the agent's, lists only its requests while the ranking keeps the others in sight; the metric and the split are remembered; a click moves nothing. At 560 the ranking goes under the chart and the totals two to a row, a wider window redraws it, a metric with no price says so, and with no request listed there is nothing; in English and Chinese, light and dark. The daily chart also checks selection, gray bars on other days, filtered rows and CSV, an empty day, clearing a selection, keyboard input and resizing.

usage-model-at-provider.test.cjs splits the Requests tab's chart by "Model · provider" (inaction on Discord): one model at two providers is two rows of the ranking, each named by both with the provider's icon and its own first token, the faster first by Speed though fewer requests went to it. A click lists that provider's requests of that model (provider and model both asked for), a click again all of them, and the page doesn't move. English and Chinese.

usage-chart-axis.test.cjs checks the usage chart's side labels, in the window's Requests tab (1180 and 560 wide) and the tray panel's Usage tab (440 and 340 wide): for tokens, cost in dollars and in yuan, and requests, small and large ("8000 万", "125 亿", "¥2000"), each label starts no further out than the card's padding, as far in as the tabs above it, ends before the plot, and nothing runs past the card or scrolls the page sideways; Chromium and WebKit, English and Chinese.

number-units.test.cjs checks Settings' Number units (John on Discord): in Chinese the Requests tab says a large count in K / M / B ("1.54B", an axis's "80M") by default, as English does (#740); picking 万 / 亿 saves chineseUnits, moves nothing on the settings page, and the totals and chart axis then say "15.4 亿" and "8000 万" while the page stays Chinese, after a reload and in the tray panel too, each label still ending before the plot. Every other count follows (#476): the Overview's tiles and chart peak, the Requests summary line, the Sessions tab's figures, and the panel's Routing "today", which turns back to K/M/B when the setting does. In English the row is hidden and counts are K / M / B anyway; Chromium and WebKit.

panel-usage.test.cjs opens the tray panel's Usage tab: the totals, a small chart and a ranking of five at most for today, seven or thirty days, the metric switch, and a click on a provider (or the picker) that switches to it — its models then tell the chart apart. A click on a control in sight leaves the panel where it is, Open Usage takes the window to that provider's requests, and the window opened so has the Requests tab with that provider and agent picked and an address without them. Nothing is cut off at 320, where the totals go two to a row; available allowances keep their tab; in English and Chinese. Daily bars select that day's totals and ranking while keeping the whole chart; refresh and metric changes keep the selection, a period change clears it, and clicking the selected day clears it too, including days with no calls.

usage-refresh.test.cjs sets the Usage page's refresh period, with a clock in place of time: every 5 s to begin with and no read before that, the picker's five (off, 5 s, 10 s, 30 s, a minute), off reading no more, a minute reading at its end and not at half of it, the button beside it reading at once, turning while it does and saying when in its title, on Overview the summary and the allowances too, asked for (usage/quotas?asked=1) as the allowances' own Refresh did, with no second Refresh on the page and a title saying so (#486), and the choice remembered by the next window; in English and Chinese.

usage-ledger-content.test.cjs opens the rows of the Requests tab on what was said in them, read from the agent's session file when the row is opened: loading, then the input and the output as parts (who said each, a tool's call with its name), the reasoning and the agent's own context folded, a long part showing some of itself and unrolling, what is left off a part or the whole said; a gateway request is asked for by its session and the span it took, a call of a session file at its own time; a request the files can't tell says why (no session, an agent whose files aren't read, no such call); a row opened again, or the list read anew, asks no more; the details fit the table's box at 560; in English and Chinese.

shared-skills.test.cjs opens the Library's Skills tab with skills found in the user-wide ~/.agents/skills (#227): one row for a skill there that agents link or junction to, "shared in ~/.agents/skills/…" with those agents' icons and no "differs in"; a link in the shared folder also saying where it points; a copy of an agent's own still "differs in ZCode"; Bring in saying it stays where it is and posting the name; in English and Chinese.

library-toggle-status.test.cjs opens the Library's Skills and MCP tabs and clicks an agent's icon on a row (#332): turned on, the status says "Written to DeepSeek Harness"; turned off again, "Removed from DeepSeek Harness", and an agent that had it from the start turned off, "Removed from Codex"; Chromium and WebKit, in English and Chinese, with /api/plugins faked.

import-all-skills.test.cjs opens the Library's Skills tab with three skills found in the agents: Bring in all beside "In your agents" posts every name at once, the rows go and the toast says "3 skills are in the library now", or names the one that couldn't be brought in, which stays listed; the click scrolls nothing; in English and Chinese, with /api/plugins faked.

every-skill.test.cjs opens the Library's Skills tab with three skills and three agents that take skills, and one that doesn't (#443): Turn all on beside "In the library" posts the three agents with on, at once, and every chip is lit; Turn all off asks in the page first (a browser dialog fails the test), Cancel posts nothing, and its own button posts them with off and no chip is lit; each button is greyed once there's nothing for it to do; the clicks scroll nothing; Chromium and WebKit, in English and Chinese, with /api/plugins faked.

by-agent.test.cjs opens the Library's Skills and MCP tabs with Copilot CLI hidden on the Agents page but still given two skills and a server (#475): By agent beside "In the library" opens a sheet of the agents that take them, each with how many it has; the hidden one is tagged Hidden and has no Turn all on, and its Turn all off posts it alone to …/agents-all with off and leaves it listed with none; Codex's Turn all on posts Codex alone with on and lights its chip on every row while Claude Code keeps what it had, and for servers isn't offered again once the only one it can reach (not the SSE one) is on; the clicks scroll nothing; Chromium and WebKit, in English and Chinese, with /api/plugins faked.

remove-all-skills.test.cjs opens the Library's Skills tab with three skills, two kept by magpie and one linked from a folder of the user's (#449): Remove all beside Turn all on/off is red, and asks in the page first (a browser dialog fails the test), naming the agents that lose them, the two folders that go to magpie's backups and the one only unlinked; Cancel posts nothing; its own button posts every skill's name to library/skills/remove-all once, and no row is left; the clicks scroll nothing; Chromium and WebKit, in English and Chinese, with /api/plugins faked.

header-centre.test.cjs sweeps a Windows window from 1100 to 560 points at text size 100, 110 and 125%, with an Update pill waiting (emo172, #442): the header's tabs never touch the buttons; whenever they aren't in the row they're in the middle of the window, and the closer-together tabs are tried there before the row; in the row they have as much room either side, give or take the header's padding against the gap before the buttons; Chromium and WebKit, in English and Chinese, with /api/plugins faked.

library-row-open-scroll.test.cjs opens the Default set of Library → Instructions, wheels down to the foot of the agents' list and clicks an agent's row open and shut (emo172, #458): the Library draws its whole page again on a click, and the row clicked still stays where it is on the screen, under a long set in a short window and a shorter set in a tall window. Chromium and WebKit, English and Chinese, with /api/plugins faked.

header-zoom-fit.test.cjs sweeps a Windows window from 1300 to 700 points at text size 110 and 125%, with an Update pill waiting (emo172, #457): a wider window is never drawn in more than a narrower one, and with room to spare the header is as at 100%. Chromium zooms the page as the webview does (its default zoom level), where the buttons' edge measures a hair past the header's padding; WebKit, which has no page zoom to set, uses the device scale factor. English and Chinese, with /api/plugins faked.

strip-scrollbar.test.cjs lays out an overflowing tab strip (segs regions, the Sessions page's agents and a provider's regions) in Chromium with its scrollbars shown (ARNO on Discord): off a Mac it scrolls under the app's 6px thumb, not Windows' classic bar; on a Mac the system's bar is left.

own-skill.test.cjs opens the Library with Claude Code's own impeccable in the library skill's way (StringKe, JasonLeeForOnly on Discord): the warning row says so in the reader's language and has "Use the library's" and "Keep Claude Code's", which post skills/use-library and skills/keep-own with the skill and the agent; the warning goes, the toast says what was done, and the click scrolls nothing; no coloured stripe down the card or the row. A found skill with a byte copy in another agent shows both agents' icons and no "differs in"; one that really differs still says so. Chromium and WebKit, in English and Chinese, with /api/plugins faked.

model-pick.test.cjs picks Claude Code's model with the Agents page scrolled while a faked /api/set takes 2.5s to answer: the row shows the new model at once, in the window and in the tray panel's opened row and its line, the page left where it was; the answer keeps it and says so; a refused pick puts the old model back with the reason; in English and Chinese.

claude-direct.test.cjs shows Claude Code with "model": "sonnet" in its settings.json: the row reads the alias as the model it stands for, with Claude's logo; its tooltip says a model Claude Code asks Anthropic for itself isn't through magpie, so settings.json names no magpie endpoint, and picking one says "straight to Anthropic"; a magpie model says neither, and no pick moves the scrolled page. Chromium and WebKit, in English and Chinese.

omarchy.test.cjs holds Omarchy's look to Omarchy: with no Omarchy theme in boot.js the page has no omarchy class or theme style, shows the Appearance choices and no Bar icon row, and asks nothing of /api/omarchy; with one, the theme's background, square corners and its name in place of the choices, and Settings → Bar icon, whose On and Off each post and move nothing, hidden when the app says Omarchy's bar isn't there; in English and Chinese.

brand.test.cjs holds the header's logo and name: shown in magpie web on Windows, macOS and Linux alike, beside the tabs; hidden in the Windows window, whose title bar has them; shown in the Mac window.

model-test-one.test.cjs tests one model on its own from a provider's editor, in Chromium and WebKit, English and Chinese: a model chip's right-click opens "Test this model", which posts provider/test with that model alone; its dot and title show the answer, a second model's test keeps the first's, and the footer names the model. The right-click neither picks the chip nor moves the page, Esc closes the menu only, and Test models still asks every model. The API is faked.

model-rate.test.cjs shows a model's credit rate by it (01huadalang on Discord: pick the cheap Qoder or WorkBuddy models without opening their apps), in Chromium and WebKit, English and Chinese: the provider editor's chips carry a small grey badge, Qoder's 0.5× or WorkBuddy's 0.03×, a discount's price before it struck through (0.5× 0.2×; a free model's FREE then 0.1× struck through), its title saying what a request costs; a model with none has no badge. The badge is the context badge's grey, not FREE's green, and no stripe. A click on it picks the chip and moves nothing. An agent's model picker shows the same badges. The API is faked.

provider-detect-models.test.cjs asks which API serves which model, in Chromium and WebKit, English and Chinese: Detect APIs' "Each picked model" posts provider/detect with every model picked (detectModels) and shows model by API, each a ✓ with the time or a ✗ with why; "Use these" sets the URLs that answered and gives each model that answered on one API only that API in Names & levels, unsaved, sending nothing until the Save carries them as modelPrefs, while a model answering on two stays Auto. A chip's right-click Test says the API it was asked on ("answered via Responses"). No click moves the page, nothing has a left-border accent, and every string has its Chinese. The API is faked.

provider-add-models.test.cjs picks a provider's models from its vendor's list while it is added (#578: 添加供应商的时候,希望添加可以获取全模型的按钮), in Chromium and WebKit, English and Chinese: the custom provider's add form has Fetch models beside its Models field, which asks nothing without a URL, then posts provider/list with the URL and key typed and no id; a vendor refusing says why in the form; the list comes as chips, a model's name with its id in the title, filtered when long; a chip ticked is in the field and an id typed ticks its chip; Pick those shown, Pick none (one typed by hand stays) and Pick all; the Add sends the models picked. No click moves the page, nothing has a left-border accent, and every string has its Chinese. The API is faked.

whats-new.test.cjs shows what changed after an update (a Discord user: to see whether their issue was fixed): after an upgrade the window opens a dialog with every release's notes since the version last run, newest first, once (the app is told they were seen, and a reload shows nothing); a fresh install and the tray's panel show nothing, and magpie web's page shows it as the window does. Headings, bullets, bold and code are drawn, #464 links the issue and, like a markdown link, opens in the browser through the app (in a tab in magpie web) without moving the page; a javascript: link stays text and HTML in the notes is shown as text, never run. Settings' What's new row (under Version) opens them again, the waiting update's notes first, marked Not installed yet. The dialog shown by itself has "Don't show again today" (#525), told to the app (whatsnew/today) as it is ticked or unticked, without moving the page; Settings' notes have no such box. Whether it shows at all after a restart to update is the app's (TestWhatsNewQuietAfterRestartToUpdate). No left-border accent, every string in Chinese. Chromium and WebKit, English and Chinese, API faked.

sessions-manage.test.cjs opens the Sessions page (TJHHHH on Discord): an agent's sessions by the folder they ran in, the latest folder open, each with its resume command; unfolding a folder, opening a row and picking a session leave the page where it is. Delete asks in magpie's own dialog (a browser confirm() fails the test), Cancel sends nothing, Delete posts the ids picked to sessions/delete, a session still being written to is said so, the Trash lists what went and Restore posts its key; an agent whose sessions magpie can't delete (OpenCode) shows no delete. No left-border accent, every string in Chinese. Chromium and WebKit, English and Chinese, API faked.

sessions-talk.test.cjs shows a session's conversation and carries a Pi session on in oh-my-pi (sxwedo, #845). Show conversation in a session's details reads sessions/transcript by agent and id, once, and lists the user's words, the model's, each tool call by name and its result, thinking folded; an agent magpie can't read (OpenCode) has no such button. Continue in is the app's menu, not a <select>: copy posts the omp --fork command, the terminal posts agent, id and in: "omp"; the details say the command. No click moves the page, no left-border accent, every string in Chinese, Japanese and German. Chromium and WebKit, English and Chinese, API faked.

sessions-usage-open.test.cjs keeps one list of sessions (#752): each Sessions page row shows the tokens, cost and models Usage → Sessions showed, its details what each model spent and where magpie routed it, and its filter finds a model. Usage's latest sessions keep their numbers but no Resume; a row there opens the session on the Sessions page (its agent, its folder unfolded, the row opened and brought into sight by that click, with no scroll refused), another agent's switches the page to it, and All sessions opens the page. At 420px a row still fits with its title readable. English and Chinese, WebKit (Chromium too where it launches), API faked.

sess-agent-strip.test.cjs keeps Usage → Sessions' agent filter inside the window (#929): with eleven agents in range the strip is wider than an 866px window, and it scrolls in itself instead of the page scrolling sideways, the way the regions strip and Connect's five APIs do; its first and last option are both reachable and both choose their agent, the model and folder picks beside it stay visible and open, and in a window wider than the strip nothing scrolls and the options keep their own widths. One agent in range still leaves the filter hidden, and the other strips on the page and elsewhere keep their behaviour. No left-border accent. English and Chinese, Chromium and WebKit, API faked.

sessions-purge.test.cjs erases sessions in magpie's trash for good (#487): a trashed row's Delete forever and the Trash's Empty trash each ask in magpie's own dialog (a browser confirm() fails the test), Cancel sends nothing and leaves the page where it is, the confirm posts sessions/purge with the row's key or all, the list empties, and the note says magpie never erases them by itself. No left-border accent, every string in Chinese. Chromium and WebKit, English and Chinese, API faked.

sessions-folder-delete.test.cjs deletes every session of project folders at once (#527): each folder's row starts with a box, as each session's does, that picks every session of the folder shown, folded or not; the bar counts sessions, not folders, a session's own box part-ticks its folder's, and a filter picks only what it shows. The bar's Delete asks in magpie's own dialog (a browser confirm() fails the test), naming the folder and its path for one whole folder and how many folders for several; Cancel posts nothing; the confirm posts sessions/delete with their ids, so they go to magpie's trash as single deletes do; a session still being written to is left and said so, other folders stay, and an agent magpie can't delete from has no boxes. No click moves the page, no left-border accent, every string in Chinese. Chromium and WebKit, English and Chinese, API faked.

sessions-codex-provider.test.cjs moves Codex sessions made with another provider to the one Codex uses now (#887): the Codex tab says how many there are and which provider, tags each, and Move opens magpie's own dialog with each ticked; Cancel posts nothing, an unticked one isn't moved, Move posts sessions/codex-provider with each id and Codex's provider, and Undo posts each back to the provider it had. No click moves the page, no left-border accent, every string in Chinese, Japanese and German. Chromium and WebKit, English and Chinese, API faked.

sessions-toolbar.test.cjs uses ten agents to check that fitting tabs stay visible at 1800px, while 900, 660 and 320px windows use a compact agent menu. Resizing keeps the current agent; search and Trash fit, the menu fits the narrow window, arrow keys choose, Escape returns focus, and an outside click closes it. Choosing an agent leaves Trash and keeps keyboard focus through loading; 150% CSS zoom switches to the menu and back. Light and dark themes, English and Chinese, Chromium and WebKit, API faked. Run it with:

node --test internal/gui/tests/sessions-toolbar.test.cjs

routing-wb-refused.test.cjs opens a request WorkBuddy refused "from an unapproved channel" (Codex's system prompt, #182) on the Routing page: the vendor's words are given without the hint the gateway adds, and the hint is on its own line in English and Chinese; another 400 gets none, and picking the request leaves the page where it is. Chromium and WebKit, API faked.

routing-blocked.test.cjs opens a request a vendor's edge firewall blocked (Alibaba Cloud's 405 page in front of zcode.z.ai) on the Routing page: what the gateway made of it is given without the hint it adds, and the hint (the firewall blocked this IP; wait, or switch network or proxy) is on its own line in English and Chinese; another 405 gets none, and picking the request leaves the page where it is. Chromium and WebKit, API faked.

routing-zcode-blocked.test.cjs opens a request ZCode's Start Plan turned away (#425: 405 "request has been blocked due to unusual activity") on the Routing page: what the gateway made of it is given without the hint it adds, and the hint (ZCode still turned it away though magpie sends it as the ZCode app does: the network blocked this IP, or ZCode checks something new; use a GLM Coding Plan or another provider in the group) is on its own line in English and Chinese; another 405 gets none, and picking the request leaves the page where it is. Chromium and WebKit, API faked.

routing-shape.test.cjs opens a group request whose first member answered 422 because its API couldn't read the request (#350: SuperGrok and Codex's additional_tools item) and whose next member answered: the Routing page says it went on to the next and that the first doesn't rest, tagged "request not understood", in English and Chinese, never "an error another account wouldn't fix"; picking the request doesn't move the page. The API is faked.

routing-proxy.test.cjs opens a group request whose first member's proxy wasn't listening (#381: "proxyconnect tcp: … 127.0.0.1:1082: connection refused") and whose next member answered: the Routing page says the request never reached the vendor, went on to the next and that the first doesn't rest, tagged "proxy not reachable", in English and Chinese, never "an error another account wouldn't fix"; picking the request doesn't move the page. Chromium and WebKit, API faked.

s3-sync.test.cjs sets up sync to an S3 bucket from Settings (#296). It works as follows, in English and Chinese:

  • WebDAV is picked first. Picking S3 shows the fields for the endpoint, bucket, prefix, region, access key, secret and path-style, each label fitting its column. Switching between WebDAV and S3 leaves the page where it was, and what was typed for WebDAV is still there after switching back.
  • With no bucket, the form says so and posts nothing.
  • Save posts s3://bucket/prefix, the access key as the user and the secret as the password, together with the endpoint, region and path-style. The row then reads "S3 sync" with the bucket and server.
  • Edit opens the form with S3 picked, marked "S3 · on", and the fields as saved. The secret field is empty, with "saved" as its placeholder.

sync-other-kind.test.cjs checks that moving sync between WebDAV and S3 keeps the other's settings (ARNO on Discord: trying S3 wiped the WebDAV address, user and password). With S3 synced to and a WebDAV server kept, the row ends "WebDAV settings kept", the form marks S3 "on" and says the WebDAV settings are kept; picking WebDAV, which leaves the page where it was, shows the kept address and user, the password as saved, says saving moves sync there and the button reads "Move sync to WebDAV". Saving posts that server with the password left empty for the one kept, and the row then reads "WebDAV sync", "S3 settings kept". English and Chinese, Chromium and WebKit, API faked. Before the fix the WebDAV fields were empty and nothing said which was on or kept.

webdav-settings-refresh.test.cjs checks the Settings page's sync row against a setup changed behind the window (#305: magpie webdav / magpie s3 at the terminal): the faked /api/davsync answers differently between reads, and the window's focus — or the page becoming visible again — must re-read it, the row, its status and the form's ticks following what the CLI left (a WebDAV setup with agents and the library out, then an S3 one) rather than what the page first read. A focus while a form is open never rebuilds it: what was typed stays (#104). Off, the description is built from the view's toggles and names only what is turned on — providers without their API keys, no agents' models — as the CLI's list does; a view that says nothing of the toggles reads as the whole lot. English and Chinese, Chromium and WebKit. Before the fix, the row kept the first view it had read and the description was the hardcoded whole list.

market-have.test.cjs checks that the Library's Discover cards follow the library without the window being focused again (#300). Removing a server (Remove from the library) turns its card from "✓ Added" back to Add at once and asks the market again; a server added by hand with the same name is marked added; a skill removed turns its card back to Add. No window focus or visibility change happens during the test, the click does not scroll the page, and the checks run in English and Chinese.

sessions-today.test.cjs checks that today's bar in Usage › Sessions › By day keeps its tooltip while the sessions are read again (#309). The fake API has 30 days up to today (Asia/Shanghai), and today's tokens grow with each read. The last bar is today, and its title gives today's usage. With the pointer on it and the page clock run past the 15-second reread, the same bar element is still there and hovered, its title shows the new total, the chart still has 30 bars and the page has not scrolled. A metric picked after that draws the latest numbers. The checks run in English and Chinese.

provider-remove.test.cjs checks Remove in a provider's editor. If the delete fails after the provider is already gone (an agent's file could not be rewritten), one click closes the editor, the provider's row disappears, the other rows stay, and the status line shows the error; no second Remove is needed. If the delete succeeds but leaves an agent on a model magpie no longer serves, the status line warns about it and has no left-border accent. If the delete fails while the provider is still there, the editor stays open and shows the error. The checks run in English and Chinese, in Chromium and WebKit.

signin-link.test.cjs checks a sign-in that is waiting (Factory's device flow). Its link appears on one line inside the box and is selected whole with a click, with no left-border accent. The copy button beside it (titled Copy link) posts the whole link to /api/copy, and the status line says it was copied. Open again is still there, and nothing scrolls. The checks run in English and Chinese, in Chromium and WebKit.

copy-command.test.cjs copies from a tab with no clipboard API, as magpie web on a NAS is when opened from another computer over http: /api/copy refuses and navigator.clipboard is missing, so the copy command copies the sign-in link, the status line says it was copied, the textarea it used is gone, and nothing scrolls. English and Chinese, in Chromium and WebKit.

signin-close.test.cjs puts away a sign-in for another account left unfinished (#526). In Qoder's editor at the issue's window size, Add another → Sign in anyway waits with Qoder's long device link: the box and every button in it stay inside the account list (the link is cut short, Cancel in sight), and Cancel is the one button that puts it away: Open again stands alone, with no Close beside it doing the same. Cancel posts signin//cancel, stops the polling and brings Add another back without moving the page, and so do the editor's Cancel and Save, so the editor opened again has no box. English and Chinese, Chromium and WebKit; the API is faked.

signin-paste-codex.test.cjs checks a ChatGPT sign-in finished from its pasted address, for a magpie the browser can't reach (on a server, in Docker). While the sign-in waits, the box says to copy the address of the page that won't load, and a Callback URL field sits inside it with no left-border accent. Finish sign-in stays off until something is typed, then posts the address to the sign-in's callback route once, and the account is named in the status line. Nothing scrolls. The checks run in English and Chinese, in Chromium and WebKit.

signin-paste-key.test.cjs checks Command Code signed in to from a browser that can't reach magpie (on a server, in Docker), where Command Code's page posts its key to magpie unseen and leaves no address to paste. While the sign-in waits, the box says to make an API key on Command Code's keys page, a link opens that page, and a hidden API key field sits inside the box with no left-border accent; Finish sign-in posts the key once to the sign-in's callback route. A plugin's browser sign-in offers "Use an API key instead" while it waits: the click cancels it and asks the plugin's API key way. Nothing scrolls. The checks run in English and Chinese, in Chromium and WebKit.

tray-inuse.test.cjs checks Settings, Allowances in the menu bar. A subscription with two accounts lists "Account in use" before each account; one with a single account does not. Ticking it saves claude|*, the pill names the subscription, and reopening the menu shows it ticked. No click moves the page. The checks run in English and Chinese, in Chromium and WebKit.

codex-auto-reset.test.cjs checks the Auto-use toggle beside a Codex account's resets, on the Usage page's card and on the menu bar panel's. It is off until turned on; a click posts settings/codex-auto-reset for that account, shows it pressed and says so, and a second click turns it off. A GLM team's resets get no toggle. No click moves the page, and nothing has a left-border accent. The checks run in English and Chinese, in Chromium and WebKit.

library-all.test.cjs opens the Library with an All chip ahead of each server's and skill's agent chips (Discord: MCP 服务器 SKILL 都得一个个选): one click posts the agents that can take it in one write — not Pi for a server (no MCP file), not Codex or Droid for an SSE server (no SSE), a hidden agent keeping what it has — the chips light, the toast says it is on for all of them or, when one couldn't take it, "on for 1 of 2 agents" and which and why; a second click takes it from all; the click scrolls nothing and doesn't open the row. Chromium and WebKit, in English and Chinese.

library-own-folder.test.cjs (#595) shows a skill put in the library's own folder by hand among the skills found, said to be "in the library's folder, not listed" with its path, and Bring in saying it is listed where it is; a library skill kept in ~/.agents/skills has Codex's and Gemini CLI's chips lit and disabled, each saying it reads that folder itself, and a click on Pi's chip or on All never sends them; Remove says a linked skill's folder stays and the library's own is moved to the backups. No click scrolls the page, and nothing has a left-border accent. In English and Chinese.

routing-auto-reset.test.cjs checks the Routing page's story for a Codex reset used by itself: a try out of its week says whose reset was used and that the request was asked again, and Codex's own sign-in answering after one says so before it answered; English and Chinese, Chromium and WebKit.

claude-risk.test.cjs checks starting a Claude sign-in first says Anthropic may ban the account, as Command Code's does, and asks nothing of the backend until "Sign in anyway"; the click moves nothing, no left-border accent. English and Chinese, Chromium and WebKit.

claude-usage-asked.test.cjs checks a Claude account's usage is only asked for when the reader opens Usage or presses Refresh (the header's, the only one since #486): opening the page and Refresh load usage/quotas?asked=1, the timed reload and the window coming back don't; an asked load isn't swallowed by one already on its way. Chromium and WebKit. The same test covers cached Claude snapshots on Usage, the tray's Allowances tab and the provider's account editor: historical percentages stay dated and visibly say when an expired window's current allowance is unknown; past reset times say they have passed. A fresh reading clears those markers. The tray's cached subscription date is as short as a balance card's, while expired-window warnings remain visible and the tooltip keeps the explanation. English and Chinese, Chromium and WebKit. panel-effort.test.cjs opens a row in the tray panel whose effort is not one of the levels offered (omp at auto, an agent with none set): the slider shows it as it is ("auto", "default"; 自动 in Chinese) at a stop of its own, a touch there posts nothing — it had shown the lowest level and a touch wrote it — and the next stop is the lowest level. A level past the ones offered (max, the levels going to high) stands after high, both ends reading "low … max" (低 … 最高) rather than "max … high", and a touch on it posts nothing. The effort icon and the panel's bars light as far as the slider's stop goes: max past high all of them, medium between low and high two of three. auto, none and the default light no bar, where minimal lights one, and none is no level to count: Hermes at low, second of five, lights one of three. Chromium and WebKit.

claude-effort.test.cjs checks Claude Code's effort and ultracode by its model (#352, #353, #354): Opus 5.5's row has an ultracode square, and Opus 4.5's and Haiku 4.5's rows don't; Haiku 4.5 has no effort control. With the page scrolled, a click on the square posts ultracode on, lights it and says that an open Claude Code session must restart. A second click posts it off. Neither click moves the page. In the tray panel, Opus 4.5's slider ends at high, and a step posts that level and says the same restart. Chromium and WebKit, in English and Chinese, with /api/plugins faked.

remote-magpie.test.cjs adds a Remote magpie (another computer's magpie, shared on its network) from the Add sheet: its tile, the address field with its placeholder and a hint on where the other magpie shows its address and key; saving without an address says the other magpie's address is needed, and saving posts the remote-magpie preset with the key and the address as typed. OpenAI's editor has no address field. No click moves the page; English and Chinese, Chromium and WebKit.

qoder-cn-tile.test.cjs checks Qoder CN (#349): the Add sheet has a Qoder CN tile beside Qoder's, which is labelled the international site's (qoder.com); each tile's title says which accounts it is for, Qoder CN's sign-in warning says it takes qoder.cn accounts (Alibaba Cloud or phone number) above Qoder's risk note, nothing is asked of the backend until "Sign in anyway", which asks for qoder-cn, and the click moves nothing; English and Chinese, Chromium and WebKit.

library-loading.test.cjs holds the Library while it loads: the real tabs, the last one open picked, its rows in outline that come in only after a moment; a tab picked while waiting shows its own outline and stays picked; the Library folder waits. When the library comes the outline goes, the tabs get their counts and don't move.

provider-typed.test.cjs tries a key pasted over a provider's saved one before a Save: Refresh, Test models, a model's own test and the endpoints' Test send the editor's form with typed: true, the editor stays open with the key still typed, nothing is saved; a new key's box focuses the key and its name says it is optional. In English and Chinese, Chromium and WebKit.

phone-web.test.cjs opens magpie web on an iPhone 13: the magpie and the icons over the tabs, which take a row of their own at a size to read and tap and slide to keep the one open (Plugins) in sight, nothing wider than the screen, iOS kept from zooming into a field; no setting's name cut short. A flick's scrolls after the finger lifts stay the reader's (they were put back, the page jerking), while a tap's click is no flick. A desktop browser and the app's window keep the one-row header. Chromium and WebKit.

usage-alert.test.cjs sets the usage alerts (#368) on the Settings page: "Allowance alert" and "Low balance alert" start Off; On saves 80% (and 5 for a balance), the field beside it saves the number typed and puts back a share past 100 unsaved, and a setting saved later (the currency) keeps both alerts. With magpie's notifications turned off in the system, both rows say so. No click moves the page and no row has a coloured left border; English and Chinese, Chromium and WebKit, with /api/plugins faked.

quota-families.test.cjs checks Antigravity's allowance one figure a model family (01huadalang on Discord: several accounts, every level of every model, read as bloat): windows that name a family show as one a family, Gemini and Claude first, each the most used of its models, on the Usage page's card, the menu bar panel's rings and the provider's account rows; each family's tooltip lists its models level by level, and "Every model" turns an account's card to each window and back without moving the page. Claude Code's windows, naming no family, are as they were. No left-border accent; English and Chinese, Chromium and WebKit, with /api/plugins faked.

provider-search.test.cjs checks a custom provider's "Searches the web by itself" (#359): the editor's Web search row opens as saved, ticked for one saved as searching, and Save posts searches ticked or not. A relay with only a Chat address has no such row; it comes once an Anthropic URL is typed under More endpoints and goes with it, and Save then posts searches: false. The row says the relay can be named as the searcher but is not picked automatically. A signed-in account (Codex) has no such row. Every string has its Chinese; English and Chinese, Chromium and WebKit.

lan-docker.test.cjs checks Share on local network in a container (Discord: magpie in Docker on a NAS showed the container's own 172.17.x address). Opened over the network from magpie web, the address is the host the page was opened at, on the gateway's port, with an "In a container" row saying so; the OpenAI/Anthropic switch keeps that host and doesn't move the page. Opened at localhost, the container's address stays and the row says it is the container's own and to set MAGPIE_PUBLIC_URL. Outside a container nothing changes. No coloured left border; English and Chinese, Chromium and WebKit, with /api/plugins faked.

routing-time.test.cjs checks a routing group's rule for hours of the day (Discord: send a provider's peak-price hours, DeepSeek's or GLM's, to another). The group's rule and the trace read it as its hours and days (22:00–08:00 Mon–Fri; 周一–周五 22:00–08:00); in the editor the hours are two times typed into one pill, a click on it types the first and moves nothing, past midnight is noted, a time that isn't one is marked once left, the days are picked like the agents (weekdays, then Saturday too: Mon–Sat), and the rule is saved as time: {from, to, days}. English and Chinese, Chromium and WebKit, with /api/plugins faked.

unlisted-models.test.cjs checks a provider set to "Only through routing groups" (只通过路由分组使用; 悠悠哥 on Discord: hy4 vanished from the pickers). The model picker's filter, finding such a model, says why it isn't offered: hy3, in the group Mine, is reached by picking Mine ("Pick Mine" sets it); hy4, in no group, can't be used, and "Make a routing group of it" opens the Routing page on a new group of it, added with members: ["hunyuan/hy4"]. The provider's editor names its models in no group under the tick (not once unticked), each a click from a group; ?view=routing&newgroup= opens the window on one, and the tray panel asks window/main for it. The page doesn't move, no left-border accent; English and Chinese, Chromium and WebKit.

request-archive.test.cjs turns the request archive on and off from its switch at the top right of the Usage page's request list (Jorben on Discord): off unless turned on, refused with sync not set up with an s3:// bucket (the reason in the footer, the switch left off), on with one, the bucket in its tooltip, and a failed upload said beside it. The Gateway page has no switch any more, and its recent calls, even one the archive kept, show their own bodies with no "Fetch from archive" under them. No click moves the page; no left-border accent. English and Chinese, Chromium and WebKit, with /api/plugins faked.

request-archive-usage.test.cjs opens the request archive from the Usage page's Requests (jorben, #447): a request the archive kept names it in its row's details, with "Fetch from archive" and "Download"; fetched, a body past 256 KB is said by its size and a note to download it, and an archive too large to read on the page says so. Download in the app posts archive/export and says where the file was saved; in magpie web it follows a download link to archive/file. A request with no copy, with the archive on, says "Not archived". No click moves the page; no left-border accent; every new string has its Chinese. English and Chinese, app and web, Chromium and WebKit.

providers-off-fold.test.cjs checks that providers switched off are kept apart (01huadalang on Discord): the Providers list holds the ones on, and the ones off sit below it under a "Turned off (N)" fold (已关闭的供应商), folded at first. Opening it with the page scrolled leaves the head and the page where they are, the fold is remembered across a reload, and a provider switched off moves under it. English and Chinese, Chromium and WebKit, with /api/plugins faked.

account-return.test.cjs checks the accounts of Codex after magpie signed it in to another because the first was all but used up (#408: 智能路由会自动把 所使用账号设为首选). The account it stands in on says "First for now" (暂为首选), its title naming the one it goes back to, and that one "First again once it has room" (额度恢复后切回首选); with nothing to go back to, the account Codex is on is just "First". The Routing note says magpie moves back. The page doesn't move; English and Chinese, Chromium and WebKit.

keep-login.test.cjs checks the tick under Codex's accounts that keeps Codex signed in (#524: 始终登录首选账号), and the pick beside it that keeps it signed in to an account of the user's choosing rather than the first (Koohoko on #524: 首选只是为了按顺序先消耗它的额度). Picking one posts provider/keeplogin with keepLoginAs; the order holds, the first saying First and the chosen one Signed in (已登录), Make first on it posting provider/arrange rather than a sign-in; picking "the first account" goes back. The Routing note follows each, In order says magpie moves Codex on once used up (#530), and one account has no tick. The page doesn't move; English and Chinese, Chromium and WebKit.

signin-again.test.cjs checks a sign-in to an account magpie lists already (#413: WorkBuddy's page offers the account WorkBuddy is signed in to). Adding another WorkBuddy account comes back done with again: the status says it is already listed and its sign-in renewed (该账号已在列表中, 已更新登录), not "added"; a new account still says added. The page doesn't move; English and Chinese, Chromium and WebKit.

web-page.test.cjs checks the page magpie web serves to a browser (Jorben on Discord: no request archive switch and no usage chart icons in the remote web UI, and a 404 for /wails/runtime.js). With web set the page never asks for /wails/runtime.js, which the browser has none of, and nothing fails to load; the app's window still loads it. The request archive's switch over the Usage page's requests is there and posts settings/archive without moving the page, and the Usage page's chart draws its columns with each provider's icon in its ranking. English and Chinese, Chromium and WebKit, with the API and /api/plugins faked. Go's TestPageFilesRevalidate checks that the page's files go out with Cache-Control: no-cache and an ETag of their content, so a cache in front of magpie web can't keep an older version's app.js.

balance-parts.test.cjs checks a custom provider's balance card (#420): a balance field with several amounts shows each on a line of its own, the user's label quiet and the first amount the larger, "Balance" only where no label was given; a percent is a meter, amber from 90%. Each card says when it was read ("Updated 3 minutes ago", 3分钟前更新), or "As of … — couldn't be read just now" (截至 …,暂时无法获取最新额度) when it stands in for a reading that failed, windows' cards too; the menu bar panel's Balances show the same amounts, the meter and "As of" (截至). One amount reads as it did. No left-border accent, nothing runs out of a card; English and Chinese, Chromium and WebKit, /api/plugins faked.

search-api.test.cjs checks Settings' Web search section (#419: a search API of one's own for when no provider can search). It lists the search APIs in the order they are tried and has a row to add one: picking SearXNG asks for its address, Add posts {vendor, key, url}, an API that needs a key sends nothing without one, a key magpie refuses is said in the row with the API and what was typed kept, and Remove posts {vendor, remove: true}. No click moves the page. English and Chinese, Chromium and WebKit, with /api/plugins faked.

searcher-pick.test.cjs checks the Web search section's "Searches for other models" row (01huadalang on Discord: pick which provider searches for a model that can't). With none named it shows Automatic and magpie's own pick; its picker offers Automatic first, each provider that can search by its small model, and each of its models, including a relay said to search; the row says those relays require manual selection and are not automatic fallback choices. Costs and fallback are explained for every search provider; providers left out are described as unavailable in the current configuration, with both other search providers and configured search APIs named as alternatives. A model picked is saved as searcher: "<provider>/<model>", a provider by its small model as searcher: "<provider>", and shown; another setting saved still sends the pick; one named that magpie can't use (turned off) is said in the row with magpie's pick shown instead; Automatic sends searcher: "". No click moves the page. The picker is checked in English and Chinese; the help also in Japanese and German, at 560 and 1000 pixels in light and dark themes, including a selected relay and empty relay/unavailable lists. Desktop text keeps its single-line ellipsis and full hover title; the existing narrow-web layout is unchanged. Chromium and WebKit, with the API faked. Set ARTIFACT_DIR to retain screenshots of the help.

provider-file-error.test.cjs checks a providers.json that is there but can't be read (#415's review): the Providers page says over the list that the file can't be read (无法读取供应商配置文件), was left unchanged and is to be fixed or moved aside, with magpie's reason; the Add sheet doesn't open as for a first use, a signed-in account still listed stays under it, and a file that reads shows none of it. English and Chinese, Chromium and WebKit, with /api/plugins faked.

public-url.test.cjs checks the address MAGPIE_PUBLIC_URL puts beside this computer's on the Gateway page's Connect section, and what its hint says about the gateway: loopback, open to the network (listening beyond loopback with nothing shared), and a network address that needs an enabled gateway key. English and Chinese, Chromium and WebKit, with /api/plugins faked.

provider-picks-stay.test.cjs checks that the models picked in one provider's editor never go to another's (#464): a Refresh or a Forget whose list comes back after the editor was left for another provider's leaves that editor with its own picks only, none shown as added by hand, and its own Refresh and Forget keep them. In the editor they were pressed in, a pick not yet saved and an id typed in that the vendor doesn't list (added by hand) outlive both, and Save sends them. No click moves the page. English and Chinese, Chromium and WebKit, with /api/plugins faked.

quota-curve.test.cjs checks the quota cards' curves (#651): a line a window under the meters, broken at a reset, a dashed even burn and a now line, the 2 days / Cycle control turning every card without moving the page, no curve without readings, the theme's chart colours in light and dark, and the tray card's thin current-cycle line, in Chinese and English.

balance-curve.test.cjs checks a key's balance over time on the Usage page: its readings as a line, the least-squares line since the last top-up dashed on to zero, "Runs out in 3d at this pace" and the spend a day, a click (or Enter) enlarging the plot and another taking it back without moving the page, no curve for a key of one reading, and the allowances' Off taking it away, in Chinese and English, light and dark.

With Node.js and Playwright available, make test-ui manually runs the whole suite: every internal/gui/tests/*.test.cjs, discovered automatically. Files are independent and run a few at a time (UI_TEST_CONCURRENCY, default 2); set UI_TEST_CONCURRENCY=1 to run one at a time when diagnosing a flaky file.

make test-ui

If Playwright is installed outside the repository, set NODE_PATH to the directory containing its package. The suite uses Playwright's Chromium and WebKit binaries (playwright install chromium webkit). No frontend dependency is needed by the app itself. Tested with Playwright 1.62.1.

For the callback test, PLAYWRIGHT_CHANNEL=chrome uses an installed Chrome instead of Playwright's Chromium.

Set BROWSER=chromium or BROWSER=webkit for one engine. Set ARTIFACT_DIR to an external directory to retain screenshots and Playwright traces, including failed assertions. These browser checks run through make test-ui, separately from make test (which is Go only) and from the UI preview workflow (which only records screenshots). The Test workflow does not run this suite.

usage-stale-answer.test.cjs and providers-fetching-retry.test.cjs use Node's VM and need no browser. Set MAGPIE_APP_JS to another revision's app.js to run their behavior checks against that source.

The request-ledger review regressions also cover the explicit data-source notes (including the exclusion of local rejections), the local-session label, and native panel fitting including Usage (with the desktop's 560px maximum). The detail and content tests, along with ledger pagination and filters, bring controls into sight through real wheel input before a click, so WebKit's wheel steps and the app's scroll protection do not fight Playwright's automatic scrolling. Empty allowances hide their panel tab.

session-identity.test.cjs checks historical emails and recorded official vendors on session-log rows. Models and current configuration do not establish historical routes; session rows without routing evidence display "Local session", including with a known session creator. Unknown gateway providers retain their own label. Creator emails and recorded provider IDs appear separately in request details. Third-party OpenCode gateway calls retain the actual relay. These regressions run in Chromium and WebKit, English and Chinese.

otel.test.cjs checks OTLP export and metrics start off, endpoint and masked headers save in English and Chinese, environment overrides are indicated, and other preference saves retain OTLP choices. It also checks all three content modes, reload persistence, and readable consent and full-width fields in narrow windows. It runs in Chromium and WebKit with the real assets and an isolated API fixture:

node --test internal/gui/tests/otel.test.cjs

account-arrange.test.cjs drags a subscription's accounts and a provider's keys by their rows (no handle) in every routing mode, and by Alt+↑/↓: the first row, its First mark and the order saved for routing agree, one save goes out per drop, Escape and a failed save put the rows back, a disabled account is not made first, and account text stays selectable. It runs in Chromium and WebKit with an isolated API fixture:

node --test internal/gui/tests/account-arrange.test.cjs

routing-account-order.test.cjs checks that Routing's live stage seats each provider's accounts and keys in the order its page lists them and a drag sets — the order the gateway tries them — not by name nor as one request weighed them (#217): one request through a group of a subscription, a provider's keys and a plugin's accounts, weighed least used first, reads in each list's order; after the subscription's accounts are moved with Alt+↓ in its editor and the plugin's rearranged, the stage, shown again without a reload or a new request, reads in the new orders. It runs in Chromium and WebKit, in English and Chinese:

node --test internal/gui/tests/routing-account-order.test.cjs internal/gui/tests/update-auto.test.cjs

seg-contrast.test.cjs checks the header's view switch and the text buttons against WCAG AA (#477): with the window light, dark, and dark by the system's choice, it reads the colours the browser computed, each background laid over the ones under it, and asserts that the switch's other views on its track, the view shown on its thumb, and a text button on the page, a card and the inset editor read at 4.5:1 with margin (4.7), and that the thumb stands off the track by more than it did (1.2:1 light, 1.5:1 dark). It runs in Chromium and WebKit, in English and Chinese:

node --test internal/gui/tests/seg-contrast.test.cjs

agent-grip.test.cjs checks an agent row's grip: with the pointer away no row in the Agents list draws it (always there it was clutter, the owner: 一直出现太丑了), the row under the pointer draws it in its left margin, beside its logo, and it goes again when the pointer leaves; the logo stays the logo on hover (no second grip over it); dragging the grip moves the row, and clicking it opens the row's menu, whose Hide puts the row in the fold at the foot of the list. Opened, the fold's Show button is there without the pointer on its row, and brings the row back; no click moves the page. It runs in the window and the tray panel, in Chromium and WebKit, in English and Chinese:

node --test internal/gui/tests/agent-grip.test.cjs

`account-models.test.cjs` keeps an account of a subscription, and a key of a
provider, for some of the provider's models only (#474): an account with a
list of its own has a badge saying how many, always shown, and one without
says All models on hover; a click opens the provider's models under the row
as chips, Save waits for one picked and posts `provider/accountmodels` with
the account (or the key's id) and the models picked, All models posts none,
and the badge then says what it has. A click moves no row and scrolls
nothing; a subscription with one account has no badge; no border stripes,
every string has its Chinese. It runs in Chromium and WebKit, in English and
node --test internal/gui/tests/account-models.test.cjs

`favorite-star.test.cjs` checks a model's favourite star in the model
picker (#482): not a favourite, an outline star on hover; a favourite, a
filled star shown always, with its title and aria-pressed to match. In the
window and the tray panel, light and dark, English and Chinese, Chromium and
WebKit:
node --test internal/gui/tests/favorite-star.test.cjs

`claude-picker-fold.test.cjs` checks Claude Code's model picker (#496): the
models magpie serves on the very account Claude Code is signed in to fold
into one row after Claude Code's own, though the account comes after the
providers the user added (Claude Code headed once); a click or Enter opens
and closes it in place, the page not moving and the keys left to the
filter (the arrows, Esc); the value set is a row of its own; a query shows
every row it finds, so an id typed in full is picked with Enter; an alias
and its dated id are one row, the dated one's while it is the value set or
starred where the alias isn't. Chromium and WebKit, English and Chinese,
the API faked:
node --test internal/gui/tests/claude-picker-fold.test.cjs

`list-sort.test.cjs` checks the installed lists' order, the reader's pick
(#481): the Library's MCP servers and the installed plugins by name A→Z (the
plugins no longer in the order they were installed) or Z→A, and the
Library's skills by source, as before, or as one flat list by name A→Z or
Z→A; each list keeps its own pick through a reload, and a pick leaves its
control where it was on the screen. It runs in Chromium and WebKit, in
English and Chinese:
node --test internal/gui/tests/list-sort.test.cjs

`quota-name-translated.test.cjs` holds the rule that a window's name is
what magpie translates while its display is shown as it came (#1001): a
plugin that puts its sentence in the display keeps the card English in
every language, as the ZCode plugin's claim line did. The fixture is that
line with its name a sentence magpie knows and a display of the plugin's
own ("1 · ZCode Trust Build"); the name reads in the language and the
count and plan name do not. It covers every language magpie has (en, zh,
zh-TW, ja, de), and each is rendered at 440, 560 and 1000px in Chromium
and WebKit — a translated name is a sentence, and `.quota-labels > span`
is nowrap with no ellipsis, so one wider than its `.quota` is cut off
mid-word in silence. 440 is the narrowest the card is seen at and 560 the
window minimum; 1000 alone could never catch a name that is too wide:
node --test internal/gui/tests/quota-name-translated.test.cjs

`quota-pools.test.cjs` checks Antigravity's allowance a row a pool of
models, each with its 5-hour and its weekly window (a user on Discord: the
three models read the same, show the 5 hours and the week left): windows
that name a `pool` show as the pool's 5 hours then its 7 days, Gemini
first, on the Usage page's card (the pool's models in the tooltip), the
menu bar panel's four rings (Gemini 5h, Gemini 7d, Claude 5h, Claude 7d,
each label whole) and the provider's account rows (a pool's name, then its
5h and 7d meters); "Every model" lists the models' own windows, not the
pools' again, and back, without moving the page. Claude Code's windows are
as they were. No left-border accent; English and Chinese, Chromium and
WebKit, with `/api/plugins` faked:
node --test internal/gui/tests/quota-pools.test.cjs

`plugin-progressive.test.cjs` checks the Plugins page is drawn in parts
(#488) while `/api/plugins/market` never answers: Installed from
`/api/plugins` at once (no skeleton, no update chip yet), Discover's cards
from `/api/plugins/listings` with their buttons waiting on npm, and once
`/api/plugins/npm` answers (held until then) the downloads, the version,
Install and the installed plugin's "v… out". It runs in Chromium and
WebKit, in English and Chinese:
node --test internal/gui/tests/plugin-progressive.test.cjs

`lightweight.test.cjs` checks Settings → General's Lightweight mode row
(#580): off by default, On and Off saved as `lightweight` true and false
without the page moving, another setting's save keeping it, no left-border
accent, and no such row on a browser's page (`web`). English and Chinese,
Chromium and WebKit, with `/api/settings` faked:
node --test internal/gui/tests/lightweight.test.cjs

Mobile Web

mobile-web.test.cjs checks all eight web pages and their navigation in Chromium and WebKit, English and Chinese, at touch widths 360/390/430/820. Agent fixtures include model/effort controls, Codex's subagent/sign-in squares and omp's three role squares. Every phone field must stay inside its row, model names must be readable, and each picker must open; desktop comparisons use those same populated rows. It checks that Library's folder button is reachable, Usage tokens, costs and stat explanations are readable, and chart dates do not overlap. A released touch's continuous scroll can outlast the input window, while scripts cannot move an idle page, including after momentum stops. This uses Settings → Allowances at 390×600, where the real settings rows leave room to scroll. The API is faked; no user configuration is read or changed.

At desktop widths 900/1280, screenshots are compared with the same pages from origin/main. Dimensions must match; a pixel counts as different only when any RGBA channel differs by more than 48, and more than 300 such pixels fails. This tolerates Chromium's small antialiasing differences while still detecting layout changes. Fetch that branch before running; set BASE_REF to another local Git ref (HEAD for a self-comparison) to select a particular baseline. PNG decoding uses the copy bundled with Playwright, without another dependency. The empty allowance fixture completes after usage totals render, so both pages compare the same completed API state rather than a loading-order difference.

node --test internal/gui/tests/mobile-web.test.cjs internal/gui/tests/old-webkit.test.cjs

Routing request metrics

routing-request-metrics.test.cjs checks request-row cache hit rates (cache reads divided by uncached input, cache reads and cache writes across billable tries) and output speeds (reply output over the time after its first token). It covers known zero hits, missing historical tiers, unstreamed and burst replies, retries, unfinished and failed requests, the cache-rate description in the story, weighted session cache rates and decode speeds (with coverage in tooltips), folds, history, live updates, narrow layouts and persisted metric choices. Missing values are omitted from request rows and session headings; known zero cache rates and costs remain visible. The first-token label matches the metric picker and the Chinese routing story. Request and session token totals have tooltips with exact uncached input, output, cache-read and cache-write counts. Billable retries are included and explained; missing historical tiers and partial session coverage are explicit. The six choices apply immediately while the menu stays open, with focus kept on the checkbox. Mouse and keyboard changes work in both directions, including when none are selected. It runs in English and Chinese on Chromium and WebKit. Set ARTIFACT_DIR to keep screenshots. APIs are isolated fixtures.

node --test internal/gui/tests/routing-request-metrics.test.cjs

routing-columns.test.cjs checks that side-by-side requests get more room than accounts, whose column stays between 280 and 400 CSS pixels. It checks aligned list bottoms, account model labels, and no horizontal overflow at 940, 1100, 1440 and 1920 pixels, then the full-width stack at 700 pixels.

node --test internal/gui/tests/routing-columns.test.cjs

routing-scroll-end.test.cjs wheels the Routing page to its end while the trace redraws it, at 440x620, 600x700, 900x500, 1200x600 and 1400x700, and checks that it stays there (#1249: in WebKit, container queries pulled it back). It also checks the stage, request and column layouts that those widths select. context-scroll-end.test.cjs does the same on Usage's Context tab, with a session open, across its timed reads. Both run in English and Chinese, on Chromium and WebKit.

node --test internal/gui/tests/routing-scroll-end.test.cjs internal/gui/tests/context-scroll-end.test.cjs

Automatic Codex title grouping

routing-sessions.test.cjs checks automatic grouping after a Codex title write, conflict revocation, costs and folds, plus name refresh on large history days. It uses isolated API fixtures in English and Chinese on Chromium and WebKit.

node --test internal/gui/tests/routing-sessions.test.cjs

Names on an agent's settings

agent-field-names.test.cjs checks that every setting in a connected agent's opened row is named, in the window and in the tray panel, in English and Chinese. A setting nobody has picked is the agent's own logo on the shut row, for want of room, so Aside's four task roles and its picture model all read "default" under one logo there; the panel already names them as its rows open and the window now does the same. It also checks that no name is cut short by its column (Aside's standard, Claude Code's thinking, in the panel's 44px). It uses Settings → Agents at 1240×900 and the panel at 440×720. The API is faked; no user configuration is read or changed.

node --test internal/gui/tests/agent-field-names.test.cjs

import-row.test.cjs checks the row of an app magpie is added to by a link of its own (Cindy) on the Agents page: a line under its name says whether magpie is added (green once it is), its button stands where the other rows' model picker does, as wide and lined up with it at 960 and 700px, and a click opens the app's link. English and Chinese, Chromium and WebKit.

omp-profile-connect.test.cjs checks an omp named profile's switch (#1187): clicking omp · work posts /api/agents/connect/omp%23work, not a path the browser cuts at #. A query string on the same page (/api/usage?period=today) stays a query. English, Chromium and WebKit. The API is faked. Without the # encoding in api() the posted path is /api/agents/connect/omp.

node --test internal/gui/tests/omp-profile-connect.test.cjs

privacy-hides-accounts.test.cjs checks that Privacy's Mask personal data hides the accounts on screen too (inaction on Discord): until Hide accounts is chosen on a computer it follows the setting, so Usage's email address is blurred and its button pressed; an address shown on purpose with the button stays shown; on Settings › Privacy, turning Mask personal data on turns Hide accounts on, and Privacy's own Hide accounts row says so and turns it off, without scrolling. English and Chinese, Chromium and WebKit.

cli-path.test.cjs checks Settings › About › Command line (PAMI on Discord): it says what magpie in a new terminal runs, with a dot for each shell (no left border, no native select); Add to PATH posts /api/cli with no shell and the row turns green, a shell whose PATH lacks the folder has a button naming its profile that posts that shell alone, and no click scrolls. On Windows it lists PowerShell and cmd with no profile buttons, and a program not named magpie.exe (the site's portable download, #942) is said to get a magpie.cmd that runs it, in Add's tooltip, the row and the status; a translocated app gets no button. Every string has zh, ja and de. English and Chinese, Chromium and WebKit; the API is faked, no shell profile is read or written.

node --test internal/gui/tests/cli-path.test.cjs

update-mirror.test.cjs checks Settings › About › Download source (#893): the row says updates come from GitHub; Use a mirror… opens an address field, an http:// address is refused with the reason in red and the draft kept, an https:// one saved with Enter posts /api/settings/update-mirror and is shown, and Use GitHub posts an empty mirror. A download that failed through the mirror says so on the Version row and offers Download from GitHub, which posts the empty mirror then /api/update/install. No native select, no left border, no click scrolls. English and Chinese, Chromium and WebKit; the API is faked.

node --test internal/gui/tests/update-mirror.test.cjs

lane-limit.test.cjs checks each key's and account's own limit on requests at once (#892): its row's pill says what runs and waits under it while requests are out ("1/1 running · 2 queued"), read again from /api/lanes every two seconds and written into the same pill, else its limit (its own or the provider's); a row with none shows it on hover only. A click opens the app's menu (no native select, no scroll): a number, the provider's (null) or Other… typed in place, each posted to provider/accountconcurrency. The editor's Queue size and Queue wait sit under Concurrency, as narrow, labels on one line, saved with it and refused out of range. No left border; every string has zh, ja and de. English and Chinese, Chromium and WebKit.

node --test internal/gui/tests/lane-limit.test.cjs

ledger-protocol.test.cjs checks the protocol each request was sent upstream in, on Usage › Requests (蓝猫 on Discord): a row translated from its agent's protocol is tagged "Chat → Anthropic", one sent as it came "Anthropic" or "Responses", one read from a session file nothing. A request's details say Protocol and the upstream's own stop reason, when it gave one. A click opens the details and moves nothing; no left border; every string has zh, ja and de. English and Chinese, Chromium and WebKit.

node --test internal/gui/tests/ledger-protocol.test.cjs

library-rtk-nohook.test.cjs checks that an agent RTK has no hook for (DeepSeek Harness) is listed in Library › RTK with a No RTK hook tag saying why, and that its switch can't be turned on, while Pi's can. English and Chinese, Chromium and WebKit.

node --test internal/gui/tests/library-rtk-nohook.test.cjs

Local Provider Discovery

provider-discovery.test.cjs checks the automatic Providers-page hint on Chromium and WebKit: discovery runs without blocking the list or resetting an Add-sheet search, details are read only when the picker opens, and only selected entries are imported. It covers refresh after partial and complete imports, late scan responses, a prominent first-use hint and a compact entry beside Add provider for existing users, a later visit finding new configurations, persistent dismissal across reloads and partial imports, newly added or changed configurations without selecting previously ignored entries, manual import of ignored entries, a scan containing only an ID collision or a disabled Alma provider (no hint, but both remain selectable in manual import), an empty scan, a failed scan, and a narrow window. The main flow runs in English and Chinese; all credentials and API responses are fixtures. Run with node --test internal/gui/tests/provider-discovery.test.cjs and the Playwright environment described in this file. TestLocalProviderDiscovery exercises the real readers and summary endpoint in an isolated home, including duplicate and unimportable entries, unreadable sources, stable fingerprints despite ID collisions, changed configuration fingerprints, excluding unticked collision and disabled Alma entries while preserving the add-key case, and keeping credentials server-side.

Filters typed through an IME

filter-ime.test.cjs covers #1055: a digit typed into the Routing page's group filter went in twice (1, 2 → 1122), because the filter's own input event drew its row again and took the field out of the page while an IME committed. It types into the routing group filter, a provider's keys filter and the plugins search, through an IME in Chromium (CDP's Input.imeSetComposition, then Input.insertText) and by key in WebKit, and checks one character a key, the focus, the caret, and that the field never leaves the page. The rows around each still follow it: the groups and keys it matches, the routing header in the other language after setLocale, the plugins listings arriving while the reader types. In English, Chinese, Japanese and German, at 1100px and 440px. MAGPIE_FILTER_ASSETS points it at another assets folder, to see it fail on the old code.

Volcengine Ark access key

volc-access-key.test.cjs covers #1427: an Ark provider's editor (saved or being added) asks for the account's AccessKey ID and Secret Access Key, which the Usage page reads the Coding or Agent Plan's windows with. The saved Secret is never in the page: only that one is saved, as the placeholder. Save posts the ID and a Secret only when a new one was typed; Remove posts clearAccessKey; an ID with no Secret is refused before anything is posted. A relay and DeepSeek have neither field and post none. Every string is in zh, zh-TW, ja and de. English and Chinese, Chromium and WebKit. TestProviderSaveKeepsVolcengineSecret checks the server side.

node --test internal/gui/tests/volc-access-key.test.cjs
go test -tags nogui ./internal/gui -run TestProviderSaveKeepsVolcengineSecret