Files
magpie/internal/gui/tests
yetone 258eae592d agent: Cursor Private Inference, which magpie finds by its product.json and starts on magpie with a command its row copies, and whose /models gives each model's api_types and capabilities (#299, mamba on Discord) — the build (Cursor 3.23.x, the same app name, bundle id and ~/.cursor as Cursor) runs its agent locally and asks the endpoint in CURSOR_LOCAL_AGENT_BASE_URL / CURSOR_LOCAL_AGENT_API_KEY when its Open configuration has none; mamba confirmed it works with any account pointed at magpie. magpie writes nothing of Cursor's (its settings share Cursor's state.vscdb): the Agents page lists it when an app's product.json says "Cursor Private Inference" (/Applications, ~/Applications; %LOCALAPPDATA%\Programs on Windows; /opt and the like on Linux), and its row's one control, Copy launch command (复制启动命令), copies CURSOR_LOCAL_AGENT_BASE_URL=<gateway>/v1 CURSOR_LOCAL_AGENT_API_KEY=magpie-cursor-local '<app>' (PowerShell on Windows); in the tray panel the row copies it. Its notice says to quit it first (a second start hands over to the running one) and that a base URL in Open configuration comes first. /v1/models asked with its key gives each model api_types (the APIs its provider serves it on, all three for a group or a translated one; it asks Anthropic Messages only when no OpenAI one is listed) and capabilities (context_length, max_output_tokens, supports_vision); any other client's list is unchanged. usage.AgentOf matches a name before a UA prefix, so cursor-local isn't counted as Cursor. TestCursorLocal, TestCursorLocalModels and cursor-local-launch.test.cjs (WebKit, en and zh) fail on the old code; agy-launch and agent-* GUI tests pass on WebKit. Not tried with the real app (not installed here). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 23:38:19 +08:00
..
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
2026-10-03 00:18:16 +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
providers: each account of a subscription, and each key, can be kept for some of the provider's models only (#474: 增加账号级别的模型选项, a low-quota but verified account kept for the cheap models) — every account or key a provider had served every model it did, so one couldn't be spared for some. An account's or key's row in its provider's editor now has a models badge: a click opens the provider's models as chips, Save keeps the account for those picked, All models gives it every one again; one kept for some says how many, always shown. magpie provider account-models <id> [account|key [models…|all]] does the same and lists them. It is kept in providers.json as accountModels, an account by its name in lower case, a key by its id; one not in it serves every model, as before. The gateway never sends a model to an account or key whose list leaves it out, in every routing mode, in drag order, in a group and as a fallback, and never falls back to it all the same; the trace says it was left out as set to serve other models. When every account or key there is leaves the model out, the request is refused with a 403 saying so and how to change it, not "none of X's models is ready". TestKeyModelsNarrowCandidates, TestAccountModelsNeverChosen, TestAccountModelsCmd and account-models.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; account-models, click-scroll, old-webkit, account-arrange, account-proxy, account-return, routing-account-order, panel-routing, unlisted-models pass.
2026-10-02 05:50:23 +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
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: with no Node.js on the machine, Install another agent's npm commands install Node first, in the same line (#727, Sun1090 on v0.1.801: "没写curl的命令… curl xxx && npm xxxx", confirmed as option 1 — Pi, Copilot CLI, Crush and the rest had only npm install -g …, which a new machine without Node can't run) — installCommands now takes whether npm is here (nodeHere: exec.LookPath("npm"), then the folders proc.UserBinDirs knows — nvm's, fnm's, mise's, volta's, Homebrew's, the ~/.npmrc prefix — and on Windows also the registry's PATH (proc.LoginPath) and %ProgramFiles%\nodejs; npm.cmd there), and where it isn't, npmInstall gives on a Mac or Linux export NVM_DIR="$HOME/.nvm" && mkdir -p "$NVM_DIR" && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash && . "$NVM_DIR/nvm.sh" && nvm install --lts && npm install -g <pkg> (nodejs.org's nvm route; NVM_DIR is set because the installer goes by $XDG_CONFIG_HOME/nvm when that is set, and nvm.sh is read in the same shell since the profile line only reaches new terminals; nvm's Node is the user's, so no sudo), and on Windows, for PowerShell, winget install -e --id OpenJS.NodeJS.LTS; $env:Path = <Machine + User Path from the registry>; npm.cmd install -g <pkg> (";" as Windows PowerShell 5.1 has no "&&", PATH re-read since the MSI only adds Node to new terminals, npm.cmd since npm.ps1 is refused under the default execution policy). Each such command carries node (nvm, nvm-mac or winget); the row labels it "Node.js + npm" and the note says the npm commands install Node with nvm/winget first — on a Mac that nvm's installer stops without Xcode's Command Line Tools (run xcode-select --install), on Windows to run them in PowerShell and open a new terminal if npm still isn't found. With Node here everything is as before. magpie still only shows commands to copy (the list never had a one-click install; the page's Update all/update pill run only for CLIs already installed, unchanged). The via column is 84px so "Node.js + npm" stays on one line. TestInstallCommandsWithoutNode (Pi on darwin/linux/windows, Claude's installer untouched, npm alone with Node), TestNpmIn and TestInstallWithoutNodeParses (bash/zsh/sh -n read the line) are new; agent-update-all.test.cjs gains "no Node.js here" (label, note with/without Xcode, the whole line copied, no sideways scroll, winget note in Chinese) and passes in WebKit (8 tests). go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but for TestCursorSignInJoinsAWrappedLink, which timed out under load and passes alone. Nothing was installed: the commands were only parsed, not run, and not tried on a machine without Node.
2026-10-04 10:13: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
2026-10-04 11:28:50 +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
tests: cli-update's tight agent row is looked for where a row is tight now — the subtest made the window 620px and 604px and expected the update pill to keep only its arrow and then the version to go, as they did while an agent's fields stood beside its name. Bisected: it first failed at f3ba9184 (#391, magpie web on phones), where magpie web 760px wide or less puts the fields under the name; the fixture boots as magpie web (bootPrefs.web), so the name's line spanned the row at 620px (~378px of room) and the pill kept its words. Booted as the app's window it would have failed from d0d098c6 (the 「接入」 rows): a connected agent's fields fold away under the row, so the name's line is 378px at 620px (164px before it) and 318px at the window's narrowest, 560px. No behaviour regressed: the container rules (pill to its arrow under 250px, version gone under 150px, name last) are as before, and in magpie web the pill keeps only its arrow from about 490px down, the name never cut, to 320px; the name's line never gets under 150px now, so the version stays. The fixture can boot as the window (open(lang, posts, web)); the subtest checks the window wide, at 620px and at 560px shows everything, and magpie web shows everything at 620px and 540px and the arrow alone, the name whole, at 480, 430, 400, 375, 340 and 320px. It fails with the pill's container rule taken out. Webkit cli-update passes (5/5); agent-update-all, agent-connect, agent-layout, agent-grip, cli-behind, click-scroll and agent-connected-unfolded pass (click-scroll on its own; run in one batch with the others it failed once under load). No app code changed.
2026-10-04 08:59:11 +08:00
provider: a Codex reset about to run out is spent half an hour before it does, not three hours, and at once only when the account is held up past then (#718, thedavidweng: account A, held up for its five hours until 17:09 with 32% of its week used, spent a reset expiring that afternoon at 15:20 — within resetExpiryLead (3h) and its windows used — losing the 68% of its week it could have used between 17:09 and the reset running out). Spending a reset starts the windows again and loses what they have left at that moment, while the fresh windows are the same whenever it is spent, so check now spends it as late as is safe: resetExpiryLead is 30 minutes; an account holding a reset that runs out is looked at every 30 minutes (resetExpiryWatch), never past the start of the reset's last hour, and every 5 minutes within it (resetExpiryClose, nextResetLook), a failed read (by the reset last read, kept per account) or spend there tried again in 5, not 30; the loop goes over the accounts every 5 minutes (each read only when due), and check compares wall-clock times (Round(0)), so a Mac asleep past the lead spends it on the first pass awake. It is spent at once when the account is held up now (usedUp: a window that stops it at 100%) and won't be free again (BackAt; not known counts as not) before the reset would be spent: waiting can use nothing more, and spending it frees the account — any held reset that runs out, however far off. Windows not used still spend nothing; the week-used-up AutoUseCodexReset path is as before. expiringResetSpent takes whether the account is stopped and when it is back, and routing's Allowance.Restarts passes the same from its limits (heldUp), so Weekly pace's and Smart's deadline (dueRestart) is when check spends it. The GUI's tooltips (en, zh: 到期前约半小时, 若账号正被限流、要到那之后才解封,则立即用掉), quota auto-reset's help and settings' comment say so. TestResetSpentAsLateAsSafe walks the report's afternoon in 5-minute passes (reset running out 18:15: five hours free at 17:09 or 17:40 → spent 17:45; free at 17:50, after it runs out, not saying when, or the week used up past it → spent 15:20; not held up → 17:45; nothing used → never; at most 20 reads), TestResetExpiryLastHour (look cadence, a failed read and spend retried in 5 minutes, spent after sleeping past the lead) and TestRestartsWhenHeldUp fail on the old code; the existing tables move from 3h to the new times. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once under load, passes alone and in the package rerun); codex-auto-reset and routing-reset-deadline pass in webkit. Not tried with the reporter's accounts. The spend reads the account again under autoReset's lock (stillExpiring, spendExpiringNow) and spends nothing if the week's used-up spend has started its windows again since the look, so a held-up account isn't given two resets; TestExpiringSpendReadAgain.
2026-10-04 07:03:16 +08:00
codex: Settings decides where Codex's thread-title requests go — Codex's own ChatGPT as before, off, or a magpie model (#705, DuGuaiYA: Codex signed in with ChatGPT sends thread_title / thread_title_reconsideration requests that magpie relays through ChatGPT even when the conversation is on a magpie provider) — a new setting codexTitles ("" = as Codex sends them, the default; "off"; or a model's id such as deepseek/deepseek-v4-flash or a group) is read on /backend-api/codex/responses for a request whose x-openai-subagent or turn metadata thread_source is a title kind. Off answers at once with an empty completed response (Codex then keeps no title, quietly, as its source shows), contacts nothing, and is still recorded: a Routing trace "Titles off" and a usage row of Kind thread_title marked rejected (left out of stats). A model gets the request through serve (so it is recorded as a title request on that provider), and its answer is given back as one Responses message holding {"title":…}, since Codex's text.format schema does not reach Chat or Anthropic upstreams. Settings has a row "Codex thread titles" (zh: Codex 会话标题) with the app's model menu (Codex's own, Off, magpie's models and groups), saved on its own through POST /api/settings/codex-titles, which refuses a model magpie doesn't serve; the page's other saves keep it. TestCodexTitlesSetting (gateway) fails on the old code (off still went to ChatGPT) and checks the ledger kinds, rejected rows and trace; TestTitleJSON; TestCodexTitlesSetting (gui) for the endpoint; codex-titles.test.cjs (en/zh, webkit) fails on the old assets: no select, the picks post, the subtitle follows, no click scrolls. go vet, builds (darwin nogui, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real Codex TUI: no end-to-end run of Codex's title generation against magpie.
2026-10-04 00:23:05 +08:00
copilot: an account on GitHub Enterprise Cloud with data residency (*.ghe.com) signs in, is read from copilot login --host and is served at its enterprise (#723, guitaoliu: magpie skipped a Copilot CLI sign-in whose host wasn't github.com and its own sign-in was fixed to github.com) — a Copilot account now remembers its host (copilotApp.Host, "host" in its logins.json auth; none for github.com) and every URL it uses follows it, as VS Code's Copilot Chat, copilot.lua and avante do (checked against the issue's references and GitHub's docs): the device flow at https://<name>.ghe.com/login/device/code and /login/oauth/access_token with the same Copilot OAuth app, the device page there, whose account at https://api.<name>.ghe.com/user, its plan and usage at api.<name>.ghe.com/copilot_internal/user, its session token at api.<name>.ghe.com/copilot_internal/v2/token, and its requests, model list, Auto and terms at the session's endpoints.api, or https://copilot-api.<name>.ghe.com when it names none (the provider's base too). A github.com account has no host and goes where it always did, the same vars and the same bytes. CopilotHost (copilot_ghe.go) takes only github.com or one <name>.ghe.com label (a scheme and a trailing slash are dropped; a port, path, user, deeper subdomain or any other host is refused), in the sign-in, the CLI and a host read back from logins.json, so a GitHub token is never sent to a host of the user's choosing. The Copilot CLI's sign-in: a github.com account first, as before; then one copilot login --host <name>.ghe.com signed in, its token under "https://<name>.ghe.com:<login>" in copilotTokens or the keychain; a GitHub Enterprise Server host is still left out. An editor's apps.json entry keyed "<name>.ghe.com:…" is read after the editors' github.com one and the CLI's. GUI: a Copilot device code's panel has "Account on GHE.com?" (zh 账号在 GHE.com 上?), which ends that sign-in and asks "Your enterprise on GHE.com" (你的企业在 GHE.com 上的地址) in a field (acme.ghe.com; Sign in stays off until it reads as <name>.ghe.com), then signs in there; that sign-in's panel offers "Sign in on github.com instead" (改为在 github.com 登录), as does a failed one. CLI: magpie accounts add copilot [--host <name>.ghe.com]. TestCopilotHost, TestCopilotGHESignIn (an httptest server standing in for acme.ghe.com, api.acme.ghe.com and copilot-api.acme.ghe.com through copilotGHEOrigin: sign-in, what is kept, models, a signed request, usage, nothing sent to github.com, a bad host refused) and TestCopilotCLIGHELogin are new and fail on the old code; copilot-ghe.test.cjs (webkit and chromium, en and zh: the link, the field refusing evil.com, a.b.ghe.com, ports and the like, the posted site, the way back, cancels, no native select, no left-border accent) fails on the old code. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass; signin-link, signin-close, signin-again and routing-signin pass in webkit, signin-callback fails before and after the change alike. Not tried with a real GHE.com account: the URLs follow the references, and the CLI's token key for a ghe.com host is taken to be the github.com one's form. Accounts are still told apart by login alone; an EMU login carries its enterprise's suffix.
2026-10-04 06:56:04 +08:00
usage: Hide accounts hides every account, not only an email address, and a WorkBuddy account's card says how many credits it used each day (#568: 默认情况下启动邮箱打码时,workbuddy的用户名那里不会被打码;用量统计能不能统计一下每天用了workbuddy多少积分) — the mask matched email addresses only, so a WorkBuddy account named by its nickname or phone number (and a Qoder, Kiro, Devin, Cursor, CodeBuddy, ZCode or plugin account named so) stayed in sight on a screenshot taken with 邮箱已打码. The page now learns the accounts' names from what magpie answers it (an allowance's user, a provider's account and its logins, a route's account: noteAccounts over every API answer) and hides them as it hides an address, in a row, a sentence and a tooltip, on Usage and Routing; a card's own name (WorkBuddy, ZCode signed in with no name of its own), a provider's or an agent's is never taken for an account, nor is a name inside a longer word. The tray panel hides them too, following the window's setting as it is turned (a storage event); the button is now Hide accounts (账号打码). WorkBuddy's meter tells only what the cycle has used against what it grants, the built-in's card and its plugin's alike, so magpie keeps each reading beside the one before in credits-daily.json (by provider and account: the two figures and the days' sums, nothing else) and counts what was used in between on the local day of the later reading; a reading that has used less (a cycle started again, a pack gone) counts nothing and counting goes on from it, credits granted count nothing, an error or a reading kept from before isn't one. The card, under its Credits meter, says the period's credits (今天 n / 7 天共 n) and over more than a day draws a bar a day, each day's figure in its tooltip, the days before magpie began counting an empty dashed slot saying so, and "Counted since <day>" while that falls in the period. TestDailyCredits (days, a reset, a check-in's grant, a gap, both builds, error and kept readings, another vendor, the file's size, a year on) is new; account-mask.test.cjs and credit-days.test.cjs (WebKit, en and zh) fail without the change and pass with it; mask-emails, click-scroll and balance-parts 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, TestProvidersAnswerWhileListsComeIn, TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin timed out once under load and pass alone).
2026-10-02 22:21:03 +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
agent: Cursor Private Inference, which magpie finds by its product.json and starts on magpie with a command its row copies, and whose /models gives each model's api_types and capabilities (#299, mamba on Discord) — the build (Cursor 3.23.x, the same app name, bundle id and ~/.cursor as Cursor) runs its agent locally and asks the endpoint in CURSOR_LOCAL_AGENT_BASE_URL / CURSOR_LOCAL_AGENT_API_KEY when its Open configuration has none; mamba confirmed it works with any account pointed at magpie. magpie writes nothing of Cursor's (its settings share Cursor's state.vscdb): the Agents page lists it when an app's product.json says "Cursor Private Inference" (/Applications, ~/Applications; %LOCALAPPDATA%\Programs on Windows; /opt and the like on Linux), and its row's one control, Copy launch command (复制启动命令), copies CURSOR_LOCAL_AGENT_BASE_URL=<gateway>/v1 CURSOR_LOCAL_AGENT_API_KEY=magpie-cursor-local '<app>' (PowerShell on Windows); in the tray panel the row copies it. Its notice says to quit it first (a second start hands over to the running one) and that a base URL in Open configuration comes first. /v1/models asked with its key gives each model api_types (the APIs its provider serves it on, all three for a group or a translated one; it asks Anthropic Messages only when no OpenAI one is listed) and capabilities (context_length, max_output_tokens, supports_vision); any other client's list is unchanged. usage.AgentOf matches a name before a UA prefix, so cursor-local isn't counted as Cursor. TestCursorLocal, TestCursorLocalModels and cursor-local-launch.test.cjs (WebKit, en and zh) fail on the old code; agy-launch and agent-* GUI tests pass on WebKit. Not tried with the real app (not installed here). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 23:38:19 +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
agent: Claude Desktop's row picks which magpie models Desktop's own model menu lists, several at once, and the menu names them by the model's own name (蓝猫 on Discord: wanted to pick a few models and switch among them in Desktop, and Desktop showed "DeepSeek V4.1 Flash (opencode-go)" where they want "DeepSeek V4.1 Flash") — Desktop already read its menu from the gateway's /v1/models for claude-desktop (desktopModels over provider.CatalogFor, i.e. every visible model minus the agent's HiddenModels, each under a claudeLooking alias), but its row offered no way to pick them: the start button stood the opus tier there as 「上次的选择」, and display_name was "Name (provider/model)" with that id again in description. Now Desktop's row (MENU_FROM_LIST in app.js) puts a model button where another agent's start pick stands: it names the first two picked models (+N, or "No models"), its title says Desktop reads the list as it starts so it must be quit and reopened after a change, and it opens the existing per-agent model list (openAgentModels, the am-pop multi-select posting {hidden} to /api/agent-models/claude-desktop), which stays open while several are ticked; the button, the line under the name ("Connected · n models in Claude Desktop's model menu") and, in the tray panel, the row's summary are refilled on each save instead of the row being redrawn; the panel's opened row has the same button under the provider, the expand link says Details, and the opened row's "In Claude Desktop" says the menu lists the models picked here and that Desktop reads it as it starts. The provider switch (default vs magpie) and the tiers are as before. /api/state's model count carries the first three shown models' names (modelCountJSON.Names). desktopNames gives each model its own name and keeps a suffix only where two picked models share one, case-insensitively: the provider id, or the whole id if that still collides; the description is gone. Models added later still show by default, as for every agent's list. zh strings added (no ja). TestClaudeDesktopPickedModels (two providers' DeepSeek V4.1 Flash read "(opencode-go)"/"(relay)" while both are listed, and after relay's, glm-5 and the routing groups are hidden Desktop lists exactly the three left, the DeepSeek plain, while another agent still lists all) and TestDesktopNames are new; the effort and tier tests map Desktop's rows by DesktopCatalogID instead of the description. internal/gui/tests/desktop-model-menu.test.cjs (window and panel, en and zh: the button's names and title, no 「上次的选择」, the list's ticks, unticking GLM-5 posts the whole hidden list and updates button, line and summary while the list stays open, ticking Kimi K3 back, no scroll, no native select) passes in webkit, as do desktop-tiers, agent-models, agent-connect, agent-start-pick, agent-layout, agent-grip, click-scroll, agent-models-all-hidden, agent-model-groups, agent-models-loading, agent-expand-steady, agent-disconnect, agent-tweak-fold, show-unset-agent, tray-cell-click, library-desktop-skills and by-agent. go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in real Claude Desktop: that its menu shows the plain names and only the picked models after a restart is from the /v1/models it is served, not seen in the app.
2026-10-04 05:01:12 +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: 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
group: a routing group tells agents its largest member's context window, not its shortest's (#712, Moody-Sin on v0.1.779: Pi on group/deepseek showed 82.4%/200k, where the group's DeepSeek V4.1 Flash members hold 1M) — groupEntries took the smallest window any member had (since 09c635cf/c83e03d5, "otherwise its shortest model's"), and the group 腾讯DeepSeek has OpenCode Zen's deepseek-v4-flash-free among its ten members, which models.dev gives 200000 (the rest: deepseek-v4.1-flash 1000000 at WorkBuddy and OpenCode Go, muse-spark-1.3-contributor 1048576), so /v1/models, Pi's models.json and every agent's config said 200k and Pi compacted there. No commit changed this code lately; what brought the window down is that member: added to the group, or its figure on models.dev — which of the two is not known. The shortest made sense while a too-long conversation was never sent past the member it overflowed; since b5cfca54 (v0.1.771, #700) an overflow goes on to the members left not known to be too small for it (withRoom), and the agent is told to compact only when none has room, so the group's window is its largest member's. A context set on the group (magpie group set … context=) still wins, below the members' too; a length rule (ruledEntry) never lifts past the largest, so its comment says so. The CLI's and TUI's help say "largest model's". TestGroupToldItsLargestWindow (the reporter's members, one window unknown: old code 200000, new 1048576) and TestContextSetByUser (largest by default, a lower set one wins) fail on the old code and pass; TestRuleImages now expects the group's 1M. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with the reporter's accounts: a conversation past 200k reaching the free member first costs it one refused request before the next member answers. The model tooltip in the Gateway page says so too: "Context: n tokens (the largest of its models')" / 上下文(组内模型中最大的), where it said the least.
2026-10-04 02:44:59 +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: 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
library: checking skills for updates asks GitHub with a GitHub token, set in Settings → Network and sharing or taken from GITHUB_TOKEN / GH_TOKEN, and a check GitHub limited says until when and that a token raises it (Player on Discord: 资源库检查技能更新时提示 github is limiting request,希望能设置 GitHub token) — the check asks GitHub's API for the last commit to each skill's folder, which GitHub answers 60 times an hour for an address without a token, so a library of a few repositories, checked twice, or a network shared with others, used it up, and the check said only "GitHub is limiting requests from here until 15:04", with nothing to do about it. Now the API's requests carry Authorization: Bearer <token> (5,000 an hour) when there is one: the GitHub token set in Settings → Network and sharing (a new GitHub row: typed masked, saved on its own with POST /api/settings/github-token, then shown as ghp_…0123 with Remove; one with a space is refused in the row, what was typed kept), else GITHUB_TOKEN, else GH_TOKEN, the row saying when one of those is used. gh auth token is not run: it would borrow another app's credential without asking. The token goes to GitHub's API's host alone (withGitHubToken checks the request's host against githubAPI's), never to codeload, raw.githubusercontent or anything else, and Go's client drops it on a redirect to another host; rtk's latest-release lookup on api.github.com carries it too. It is kept in settings.json as the LAN key is: the page is told only the masked one and where it is from (githubTokenMask, githubTokenFrom), a save of the Settings page keeps it, a backup or sync without keys leaves it out, and restoring one without keys keeps this machine's. Each folder's answer is kept with its ETag and asked for again with If-None-Match, so an unchanged one is a 304, which GitHub doesn't count against the limit. A rate limit (403 with the limit spent, a secondary limit's Retry-After, or 429) now says it plainly: without a token, "GitHub's rate limit for requests without a token (60 an hour) is used up until 15:04. Add a GitHub token in Settings → Network and sharing to raise it to 5,000 an hour"; with one, that the token's limit is used up until then; and the check carries limited {until, token} for the page, which says the same in its own words, in English and Chinese, in the status and on each Not checked tag. A token GitHub refuses (401) says which: the one in Settings, or GITHUB_TOKEN / GH_TOKEN. The library's test sandbox clears GITHUB_TOKEN and GH_TOKEN, so no developer's token reaches a fake GitHub. TestGitHubTokenSent (no token: none sent; GH_TOKEN; GITHUB_TOKEN before it; Settings before both; codeload never gets it), TestGitHubTokenOnlyToTheAPI (api.github.com only, not codeload, raw, a look-alike host or the gateway; dropped on a redirect), TestGitHubRateLimitSaid (until when and the token advice without one, not with one, the token never in the error, a secondary limit, a refused token from Settings and from GH_TOKEN), TestGitHubConditionalRequests (If-None-Match, and the 304 answered as the 200 was), TestGitHubTokenBackup (left out without keys, fails without the change; kept on a keyless restore), TestGitHubTokenSetting (set, masked, kept by a Settings save, refused with a space, removed with the environment's then said) and github-token.test.cjs (WebKit, en and zh: the row, a refusal keeping what was typed, saved masked with the token nowhere on the page, removed with GH_TOKEN said, nothing scrolls; a limited check's status and tag say until when, and the token advice only without one) are new; TestCheckSkills, TestSettingsSaveKeepsWhatItDoesNotSend (now with the token) pass. GitHub was never called: httptest stands in for it. settings-sections, search-api, click-scroll, library-toggle-status, library-all, otel, lan-docker, every-skill 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 on its TempDir cleanup, the known flake).
2026-10-02 21:16:50 +08:00
routing: a group can let a member go that hasn't begun answering after a set time and ask the next, and a request's detail splits its first token into magpie's time and the vendor's (Chen on X: answers through magpie felt slow, with no way to tell whether magpie or the vendor was the wait) — a group's editor has a Slow to start row (Wait, 30 s, 1 min, 2 min; Wait, the default, keeps today's behaviour). With a time set, a streamed request to a member that has sent no text, reasoning or tool call by then is cancelled before any of its bytes reach the agent (the held start is dropped; keepalives go on meanwhile) and the next member is asked; the try is marked slow ("slow to start", narrated in the route), and the member doesn't rest, since nothing is wrong with it. The last member left is always waited for. The card says "Next after 1 min without a first token". Each request now records when it went to the vendor (httptrace WroteRequest, sent_ms), and its detail shows In magpie and Vendor's first token beside First token; after a failover In magpie includes the time spent on the members let go. With the owner's real DeepSeek key in a sandbox, a group [a local member that sends its role chunk then stalls 60 s, deepseek-flash] with 3 s: the answer came from DeepSeek in 4.4 s, none of the stalled member's stream reached the client, its try read slow / "no first token in 3s" with no rest, and the ledger had ttft 3622, sent 3003. TestGroupFirstTokenFailsOver (failover, the last one waited for, one in time kept; passes -race) and group-first-token.test.cjs and usage-ledger-detail.test.cjs (WebKit, en and zh) fail on the old code. group-from-model-add's narrow-window case fails the same on origin/main; Chromium doesn't launch in this sandbox. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn flaked once, passes alone).
2026-10-04 19:33:55 +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
group: a routing group's members can be patterns, so a group follows a provider's catalog (#766: magpie group add my-free-pool models='openrouter/*:free,opencode-zen/*-free') — a group names its models one by one, so a pool of a vendor's free models went stale as the vendor added and dropped them. A group now keeps patterns apart from the models it names (Group.Match): a glob, * any run of characters (/ too) over the provider/model id in any case, or re:<regexp> over the whole id. groupsIn, which every reader goes through (Groups, FindGroup, /v1/models, MemberGroups, GroupsWith, RenameGroup, rules, Off, Pick, the GUI, TUI and CLI), puts after the named models every enabled provider's model a pattern matches, in catalog order, deduplicated, the named one keeping its place; a pattern never matches a group. What was matched is derived (Group.Matched) and never stored: store() and SaveGroup keep only the named members, so a save never freezes matches. Off and rules may name a matched model. A group with only patterns is allowed; one matching nothing routes nowhere and says so: magpie group <id> lists each pattern with how many models it matches now ("matches nothing now" in amber), the group list and the TUI too, and the GUI card shows a line per pattern with its count. CLI: models= takes patterns among the ids, models+= adds them (naming a matched model moves it to where it is named), models-=<pattern> drops one, models-= of a matched model is refused naming its pattern; the help documents patterns. GUI: the editor has a Patterns box (Add pattern / Remove, validated as the server does), matched models follow those named marked "by pattern" with their off switch and no Remove, and saves send members without the matched ones plus match; zh strings added. Tests: TestGroupPatterns, TestGroupPatternMembersOffAndRules, TestGroupPatternMatchingNothing, TestPatternSyntax (provider), TestPatternGroupRoutes (gateway: a request reaches a matched model, a model a provider adds joins, Off on a matched one, /v1/models lists the group), TestGroupPatternsCLI, and group-patterns.test.cjs (WebKit, en+zh); they fail on the old code (the Go ones don't build, the GUI one finds no pattern lines). GUI: group-patterns, group-found-off, group-member-fast, group-member-off, settings-groups, agent-model-groups, remote-group-served, routing-manual pass on WebKit; group-from-model-add has the same 2 failures (600x420) as origin/main. A request to a group whose patterns match nothing now is a 404 naming the group and its patterns (emptyGroupError), not 'magpie knows no model' and every model; TestEmptyPatternGroupSaysSo. With the owner's real config under a sandbox HOME: group/ds-pool of models='deepseek/*' listed deepseek-flash and deepseek-v4-pro by pattern, stored only the pattern, was in /v1/models and answered from deepseek-flash; none-pool of 'nobody/*' said its pattern matches nothing. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 16:52:15 +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
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
library: Claude Desktop takes skills, into Cowork's skills-plugin with their manifest entries (#638) — Desktop had MCP servers only and showed as "has no skills folder". Every account's local-agent-mode-sessions/skills-plugin/<org>/<account> (one with a manifest.json or .claude-plugin/plugin.json, in Claude and Claude-3p alike, no id hard-coded) gets a copy of each skill it is given — a real folder, as Cowork won't read a linked skill's other files from outside the plugin — and an entry in manifest.json (skillId, name, description from SKILL.md, creatorType user, syncManaged false, updatedAt, enabled), which Desktop lists from. The other entries and keys, and an entry's enabled and other fields, stay; times are written as the manifest's are (ms or ISO). A name Desktop has of its own (built in, or made there) is left and reported. The copy carries magpie's mark with the hash of what was copied: it is made again when the library's skill changes, left as it is once changed in Desktop (reported), and set aside into the backups rather than deleted when the library stops giving it; only magpie's copies and entries are ever taken out, and magpie's entry whose folder went gets it back. Desktop's own skills aren't offered to bring in. The Skills tab says Desktop shows changes once its window is reloaded (⌘R / Ctrl+R). TestClaudeDesktopSkills (two accounts, Claude and Claude-3p) and library-desktop-skills.test.cjs (WebKit, en/zh) cover it. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real Claude Desktop.
2026-10-03 13:51:37 +08:00
library: each MCP server in the Library shows whether it works — a dot and a few words beside its name (12 tools, needs sign-in, can't start: uvx not found, exited (1), no answer, connection refused, the server's HTTP status), the whole reason (the server's last stderr line, the status line) in the tooltip; checked once a session when the MCP list opens, and again by clicking it (Discord, lc: a status display for MCP servers in the Library page) — POST /api/library/mcp/check {names, fresh} runs library.CheckServers: up to four at once, 15 s each, by really connecting. A stdio server is started with its command, args and env (proc.Command + StartTree, so the whole tree is ended after, Windows too), sent initialize (2025-06-18, clientInfo magpie), notifications/initialized and tools/list over newline JSON; a missing command is "not found", an exit before the reply gives its code and last stderr line. A Streamable HTTP server is POSTed the same with Accept: application/json, text/event-stream, a JSON or SSE reply read, Mcp-Session-Id and the negotiated MCP-Protocol-Version carried, magpie's own sign-in token sent when it has one and the session DELETEd after; 401/403 is "needs sign-in" (OAuth when WWW-Authenticate says so). A legacy SSE server is read through its endpoint event. A server without tools/list (-32601) is fine with 0 tools. Results are kept in memory for the session, keyed on the server's definition and sign-in state, so an edited server is checked again; nothing is written to any agent's config. The GUI keeps a status per server name, paints it on the row (a grid in .who so .who > .name stays as other tests read it), a checking state pulses, the click stops propagation so it neither opens the server nor scrolls, and the strings are in i18n.js (zh). TestCheckStdio (ok with 3 tools after a junk stdout line and a notification, exit 3 with its stderr, not found, timeout ending the server and its child), TestCheckHTTP (JSON and SSE replies with the session and token carried, OAuth 401, key 403, 404, broken JSON, timeout, refused), TestCheckSSE and TestCheckServersOnce (cached, fresh, re-run on a changed env, no file under HOME changed) use a fake server re-executed from the test binary and httptest, -race; library-mcp-health.test.cjs (en and zh: one check for all servers on opening, texts, tooltips, dot not border, row height, no re-check on returning, a click re-checks only that server without opening it or moving the page, no <select>) fails on the old library.js and passes in webkit with the other Library tests. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against real MCP servers from a user's config.
2026-10-04 06:44:51 +08:00
library: RTK's savings are read from rtk gain's stdout alone, a count rtk couldn't give says why instead of "Nothing saved yet", and the page says whose commands the count is (#741 part 2, on Windows with magpie 0.1.807, RTK 0.51.0 and Codex: the RTK card said 还没有节省记录 while rtk gain in a terminal listed savings) — ReadRTK ran rtk gain --daily --format json with CombinedOutput and json.Unmarshal'd the lot, and took any error, from rtk or from the JSON, for nothing saved. rtk writes warnings to stderr with the JSON on stdout — on Windows the console code page it has no decoder for (warn_unmapped_codepage), a command it couldn't resolve, a history table it found damaged — and any of them in front of the "{" made the output unreadable, so the card said nothing was saved however much was. rtkGain now runs it with stdout and stderr apart (stdin closed, 30s at most), parses stdout from its first "{" to its last "}" (a notice printed ahead of it is passed over), and when rtk fails or prints no summary it can read, gainErr says so with what rtk said ("rtk gain: <last lines>", "rtk gain printed no summary magpie can read: …"), shown on the card as "magpie couldn't read what RTK saved: <why>" and by magpie library as "what rtk saved isn't known". magpie counts no agent itself: the figure is rtk gain's, global, every command any agent or terminal ran through RTK on this computer, and the card now says that after it, so an empty one isn't read as magpie counting only some agents. On Windows, Codex with its Windows sandbox on (config.toml's [windows] sandbox = "elevated"/"unelevated", or the older elevated_windows_sandbox / experimental_windows_sandbox features, and not danger-full-access) runs its commands as its CodexSandbox account, whose own %LOCALAPPDATA%\rtk\history.db keeps what RTK saved there, or with a restricted token that can't write the user's — either way not in the rtk gain the user runs; ReadRTK reports that as codexSandbox, and the card (and magpie library) says why what Codex saves isn't counted. The count is read again each time the RTK tab is opened, so commands run since show without restarting magpie. TestRTKGainRead uses a fake rtk: a code-page warning on stderr with JSON on stdout (read — the old CombinedOutput code says nothing saved), a notice line before the JSON, zero commands, an rtk that exits 1, and one that prints no JSON, with the exact gainErr; TestCodexSandbox covers each config form. library-rtk-gain.test.cjs (webkit) checks the reason and the scope line, the Codex sandbox note, and that reopening the tab re-reads the count, in English and Chinese. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass, as do the Library and click-scroll GUI tests. Not tried on the reporter's machine: which stderr line broke it there isn't known (the card now shows it if it's still there), and the Codex sandbox account's separate history is from Codex's and RTK's source, not seen on a real Windows Codex.
2026-10-04 12:15:40 +08:00
library: RTK says when rtk isn't on the PATH the agents get, and puts it there when asked (#601, Moody-Sin: magpie好像没有正确安装RTK — Pi said "RTK disabled: rtk binary not found in PATH" while the Library showed RTK working) — rtk's hooks run it by name (Pi's extension probes rtk --version and turns itself off when that fails; Claude Code's and Codex's are rtk hook …; the commands they rewrite to are rtk git status and so on), but magpie looked for rtk in more places than an agent's PATH: ~/.local/bin and ~/.cargo/bin, which the app adds to its own PATH whether the user's shell has them or not (rtk's own install.sh puts it in ~/.local/bin and only warns when that isn't on PATH), and WinGet\Links on Windows, which winget doesn't always link into. So rtk was "installed", the agents were switched on with it by its whole path, and none of them could run it. Now the PATH a terminal opened now has is read — the login shell's (proc.LoginPath, the one UserPath already asks for, kept a minute) or on Windows the registry's (moved from provider's registryPath) — and when rtk isn't in it the RTK card says so, with what Put RTK on PATH will do, and each agent that has rtk is tagged Not on PATH (不在 PATH 中). Put RTK on PATH (magpie library rtk path) links rtk into the first of ~/.local/bin, ~/bin, /opt/homebrew/bin, /usr/local/bin, linuxbrew's bin, ~/.cargo/bin that is on that PATH and writable, or on Windows adds rtk's own folder to the user's PATH in the registry (a link needs Developer Mode, and a copy is left behind when winget upgrades rtk; winget's package folder doesn't change across upgrades) and tells Explorer; with no such folder it says which folder to add to PATH in the shell profile — shell profiles are never edited. rtk is also found in winget's package folder (WinGet\Packages\rtk-ai.rtk_*), and rtk's script upgrades a linked rtk where the link points, not over the link. TestRTKOffPath and library-rtk-path.test.cjs (en, zh) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet, go test -tags nogui -count=1 ./... under a temp HOME pass but the known TestFXInState flake. The Windows registry path is built, not run on Windows.
2026-10-03 02:38:17 +08:00
library: a failed RTK upgrade says what went wrong — the tool, its exit code and what it last said — on the RTK card, and a finished one says so (#741 part 1, on Windows with magpie 0.1.807, RTK 0.51.0: Library → RTK → 升级 only flashed a red, cut-off WinGet command line) — runInstaller's error was "<the whole command line>: <its last 4 lines of output>", and the GUI showed it in the status pill, which is one line wide and times out, so a long winget upgrade --id rtk-ai.rtk --exact --silent --accept-source-agreements --accept-package-agreements filled it and nothing of why it failed was ever seen; winget also writes its spinner (-\|/) and progress bars between carriage returns to a pipe, so the "last lines" were mostly those. runInstaller now returns installFailed: "<tool> failed (exit code N)" — winget's HRESULT exit codes as 0x8A15…, whether Go reports them as the uint32 or a negative int, with what AppInstallerErrors.h means by the common ones (no newer version, package in use, no network, not found, installer failed, blocked by policy, …) — or "<tool> didn't finish in <timeout>" / "<tool> couldn't run: <err>", then " — " and the last 3 meaningful lines the tool printed (installerSaid: ANSI codes and bad UTF-8 dropped, split at \r and \n, spinner frames, progress bars and winget's licence boilerplate left out, 400 runes at most). winget's "no applicable upgrade" (0x8A15002B) isn't a failure: UpgradeRTK re-reads RTK and says winget has no newer RTK than the one installed yet (or, as before, that winget hasn't got the release GitHub has). In the GUI, the upgrade under way says "Running <cmd> — this can take a few minutes." on the RTK card; one that failed leaves "The upgrade failed: <why>" there (red, selectable, wraps), the pill only says the card has the reason, and the card is read again so the version the installer left is shown; a later try clears it, and a success says "RTK is now <v>" with the new version, as before. TestRTKUpgradeFailureSaid feeds installFailed winget-like output (spinner, progress bar, licence lines, 0x8A150101 both as 2316632321 and -1978334975) and checks the exact message, plus 0x8A15002B, a curl exit 6 through sh, and real processes: one that exits 3, one that isn't there and one that times out. library-rtk-upgrade.test.cjs (webkit) checks the running line, the failure on the card and the short pill, the re-read after a failure, and a retry that clears it and shows v0.51.0, in English and Chinese. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass, as do the Library GUI tests. Not run against a real winget: the exit codes and output shape are from winget's source and docs, not the reporter's machine, and nothing was installed or upgraded here.
2026-10-04 12:15:40 +08:00
gui: lightweight mode, off by default, lets a closed panel's or window's webview go and makes it again when it is opened (somls on #580: GUI版可否增加轻量模式,关闭面板退出webview,减少资源占用) — closing the tray panel or the main window only hid it, so each kept its page and, on a Mac, its WebKit WebContent process (about 65 MB each at start, 95–100 MB after use) for as long as magpie ran. Settings → General now has Lightweight mode (轻量模式), shown in the app only (not on a browser's page) and kept as this computer's own through a sync or a restored backup (KeepOwn): with it on, a window found closed on two looks in a row 30 s apart (lightStep) is let go 30–60 s after it was closed, so a panel opened again soon after is still the same one at once. Letting go happens on the main thread under a new h.winMu: the window field is set to nil, the panel is unhooked from the tray icon (AttachWindow(nil); left click goes through magpie's own OnClick), and the main window is marked gone so its closing hook, which otherwise cancels a close and hides, lets it close; on the Mac the WKWebView is taken out of the window first (stopLoading, delegates and script handlers cleared, removed from its superview), since Wails' NSWindow close alone left the WebContent process alive for minutes. Opening it again (the tray icon, ShowMain, an import link, the Dock) makes the window anew with the options it was started with (makeMain/makePanel, moved out of Run with their hooks), its text size and title bar or Hyprland name (madeAgain), on Windows shown only once its page has loaded; every other path that reached h.main or h.panel (fit, tint, zoom, hide, glide, the Windows title-bar tint, the Hyprland click watch, the folder picker) now goes through the main thread and does nothing to a window let go; Linux keeps running with no window left (DisableQuitOnLastWindowClosed). Measured in a sandbox build under a temp HOME on other ports: started in the tray with the setting on, two WebContent processes of ~65 MB each; about a minute later both windows were let go and both processes ended (GPU and Networking, ~56 MB, are shared and stay; magpie's own process is unchanged), so the saving is ~130 MB idle and up to ~200 MB after use; the main window and the panel were then opened again with their pages, tinted and at the tray icon, closed, let go a second time, and magpie quit cleanly, with fit/tint/zoom calls every 0.2 s through a release crashing nothing. Windows and Linux build but were not run. TestLightStep and TestLightweightKeptAndOwn are new; lightweight.test.cjs (WebKit, en and zh: the row, off by default, On/Off posted as lightweight true/false, another setting's save keeping it, no scroll, no left-border accent, no row on a browser's page) is new and passes, as do click-scroll, settings-sections and searcher-pick. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 00:24:49 +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: 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
gui, mcp: Sign in in an MCP server's editor signs in to the server the form shows, saving it first, and a server that needs no sign-in is said to work as it is (lc on Discord: exa mcp 无法OAuth使用,x-api-key + key也无法使用 agent使用的是omp) — Exa signs in only at mcp.exa.ai/mcp?login (401 with resource_metadata, auth.exa.ai, S256, dynamic registration); plain /mcp answers without one. Sign in sent only the name, so the backend probed the saved URL: a URL just changed to ?login, unsaved, got "this server answers without a sign-in: there is nothing to sign in to". Now Sign in saves the form first when its name, transport, URL or headers aren't the saved server's, leaves the dialog open, and signs in to it; a server being added, or one switched to HTTP, has Sign in too, and a later Save is of the server as saved. mcpauth.ErrNoSignIn says the server already works without signing in, so the agents can use it as it is, a key goes in Headers and a server that signs in at another address (Exa's ?login) needs that URL; the GUI shows it in Chinese. The gateway's /mcp/<name> relay already kept the signed-in URL's query (?login reaches the server) and passed the Library's headers (x-api-key) on, dropping only the gateway's own key: TestMCPProxyKeepsQueryAndHeaders holds it (the fake now records the query). omp reads magpie's mcp.json entry as written (type/url/headers, discovery/builtin.ts) and sends headers on every request (mcp/transports/http.ts): with omp 18.4.9's own client under a temp HOME, mcp.exa.ai/mcp with x-api-key listed Exa's tools and ?login without a sign-in got the 401, so the encoder is unchanged. mcp-signin.test.cjs (webkit, en and zh): an unchanged Exa says it works as it is, with nothing saved; its URL changed to ?login, Sign in saves it then signs in, the dialog stays and no click moves the page; old code started the sign-in without saving. 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 on rerun; library and mcp GUI tests pass in webkit (chromium didn't launch here). Not tried: a real Exa OAuth sign-in in magpie, or omp through magpie's relay with a real account.
2026-10-04 01:52:46 +08:00
provider: OpenCode Zen's Jev routes groups on System One, and a preset's or a subscription's model can be asked on the API picked (Discord, 01huadalang: Zen's "Jev free" was asked as a chat model and failed with no way to switch; OpenCode Go's DeepSeek V4.1 Flash answers on Responses too, and magpie asked it on chat alone, while a custom provider could pick) — Jev isn't an Anthropic model: OpenCode's docs (anomalyco/opencode packages/web/src/content/docs/zen.mdx) list jev-1.13 and jev-1.13-free at https://opencode.ai/zen/v1/systemone alone, and models.dev has no entry for it; asked keyless, Zen answers it there and errors on chat, messages and responses. The opencode-zen preset now has Decide (https://opencode.ai/zen/v1), making Zen a mixed decision provider (#609): Jev is left out of agents' models and is a routing groups' classifier, the free one when Zen is used without a key; a Zen provider saved before gets Decide back in Normalize. Zen's and Go's other models already take their API from models.dev's npm package (catalog.APIOf), which matches the docs. The per-model API custom providers have (settings ModelAPIs, the gateway's Provider.APIs) now reaches presets and subscriptions with more than one API: ModelAPI no longer refuses an account, APIs() is the pick or ListedAPIs (what the vendor's list says, sent to the editor as a model's auto); the editor's model chip has "Asked on: Auto…" on its right-click, opening the app's menu (Auto says what the vendor's list says, then each API the provider has), tagging the chip with the pick, staged and sent with Save as modelPrefs, the page not moved; Names & levels' API row shows for subscriptions too. English and Chinese. TestOpenCodeZenJevDecides and TestPresetAndSubscriptionModelAPI fail on the old code and pass; internal/gui/tests/model-api-preset.test.cjs (WebKit, en/zh) covers the menu, no <select>, no left-border accent, no scroll, the Save's modelPrefs, a subscription's tag and a single-API provider with nothing to pick, and fails on the old assets. A sandbox magpie (keyless Zen) answered POST /v1/systemone with opencode-zen/jev-1.13-free (noul 0.44) and chat on mimo-v2.6-flash-free, Jev not in /v1/models. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, a timing test that passes alone; provider-levels-scroll.test.cjs fails the same on origin/main. Not tried with a paid Zen/Go key.
2026-10-03 16:03:32 +08:00
providers: a model chip's right-click menu copies the model's id (Discord, ARNO on v0.1.815: 希望provider的模型右击菜单除了测试模型外,还能添加拷贝模型id的功能) — the menu a chip opens in a provider's editor had Test this model (and Asked on … where the provider has more than one API) and nothing to take the id with; a chip shows the model's name where it has one, so the id (zai/glm-5.3-flash under "GLM 5.3 Flash") had to be read off its tooltip and typed. The menu now has Copy model ID / 复制模型 ID under Test this model, through the app's own menu (openRowMenu) and copy(): magpie's /api/copy, the page's clipboard and the copy command after it, as every other copy in the app; the footer says "Model ID {model} copied" / 已复制模型 ID {model}. It is on where the model can't be tested (an own-API subscription's, a classifier's) too. The app has English and Chinese strings only (i18n.js has no Japanese table). model-copy-id.test.cjs (a relay's prefixed, plain and hand-picked ids and an own-API account's: the id, not the name, is posted to /api/copy, the footer says so, nothing is tested, picked or scrolled, no left-border accent; en and zh) fails on the old code and passes in WebKit; model-test-why expects the second item beside the disabled test one. model-test-one, model-test-why, provider-typed, provider-detect-models and click-scroll pass in WebKit; Chromium doesn't launch here, so not tried there. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in the app's own window (its /api/copy path is the one the other copies use).
2026-10-04 13:36:54 +08:00
gui: with no model picked, a click on a model picks just it (#681, #702) — the provider editor's chips, with none picked, draw the models agents are served (the vendor's first 24) as served; a click on one left it out and picked every other served one in its place (#614's choice), so ChenstarMr's click on DeepSeek V4 Flash in Trae CN ticked the other 23 and not it ("我想选其中一个模型,但是点击之后它却选了其他的所有模型"), and Dazzle-sys's Cline (22 models, all served) clicked on each of its 4 free models till it showed ticked ended with all 22 picked, which Save wrote (#702: 选了4个免费模型保存出来显示22个). Now a click picks the model, alone when none is picked, then each next one joins it; a served chip is drawn without the accent (dashed grey border, no tick) so it doesn't read as picked, its title and the hint say a click picks just that one. Nothing changes on the Go side: no picks is still an empty list, Exposed still serves the first 24, and an editor saved untouched writes none, for key and plugin providers alike (same code). zh strings added. model-first-pick.test.cjs (webkit, en/zh: a 54-model plugin provider and a key provider, untouched Save writes [], one click picks it alone with the page not moving, Save writes it alone; Cline's 4 free ones clicked till ticked save the 4) fails on the old code (22 saved) and passes; provider-none-picked and model-rate now expect the click to pick. provider-draft-picks, click-scroll, add-sheet, model-pick, models-free-only, provider-picks-stay/models-rest/add-models/typed/detect-models, model-test-one/why, account-models, editor-short-window, fallback-more, model-api-preset pass. go vet ./internal/... and go test -tags nogui -count=1 ./internal/gui/... ./internal/provider/... under a temp HOME pass. Not tried with a real Trae CN or Cline account.
2026-10-04 00:13:54 +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
models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
2026-10-01 23:20:57 +08:00
system one: a decision provider's own models-list URL is asked as given, and its models can each be tested (Discord, ARNO: OpenRouter added as a System One provider with the list https://openrouter.ai/api/v1/models?limit=500&output_modalities=decisions didn't get its models, and a model typed in by hand couldn't be tested from its right-click) — a System One-only provider went to fetchDecide before its ModelsURL was read, so magpie asked …/models, which at OpenRouter lists the 466 chat models, and kept only Jev's ids of those: typesafe/jev-router alone. With a ModelsURL, fetchDecide now asks it as written, query included, and keeps every id in it, since that list is of decision models already (OpenRouter's answers 13: pplx-decider, liquid/d1, cloudflare/clef, respan/span-01, typesafe/jev-1.13 …). Without one nothing changes, Bailian included. Testing a decision model was refused outright (the chip's Test this model off, Test models leaving them out); where the API takes System One's own body (System One, TypeSafe, OpenRouter, Vercel's TypeSafe API) each model is now sent AskSystemOne's yes-or-no question, the one Detect already asks, and the editor offers Test for it (providerJSON.decideTest); Vercel's evaluation models and Workers AI, which wrap it their own way, still say why not. TestDecideModelsURLAndModelTest (a fake OpenRouter: the plain list, the decisions list, a hand-added model through /systemone, an unknown one failing) and TestModelTest's Workers AI case fail on the old code; model-test-why.test.cjs gains a hand-added System One model tested from its chip and Test models, and a Workers AI one still off (WebKit en and zh pass, with model-test-one and model-copy-id; Chromium doesn't launch in this sandbox). OpenRouter's live decisions list returns the 13 models. Not tried with an OpenRouter key: the /systemone question itself. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 18:40:13 +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
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
models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
2026-10-01 23:20:57 +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
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
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
zcode: a Start Plan account neither lists nor offers GLM-5.3, and the move to the plugin doesn't count it lost (悠悠哥 on Discord: "仍使用内置:插件不提供 GLM-5.3" moving ZCode onto its plugin) — GLM-5.3 is the Coding Plan's alone (ZCode's builtin config lists GLM-5.3-Flash, GLM-5.2 and GLM-5-Turbo for account:zai-start-plan), and the plugin serves a Start Plan account what ZCode does. But a Z.ai account with a key made and no Coding Plan bought, whose requests go to the Start Plan, was taken for a Coding Plan account until a request found its route (zcodeOnStart with no context said the Coding Plan), and afterwards was listed whatever list was fetched last (the Coding Plan's, another account's or its own before), so GLM-5.3 was listed, could be ticked, and the move found the plugin "short" of it. Now: before a route is found, the plan the account showed at sign-in says it (zcodeOnStartAs, zcodeStartNamed: "Start Plan"); on the Start Plan, a model the plan doesn't serve (zcodeStartServes: ZCode's config on the machine, else zcodeStartModels) is unusable, taken out of a fetched list and out of the picks, with a list fetched or not (Exposed); and a mover may say which models the built-in served on an account (mover.builtin), ZCode's saying a Start Plan account was never served GLM-5.3, so one the built-in didn't serve either is no loss. A model really lost is named with the account and its plan: "the plugin doesn't serve GLM-5.3 for a@x (GLM Coding Pro), though the built-in does" (插件没有为 … 提供 …(内置提供)), the why's args carrying user. TestZCodeStartPlanHasNoGLM53 (a Start Plan account before and after the Coding Plan's list was fetched, GLM-5.3 ticked or not; a Coding Plan account still has it; the mover's builtin) fails without the change (it listed GLM-5.3 GLM-5.3-Flash GLM-5.2 GLM-5-Turbo); TestMoveToPlugin's new case (a model the built-in didn't serve moves; one it did is named with "a@fake (Fake Start)"); plugin-move-unserved.test.cjs (WebKit, en and zh, with an account and without) fails without the app.js change and passes with it, plugin-move.test.cjs passes. No ZCode request or sign-in was made. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass.
2026-10-02 22:00:34 +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
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: ZCode and Pi get a project's MCP servers too, and a project can be removed keeping what magpie wrote into it (#514, follow-up to #500: zcode支持项目级mcp添加 / pi也支持项目级mcp / 删除项目但保留mcp/skill) — a project's servers went only to Claude Code, Codex, Gemini CLI, Cursor and OpenCode, and Remove took every skill and server magpie had put in the folder away, so a project given some once had to stay in magpie for good. ZCode's project file is <project>/.zcode/config.json, servers under mcp.servers as in its user-wide ~/.zcode/cli/config.json (zcode.z.ai/en/docs/mcp-services, "Configuration Files and Default Load Paths": workspace scope, read before the user's, "enable": false the user's to set), written with fmtZCode (type, command/args/env or url/headers); Pi's is <project>/.pi/mcp.json, mcpServers as Pi's own MCP reads them once the project is trusted (pi.dev/docs/latest/mcp, "Configure servers"), written with fmtPiNative (command/args/env, url/headers, no type; SSE refused, as Pi refuses it), whatever extension the user's own Pi file is written for. Both are merged as the other agents' are: the user's keys, servers and comments stay, a server of the same name that isn't magpie's is left alone and said, a file magpie made (.pi/mcp.json) goes in the .gitignore and goes again once empty, and ZCode's user-wide file isn't touched. The page learns a project file's own SSE limit (projectNoSSE), so Pi's chip is greyed for an SSE server in a project even when its user-wide file (pi-mcp-adapter's) takes SSE; the trust note now names Pi beside Codex. The Remove dialog of a project magpie put anything in has "Keep its skills and MCP servers" / 保留项目里的技能和 MCP 服务器 (a checkbox row in the library's pick style, no coloured left border): checked, the text says what stays and Remove posts projects/remove with keep, and RemoveProject(dir, keep) only takes the project off the list — skills (links still following the library's), servers in the agents' files and magpie's .gitignore lines stay as they are, and nothing magpie does later (a sync, the server removed from the library) touches them; unchecked it removes as before. A one-off add without a project first (pick a folder from a server or skill) is not in this change. TestProjectServersZCodePi (ZCode's file with the user's model, server, enable:false and key kept beside magpie's three; Pi's file made in its shape, the SSE server refused with its reason, listed in .gitignore; ZCode's user-wide file unchanged; removing takes out only magpie's and the file it made) and TestRemoveProjectKeep (every file byte for byte after the removal, after a sync and after the server leaves the library) fail without the change, as does project-remove-keep.test.cjs (WebKit, and Chromium through a temp copy on plain chromium.launch(); en and zh: the box unchecked, its text, no stripe, pressing it scrolls nothing and swaps the text, Remove posts keep: true and says they were kept; a project with nothing written has no box and posts as before) against HEAD's assets. project-servers, click-scroll, library-items, library-all pass; menu-scroll and old-webkit fail the same on HEAD. No live config was read or written. 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 on a TempDir cleanup race twice in the full gui run, passes alone and on the rerun; untouched).
2026-10-02 14:10:55 +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
providers: a custom provider's models can be picked from its vendor's list while it is added, before its first Save (Sun1090 on #578: 添加供应商的时候,希望添加可以获取全模型的按钮或者下拉框?像cc-switch那样,可以选) — the vendor's list, its chips and Refresh were only in a saved provider's editor; the custom provider's add form had a comma-separated Models field and the hint "magpie asks the vendor for its list after saving", so a user adding one had to save it, open it again and pick there, which nothing in the form said could be done sooner. The add form now has Fetch models (获取模型) beside its Models field: it posts the new provider/list with the form as it stands (the URL and key typed, its headers, models URL and proxy; no id), which builds the draft as detect does (from its preset when it has one, then typed) and asks the vendor with the new Provider.List — Fetch's own path (a NoList preset's models, Cline's feed, Kilo's list, fetchOne) without SaveLive, so nothing is kept — answering the models agents chat with, a model's name where it has one. The list shows as chips (a long one with a filter), each ticked one in the Models field and an id typed there ticking its chip, with "{n} of {all} picked", Pick all (Pick those shown under a filter) and Pick none above the chips, so a pick that reflows them moves no button; one typed by hand stays through Pick none. A vendor refusing says why under the field; no URL asks nothing. The Add sends the picks as the provider's models, as the field did. TestProviderListBeforeSave (the list asked with the key typed, an embedding model left out, no provider saved and no list kept, a 401 told, no URL refused) is new; provider-add-models.test.cjs (WebKit, en and zh: nothing asked without a URL, the post carries URL and key and no id, the refusal shown, chips with names, tick and type in sync, filter and Pick those shown / none / all, the Add's models, no click moving the page, no left-border accent) is new and passes; add-sheet, provider-detect, provider-detect-models, provider-typed and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
2026-10-02 23:48:00 +08:00
providers: Detect APIs asks every picked model on each API and shows model by API, and a model's right-click Test says the API it answered on (01huadalang on Discord: 右键选择测试只能测试出是不是通的,不能说什么协议是通的。检测协议的结果看不出对应的模型支持协议情况…有的仅支持 response 有的双协议) — Detect APIs asked one model (typed, or one picked from the vendor's list for each API), so a relay serving some models on Responses only and others on both read as "chat works, responses doesn't", and the right-click Test said only that a model answered. Detect APIs now has "Each picked model" (逐个检测已选模型): provider/detect with detectModels asks each picked model (the first provider.DetectMax, 30; an image model left out, it is asked on the images API) on Chat, Responses and Anthropic at the URLs Detect asks, provider.DetectWide (8) requests at a time, each waiting 12 s, the whole within 2 minutes (a probe not started by then is "not asked: out of time" and sent nowhere), and answers model by model plus, by API, the first model it served. The editor shows a model × API grid, each cell a ✓ with the time or a ✗ with the status and why (the whole in its title). Its "Use these" sets the URLs of the APIs that answered and gives each model that answered on one API only (of two or more asked) that API, staged in Names & levels as draft.modelPrefs like the rest of b0dc82c4 — "unsaved", nothing sent until the Save, Cancel drops it — while a model answering on two stays Auto; the hint under the grid names them ("m-resp: Responses only"). Detect for one model is as it was (its body now Provider.detecting and detectOne, shared). A chip's right-click Test now says the API it was asked on: "gpt-5.5 answered via Responses in 123 ms", the dot's title "Answered via Responses in …", a failure "didn't answer via OpenAI: …". TestDetectModels (a fake relay serving gpt-both on Chat and Responses, gpt-resp on Responses, glm-chat on Chat, claude-only on Anthropic: each model's ✓/✗ on each API, 12 requests for 4 models, the image model not asked, by API the first served; 35 models asked as 30, never more than 8 at once) and TestDetectModelsOutOfTime are new; provider-detect-models.test.cjs (WebKit, en and zh: the models posted, the grid, Use these staging m-resp on Responses and m-chat on OpenAI with m-both left Auto and nothing sent, the Save's modelPrefs, the right-click Test's "via Responses", no click scrolling, no left-border accent, every string's Chinese). provider-detect, provider-levels-save, provider-levels-scroll, model-test-one, model-test-why and click-scroll 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's known TempDir cleanup flake aside).
2026-10-02 23:10:11 +08:00
providers: a model a vendor names its own way is merged with the same model from other providers — by its id as Volcengine Ark spells it, or by the model the user says it is the same as in Names & levels (kyzhouxu, #583: 火山引擎获取到的默认 ds 4.1 flash 模型的名称跟其他供应商的名称不一样,没有自动合并到同一个模型分组) — the groups magpie finds merged a model by its id lowercased, its vendor prefix and an eight-digit date taken off and a version's dot made a dash, so Ark's deepseek-v4-1-flash-260910 (a six-digit YYMMDD snapshot) was a group of its own beside ali-tp's deepseek-v4.1-flash. sameModel now also takes off Ark's YYMMDD (a year from 23 to 39, a month and a day that are one), Vertex's "@YYYYMMDD", reads Fireworks' "p" between digits (deepseek-v4p1) as the dot and "_" as "-"; -flash, -thinking, ":batch", a four-digit release (qwen's -2507, kimi-k2-0905) and a number that is no date (-128000, -261301) stay apart. For an id no rule matches (an Ark endpoint id, a relay's own name), each model in a provider's Names & levels now has a "Same as" (等同于) box, its placeholder the name it is merged by now: the model it is the same as, as any vendor spells it, staged in draft.modelPrefs and made with the provider's Save like the other prefs (Cancel drops it, Restore default empties it, nothing sent or redrawn or scrolled before the Save). It is kept in settings' ModelSameAs by "<provider>/<model>" (renamed with the provider like every Model* map), set by provider.SetModelSame (its own id however spelt keeps nothing; a model the provider doesn't serve, or a name of no model, is refused) and taken by autoGroups, AutoStandIn and the agents' reseat (AutoGroupOf); the agents are told once. TestSameModelVendorSpellings (Ark, Fireworks, Vertex, underscores, and pairs that must stay apart) and TestModelSameAsMergesFoundGroup (c/ep-sol-x joins auto-sol once said, GroupFor("sol") finds it, back out when cleared, refusals) are new; TestProviderSaveModelPrefs covers a Save's same; provider-same-as.test.cjs (WebKit, en and zh: placeholder, staging, unsaved, Restore default, Cancel, one Save carrying exactly the changes, no scroll, Chinese for each string) fails without the change and passes with it; provider-levels-save and provider-detect pass. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 23:39:40 +08:00
provider: a providers.json that can't be read is said where the providers are listed, never backed up or synced as none, and is replaced through a rename (PR #415's review) — #415 made edits refuse a file they couldn't read or decode, but every read for the user still took it as empty: magpie providers said "no providers yet", magpie models "no models yet", the Providers page opened the Add sheet as for a first use, and backup.Collect, so an export and a WebDAV/S3 sync, carried it as no providers, which another computer's Mirror would then take as every provider removed. magpie providers and magpie models now list what is left (the signed-in accounts) and end in the error, the TUI says it over its list, and the Providers page shows a "Your providers file can't be read" box (无法读取供应商配置文件) over the list with magpie's reason and no Add sheet; start-up, the gateway and the catalog still go on with it as empty. Stored and StoredGroups return the read error, so a backup and a sync stop on it with nothing pushed. The error, for reads and refused writes alike, names the file, says magpie left it unchanged and to fix it or move it aside (errors.Is ErrUnreadable). store writes through writePrivate, a 0600 temp file renamed over it (steady.Rename waits out a Windows reader holding it, a symlink keeps its target), instead of os.WriteFile's truncate-then-write, and the read goes through steady.ReadFile. TestFileErrorSaysUnreadable, TestWriteErrorsSayLeftUnchanged, TestStoredRefusesUnreadable, TestStoreReplacesAtomically, TestSyncStopsOnUnreadableProviders, TestProvidersCmdSaysUnreadable and provider-file-error.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; provider-file-error, click-scroll, old-webkit, providers-off-fold, add-sheet pass.
2026-10-01 19:45:24 +08:00
providers: a model's reasoning levels, name and images in the editor's Names & levels are made with the provider's Save, not at each tick (ARNO on Discord: setting one model's reasoning effort applies the config at each click rather than at the provider's OK/Save, so picking levels lags) — each tick of a level (and each name typed, each Accepts images, each Restore default) was its own POST /api/provider/efforts (name, images), which saved settings and ran catalog.Changed, agent.SyncCatalog, rewriting every agent's files, then fetched the providers and the state and redrew the providers page, the editor's model list with it, before the next tick took: picking levels lagged a click behind, and Cancel had nothing to drop. Names & levels now stage in the editor's draft (draft.modelPrefs, by model id: name, efforts, images or ownImages for the vendor's answer), nothing sent and nothing redrawn on a tick; each row changed says "unsaved" (未保存), a tick back to what is saved stages nothing, and Restore default stages its own name, every level and the vendor's answer for images (the provider list's model now carries ownImages so the tick shows it). Both editors' Save (a key's provider and a signed-in account's) send modelPrefs with the rest, and Cancel drops them. The save action makes them after the provider is saved (and renamed) with the new provider.SetModelPrefs, which makes each as SetModelName, SetModelEfforts and SetModelImage do (their bodies now setModel*, telling nothing, the three Set* telling the agents when they changed something) and tells the agents once for all of them; a refused one fails the Save saying why. The per-model endpoints stay for the CLI and anything else that calls them. provider-levels-save.test.cjs (WebKit and Chromium, en and zh: ticks, a name, Accepts images and Restore default send nothing, redraw nothing, scroll nothing and say unsaved; Cancel drops them; one Save carries exactly the changed models' levels) fails without the change ("nothing is sent before the Save") and passes with it; provider-levels-scroll.test.cjs now checks that a tick leaves the list in place with nothing sent. TestSetModelPrefs (all set, agents told once, nothing changed told nothing, Restore default, a refused level) and TestProviderSaveModelPrefs (a Save's modelPrefs made; a refused level fails it) are new. click-scroll (one flake of "picking request 7", then 3/3), model-pick, models-free-only, model-test-one, panel-effort, claude-effort, editor-short-window 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 and in a second full run).
2026-10-02 17:02:42 +08:00
providers: a model's reasoning levels, name and images in the editor's Names & levels are made with the provider's Save, not at each tick (ARNO on Discord: setting one model's reasoning effort applies the config at each click rather than at the provider's OK/Save, so picking levels lags) — each tick of a level (and each name typed, each Accepts images, each Restore default) was its own POST /api/provider/efforts (name, images), which saved settings and ran catalog.Changed, agent.SyncCatalog, rewriting every agent's files, then fetched the providers and the state and redrew the providers page, the editor's model list with it, before the next tick took: picking levels lagged a click behind, and Cancel had nothing to drop. Names & levels now stage in the editor's draft (draft.modelPrefs, by model id: name, efforts, images or ownImages for the vendor's answer), nothing sent and nothing redrawn on a tick; each row changed says "unsaved" (未保存), a tick back to what is saved stages nothing, and Restore default stages its own name, every level and the vendor's answer for images (the provider list's model now carries ownImages so the tick shows it). Both editors' Save (a key's provider and a signed-in account's) send modelPrefs with the rest, and Cancel drops them. The save action makes them after the provider is saved (and renamed) with the new provider.SetModelPrefs, which makes each as SetModelName, SetModelEfforts and SetModelImage do (their bodies now setModel*, telling nothing, the three Set* telling the agents when they changed something) and tells the agents once for all of them; a refused one fails the Save saying why. The per-model endpoints stay for the CLI and anything else that calls them. provider-levels-save.test.cjs (WebKit and Chromium, en and zh: ticks, a name, Accepts images and Restore default send nothing, redraw nothing, scroll nothing and say unsaved; Cancel drops them; one Save carries exactly the changed models' levels) fails without the change ("nothing is sent before the Save") and passes with it; provider-levels-scroll.test.cjs now checks that a tick leaves the list in place with nothing sent. TestSetModelPrefs (all set, agents told once, nothing changed told nothing, Restore default, a refused level) and TestProviderSaveModelPrefs (a Save's modelPrefs made; a refused level fails it) are new. click-scroll (one flake of "picking request 7", then 3/3), model-pick, models-free-only, model-test-one, panel-effort, claude-effort, editor-short-window 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 and in a second full run).
2026-10-02 17:02:42 +08:00
gui: with no model picked, a click on a model picks just it (#681, #702) — the provider editor's chips, with none picked, draw the models agents are served (the vendor's first 24) as served; a click on one left it out and picked every other served one in its place (#614's choice), so ChenstarMr's click on DeepSeek V4 Flash in Trae CN ticked the other 23 and not it ("我想选其中一个模型,但是点击之后它却选了其他的所有模型"), and Dazzle-sys's Cline (22 models, all served) clicked on each of its 4 free models till it showed ticked ended with all 22 picked, which Save wrote (#702: 选了4个免费模型保存出来显示22个). Now a click picks the model, alone when none is picked, then each next one joins it; a served chip is drawn without the accent (dashed grey border, no tick) so it doesn't read as picked, its title and the hint say a click picks just that one. Nothing changes on the Go side: no picks is still an empty list, Exposed still serves the first 24, and an editor saved untouched writes none, for key and plugin providers alike (same code). zh strings added. model-first-pick.test.cjs (webkit, en/zh: a 54-model plugin provider and a key provider, untouched Save writes [], one click picks it alone with the page not moving, Save writes it alone; Cline's 4 free ones clicked till ticked save the 4) fails on the old code (22 saved) and passes; provider-none-picked and model-rate now expect the click to pick. provider-draft-picks, click-scroll, add-sheet, model-pick, models-free-only, provider-picks-stay/models-rest/add-models/typed/detect-models, model-test-one/why, account-models, editor-short-window, fallback-more, model-api-preset pass. go vet ./internal/... and go test -tags nogui -count=1 ./internal/gui/... ./internal/provider/... under a temp HOME pass. Not tried with a real Trae CN or Cline account.
2026-10-04 00:13:54 +08:00
2026-10-02 11:31:11 +08:00
providers: a model a vendor names its own way is merged with the same model from other providers — by its id as Volcengine Ark spells it, or by the model the user says it is the same as in Names & levels (kyzhouxu, #583: 火山引擎获取到的默认 ds 4.1 flash 模型的名称跟其他供应商的名称不一样,没有自动合并到同一个模型分组) — the groups magpie finds merged a model by its id lowercased, its vendor prefix and an eight-digit date taken off and a version's dot made a dash, so Ark's deepseek-v4-1-flash-260910 (a six-digit YYMMDD snapshot) was a group of its own beside ali-tp's deepseek-v4.1-flash. sameModel now also takes off Ark's YYMMDD (a year from 23 to 39, a month and a day that are one), Vertex's "@YYYYMMDD", reads Fireworks' "p" between digits (deepseek-v4p1) as the dot and "_" as "-"; -flash, -thinking, ":batch", a four-digit release (qwen's -2507, kimi-k2-0905) and a number that is no date (-128000, -261301) stay apart. For an id no rule matches (an Ark endpoint id, a relay's own name), each model in a provider's Names & levels now has a "Same as" (等同于) box, its placeholder the name it is merged by now: the model it is the same as, as any vendor spells it, staged in draft.modelPrefs and made with the provider's Save like the other prefs (Cancel drops it, Restore default empties it, nothing sent or redrawn or scrolled before the Save). It is kept in settings' ModelSameAs by "<provider>/<model>" (renamed with the provider like every Model* map), set by provider.SetModelSame (its own id however spelt keeps nothing; a model the provider doesn't serve, or a name of no model, is refused) and taken by autoGroups, AutoStandIn and the agents' reseat (AutoGroupOf); the agents are told once. TestSameModelVendorSpellings (Ark, Fireworks, Vertex, underscores, and pairs that must stay apart) and TestModelSameAsMergesFoundGroup (c/ep-sol-x joins auto-sol once said, GroupFor("sol") finds it, back out when cleared, refusals) are new; TestProviderSaveModelPrefs covers a Save's same; provider-same-as.test.cjs (WebKit, en and zh: placeholder, staging, unsaved, Restore default, Cancel, one Save carrying exactly the changes, no scroll, Chinese for each string) fails without the change and passes with it; provider-levels-save and provider-detect pass. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 23:39:40 +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
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
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
archive: a request the archive kept is opened and downloaded from the Usage page, with its bodies whole (jorben, #447: 请求存档 reachable from usage history, full bodies, download) — the archive's name was only on the Gateway page's recent calls, gone once a call left the list or the gateway restarted, and the archive kept the first 256 KB of each body, the recent list's cut. The usage ledger's row now names the call's archive (archive in usage.jsonl, kept through the Requests page's packed rows), and its details on Usage → Requests show the Request archive panel, Fetch from archive and Download; with the archive on, a request it has no copy of says "Not archived". The bodies are kept whole up to requestArchiveMaxMB in settings.json (32 MB unless set, at most 1024): the response is teed to a temp file as it is sent, the request body written to one when the call ends, so the upload queue holds file handles, not bodies, and one upload at a time reads them; a body past the limit is kept to it and marked cut, with its whole size. What goes upstream is unchanged, the archive stays off unless turned on, headers and bodies are scrubbed as before, and the temp files are removed after the upload or when the queue is full. The page is sent a body to show only when it is 256 KB or less (a longer one is said by its size), an archive over 80 MB is only said to be; Download streams the object whole, to Downloads in the app (archive/export, -2, -3… for a name taken) and as the browser's download in magpie web (archive/file). TestArchiveWholeBodies, TestArchiveLimit, TestRowKeepsArchive and TestArchivePreviewAndDownload fail without the change; TestArchiveOffUsage, TestArchiveTooLargeToPreview, TestArchiveOldPreview keep the rest. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; request-archive-usage (new), request-archive, click-scroll, old-webkit, usage-ledger, usage-ledger-detail, usage-ledger-content, web-page, sse-body pass.
2026-10-02 00:57:51 +08:00
gui: the request archive's switch sits over the Usage page's requests, and the Gateway page's recent calls no longer fetch from the archive (Jorben on Discord: 网关页的最近请求下遗留了取回归档和预览的逻辑,和最近请求的预览能力重叠了,建议移除;以及把是否启用请求归档开关也挪到用量页请求列表的右上方) — since #447 a request's archive is read back and previewed from its row on the Usage page, so the Gateway page's recent calls showed two previews of one call: their own bodies, and under them the archive's copy with "Fetch from archive" and "Download". A recent call now shows only its own request and response bodies; archivePanel and what it uses stay for the Usage page. The archive's Off/On row over the recent calls (#archiveList) is gone, and its switch is now a compact one — "Request archive" and a switch, role=switch — at the right of the line over the Usage page's request list, beside the requests' sum. It posts settings/archive as before; what it keeps and where, or that Sync needs an s3:// address first, is its tooltip; a refusal still goes to the footer and leaves it off; a failed last upload says "Upload failed" beside it (上传失败, the error in its tooltip). It waits for the gateway's state rather than say Off before it is in, and is changed in place on a redraw, so a switch being pressed is never swapped out (the ledger and providers both redraw it). request-archive.test.cjs now checks the Gateway page has no switch and a kept call no archive panel and no Fetch from archive, and the switch at the list's top right on the sum's line: refused with no bucket, on with one (bucket in its tooltip), off, and a failed upload said; web-page.test.cjs finds the switch on the Usage page. Playwright in Chromium and WebKit: request-archive, request-archive-usage, web-page, click-scroll, usage-ledger, usage-ledger-detail, gateway-fold, sse-body pass (72/72). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gui/ pass.
2026-10-02 13:23:41 +08:00
gui: a Codex account's Auto-use of its banked resets is a standing setting on its card, held resets or not, and says both times it uses one (#719, thedavidweng: the Usage card's Auto-use only showed with resets held, said only "when the week is used up" while the use before a reset expires (#624) was in the tooltip alone, was hidden in the multi-account brief rows, and zh called both the window's 额度重置 and the credit 重置) — every named Codex (ChatGPT) account's full card, on the Usage page and in the menu bar panel, now has a row of its own under its windows: "Auto-use resets: when the week runs out, or before one expires" (zh 「自动使用重置卡: 周额度用完时 · 重置卡到期前」) with a switch (lib-switch, role=switch, aria-checked) that posts settings/codex-auto-reset as the old toggle did, so codexAutoReset in settings keeps every existing user's on/off; it shows with 0 resets, so it can be set ahead of one. Holding resets, the row of them keeps "Use a reset" / "Use one…" and says after the nearest expiry whether that one is protected: "· auto-used before it expires" (zh 「· 到期前自动使用」, accent) or "· not auto-used" (「· 不会自动使用」), its tooltip saying how (about 3 hours before, if the account's windows have been used) or that it is lost unless used by hand. An account in brief says it beside its name: "↺ 2 · Auto-use: on" / "Auto-use: off" (zh 「自动用卡:开/关」), its full say in the title. The switch's tooltips name both triggers, one a week at most and the five hours never using one; the status after turning it on is "{who} uses a reset by itself when its week runs out, or before one expires". zh: the credit is 重置卡 throughout (1 张重置卡, 用一张重置卡, 用一张…, 用一张 Codex 重置卡?, the routing notes, outcomes and the GLM team's 5 小时/每周重置卡), the window's renewal stays 重置. A GLM team's resets, a plugin's and an account with no name get no switch, as before. magpie quota auto-reset's help and its on/status lines say the expiry use too. codex-auto-reset.test.cjs is rewritten (window and panel, en and zh: the say's visible words with both triggers, the switch for an account holding no reset, the quick switch with resets and the kept/not kept words turning, the brief rows' off/on and resets held, posts per account, no scroll, no left border, no native select, every string's zh, the credit 张/重置卡 in zh) and fails on the old code; routing-auto-reset and zcode-site expect the new zh words; quota-curve expects the Auto-use row between the curve and the next account. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass; codex-auto-reset, routing-auto-reset, quota-curve, usage-fold, click-scroll, account-mask, agent-models and routing-served pass in webkit; the rest of internal/gui/tests fails only where a clean checkout fails too, or passes alone; zcode-site and old-webkit fail before and after the change the same way (zcode-site times out at its sign-in step before reaching the reset words). Not tried with real Codex accounts.
2026-10-04 06:35:20 +08:00
usage: a reply that came in one burst at its end tells no tok/s, and is left out of the summed speed (#731, Arcadia822: a Gemini turn through Antigravity that was one write tool call, out 8264, ms 24360, ttft_ms 24359, read 8,264,000 tok/s and lifted the Usage page's average) — a reply's speed is out ÷ (ms − ttft_ms) since #196, and the sums are Σout ÷ Σ(ms − ttft_ms) (Totals.DecodeOut/DecodeMs). codeassist hands a native functionCall over whole, so a turn whose only content is one big tool call has its first content event a millisecond or two before the end: the window covers delivery, not writing, and its tokens over next to no time read millions a second and pull the summed speed up (the test's three calls: 100 tok/s each in truth, 616 with the burst counted). A decode window now counts only when it timed the writing: usage.DecodeWindow gives 0 when it is under MinDecodeMs (100 ms) or the tokens over it would be past MaxDecodeSpeed (10,000 tok/s), both far from any real stream (the fastest vendors write a few thousand a second); Totals.add (records) and Totals.addRow (ledger rows, which the Usage page sums) both go through it. Such a reply still counts as timed and its TTFT in the mean; only its speed is left out, so a model with only bursts shows no tok/s (the Usage page and the CLI already skip a speed with no decode window). The Routing page's story, which works out a try's speed itself (routing.js speedOf), applies the same bounds, so the burst says "first token in 24 s" and no tok/s. app.js's figure is decode_out ÷ decode_ms from the Go sums and needed no change. Tests: TestBurstTellsNoSpeed (the report's record with two 100 tok/s calls: 100 tok/s summed through summarize and addRow, TTFT mean keeps the burst; DecodeWindow's edges: 1 ms, 0 ms, 3 tokens in 50 ms, 8264 in 500 ms, exactly 10,000 tok/s over 100 ms, nothing written, not streamed) fails on the old rule with 616 tok/s; TestSummarizeTimesFirstTokens unchanged. routing-burst-speed.test.cjs (webkit, en and zh: the report's burst and a 500 ms one tell no speed, 100 and 50 tok/s replies keep theirs, picking a request doesn't move the page) fails on the old routing.js with "8264000 tok/s". routing-sessions, routing-served, routing-effort-change, usage-accounts, panel-usage and click-scroll GUI tests pass in webkit (routing-served failed once in a parallel run and passes on its own twice; its fixture has no TTFT). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with the reporter's ledger or a live Gemini stream; a short reply under 100 ms (a title, a few tokens) no longer counts toward the speed either, which moves a summed figure by next to nothing.
2026-10-04 11:28:50 +08:00
routing: a failed request whose route has no order opens when clicked (Discord, mythfish on v0.1.810: on the Routing page's 实时 list, clicking "11:28:08 AM • workbuddy-ai/deepseek-v4.1-flash • 400 · 失败 6.5 秒" did nothing; the request was refused with WorkBuddy AI 400 {"message":"WorkBuddy AI: Invalid request parameters","type":"invalid_request_error"}) — Route.Order has no omitempty and the trace's snapshots (Trace, the saved copy in update, sessionLatest) copied it with append([]Weighed(nil), r.Order...), so a route begun with no seats weighed reached the page as "order": null, while its tries were there. routing.js reads r.order as a list throughout: a row's click runs pick → sync → rebuild, whose setOf(r) is r.order.map(seat), and renderActs runs r.order.forEach, so pick threw (webkit: "null is not an object (evaluating 'r.order.map')") before the story was drawn and the click looked dead. Reproduced with a live trace fixture: a 400 with order null fails to open; the same 400 with order [] or a one-seat order, every failure kind, and an order whose seats don't match the try all open, so it is the null and not the 400 or the WorkBuddy error. The page now fills in a route's lists where routes come in (whole: order, tries and left default to []), for the live trace, a kept day's history and openRoute's fetched route (the tray panel's and Sessions' links), and the gateway writes Order as [] when it is empty in all three snapshots, so an older page reading a newer gateway is fine too. Tests: routing-failed-open.test.cjs (webkit, en and zh, live and a kept day: a request that answered and the report's 400 with "order": null; clicking the 400 presses its row, shows the story "How the request at … was routed" / "… 的请求是怎么路由的" with "It said: WorkBuddy AI: Invalid request parameters" / "原话:…", doesn't move the page, and the answered one opens again after; no page errors) fails all four on the old routing.js and passes; TestTraceOrderIsAList (a route begun with no order and refused with a 400: Trace's JSON has "order":[], sessionLatest's copy a non-nil Order) fails on the old trace.go with "order":null and passes. The routing-*.test.cjs and click-scroll GUI tests pass in webkit (routing-time's zh case failed once in a 4-way parallel run and passes on its own). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not found: which gateway path began the reporter's route with no order — the plain chat path weighs at least the provider it was asked for — so the fix is to the shape, not to that path; not tried with the reporter's WorkBuddy AI account. The 400 itself (WorkBuddy refusing the request's parameters) is a separate question this doesn't touch.
2026-10-04 12:23:43 +08:00
routing: Weekly pace and Smart put first a Codex account whose allowance an auto-used reset is about to wipe (#717, #718, thedavidweng: with resets' Auto-use on, account A — 80% of its week left, renewing in five days, a reset running out in four hours — went behind B with 50% left renewing in two, and an hour later the reset spent before it ran out started A's windows again, its 80% lost) — since #624 a reset about to run out is spent by itself resetExpiryLead (3h) before it does, when the account's windows have been used, so for such an account that moment is when what it has left is lost, as at its own reset. The time is now one helper, expiringResetSpent (codex_expiring.go), which the spending (check) and routing both go by, so the lead isn't kept twice; resetRunsOut says whether it counts, mirroring check: auto-use on for the account (AutoResets, Codex only), a reset held (Count > 0) that runs out, and its windows used (windowsUsed, on-demand aside). Allowances and lastAllowances mark each window of such an account with the reset's expiry (Limit.ResetRunsOut); Allowance.Restarts is when it is spent. Allowance.Pace takes the hours until then in place of the hours until the window renews when that is sooner (a five hours alone too), and due is then that time; Allowance.Renewal, which Smart ranks by, takes it for each window's renewal when sooner. Auto-use off, no reset, a reset that runs out after the week renews or has run out: nothing changes. The 90%/98% tiers, the learning-first rule, the pace bands and session affinity are untouched. The trace gains dueBy "reset" (pace's due came from the reset) and restarts (Smart's first renewal is the reset); the Routing page's story says so — "Weekly pace: {who} has the most remaining allowance per hour until one of its Codex resets about to run out is used by itself — …" and Smart's "its allowance starts again soonest — one of its Codex resets about to run out is used by itself in {d} …", with zh (the GUI has no ja strings); its cmpRenews goes by the same renewals. TestPaceUntilAutoUsedReset (with/without a reset, week sooner, within the lead, run out, five hours alone), TestResetRunsOutForRouting (auto-use off/on, none held, count none, never runs out, windows unused, on-demand only, another vendor, and through Allowances) and the gateway's TestResetSpentIsTheDeadline (pace and Smart: the issue's case, no reset, week sooner, the 90% and 98% tiers kept, both holding one, the trace) fail with Restarts returning zero and pass; TestResetSpentBeforeItRunsOut passes unchanged on the refactored check. routing-reset-deadline.test.cjs (en, zh: pace and Smart told by the reset, and by the week as before) fails on the old routing.js and passes in webkit; the other routing GUI tests pass, except routing-manual and routing-time, which fail the same on the old assets. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real accounts holding a reset about to run out.
2026-10-04 06:21:49 +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
zcode: a Start Plan request goes as the ZCode app sends it, its system prompt and headers (#425; the user allowed mimicking ZCode's client, for this vendor only) — zcode.z.ai turns a Start Plan (体验套餐) request without ZCode's own system prompt away with 405 "request has been blocked due to unusual activity", code 3012, as zcode2api found (a137460387/zcode2api src/upstream/system-prompt.js, zcode-plan.js; dengyie/zcode2api app/body_transform.py), and magpie sent the agent's request as it was. A Start Plan /v1/messages request now carries ZCode 3.11's three system blocks, each cached (its opening line; its identity, harness and desktop context; "\n\n" and its dynamic sections around an environment section that leaves the working directory and OS version unknown), the agent's own system after them, and the day in a <system-reminder> before the first user turn unless one is there; ZCode's blocks take three of the four cache breakpoints, so the agent's system and tools give up theirs and only its last in the messages is kept. Model, max_tokens and the rest of the body go as sent (numbers as written). Its headers are ZCode's as zcode2api's headers.js has them: User-Agent ZCode/3.14.3 ai-sdk/anthropic/3.0.81, X-ZCode-Agent glm, X-Release-Channel production, X-Os-Category, X-Client-Language and X-Client-Timezone from the machine, a fresh X-Request-Id and X-ZCode-Trace-Id, X-ZCode-Session-Type main, no X-Session-Id or X-Query-Id, beside the X-Title, X-Platform, HTTP-Referer and X-Device-Mid sent before. The GLM Coding Plan and team seats are unchanged. The 405 hint no longer says magpie won't pretend to be ZCode: a block that still happens is the network blocking the IP or ZCode checking something new. No ZCode account was signed in; TestZCodeStartSentAsTheApp (httptest) fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; routing-zcode-blocked, click-scroll, old-webkit pass.
2026-10-02 08:20:33 +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: 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
2026-10-04 11:28:50 +08:00
2026-10-02 08:20:33 +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
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: a subscription signed in from the Add sheet leaves no blank page (#509, Sun1090: 登录这俩个之后,供应商显示空白 — WorkBuddy AI and Qoder CN) — a sign-in done opens the account's editor and sets adding = false, so the redraw hides the Add sheet at once instead of rolling it up through closeAddSheet, which is the only place that let go of the page's hold on a click in the sheet and of the room kept for it (#433). A sign-in that finishes within the four seconds a click is held (Qoder CN's "Sign in anyway", an app already signed in) kept the page held where the sheet had been: hold() made room under a list that now ended far above, and closing the editor uncovered an empty page, Add provider and the list scrolled out of sight above it, until the reader scrolled back. renderAdd now does what closeAddSheet does whenever a redraw puts away a sheet that was open (a sign-in done, an import of an account, Import apps, a provider action): the hold on the providers view goes and its room with it. signin-closes-sheet.test.cjs (ten keyed providers, Qoder CN signed in from the sheet with its risk accepted and done at the first poll, the editor closed: no .view-room is left, the new row is in sight, nothing empty under Add provider; Chromium and WebKit, English and Chinese) fails without the change (a .view-room left, 414px in a probe). add-button, add-sheet, rollup-short, deprecated-subscriptions, account-return, factory-sub, claude-risk and click-scroll .test.cjs pass. Not seen with the reporter's own data: their screenshot is consistent with this, not proven to be it. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 13:35:16 +08:00
signin: Grok and Command Code sign in from magpie in Docker (xugui on Discord: docker版登不上grok和command code) — Grok's sign-in first installs Grok Build with its installer, curl … | bash, and the image has neither bash nor curl, so it stopped at "installing Grok Build failed (exec: "bash": executable file not found)"; the community Grok plugin, beside the built-in rather than moved onto, wasn't given the CLI at all and said "install Grok Build first". Where no shell and curl/wget can run, Grok Build is now installed as its installer does, from the same x.ai/cli (then its storage bucket) address: the stable version, its binary for the platform, checked with --version, linked as ~/.grok/bin/grok and agent, installer = "internal" noted in its config.toml; the plugin's sign-in installs it first too. The device sign-in itself needs nothing back from the browser and was fine. Command Code's Studio posts the key to magpie's 127.0.0.1 port in the background, which a browser outside the container can't reach and no address carries; while it waits, its sign-in now takes an API key made on commandcode.ai/settings/keys and pasted (as Command Code's own CLI offers), checked with whoami and its plan as Studio's was, and a plugin's browser sign-in offers "Use an API key instead", its own api way (the Command Code plugin has one). No identity, header or prompt changes. TestGrokInstallsWithoutShell, TestCommandCodeSignInPastedKey and TestGrokPluginSignInInstallsCLI fail without the change, signin-paste-key.test.cjs too. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; signin-paste-key, signin-paste-codex, plugin-signin, signin-callback, commandcode-risk, plugin-key-hint, signin-link, click-scroll, old-webkit pass.
2026-10-01 22:25:39 +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: 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: 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: a restart to update waits until the gateway is idle instead of cutting off what the agents have in flight (#577: 正式版更新时保持网关请求与工具链不中断 — a Codex reply streaming through magpie, or a tool chain under way, was cut off by Restart to update) — the app serves the gateway in its own process and a restart to update is install, quit, relaunch, so whatever was in flight ended with it: a stream mid-reply, and a Claude Code turn of the subscription bridge waiting on its caller's tool results, whose Claude Code lives only in this process. The gateway now says what a restart would cut short (Server.Busy): its requests in flight (all but GETs and the bridge's own MCP callbacks), the bridge's runs parked on tool results, and when the last request ended. Restart to update (Settings → Version, the header's Update pill, the tray's item) restarts at once only when nothing is in flight, no turn waits on tools and nothing ended in the last 10 s (a tool chain's gap, the agent running a tool between two requests); otherwise it waits, saying what for: Settings shows "restarts once the gateway is idle · 2 in flight · 1 awaiting tool results" (网关空闲后自动重启 · 2 个请求进行中 · 1 个工具调用待返回) with Restart now and Cancel, the pill reads "Waiting to update" (等待更新) and its next click (not a double click's second half) restarts now, the tray item becomes "Restart Now to Update (3 in flight)" (立即重启以更新(3 个进行中)). The wait asks the feed again once idle, so versions out meanwhile collapse into the newest, downloaded before the restart; it gives up after an hour, saying so, the update then going in at the next restart or quit. A gateway served by another magpie (magpie serve, an older one) is not this one's to wait for. Handing the gateway over to the new version instead (the dev handover, e0c6892) is not used for releases: it rests on SO_REUSEPORT, which Windows lacks, and the new app would have to start beside the old one (single-instance, the Mac's bundle swapped under it). TestBusy (a stream counts until it ends, a GET never, a parked bridge run until it is answered or ends), TestRestartToUpdateWaitsForTheGateway (fails without the change: the click restarted with 2 requests in flight), TestRestartWhenIdle (not while streaming, parked or in a tool chain's gap; then once; an hour's wait gives up), TestRestartWhenIdleIntoTheNewest (0.1.11 staged, 0.1.12 and 0.1.13 out while waiting: restarted into 0.1.13) and TestTrayRestartNow are new; update-wait.test.cjs (WebKit, en and zh: what is in flight, waiting with Restart now and Cancel, counts following the gateway, the hour's give-up, the pill's wait and restart now, an idle click restarting at once, no click scrolling, no left border) fails without the change and passes with it. update-hide, update-check, sync-update, update-auto and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside; passes on rerun).
2026-10-02 23:36:43 +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
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
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
routing: a group can let a member go that hasn't begun answering after a set time and ask the next, and a request's detail splits its first token into magpie's time and the vendor's (Chen on X: answers through magpie felt slow, with no way to tell whether magpie or the vendor was the wait) — a group's editor has a Slow to start row (Wait, 30 s, 1 min, 2 min; Wait, the default, keeps today's behaviour). With a time set, a streamed request to a member that has sent no text, reasoning or tool call by then is cancelled before any of its bytes reach the agent (the held start is dropped; keepalives go on meanwhile) and the next member is asked; the try is marked slow ("slow to start", narrated in the route), and the member doesn't rest, since nothing is wrong with it. The last member left is always waited for. The card says "Next after 1 min without a first token". Each request now records when it went to the vendor (httptrace WroteRequest, sent_ms), and its detail shows In magpie and Vendor's first token beside First token; after a failover In magpie includes the time spent on the members let go. With the owner's real DeepSeek key in a sandbox, a group [a local member that sends its role chunk then stalls 60 s, deepseek-flash] with 3 s: the answer came from DeepSeek in 4.4 s, none of the stalled member's stream reached the client, its try read slow / "no first token in 3s" with no rest, and the ledger had ttft 3622, sent 3003. TestGroupFirstTokenFailsOver (failover, the last one waited for, one in time kept; passes -race) and group-first-token.test.cjs and usage-ledger-detail.test.cjs (WebKit, en and zh) fail on the old code. group-from-model-add's narrow-window case fails the same on origin/main; Chromium doesn't launch in this sandbox. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn flaked once, passes alone).
2026-10-04 19:33:55 +08:00
gui: the request archive's switch sits over the Usage page's requests, and the Gateway page's recent calls no longer fetch from the archive (Jorben on Discord: 网关页的最近请求下遗留了取回归档和预览的逻辑,和最近请求的预览能力重叠了,建议移除;以及把是否启用请求归档开关也挪到用量页请求列表的右上方) — since #447 a request's archive is read back and previewed from its row on the Usage page, so the Gateway page's recent calls showed two previews of one call: their own bodies, and under them the archive's copy with "Fetch from archive" and "Download". A recent call now shows only its own request and response bodies; archivePanel and what it uses stay for the Usage page. The archive's Off/On row over the recent calls (#archiveList) is gone, and its switch is now a compact one — "Request archive" and a switch, role=switch — at the right of the line over the Usage page's request list, beside the requests' sum. It posts settings/archive as before; what it keeps and where, or that Sync needs an s3:// address first, is its tooltip; a refusal still goes to the footer and leaves it off; a failed last upload says "Upload failed" beside it (上传失败, the error in its tooltip). It waits for the gateway's state rather than say Off before it is in, and is changed in place on a redraw, so a switch being pressed is never swapped out (the ledger and providers both redraw it). request-archive.test.cjs now checks the Gateway page has no switch and a kept call no archive panel and no Fetch from archive, and the switch at the list's top right on the sum's line: refused with no bucket, on with one (bucket in its tooltip), off, and a failed upload said; web-page.test.cjs finds the switch on the Usage page. Playwright in Chromium and WebKit: request-archive, request-archive-usage, web-page, click-scroll, usage-ledger, usage-ledger-detail, gateway-fold, sse-body pass (72/72). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gui/ pass.
2026-10-02 13:23:41 +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
gui: a Codex account's Auto-use of its banked resets is a standing setting on its card, held resets or not, and says both times it uses one (#719, thedavidweng: the Usage card's Auto-use only showed with resets held, said only "when the week is used up" while the use before a reset expires (#624) was in the tooltip alone, was hidden in the multi-account brief rows, and zh called both the window's 额度重置 and the credit 重置) — every named Codex (ChatGPT) account's full card, on the Usage page and in the menu bar panel, now has a row of its own under its windows: "Auto-use resets: when the week runs out, or before one expires" (zh 「自动使用重置卡: 周额度用完时 · 重置卡到期前」) with a switch (lib-switch, role=switch, aria-checked) that posts settings/codex-auto-reset as the old toggle did, so codexAutoReset in settings keeps every existing user's on/off; it shows with 0 resets, so it can be set ahead of one. Holding resets, the row of them keeps "Use a reset" / "Use one…" and says after the nearest expiry whether that one is protected: "· auto-used before it expires" (zh 「· 到期前自动使用」, accent) or "· not auto-used" (「· 不会自动使用」), its tooltip saying how (about 3 hours before, if the account's windows have been used) or that it is lost unless used by hand. An account in brief says it beside its name: "↺ 2 · Auto-use: on" / "Auto-use: off" (zh 「自动用卡:开/关」), its full say in the title. The switch's tooltips name both triggers, one a week at most and the five hours never using one; the status after turning it on is "{who} uses a reset by itself when its week runs out, or before one expires". zh: the credit is 重置卡 throughout (1 张重置卡, 用一张重置卡, 用一张…, 用一张 Codex 重置卡?, the routing notes, outcomes and the GLM team's 5 小时/每周重置卡), the window's renewal stays 重置. A GLM team's resets, a plugin's and an account with no name get no switch, as before. magpie quota auto-reset's help and its on/status lines say the expiry use too. codex-auto-reset.test.cjs is rewritten (window and panel, en and zh: the say's visible words with both triggers, the switch for an account holding no reset, the quick switch with resets and the kept/not kept words turning, the brief rows' off/on and resets held, posts per account, no scroll, no left border, no native select, every string's zh, the credit 张/重置卡 in zh) and fails on the old code; routing-auto-reset and zcode-site expect the new zh words; quota-curve expects the Auto-use row between the curve and the next account. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass; codex-auto-reset, routing-auto-reset, quota-curve, usage-fold, click-scroll, account-mask, agent-models and routing-served pass in webkit; the rest of internal/gui/tests fails only where a clean checkout fails too, or passes alone; zcode-site and old-webkit fail before and after the change the same way (zcode-site times out at its sign-in step before reaching the reset words). Not tried with real Codex accounts.
2026-10-04 06:35:20 +08:00

Dropdown browser regression

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-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.

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/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. It uses isolated API fixtures. Run with node --test internal/gui/tests/purpose-filter.test.cjs and the Playwright environment described below.

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.

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-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.

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.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.

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.

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.

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.

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.

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.

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.

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. 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

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.

sync-update.test.cjs checks the header's refresh button looks for a newer magpie too: one click, one check, and the Update pill shows what it found. Its tooltip says so. 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-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.

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.

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.

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-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-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.

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-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 are never picked automatically and why. 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. English and Chinese, Chromium and WebKit, with the API faked.

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.

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-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

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