Files
magpie/internal/gui/tests
1927f82c01 Fix model reasoning-level list jumping to top (#357)
* fix(gui): keep provider model list position after level change

* gateway: a Claude Code run whose subagent ended on StructuredOutput is let go, and runs left waiting on tool calls are bounded (#345) — a workflow's subagents end on StructuredOutput, which the client answers itself, so the bridge's claude -p waited 30 minutes for a result that never came, one process per subagent. Such a run now waits 30s (for a schema error the client sends at once) and ends; of other runs waiting 5 minutes or more on their caller's tools, 16 are kept, the longest waiting let go first (one that then gets its results starts anew with the whole conversation). TestClaudeRunEndsAfterStructuredOutput and TestClaudeParkedRunsAreBounded fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.

* plugins: a built-in moved onto its plugin looks and works as it did (你确保插件版的订阅 provider 和其对应的内置的…都保持一模一样了吗?) — moved onto its plugin, a built-in lost pieces: its tile's name, icon, plans and risk note went to the plugin's own, "Add account" still opened the built-in's sign-in (in the app, on the server and in `magpie accounts add`), an agent's own sign-in the built-in still found showed a second usage card of the same account, the account's plan badge went, WorkBuddy's daily check-in stopped, the provider's icon and site were the plugin's, WorkBuddy AI's card was named WorkBuddy, and Devin, which had no card of its own, lost the plugin's. Now a moved built-in keeps its tile and risk sheet and signs in through its plugin; its cards take the built-in's place and name, and it has none of its own; the plan the plugin tells stays with the account; WorkBuddy checks in through the plugin, which signs the call; a card is left out only where its built-in's slot showed it. TestMovedBuiltinQuiet, TestWorkBuddyCheckinThroughPlugin, TestMoveToPlugin (Devin-like card) and TestPluginUsage (plan kept) fail without the change. Command Code and Devin moved and back in a sandbox HOME: one card each, plan, name. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-move, plugin-signin, commandcode-risk, old-webkit pass; click-scroll's WebKit fails the same on origin/main under this load.

* provider: the Providers and Gateway pages no longer wait on `claude --version` (deluxe on Discord: on Omarchy the two pages never loaded) — on Omarchy `claude` is a mise wrapper that installs or updates Claude Code before it answers, which on a slow network takes minutes or never ends; magpie asked it with no limit and with a lock held, so /api/providers (fetching the Claude account's models) and every Claude request waited with it. The CLI is now asked in the background, given a minute, and a caller waits at most 3s before the version it has stands in; a late answer is taken once it comes. Reproduced on an Omarchy VM (/api/providers unanswered after 100s on v0.1.516, 3.7s with the change). TestClaudeVersionDoesNotWaitForAStalledCLI and TestClaudeVersionTakenWhenItComes fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.

* plugins: a plugin's account behaves as its built-in's (the user: 插件版的订阅 provider 和内置的…保持一模一样了吗) — a plugin's failed sign-in now says why ("the sign-in failed: the sign-in page expired") instead of a bare failure; a plugin account the provider refuses with 401 is marked lapsed, "<user>'s <provider> sign-in has expired — sign in again", beside the account and on its usage card as a built-in's is, and the mark goes when it answers again or is signed in anew; a Devin or Command Code account taken from the agent's CLI moves to the plugin marked as the CLI's and back as the agent's own, so the one it follows stays the one the CLI is signed in to. TestPluginProvider (lapsed), TestMoveSideState (cli) and TestPluginSignIn (reason) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.

* agents: "Hide all" in an agent's model list keeps its line, and an uninstalled Gemini CLI goes from the Agents page (#356, #230) — the line under an agent ("Showing 0 / N models") came only when a picker still listed one of magpie's models, so hiding them all took away the one way back; it now stays while any is hidden. Gemini CLI was taken as present from what it leaves in ~/.gemini (settings.json, .env, installation_id…), which stays after it is uninstalled, so its row stayed, offering to set up a CLI that can't run; it is now its gemini binary. TestAgentModelLineAllHidden and TestGeminiNotAntigravity fail without the change; agent-models-all-hidden.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/agent ./internal/gui ./internal/library ./internal/provider pass; agent-models, click-scroll, old-webkit pass.

* gateway: Codex's Responses Lite tools reach SuperGrok and every other backend, and a group moves on when a member can't read a request (#350) — Codex sends a Lite model's tools as an additional_tools input item, its own functions in a "functions" namespace; magpie relayed the item to xAI, which 422s on it (unknown item type), and translated routes (Cursor, Anthropic, Chat…) dropped it, so the model had no tools. Outside OpenAI the item's tools now go in tools, the functions namespace opened into plain tools and earlier calls un-namespaced; OpenAI and the ChatGPT account still get Codex's own form. A 400/422 over the request's shape (failed to deserialize, unknown item type/parameter…) now moves to the group's next member, each asked once, the member not resting; the Routing page says so. TestAdditionalToolsRelayed, TestAdditionalToolsTranslated and TestGroupFailsOverARequestShape fail without the change; routing-shape.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, routing-wb-refused pass.

* agents: omp's roles pick their own models (subagents, smol, slow), keep their thinking level, and get back what they had before magpie (#325)

* agents: omp's subagents and small role pick their own models, through magpie too

* agents: omp keeps a role's thinking level and the model it had before magpie, and offers the slow and designer roles

A role set to "anthropic/claude-opus-5:max" and switched to a magpie model lost its ":max", and resetting it deleted the key rather than putting the user's model back. The level now goes with the new model (omp clamps one the model lacks, never fails), the model the role had before magpie is stashed and restored, and the reviewer's @slow and the designer's @designer roles are fields beside subagents and small.

* gui: omp's slow and designer roles follow the model, as its subagents do

Unset, both roles take the session's model, yet the picker offered them omp's Default and no typed model. They are now squares beside subagents, with Same as model first and a model typed in taken as it is.

* agents: omp's thinking level no longer hides a role's model from magpie, and the model alone clears it — magpie omp magpie/x/y:high is taken, a role moved back to omp's own is drift, a list of models comes back on reset, and designer (gone in omp 18.1.5) goes

A role on "magpie/deepseek/pro:max" matched the picker only through the
option offered for its level beside the model, and only for the value in
the file now. So the CLI refused a model typed at a level, a recorded
value at a level the current one didn't share wasn't known for magpie's
(no "replaced" drift), and a level could never be taken away: a model
picked without one took the old one on.

An agent may now say what it keeps after the model (Agent.SplitSuffix;
omp's is its level). Drift, Reseat, RenameProvider, Spell and the CLI's
field-for-value match the model and put the suffix back after the one
they move to; agents without it are unchanged. The option at the level
stays: it keeps the level. Another model keeps it too; the same model
picked alone clears it.

A role omp falls back through (a YAML list, or the comma list omp joins
it into, settings.ts modelRoleValueFromUnknown) read as nothing, so it
wasn't stashed and a reset deleted it. It reads as that comma list now,
is stashed and comes back; a list has no level of its own.

omp 18.1.5 removed the designer role (CHANGELOG), so its field goes. The
smol role, like task and slow, gives its agents the session's model when
unset; its field is labelled smol, as omp names it, so the GUI can show it
as the others without touching other agents' "small".

* gui: omp's smol role follows the model as its subagents and slow role do, and designer goes

smol unset gives omp's scout and sonic the session's model, so it is a
square after the pickers with "Same as model" first, as subagents and
slow are (a feather). Its label is smol, not small: OpenCode's, Crush's
and OpenChamber's small model stays a picker. designer, a role omp 18.1.5
removed, leaves FOLLOWS_MODEL, the glyphs and i18n.

* agents: the note on omp's fallback chains says their keys are roles, and from omp 18 models too

ompRefKeys' comment had retry.fallbackChains keyed by model. omp 16.3.5 takes only a role's name there (session/agent-session.ts: "a mapping of role names to selector arrays"); omp 18.4.4 takes a role's name or a model selector (session/retry-fallback-chains.ts). Walking the keys stays: a role's name is no magpie model and is passed over. Follow-up to the review of #340.

* agents: omp reads whether it is on magpie one way, and a role at a level that a fallback chain names too is renamed once in each

Rebased on #340, one scan over omp's config (ompRefs) answers both questions: magpie's provider stays in models.yml while anything under ompRefKeys is on magpie, as #340 has it, and Check looks at the wiring while any role under modelRoles is, as this PR had it; a fallback chain or enabledModels alone keeps the provider without being checked.

A provider renamed moves a role at a thinking level through SplitSuffix first, then RenameRefs the rest, which finds no old name left in the roles: each place moves once, the level kept.

* agents: a fallback chain alone on magpie has omp's wiring checked, and a role set off magpie mends it

Check now reads the same keys dropMagpie does (ompRefKeys: every role, retry.fallbackChains, enabledModels, task.agentModelOverrides): whatever keeps magpie's provider in models.yml has its baseUrl checked, so a chain on magpie whose wiring something else moved no longer fails its fallback silently. Applying again sets the model role, which may be omp's own; a role set off magpie while something else still names it now writes magpie's provider afresh instead of leaving it as it is, so that mends the wiring. Review of #325.

* agents: an omp role that is a list of models stays the user's own, even starting on one of magpie's, and comes back as it was written

A role omp falls back through ([magpie/deepseek/flash, openai/gpt-6], or the same as a comma-separated string) was taken for one of magpie's models because it started with magpie/: magpie taking the role over stashed nothing, and a reset deleted it. Only one model, no comma, is magpie's now: SplitSuffix answers whether a value is one model, and Spell, drift (magpieValue), Reseat, RenameProvider and the CLI's fieldForValue leave a list alone. The stash keeps a YAML list as the YAML it was written in (edit.GetYAMLText), and a reset puts it back so, flow or block; a comma-separated string comes back a string. A provider renamed moves the names inside a list through RenameRefs, once, and in the lists the stash keeps too. Review of #325.

* gui: a test holds omp's subagents, smol and slow roles as squares that follow the model, and OpenCode's small model a picker

omp-roles.test.cjs, in Chromium and WebKit, English and Chinese, API faked: the three roles are squares after the model's picker, unset named "same as model" (「同主模型」) and set naming their model; smol's picker opens on "Same as model" with the model beside it, and a pick sends it and shows it. OpenCode's small model stays a picker. With smol and slow taken out of FOLLOWS_MODEL it fails at "small is a square". Review of #325.

* gateway: a Claude Code run whose subagent ended on StructuredOutput is let go, and runs left waiting on tool calls are bounded (#345) — a workflow's subagents end on StructuredOutput, which the client answers itself, so the bridge's claude -p waited 30 minutes for a result that never came, one process per subagent. Such a run now waits 30s (for a schema error the client sends at once) and ends; of other runs waiting 5 minutes or more on their caller's tools, 16 are kept, the longest waiting let go first (one that then gets its results starts anew with the whole conversation). TestClaudeRunEndsAfterStructuredOutput and TestClaudeParkedRunsAreBounded fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.

* plugins: a built-in moved onto its plugin looks and works as it did (你确保插件版的订阅 provider 和其对应的内置的…都保持一模一样了吗?) — moved onto its plugin, a built-in lost pieces: its tile's name, icon, plans and risk note went to the plugin's own, "Add account" still opened the built-in's sign-in (in the app, on the server and in `magpie accounts add`), an agent's own sign-in the built-in still found showed a second usage card of the same account, the account's plan badge went, WorkBuddy's daily check-in stopped, the provider's icon and site were the plugin's, WorkBuddy AI's card was named WorkBuddy, and Devin, which had no card of its own, lost the plugin's. Now a moved built-in keeps its tile and risk sheet and signs in through its plugin; its cards take the built-in's place and name, and it has none of its own; the plan the plugin tells stays with the account; WorkBuddy checks in through the plugin, which signs the call; a card is left out only where its built-in's slot showed it. TestMovedBuiltinQuiet, TestWorkBuddyCheckinThroughPlugin, TestMoveToPlugin (Devin-like card) and TestPluginUsage (plan kept) fail without the change. Command Code and Devin moved and back in a sandbox HOME: one card each, plan, name. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-move, plugin-signin, commandcode-risk, old-webkit pass; click-scroll's WebKit fails the same on origin/main under this load.

* provider: the Providers and Gateway pages no longer wait on `claude --version` (deluxe on Discord: on Omarchy the two pages never loaded) — on Omarchy `claude` is a mise wrapper that installs or updates Claude Code before it answers, which on a slow network takes minutes or never ends; magpie asked it with no limit and with a lock held, so /api/providers (fetching the Claude account's models) and every Claude request waited with it. The CLI is now asked in the background, given a minute, and a caller waits at most 3s before the version it has stands in; a late answer is taken once it comes. Reproduced on an Omarchy VM (/api/providers unanswered after 100s on v0.1.516, 3.7s with the change). TestClaudeVersionDoesNotWaitForAStalledCLI and TestClaudeVersionTakenWhenItComes fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.

* plugins: a plugin's account behaves as its built-in's (the user: 插件版的订阅 provider 和内置的…保持一模一样了吗) — a plugin's failed sign-in now says why ("the sign-in failed: the sign-in page expired") instead of a bare failure; a plugin account the provider refuses with 401 is marked lapsed, "<user>'s <provider> sign-in has expired — sign in again", beside the account and on its usage card as a built-in's is, and the mark goes when it answers again or is signed in anew; a Devin or Command Code account taken from the agent's CLI moves to the plugin marked as the CLI's and back as the agent's own, so the one it follows stays the one the CLI is signed in to. TestPluginProvider (lapsed), TestMoveSideState (cli) and TestPluginSignIn (reason) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.

* agents: "Hide all" in an agent's model list keeps its line, and an uninstalled Gemini CLI goes from the Agents page (#356, #230) — the line under an agent ("Showing 0 / N models") came only when a picker still listed one of magpie's models, so hiding them all took away the one way back; it now stays while any is hidden. Gemini CLI was taken as present from what it leaves in ~/.gemini (settings.json, .env, installation_id…), which stays after it is uninstalled, so its row stayed, offering to set up a CLI that can't run; it is now its gemini binary. TestAgentModelLineAllHidden and TestGeminiNotAntigravity fail without the change; agent-models-all-hidden.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/agent ./internal/gui ./internal/library ./internal/provider pass; agent-models, click-scroll, old-webkit pass.

* gateway: Codex's Responses Lite tools reach SuperGrok and every other backend, and a group moves on when a member can't read a request (#350) — Codex sends a Lite model's tools as an additional_tools input item, its own functions in a "functions" namespace; magpie relayed the item to xAI, which 422s on it (unknown item type), and translated routes (Cursor, Anthropic, Chat…) dropped it, so the model had no tools. Outside OpenAI the item's tools now go in tools, the functions namespace opened into plain tools and earlier calls un-namespaced; OpenAI and the ChatGPT account still get Codex's own form. A 400/422 over the request's shape (failed to deserialize, unknown item type/parameter…) now moves to the group's next member, each asked once, the member not resting; the Routing page says so. TestAdditionalToolsRelayed, TestAdditionalToolsTranslated and TestGroupFailsOverARequestShape fail without the change; routing-shape.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, routing-wb-refused pass.

---------

Co-authored-by: yetone <yetoneful@gmail.com>

* plugins: a plugin's models show as the built-in's did (插件版…保持一模一样) — a model a plugin says its plan serves at no cost (WorkBuddy's x0.00 credits) is marked free, as the built-in marked it; Cursor's plugin models not named 1M hold what the catalog knows their base to (GPT-4o 128K, Kimi K2 131K), not Cursor's 200K, as the built-in's list; ZCode's own account moves marked as ZCode's, for the plugin (0.1.2) to read ZCode's own sign-in for each request and follow its switches, and comes back as ZCode's own with nothing saved. TestPluginUsage (free), TestCursorPluginContext and TestZCodeMover fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.

* agents: Claude Code's effort reaches Opus 5.5 and Sonnet 5.5, follows each model's levels, and gets an ultracode switch (#351, #352, #353, #354) — Claude Code 2.1.251 on reads Opus 5.5's, Sonnet 5.5's and any newer Claude's effort only from modelSettings.<model>.effortLevel, so the top-level effortLevel magpie wrote left them at medium; it now writes modelSettings for the model (and the top key for the models that still read it), and a reset takes out only its own level. The levels offered follow what Claude Code 2.1.285 sends each model: Opus 4.5 low–high, Opus/Sonnet 4.6 without xhigh, Haiku 4.5, Sonnet 4.5 and older none; a level a model doesn't take is refused. A new ultracode square writes "ultracode": true, on models taking xhigh, and magpie claude ultra no longer writes "ultra" as the model. An open session keeps the effort it started with, so a change says to restart it while Claude Code runs. Checked against real Claude Code 2.1.285 in a sandbox HOME with a fake API recording the effort sent. TestClaudeEffortModelSettings, TestClaudeEffortPerModel, TestClaudeUltracode and TestClaudeEffortNotice fail without the change; claude-effort.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; omp-roles, panel-effort, click-scroll, old-webkit pass.

* providers: a Remote magpie provider uses another computer's shared magpie, each request translated at most once (Jorben on Discord: 远程 magpie / magpie 接力) — a custom provider pointed at another magpie has one API per address and couldn't know which API each model's own provider serves, so a request was translated on both computers (Chat → Messages here, then again there). magpie's /v1/models now tells each model's native_endpoints (the APIs its provider serves it on; none for a routing group) and its image input, and reads them, with max_output_tokens and supported_reasoning_levels, from a list; the new remote-magpie preset takes the other magpie's address however typed (host:port, /v1, /v1/messages) for all three APIs and lists all its models, its user's choice already. Add sheet tile with the address field and a hint to Settings → Share on local network; CLI: magpie provider add remote-magpie <key> url=http://<ip>:3425. Checked with two sandbox magpies over the LAN address with the key check on and a Messages-only fake upstream: Chat, Messages (streamed), count_tokens, Responses, a routing group and Gemini all reached it as /v1/messages, translated once. TestRemoteMagpieNativeAPI, TestFetchMagpieFields and TestRemoteMagpie fail without the change; remote-magpie.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (1ce05f0) as well; click-scroll, old-webkit pass.

* subscriptions: Qoder CN, for accounts on qoder.cn (#349) — Qoder's China site (qoder.cn, Alibaba Cloud or phone sign-in) has its own accounts, sign-in page, account API (openapi.qoder.com.cn) and gateway (gateway.qoder.com.cn), so a qoder.cn account couldn't sign in through magpie's Qoder, which only knows qoder.com. A new Qoder CN subscription signs in there with the client id Qoder CN's own CLI sends, and everything after — polling, job token and refresh, user info, models, usage and chat — follows the site saved with the account; if qoder.cn refuses the job token, the account chats with its device token and renews it, as that CLI does. Qoder's tile is now Qoder (international), each tile saying which accounts it is for. Accounts saved before keep working on qoder.com unchanged. Not tried with a real qoder.cn account. TestSites, TestCNDeviceChatRefresh, TestQoderCNSignIn, TestQoderCNDeviceChat, TestQoderGlobalUnchanged and TestQoderCNGateway fail without the change; qoder-cn-tile.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (1ce05f0) as well; click-scroll, old-webkit pass.

---------

Co-authored-by: heyallencao <258265534+heyallencao@users.noreply.github.com>
Co-authored-by: yetone <yetoneful@gmail.com>
Co-authored-by: cyxer000 <48980829+cyxer000@users.noreply.github.com>
2026-10-01 01:48:05 +08:00
..
providers: each account of a subscription holding several can go through its own proxy — the provider's, none, or its own address (Discord, gakki: 是否可以为不同的codex账号设置不用的代理,现在是同一个供应商下使用相同的代理) — the per-provider proxy (#237) was one for all of Codex's signed-in accounts. A subscription's entry in providers.json now keeps accountProxies, by the account's name in lower case, each checked by settings.CheckProxy on save (ftp:// refused), addresses trimmed, accounts following the provider's proxy left out. Provider.ProxyChoice picks an account's own proxy, else the provider's, else the global one, so everything that already went through Provider.Via follows the account: gateway turns and fallback across accounts, usage and quota, token refresh, Codex's warm-up, reset and saved accounts' model lists, Claude's warm-up and the *_PROXY of Claude Code runs (ViaLogin / ProxyOfLogin), and Codex's own sign-in relayed as it came (ViaSignedIn). Extra accounts of Copilot, ZCode, Kiro and the like, made afresh without the provider's settings, now read them from providers.json, so they honour the provider's proxy too. A save without accountProxies keeps them; {} clears them. The provider editor, for a subscription with two or more accounts, lists each under the Proxy row: Provider's proxy | Direct | Custom, the address field's room kept so a pick moves nothing; Custom with no address is refused before anything is posted, its field focused without scrolling. en and zh together; no border stripes. Tested with internal/gateway/account_proxy_test.go in a sandbox HOME against fake proxies and a fake ChatGPT backend: three Codex accounts (own proxy, direct, following the provider) each reach the backend their own way in one turn and for usage; clearing an account's proxy sends it through the provider's, clearing the provider's through the global one, direct staying direct; ftp:// refused; TestClaudeAccountProxyEnv covers each Claude account's HTTPS_PROXY. Both fail with the per-account lookup off. account-proxy.test.cjs, Chromium and WebKit, en and zh (new), provider-proxy, click-scroll and old-webkit pass. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against a real proxy, real accounts or the real Wails window; Grok's per-account usage and Kiro's usage on a key still use the provider's proxy.
2026-09-30 04:52:03 +08:00
providers: the add sheet's rows carry no region tag, the logos stay put when a dialog opens or closes, and "Add provider" sits at the view's foot and reaches the sheet on the first click (Image #27: 国际中国还是有点像狗皮膏药; 每次打开或者关闭弹窗的时候,Provider的logo都会重新刷新一遍; Image #28: 添加供应商要固定在底栏底部…第一次点击的时候没有任何事情发生) — a vendor's merged global/China row lost its "Global · China" tag (design B1): the region is picked in the editor's Region switch and the row's title still lists each region's host. Every redraw of the Providers page made each logo anew, so each one faded in again whenever a dialog opened or closed; renderProviders now hands the logos it drew to icon() to reuse (the editor drawing its own), so they are moved, not remade. "Add provider" is sticky at the view's foot over a long list and steps aside while the sheet's head is in sight; the first click opened the sheet but WebKit clamped the scroll before it grew, and unrollSheet took that for the reader scrolling and gave up — it now starts from where the view really is, and a click with the sheet open eases the view to it. Tested with add-button.test.cjs (new: the bar at the foot at the top and the end, one click reaching the sheet, the bar away and back, a dialog opened and closed keeping every logo; fails on main) and add-sheet.test.cjs (no tag, the hosts in the title), Chromium and WebKit, en and zh; click-scroll passes; the full GUI suite passes but login-import, which fails on main under load. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass.
2026-09-30 02:26:28 +08:00
providers: the add sheet's rows carry no region tag, the logos stay put when a dialog opens or closes, and "Add provider" sits at the view's foot and reaches the sheet on the first click (Image #27: 国际中国还是有点像狗皮膏药; 每次打开或者关闭弹窗的时候,Provider的logo都会重新刷新一遍; Image #28: 添加供应商要固定在底栏底部…第一次点击的时候没有任何事情发生) — a vendor's merged global/China row lost its "Global · China" tag (design B1): the region is picked in the editor's Region switch and the row's title still lists each region's host. Every redraw of the Providers page made each logo anew, so each one faded in again whenever a dialog opened or closed; renderProviders now hands the logos it drew to icon() to reuse (the editor drawing its own), so they are moved, not remade. "Add provider" is sticky at the view's foot over a long list and steps aside while the sheet's head is in sight; the first click opened the sheet but WebKit clamped the scroll before it grew, and unrollSheet took that for the reader scrolling and gave up — it now starts from where the view really is, and a click with the sheet open eases the view to it. Tested with add-button.test.cjs (new: the bar at the foot at the top and the end, one click reaching the sheet, the bar away and back, a dialog opened and closed keeping every logo; fails on main) and add-sheet.test.cjs (no tag, the hosts in the title), Chromium and WebKit, en and zh; click-scroll passes; the full GUI suite passes but login-import, which fails on main under load. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass.
2026-09-30 02:26:28 +08:00
agents: Antigravity CLI (agy) takes magpie's models (Discord Str: asked to use agy through magpie) — agy is on the Agents page like the others. Picking one of magpie's models writes ~/.gemini/antigravity-cli/settings.json: modelProvider "gemini", an entry per magpie model under customModelsConfig.customModels (magpie/<ref> → {apiProvider: API_PROVIDER_GOOGLE_GEMINI, modelName: <ref>}) beside the user's own, and model. Every other key is kept, and "as installed" gives back the user's model and modelProvider and takes magpie's entries out (the file byte-identical to before). agy reads its endpoint and key only from GEMINI_API_KEY / GOOGLE_GEMINI_BASE_URL, and in agy 1.2.13 a custom model chosen by settings or its picker fails ("model is empty"): only --model resolves it. So the agent has a Launch command, GEMINI_API_KEY=magpie GOOGLE_GEMINI_BASE_URL=<gateway> agy --model '<model>' (a $env: form on Windows), which magpie agy prints as its notice and the row's new square copies in one click (tooltip shows it; no scroll; zh strings). Check says unwired when the entry is gone or modelProvider isn't gemini. Tested with TestAgy (set/switch/drift/restore, a user's custom model kept, a file with nothing of the user's left as it was) and agy-launch.test.cjs in Chromium and WebKit, en and zh (fails without the app.js change); end to end in a sandbox HOME with agy 1.2.13 against a sandbox gateway on 38472 and a fake OpenAI upstream: magpie agy magpie/fakeup/echo-1 wrote the settings, agy (interactive and --print) answered from the upstream through the gateway, magpie agy default restored the file. Not tested: the Windows command, a real Gemini upstream; agy's requests show in usage as google-genai-sdk (its UA), and its title requests for gemini-3.1-flash-lite-preview get a 404. go build (nogui, linux, windows) and go test -tags nogui ./... pass; the GUI suite passes but for click-scroll's WebKit case, failing already
2026-09-29 19:41:29 +08:00
provider: Azure OpenAI, asked at the user's own resource on its v1 API with the key in api-key, for Codex, Claude Code and chat clients alike, and brought in from other apps instead of left out — Azure OpenAI had no preset, and importing an Azure entry from Alma was skipped with "magpie doesn't support Azure OpenAI yet". A new Azure OpenAI preset (vendor) has no URL of its own: its editor asks for the resource's endpoint in a field above the key, focused first, with https://<resource>.openai.azure.com as the placeholder and a hint on where the portal shows it and that the model ids are the deployments' names (en and zh); adding without one says "Your resource's endpoint is needed" and sends nothing, and Save refuses one without an endpoint too. Whatever is pasted — the bare endpoint, …/openai, …/openai/v1/, a classic deployment URL with its api-version, a *.cognitiveservices.azure.com or *.services.ai.azure.com host, or only the resource's name — is saved as https://<host>/openai/v1 for both chat completions and Responses. magpie uses the v1 GA API rather than the classic /openai/deployments/<name>/…?api-version= paths: the paths match OpenAI's, the body's model is the deployment's name, and no api-version is needed (the reasoning is in internal/provider/azure.go). The key goes in api-key, never as a Bearer, which Azure takes as an Entra ID token; nothing sends Authorization or x-api-key there. Deployments named after GPT/o/codex models go to Responses first, as OpenAI's do, and others to chat completions. Chat bodies ask max_completion_tokens instead of max_tokens and drop thinking/enable_thinking, both in passthrough and in Claude Code's translated requests, and the Test probe asks max_completion_tokens as well. The model list is the resource's deployments (/openai/deployments?api-version=2022-12-01), falling back to /openai/v1/models, with the key in api-key; when neither answers, the error says to type the deployment names in as model ids. Before a list, no catalog models are offered, since a model the resource hasn't deployed would be turned away. An Alma azure entry, and any imported entry at a resource's host, becomes the preset at its v1 base; an Alma azure entry with no endpoint is skipped as "it has no Azure OpenAI endpoint", and one at a non-Azure host (an API Management gateway) keeps its URL and is still signed with api-key. Adds the lobe-icons Azure icon. Tested in Go against fake resources (httptest): TestAzureRoutes checks that Codex /v1/responses, a chat client's /v1/chat/completions and Claude Code's /v1/messages on both a GPT-named and another deployment reach /openai/v1/responses or /openai/v1/chat/completions with api-key set, no Authorization or x-api-key, the deployment name as the model, max_completion_tokens and no thinking; TestAzureBase covers each pasted form and the hosts that aren't resources; TestAzurePresetSaved covers the endpoint required, both URLs on the v1 base, api-key-only headers, Native, the probe's URL and body, and nothing available before a list; TestAzureModels covers deployments first, the v1 fallback and a refused key's message; TestImportAzure covers Alma rows with an endpoint, with a bare resource name and with none, plus a classic URL from another app. azure-endpoint.test.cjs, in Chromium and WebKit, en and zh, checks the field, placeholder, hint and focus, the warning without an endpoint with nothing sent and the page not scrolled, the endpoint sent as chat and responses with the key, and that OpenAI's editor has no such field. add-sheet and old-webkit pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet and go test -tags nogui ./... pass (TestClaudeAccountNamedAsItsLogin failed under load and passes alone). Not tried against a real Azure resource: whether a key still lists deployments with api-version 2022-12-01, whether Azure's Responses API accepts every field Codex sends, Claude models on Azure AI Foundry's Anthropic endpoint (not wired), Entra ID tokens, and API Management custom domains beyond keeping their URL. Not tried in the real Wails window.
2026-09-30 02:17:15 +08:00
balance: a custom provider's balance token can't be left where it is never asked, and new-api's refusals say what to do (TJHHHH: OK了,希望有防呆机制 — a new-api relay's System Access Token pasted beside a Balance URL of /api/usage/token, no New-Api-User header, and the Usage page never showed a balance) — new-api has two balance queries: /api/usage/token tells a key what is left on it and takes the key alone, and /api/user/self tells the account's quota to the access token with the user's id in New-Api-User. A token beside the first could only ever be turned down, and the second without the header answered {"success":false,"message":"无权进行此操作,未提供 New-Api-User"}, which a 401 showed as JSON and a 200 read as a field missing. The editor now says so under the token as soon as it is there: beside …/api/usage/token, or with no Balance URL at all (the token field is now offered on every custom provider, not only one with a URL saved), a warning with one button, Use https://<site>/api/user/self, which sets the URL, sets the Balance field to $data.quota / 500000 when it was empty or named /api/usage/token's fields, and opens More endpoints to show it; once the URL is /api/user/self and no New-Api-User header has a value, it asks for New-Api-User = your user ID (shown in the site's personal settings) until one is typed. The URL is not switched behind the user's back: what the editor shows is what is asked, and the field typed for /api/usage/token's reply wouldn't read /api/user/self's anyway. Behind it, Balance never sends a token to /api/usage/token and says what to set instead; a reply of success:false with a message, at a 401 or a 200, is that message as the error, with the header to add put in front when it is about New-Api-User (not provided, format error or mismatch, in new-api's Chinese or English); and /api/user/self with an empty field reads the quota, $1 to 500000. The Balance field has a Check balance button (POST /api/provider/balance) that asks as the form has it, before a Save, the saved provider with the form's URL, field, headers and token; it and the Usage page's card say either fix plainly, the relay's own words on hover. A JWT beside new-api's URL is not warned about: new-api's own dashboard sign-ins are JWTs now, so a token's shape can't tell new-api from a sub2api panel. Tested with new TestKeyUsageWithAccessToken (never asked, the fix said, the key alone still asks), TestUserSelfDefaultField and TestNewAPIUserRefusal (401 and 200, Chinese and English, the mismatch, and an unrelated refusal left as it came), which all fail on main; and a new internal/gui/tests/balance-fix.test.cjs in Chromium and WebKit (the warning on the saved setup, the click, the header hint going once typed, Check balance with and without the header and what it posted, Save, the Usage card's words, every new string in Chinese, and a new provider with a token and no URL). go build (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the other GUI tests pass
2026-09-29 17:56:43 +08:00
settings: a Text size preference (文字大小) that makes everything in magpie's windows larger, 100/110/125/150%, with Ctrl/Cmd + = − 0 in the window and the tray panel, and Windows' text drawn in its own UI faces (Discord, Clarence Dan: the font size is hard on the eyes on Windows) — Settings → Preferences gains a Text size row under Language whose pick posts to a new POST /api/settings/text-size (saved as settings.TextSize, validated against 100/110/125/150, 0 read as 100, and kept by the Settings page's general save, which never sends it). The size is each webview's own zoom, as a browser zooms, not CSS zoom: CSS zoom leaves getBoundingClientRect and clientX in zoomed pixels while offsets, scrollTop and 100vh are not, and app.js, routing.js and library.js place menus, popovers and the panel's height by those measures; under the webview's zoom the page is laid out again in fewer, larger CSS pixels and every measure agrees. Windows and Linux use Wails' SetZoom (WebView2's zoom factor, WebKitGTK's zoom level; the Zoom window option at creation, so the pages open at the saved size), the Mac WKWebView.pageZoom through cgo (macOS 11+), since Wails' Mac zoom is magnification, which lays nothing out again. None is under 100% because Wails clamps SetZoom below 1 on Windows and Linux. The windows grow around the pages: the panel's width and the height its page asks for (still in CSS pixels) are multiplied by the zoom, capped at the screen's work area (textsize.go panelFrame), and resized at once when the size changes; the main window's minimum is 560×420 CSS pixels at the size, capped by the screen, so a window smaller than that grows. Runtime changes wait for the app to be ready (whenReady), and at startup the Mac applies the zoom once its webviews exist. The Mac header, drawn under the native traffic lights which do not grow, divides its height, left padding and the modal's top by --zoom (set from boot.js before the first paint and from the settings), so it stays 50 points tall and level with them. The shortcuts are Cmd on the Mac and Ctrl elsewhere (the other modifier does nothing), step through the sizes and 0 goes back to 100%, with a "Text size 125%" toast; in magpie web the row is hidden and the keys are left to the browser's own zoom. On Windows the page names its faces (html.win): Segoe UI Variable Text, Segoe UI, then Microsoft YaHei UI for Chinese rather than whatever WebView2 falls back to; the Mac and Linux stack is unchanged, and there is no automatic size bump. New strings in en and zh. Tested: internal/gui/tests/text-size.test.cjs in Chromium and WebKit, en and zh, light and dark — a pick posts and does not scroll the settings page, stays picked after a reload with --zoom from boot.js, the Settings page's other saves leave it, the shortcuts step and reset in the window and the panel, and at 150% (the smallest window, 840×630 points, as 560×420 CSS pixels at device scale 1.5) no view, the header or the panel runs off to the side, the Mac header is 50 points; screenshots at 100% and 150% looked at. currency.test.cjs's viewport is 480 tall instead of 420, as settings-groups' is: the new row put the Currency control under the footer, where WebKit's hover could not reach it. Go: TestTextSizeFrames (panel and window sizes) and TestTextSizeSetting (the endpoint saves, zooms the windows, rejects 133, survives the general save, boot.js carries it) in internal/gui, and the settings round trip. go build -tags nogui (and GOOS=linux, GOOS=windows, plain darwin), go test -tags nogui ./... (TestUserPath flaked once under load and passes alone) and the whole GUI suite with --test-concurrency=2 pass. Not tried in the real Wails window on any platform: the Mac pageZoom, WebView2's zoom factor with the resized panel and the Windows font are untested there.
2026-09-30 02:51:17 +08:00
factory: a Factory subscription (the account Droid signs in to) is signed in inside magpie and its models served through the gateway to any agent (#242 thedavidweng: 希望支持 Factory 订阅作为 provider) — read from droid 0.229.0's shipped bundle (npm @factory/cli-darwin-arm64), droid not run: the sign-in is WorkOS's device flow (api.workos.com/user_management, droid's client id: /authorize/device, then /authenticate with the device-code grant, authorization_pending / slow_down / access_denied / expired_token), refresh with grant_type=refresh_token and the org, each refresh a new refresh token; a token naming no org is moved into the first of GET /api/cli/org; GET /api/cli/whoami's region picks api.eu.factory.ai for eu. Claude and MiniMax M2.7 go to /api/llm/a/v1/messages, GPT and Grok to /api/llm/o/v1/responses, the open models Factory hosts to /api/llm/o/v1/chat/completions, with droid's headers (Bearer, X-Factory-Client cli, X-Client-Version, User-Agent factory-cli/0.229.0, X-Factory-Org-Id, x-session-id, x-assistant-message-id, x-api-provider, x-provider-routing-source, OpenAI-Platform for OpenAI's); Gemini, on Factory's own /api/llm/g format, is left out. Usage from GET /api/billing/limits: 5 hours, 7 days and 30 days for the standard pool and "Droid Core · …" for the hosted models, and Extra usage in dollars, a full window not stopping routing while extra usage is left. Droid's own login is encrypted with a keychain key and is not read: magpie keeps its own in logins.json. Several accounts, switched, on/off and forgotten as the other subscriptions; a token renewed before its end, a refused refresh marking the account for a new sign-in. The add list has a Factory tile (Pro · Plus · Max) that says Factory may treat this as third-party use before anything is posted, and the code screen names Factory's sign-in page; en and zh. magpie accounts takes factory or droid. Token counts are estimated locally. The model list is droid 0.229.0's registry (30 models), fixed. Tested with internal/provider/factory_test.go against fake WorkOS and Factory servers in a temp HOME (the device flow, org lookup and refresh, an EU account's requests on the EU host with each model on its route and headers, usage read; a token near its end renewed once and the new refresh token kept, a refused refresh marking the account and a new sign-in clearing it; two accounts each signing with its own org, on/off, switch, forget), also under -race; internal/gui/tests/factory-sub.test.cjs, Chromium and WebKit, en and zh (the tile, its warning, nothing posted before Sign in anyway, then {agent:"factory"}, the code with Factory's wording, nothing scrolled). Both fail without the change. click-scroll, old-webkit, zed-sub and add-sheet pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Factory account: no real sign-in, request or usage call, the EU region, windowEnd's format, or whether Factory wants more than droid's code shows; not tried in the real Wails window.
2026-09-30 07:31:05 +08:00
providers: ChatGPT and Claude accounts come in from CLIProxyAPI's auth files (#124 Mrhe525: 要是能把那什么cpa的json文件可以导入直接使用就无敌了) — the ChatGPT (Codex) and Claude subscriptions now take the import Antigravity had: Import accounts from a file… under the accounts, and Import from a file instead… while a browser sign-in waits. It reads CLIProxyAPI's files ({type:"codex"|"claude", access_token, refresh_token, id_token, account_id, email, …}), one or a list or {accounts:[…]}, Codex's own auth.json ({tokens:{…}}), Claude Code's .credentials.json ({claudeAiOauth:{…}}) and bare refresh tokens one a line. Each account is checked as a sign-in in magpie is finished: its refresh token traded with the agent's own OAuth client (codexRefresh / claudeRefresh), then the ChatGPT claims or Claude's profile read (plan, tier, account and organization), and it is kept as that sign-in would keep it: Codex's or Claude Code's own if the agent has none, else added and ticked. The refresh rotates the token, so the file's copy stops working; the box says so. A refresh token magpie holds already (saved or the agent's own) is "Already in magpie" and never refreshed; another app's file ("a claude sign-in, not Codex's"), an API key, a token in the files twice and one the vendor turns down ("used since … expired or signed out") are listed as not added with the reason; tokens are never in what comes back. codexExchange and claudeExchange now build their sign-in through the same codexLogin / claudeLogin. Also: a Claude sign-in with no ~/.claude yet failed writing .credentials.json (the directory is made now), and the magpie://import dialog was drawn by the subscription import's renderImport, declared later under the same name (renamed renderLoginImport). zh strings added. Tested with new TestImportCodexLogins (a fake token endpoint: a CPA file and a native auth.json added with the new tokens and the file's account id, the live account untouched and reported as there, a Claude file, a spent token and a duplicate refused, a second import "exists", garbage an error) and TestImportClaudeLogins (with no sign-in the CPA file becomes Claude Code's credentials, plan max, active; a spent .credentials.json and a Codex file refused), and a new internal/gui/tests/login-import.test.cjs in Chromium and WebKit, en and zh (both ways in, the texts, what is posted, the outcomes, the account listed). go build -tags nogui (and GOOS=windows, GOOS=linux), go test -tags nogui ./... and the other GUI tests pass on fe22692 (click-scroll's WebKit case fails after f1a8caf, with or without this change)
2026-09-29 18:34:08 +08:00
mimo: a Xiaomi MiMo account (the one MiMo's app signs in to) is signed in inside magpie and its models served through the gateway to any agent, and MiMo Code is an agent magpie configures (#249 thedavidweng) — magpie had neither. The subscription (id mimo-app, shown as Xiaomi MiMo) signs in with Xiaomi's long-poll login (account.xiaomi.com/longPolling/loginUrl, sid mimosgp) and keeps the MiMo server's cookie session (serviceToken, userId) in logins.json at 0600; the passToken is replayed to renew the session after a day or on a 401. Requests go to POST <base>/route/chat/completions on mimo-server-{sgp,ru,in}.xiaomimimo.com/api with X-Mimo-Source mimocode-cli-free and X-Client-Version; mimo-auto is sent as mimo-pro; the models are mimo-pro and mimo-flash. Usage from /user/usage and /user/xiaomi/subscription/self. Several accounts, switched, on/off and forgotten as the other subscriptions. The add list has a Xiaomi MiMo tile (Free · Starter · Plus · Pro · Ultra) that says Xiaomi may treat this as third-party use before anything is posted; en and zh. magpie accounts takes mimo. MiMo Code (agent mimocode, OpenCode-like) is configured in its config dir (MIMOCODE_HOME/config when set), reading mimocode.jsonc, mimocode.json or config.json, whichever is there. Tested with internal/provider/mimo_test.go against fake Xiaomi SSO and MiMo servers in a temp HOME, internal/agent tests for MiMo Code's config, and internal/gui/tests/mimo-sub.test.cjs, Chromium and WebKit, en and zh; they fail without the change. factory-sub, click-scroll, old-webkit, zed-sub and add-sheet pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Xiaomi account: the long-poll's success payload, the SSO cookie chain, whether /route accepts magpie's requests, the UTC+8 times, and whether MiMo Code picks up the config; not tried in the real Wails window.
2026-09-30 07:43:58 +08:00
agents: a model picked for Claude Code (or any agent) shows on its row at once, in the window and the tray panel, and /api/set answers in ~0.1s instead of seconds (Discord: switching Claude Code's model took 5–10s to show, magpie 0.1.410+) — /api/set writes the config and answers with the whole state, and the frontend drew the pick only from that answer. Every state asked the devin CLI (devin models list, ~1.5–3s a run) again several times whenever Devin was installed: its model's family, its options, its effort's options and Drift's re-read, and a CLI that gave no list (signed out, offline) was asked anew on every state, since only a list was cached. With a 4s CLI one set took 28s (measured, before). Now DevinFamilies keeps a failure for a minute, serves a list past its 5 minutes while it is read again in the background, and only a caller with nothing known waits — never one marked DevinNoWait, which Devin's field reads and options are (its Set still waits, so what is written is unchanged). Goose's detection read a binary's whole build info on every state (~150ms); the answer is kept while the file's size and time are the same. In the page, commit() draws the picked model and its flash before /api/set is sent, then draws the answer (a later pick's answer winning over an earlier one's); a refused pick puts the old model back and gives the reason. What is written to the agent's config is as before. Measured in a sandbox HOME with magpie web: /api/state 2.8–3.1s and /api/set 2.9s before, 0.1–0.5s after (state() ~110ms steady in-process); a provider whose endpoint never answers made no difference either way. Tested with internal/gui/set_fast_unix_test.go (devin CLI that sleeps 4s and a provider that never answers: three sets and the states after them each under 1.5s, ~5ms, settings.json as before; fails before the fix at 28s a set) and internal/gui/tests/model-pick.test.cjs (a set held 2.5s: the pick shown within 600ms in the window scrolled and in the tray panel's opened row and line, the page not moved, the answer keeping it, a refusal restoring the old model, en and zh, Chromium and WebKit; fails on main). click-scroll, panel-fold, menu-scroll, agy-launch, cli-update and old-webkit pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet and go test -tags nogui ./... pass (TestClaudeAccountNamedAsItsLogin failed under load and passes alone). Not tried in the real Wails window or tray, nor with a real signed-in Devin CLI.
2026-09-30 03:21:24 +08:00
omarchy: on Omarchy, magpie is drawn in Omarchy's own design language, follows its theme, drops its panel under the bar as Omarchy's own menus do, and can put its icon in Omarchy's bar (让整个UI/UX完全符合Omarchy的设计语言…单独对Omarchy做这个支持,现在的UI/UX当然要保留) — on Hyprland magpie looked like any other GTK app: its own light/dark palette, rounded shadowed popups, and a tray panel floated framed and dimmed in the middle of the screen, labelled with the program's name, never closing on a click elsewhere (focus follows the mouse there, so Wails turns off focus-lost hiding), and its tray icon was hidden in the bar's drawer. Now, only when Omarchy is detected (internal/omarchy: ~/.local/share/omarchy and its current theme), boot.js hands the page the current theme (colors.toml, the shell's theme TOML, Hyprland's border size, colour and rounding, Omarchy's font) as CSS variables before the first paint, and omarchy.css, all under html.omarchy, draws flat as Omarchy's shell does: the theme's background, no shadows or blur, corners as round as Hyprland's windows, popups and dialogs framed by the active border's colour, uppercase letter-spaced labels, rows and picks as a faint fill with the text in the accent. /api/omarchy is polled every 2.5s, so a theme picked in Omarchy's menu reaches the windows at once; Settings → Appearance shows the theme's name instead of Light/Dark/System. Under Hyprland the tray panel gets a title of its own ("magpie panel", set through GTK since Wails leaves frameless windows untitled), and each opening gives Hyprland a named Lua window rule placing it under the bar at the click, 6px in from the edges, pinned, opaque, borderless; its height is capped to the work area. While it is open a non-consuming left-click bind emits a custom event on Hyprland's socket2; a click outside the panel and below the bar closes it (the bar is left to its icons, so the icon toggles), and the bind is removed on close or, after Escape, on the next click. The tray item's SNI id is "magpie" instead of "Wails", so it can be pinned. Settings → Bar icon (状态栏图标), shown only in the app on Omarchy's Hyprland, puts an Omarchy bar-widget plugin (usemagpie.magpie, a bird glyph, in ~/.config/omarchy/plugins, then rescanPlugins and plugin enable) beside Omarchy's own icons: a click runs magpie panel, a right-click magpie app; Off disables and removes it; a widget pointing at a moved or older magpie is rewritten at start. magpie panel (new) opens the quick panel, starting magpie if needed, or toggles it in the running instance. Elsewhere nothing changes. Tested in a Hyper-V VM on the latest Omarchy (Hyprland 0.56, 2528×960 at scale 1.6): the look with tokyo-night; the panel at [1134,32] under the bar; clicks on the panel and the bar keep it open, a click on another window closes it, the icon toggles it, Escape closes it and the bind goes; Bar icon On adds the bird to the bar, its click opens the panel under it (also with magpie not running), a second click closes it, right-click opens the window, Off removes it; autostart becomes app-magpie@autostart through systemd's XDG generator; the launcher entry and icon are there. Go: internal/omarchy tests (theme parsing, panel placement on scaled and turned monitors, click-outside geometry, widget files quoted with the executable, KeepWidget rewriting only a stale one). A new internal/gui/tests/omarchy.test.cjs, Chromium and WebKit, en and zh: with no Omarchy theme the page has no omarchy class or theme style, the Appearance choices show, no Bar icon row, nothing asked of /api/omarchy; with one, the theme's background, square corners, its name, and Bar icon's On and Off each posted with nothing scrolled, hidden when the bar isn't there. old-webkit now lets omarchy.js load in the head beside boot.js (it does nothing off Omarchy). The full GUI suite, gofmt, go vet, go build (GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried: other Hyprland setups than Omarchy's, a multi-monitor VM, and the GTK4 build in the VM (gtk3 was used).
2026-09-30 04:58:11 +08:00
omarchy: on Omarchy, magpie is drawn in Omarchy's own design language, follows its theme, drops its panel under the bar as Omarchy's own menus do, and can put its icon in Omarchy's bar (让整个UI/UX完全符合Omarchy的设计语言…单独对Omarchy做这个支持,现在的UI/UX当然要保留) — on Hyprland magpie looked like any other GTK app: its own light/dark palette, rounded shadowed popups, and a tray panel floated framed and dimmed in the middle of the screen, labelled with the program's name, never closing on a click elsewhere (focus follows the mouse there, so Wails turns off focus-lost hiding), and its tray icon was hidden in the bar's drawer. Now, only when Omarchy is detected (internal/omarchy: ~/.local/share/omarchy and its current theme), boot.js hands the page the current theme (colors.toml, the shell's theme TOML, Hyprland's border size, colour and rounding, Omarchy's font) as CSS variables before the first paint, and omarchy.css, all under html.omarchy, draws flat as Omarchy's shell does: the theme's background, no shadows or blur, corners as round as Hyprland's windows, popups and dialogs framed by the active border's colour, uppercase letter-spaced labels, rows and picks as a faint fill with the text in the accent. /api/omarchy is polled every 2.5s, so a theme picked in Omarchy's menu reaches the windows at once; Settings → Appearance shows the theme's name instead of Light/Dark/System. Under Hyprland the tray panel gets a title of its own ("magpie panel", set through GTK since Wails leaves frameless windows untitled), and each opening gives Hyprland a named Lua window rule placing it under the bar at the click, 6px in from the edges, pinned, opaque, borderless; its height is capped to the work area. While it is open a non-consuming left-click bind emits a custom event on Hyprland's socket2; a click outside the panel and below the bar closes it (the bar is left to its icons, so the icon toggles), and the bind is removed on close or, after Escape, on the next click. The tray item's SNI id is "magpie" instead of "Wails", so it can be pinned. Settings → Bar icon (状态栏图标), shown only in the app on Omarchy's Hyprland, puts an Omarchy bar-widget plugin (usemagpie.magpie, a bird glyph, in ~/.config/omarchy/plugins, then rescanPlugins and plugin enable) beside Omarchy's own icons: a click runs magpie panel, a right-click magpie app; Off disables and removes it; a widget pointing at a moved or older magpie is rewritten at start. magpie panel (new) opens the quick panel, starting magpie if needed, or toggles it in the running instance. Elsewhere nothing changes. Tested in a Hyper-V VM on the latest Omarchy (Hyprland 0.56, 2528×960 at scale 1.6): the look with tokyo-night; the panel at [1134,32] under the bar; clicks on the panel and the bar keep it open, a click on another window closes it, the icon toggles it, Escape closes it and the bind goes; Bar icon On adds the bird to the bar, its click opens the panel under it (also with magpie not running), a second click closes it, right-click opens the window, Off removes it; autostart becomes app-magpie@autostart through systemd's XDG generator; the launcher entry and icon are there. Go: internal/omarchy tests (theme parsing, panel placement on scaled and turned monitors, click-outside geometry, widget files quoted with the executable, KeepWidget rewriting only a stale one). A new internal/gui/tests/omarchy.test.cjs, Chromium and WebKit, en and zh: with no Omarchy theme the page has no omarchy class or theme style, the Appearance choices show, no Bar icon row, nothing asked of /api/omarchy; with one, the theme's background, square corners, its name, and Bar icon's On and Off each posted with nothing scrolled, hidden when the bar isn't there. old-webkit now lets omarchy.js load in the head beside boot.js (it does nothing off Omarchy). The full GUI suite, gofmt, go vet, go build (GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried: other Hyprland setups than Omarchy's, a multi-monitor VM, and the GTK4 build in the VM (gtk3 was used).
2026-09-30 04:58:11 +08:00
kiro: removing kiro-cli's or the Kiro IDE's own sign-in hides it in magpie, and it shows again when Kiro signs in anew (Discord 碳碳双键: 本机没安装 kiro,但旧的 Kiro 数据还在,magpie 列出一个"Kiro account"(登录已失效,请重新添加此账号),添加新账号并设为首位后,旧的数据无法移除) — magpie reads Kiro's own sign-in from kiro-cli's database or ~/.aws/sso/cache and lists it among the accounts it signed in; Remove on it said "that is kiro-cli's or the Kiro IDE's own sign-in; run kiro-cli logout or sign out in the IDE", which with neither installed left a dead account there for good. The same refusal was shared by every agent whose own sign-in magpie only reads (forgetSideLogin: Kiro, Devin, Grok, Copilot, ZCode, WorkBuddy and WorkBuddy AI, Command Code, Gemini CLI, Antigravity). Now Remove on the agent's own sign-in hides it: logins.json's entry for it keeps a Hidden mark, and it is left out of the account list and so of the gateway's tries (the first and the ones behind it both come from that list); the agent's files are never deleted or written. It shows again when that sign-in changes: for Kiro, by a mark that is a digest of where the sign-in is (kiro-cli's row or the IDE's file) and its refresh token (never the token itself), so a new kiro-cli login or IDE sign-in, to any account, brings it back; for the others, which have no such mark, when it is another account. Removing it while it is first is allowed now, the next account being put first (saved, so it stays first when the own one comes back); removed when it was the only one, the agent has no account left. One of magpie's own accounts is removed with its home as before, and still can't be while it is first. The error and its per-agent sign-out hints are gone. Logins carry own, and the accounts list shows Remove on the agent's own sign-in also while it is first, its title "magpie stops showing and using Kiro's own sign-in; its files are left as they are, and it shows again when Kiro signs in anew" (magpie 不再显示和使用 Kiro 自己的登录;它的文件保持原样,Kiro 重新登录后会再次出现). Tested with a new internal/provider/side_logins_hide_test.go in a sandbox HOME: an expired kiro-cli sign-in ("Kiro account") behind a Google one magpie signed in is removed, stays gone across reads, isn't the account used nor among AlsoOn, kiro-cli's database byte-for-byte and mtime as it was, the refresh token nowhere in magpie's files; a new kiro-cli sign-in brings it back behind the one first; put first and removed, the next is first; one of magpie's removed takes its home and leaves the other's; the only account removed leaves Kiro with none; Grok's CLI sign-in hidden with auth.json untouched, back once it is another account. The agents' tests that expected the refusal (Command Code, Copilot, Devin, Grok, WorkBuddy, ZCode) no longer do. A new internal/gui/tests/own-signin-remove.test.cjs in Chromium and WebKit, en and zh: kiro-cli's sign-in behind another and alone first both have Remove with that title and post login/forget, magpie's first account has none. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (138) pass. Not tried with a real Kiro, kiro-cli or the Kiro IDE.
2026-09-29 23:52:33 +08:00
library: an agent's own copy of a library skill gives way to it, and one that differs is settled from the warning (StringKe, JasonLeeForOnly on Discord: after Bring in all, "Claude Code already has a skill of its own called impeccable", 这里没有强制覆盖的能力) — a skill is brought in from the agent with the very folder and given to the agents that had that folder; another agent's copy (CC Switch copies a skill into each agent) was listed as "differs in" and left out, and giving the skill to it, or a copy put back into an agent after its folder became magpie's link (by CC Switch or whatever installed it), was a hard failure with no way out. A folder there holding the same files as the library's (bytes compared; .DS_Store, .git and magpie's copy mark aside) is now set aside into backups/<when>/<agent>/skills and the library's linked in its place; a skill brought in takes the agents with such a copy too, which are no longer said to differ; a move that copied it all but couldn't remove the folder brings it in all the same, what was left set aside. One that differs keeps the warning, now in the reader's language with "Use the library's" (the agent's set aside, the library's linked) and "Keep <agent>'s" (the agent, and any reading the same folder, taken off the skill) on its row — skills/use-library and skills/keep-own, magpie library skill use-library|keep-own <name> <agent>, which the CLI suggests under the problem. Backups holding a skill set aside are never pruned. TestOwnSkillInTheWay, TestSameTree, TestPruneKeepsSkillsSetAside and own-skill.test.cjs (Chromium, WebKit, en/zh) fail without the change; TestSharedAgentsSkills now expects the byte copy to be the library's. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; own-skill, import-all-skills, library-items, shared-skills, market-have, click-scroll, old-webkit pass.
2026-09-30 19:42:43 +08:00
tray: expanding and collapsing on the Agents page keeps what was clicked where it is, and "Show {n} more" unrolls the rest under it (碳碳双键: 这个地方展开 ui 会偶现疯狂抖动,必现拉扯; #199: agent页面展开与收纳会抽搐 甚至跑在窗口外面去; willz: 点击展开的时候,列表不是向下展开 而是向上展开) — the rest unrolled above the button, so in a panel at its tallest with the list scrolled the button slid down and off the foot of the view. What a click holds in place was passed over while anything played on it, a colour easing in included: the button's own hover fade and its label's fade meant the page was held by the list instead, so the button went off-screen for ~300ms and then the page snapped 90px back to it; and a button slid out from under the pointer and back fades its hover again, so the page swung between the two (484 → 563 → 484 → 572 … frame by frame in Chromium). Held elements were also put back a frame late, from requestAnimationFrame, so what was clicked trembled by a frame's growth. Now only a play that moves an element (transform, top, margin …) passes it over, the hold is put back as soon as a growing part is laid out (a ResizeObserver, before paint), and the button comes before what it unrolls: the rest come in under it, the rows above stay with it, and in a view with no room below the view goes down with them (scrollOnPurpose with the click), led by the fold's foot frame by frame, never past the button's top; Show less rolls them up under the button, which stays put. Tested in Playwright Chromium and WebKit with a stub panel (13 agents, Codex open, list scrolled to its end) and the window: new internal/gui/tests/panel-fold.test.cjs checks each painted frame of Show more (the view only goes down, the row above keeps with the button, the rest below it, then in sight), Show less and a row opened and closed (what was clicked within 2px); it fails on main (button moved 99px, the swinging in Chromium). All node tests and go test -tags nogui ./internal/gui/... pass; click-scroll's "the view must be at its end" also flakes on main under load
2026-09-29 16:40:03 +08:00
panel: profiles open from Profiles at the Agents tab's foot, on its left, instead of a tab of their own (Image #65: tray window 中方案不应该有一个单独的 tab,在 Agents tab 左下角有个方案即可) — the tray panel's tabs are Agents and Routing; the footer's left corner, on the Agents tab only, has Profiles (方案) with how many there are, and a click opens them upward over the list as a popover (the .pop look and fade, reduced motion kept): the chips, + Save current and its name field as before, the popover scrolling within itself when there are many, sized between the tabs and the footer. A click outside, Escape (the name field's Escape closes the field alone first), another tab or a profile applied closes it; the list under it is never moved. A remembered Profiles tab opens Agents. fit() no longer measures the profiles. Tested: panel-profiles.test.cjs reworked (no Profiles tab; the popover above the button at the foot's left and under the tabs, the count shown; scrolling to + Save current inside it, the field whole and nothing moving; Escape, outside click, Routing tab and applying a profile close it; Profiles only on Agents) and old-webkit.test.cjs opening it before Save, in Chromium and WebKit, en and zh. Screenshots looked at in light and dark, en and zh. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the GUI suite pass (click-scroll, panel-profiles, panel-routing and routing-kind failed under --test-concurrency=4 and pass alone). Not tried in the real Wails window.
2026-09-30 01:05:43 +08:00
panel: the Routing tab gets a small stage, agents → magpie → where their requests went, each new request a dot flying there and back, and its rows line up (the user: 这个页面有点乱,另外这个在Tree的window中能否也能有一个比较迷你版的路由动画,直观、优雅。) — the tab listed eight two-line rows with an agent's colour dot and a status dot (both green for Pi), the times left in their cells so the right column ran jagged. Over the list there is now a stage, 104px, in a card: up to three agents that asked lately on the left (icon and name), magpie's bird in the middle, up to three accounts they went to on the right (provider, then the account or key in grey, and a dot for how its latest try there went: green, grey pulsing while answering, red when it failed), joined by thin curved wires. Each place keeps its spot while it stays; a newcomer takes the place of the one heard of longest ago. A request as it comes plays as a dot in its agent's colour: along its agent's wire into magpie, out on the wire to the account it was routed to (the wires it travels lit in that colour, the hub's ring pulsing), waiting there, pulsing, while that answers; a try that failed turns red and comes back to magpie for the next account, and the answer flies home green, or red when nobody could answer. At most four play at once, from the same trace the list is drawn from (no new endpoint); nothing plays with reduced motion or while the tab isn't shown. The rows: the agent's colour dot is gone (its icon says who), the second line starts with a faint → (accent when moved, red when failed, pulsing while waiting) instead of a second dot, and the time (now 24-hour, 20:50:53) and the tokens · time taken are both right-aligned; six rows instead of eight, the panel about as tall as before. No border stripes, nothing scrolls. Tested in panel-routing.test.cjs, Chromium and WebKit, en and zh: the stage lists Codex and Claude Code, Relay·team and Claude·ann@example.com with their status, four drawn wires, the hub, everything inside it and above the list, no left border; every row's time and totals share one right edge and each row has one mark; and, with motion allowed, a request coming once the tab is shown makes a dot fly, the wire lit and the hub busy, the dot waiting beside the Claude account while it answers, turning green and gone after, the row's tokens updated. The whole GUI suite passes (login-import, a timing test of an import's passing status, failed once under concurrency and passes alone), go build -tags nogui (and GOOS=linux, GOOS=windows) and go test ./internal/gui pass. Checked by eye with a fake live trace in light and dark (a 429 on Relay failing over to DeepSeek, then a Claude request). Not tried in the real tray panel.
2026-09-29 21:16:56 +08:00
providers: a built-in subscription moves onto its community plugin and back, its accounts with it (the user: 慢慢下掉内置的那些 Subscription Provider…不要让用户一升级完之后,突然发现他的所有服务都不可用了) — eleven built-ins (Zed, Qoder, Factory, MiMo, Devin, Grok, Command Code Plan, Cursor, Kiro, WorkBuddy, ZCode) each get a mover to their @magpie-community package. A move installs the plugin (updated when older than the mover needs), hands it every account in the plugin's shape, checks the plugin lists models for them, and only then lets the plugin serve the same provider id, so agents' model ids don't change; any failure rolls back and the built-in keeps serving. The built-in's accounts are kept as a backup, and "Run built-in again" (magpie plugin move-back <id>) takes the plugin's current sign-ins back, never copying a rotating token: they are taken out of the plugin in one step, and an account the built-in renewed meanwhile keeps its newer token. An account only the plugin can hold (Cursor's browser sign-in) stays with it. The provider editor shows "Runs on" with the button, a failed move's reason, and the CLI has magpie plugin move / move-back. Automatic moves (Retiring) are wired in but the list stays empty until the packages are on npm. Checked with real accounts in a sandbox HOME: Command Code Plan and Devin moved, answered a chat through the gateway on the plugin, moved back and answered again on the built-in. TestMove*, TestPutBackKeepsNewer, the movers' round-trip tests and plugin-move.test.cjs (Chromium, WebKit, en/zh) are new. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit pass.
2026-09-30 21:21:58 +08:00
plugins: a plugin's provider takes more than one account, as a built-in subscription does (the user: 插件当然要支持多账号了) — a plugin kept one sign-in per provider, and signing in again replaced it. Now each sign-in goes beside the others: plugin-auth.json keeps the first under the provider's id, as OpenCode does (so what is signed in already stays the first account), the rest under id#slot; logins.json lists them as it lists the built-ins' accounts, so the editor shows them all, one first, the others ticked on behind it or off, any made first, any removed (which signs it out). The host runs every request in its account's scope, so a plugin's getAuth, client.auth.set (a refreshed token), loader and provider.models see that account alone; a sign-in to an account already there (the same id, else the same secret) replaces that one's rather than listing it twice. The gateway spreads and fails over across the accounts on, as for the built-ins. Accounts with no id are told apart by the end of their key ("API key …abcd"), else by their slot. Also on the Devin page: the header shows the plugin's icon, and Cognition's opencode-devin is off the market (its gateway answers 404 model_not_found for models and chat). TestPluginAccounts (plugin: per-account requests and refreshes, dedupe, sign-out; provider: first, on/off, switch, forget, labels), TestPluginProvider (gateway: a 429 on the first account goes on the second) and plugin-signin.test.cjs (add a second account in the editor, make it first; fails without the change) are new. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-signin, plugin-market, old-webkit, click-scroll pass.
2026-09-30 19:58:22 +08:00
providers: each provider, a signed-in account like Codex's among them, goes through its own proxy — the one in Settings, none, or its own address (#237 xosi: 目前似乎是全局设置代理的,codex和国模供应商无法单独区分设置代理) — magpie had one proxy, Settings' (or the environment's, or the system's), for every request it made, so Codex, which needs a proxy from China, and a vendor at home, which is slower or refused through one, couldn't both be right. A provider now has a proxy, "" (following the global one, as before and the default), "direct" (none, whatever Settings says) or an http://, https:// or socks5:// address (host:port meaning http), kept as proxy in providers.json only when set, and for an account provider in its picks entry beside its models. It is checked on save by the same rule Settings' proxy is (settings.CheckProxy, now shared), so ftp:// and the like are refused. netproxy.With puts a provider's choice on a request's context and netproxy.Func honours it; netproxy.Dispatch sends such a request through a clone of the transport kept for that proxy alone (at most 32, cleared past that), since a transport shares an HTTP/2 connection by host whatever proxy dialled it; loopback is never proxied. The gateway's client and http.DefaultClient go through Dispatch, and every request made on a provider's behalf carries its choice: the gateway's turns (attempt, forward, draw, decide, the Codex backend), and the model lists, balances, plan and subscription usage, tests, sign-in, Codex's warm-up and reset, while the Claude subscription's and warm-up's CLI runs get the account's proxy in their *_PROXY (netproxy.EnvWith). The provider editor, a key provider's and a signed-in account's alike, has a Proxy row after Fallback: Global proxy | Direct | Custom (全局代理 | 直连 | 自定义), the address field beside the options only for Custom, its room kept otherwise so a pick never changes the centred dialog's height and moves nothing, and a line under it saying what the pick does; Save posts proxy ("" , "direct" or the trimmed address), a save without it keeps the old one, and Custom with no address is refused before anything is posted ("Proxy: type its address, like http://127.0.0.1:7890"). No border stripes; en and zh strings together. Tested with a new internal/gateway/proxy_test.go in a sandbox HOME: with a global proxy set, provider A's turns and model list go through its own fake proxy, B's (direct) straight to the vendor, C's (following) through the global one, none through another, and A set back to following goes through the global one; ftp:// is refused (fails with With neutered). netproxy_test.go's TestWith covers Func, Dispatch's per-proxy transports and EnvWith. A new internal/gui/tests/provider-proxy.test.cjs in Chromium and WebKit, en and zh: a key provider opens on Global proxy with no address, Custom unfilled is refused and nothing posted, a padded socks5 address saved trimmed, Direct saved as "direct" and Global as ""; Codex's account opens on Custom with its address and saves Direct with its models; every pick leaves the option within 1px; no left border. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./..., provider-proxy, click-scroll, old-webkit, balance-fix and add-sheet pass (click-scroll's WebKit case failed once alongside the others and passes alone, twice). Not tried against a real proxy or the real Wails window.
2026-09-30 02:15:50 +08:00
providers, sign-in, tray, plugins: four of StringKe's reports on Discord — (1) Removing a provider left its editor open and listed, and a second Remove said there was no such provider: the removal had been made, but moving the agents off its models failed on one agent whose config couldn't be written, and Reseat returned that as the change's error; now such an agent is a move with its error ("still on X, not moved: …", a warning in the panel, a line in the CLI and TUI), the others are moved, and the panel, told a delete failed, re-reads the list and closes the editor when the provider is gone. (2) The sign-in waiting sheet shows the sign-in link itself, selected whole on a click, with a copy button, for a browser other than the one that opens. (3) A subscription with several accounts offers "Account in use" among the menu bar's usages, following the account the gateway goes to first. (4) The Antigravity plugin's sign-in never started: magpie called every plugin's authorize with an inputs object, the way OpenCode's CLI does, and opencode-antigravity-auth then asked its Project ID on the terminal — magpie's stdin — until the minute ran out; inputs now go only to a method with prompts, as OpenCode's TUI passes them (checked against the real package under Bun in a sandbox: the Google sign-in URL comes back at once). TestReseatUnmovable, TestTrayInUse, TestInUseOf, TestAuthorizeNoPrompts, provider-remove.test.cjs, signin-link.test.cjs and tray-inuse.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, tray-usages, plugin-signin, signin-callback, factory-sub pass.
2026-09-30 20:45:23 +08:00
providers: a Remote magpie provider uses another computer's shared magpie, each request translated at most once (Jorben on Discord: 远程 magpie / magpie 接力) — a custom provider pointed at another magpie has one API per address and couldn't know which API each model's own provider serves, so a request was translated on both computers (Chat → Messages here, then again there). magpie's /v1/models now tells each model's native_endpoints (the APIs its provider serves it on; none for a routing group) and its image input, and reads them, with max_output_tokens and supported_reasoning_levels, from a list; the new remote-magpie preset takes the other magpie's address however typed (host:port, /v1, /v1/messages) for all three APIs and lists all its models, its user's choice already. Add sheet tile with the address field and a hint to Settings → Share on local network; CLI: magpie provider add remote-magpie <key> url=http://<ip>:3425. Checked with two sandbox magpies over the LAN address with the key check on and a Messages-only fake upstream: Chat, Messages (streamed), count_tokens, Responses, a routing group and Gemini all reached it as /v1/messages, translated once. TestRemoteMagpieNativeAPI, TestFetchMagpieFields and TestRemoteMagpie fail without the change; remote-magpie.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (1ce05f0) as well; click-scroll, old-webkit pass.
2026-10-01 01:35:18 +08:00
rules: a routing group's rule can be for the agent compacting its conversation, so a cheaper, faster model writes the summary while the conversation stays on its own (X, MR.Guo: 希望 Magpie 能像 OpenCode 一样支持单独指定 context compression model:当 Agent 需要做上下文压缩时…指定一个更便宜、更快的小模型专门负责总结和压缩历史上下文) — a compaction went to whatever model the conversation was on, the priciest in the group as often as not, and a long session paid for each summary at that price. A rule's new condition, compact (provider.Rule.Compact, "compacting"; magpie group rule add <group> use=<model> compact, compact=yes|no; the GUI editor's 压缩上下文时 / compacting toggle; the TUI hint), matches a request that is an agent compacting, told from the agent's own prompt (internal/gateway/compacting.go, read from each one's release): the system prompt of Claude Code 2.1's /compact and auto-compact, OpenCode's compaction agent, Pi, Gemini CLI's state_snapshot and Qwen Code, or the last user message of Claude Code's, Codex's (and magpie's own for a Codex compaction_trigger) and Kimi Code's; a prompt only quoted earlier in a conversation doesn't count. Only in a group with such a rule is a request looked at this way, so nothing else changes. A compaction is decided on its own, never held by the turn's rule, its classifier never asked; it isn't remembered for the turn, and neither the conversation's affinity nor the turn's size is taken from its answer, so the requests after it go on on the model the turn was on and its cache (without that, the next request of a Claude Code turn followed the summary to the cheap model). A member whose context is less than the compaction is passed over for the next rule that matches, or the group's order (RuleHit.Small). The trace tells it ("The agent is compacting the conversation, so rule 1 sends the summary to …", the models passed over named), and the editor notes a compact rule's model taking less than another member. Docs: start#rules (en, zh). Tested: internal/gateway/compacting_test.go — each agent's prompt taken, a plain and a quoted one not; in a group with rules compact→a/small and tokens≥2000→b/big, a long Claude Code turn on b, its /compact to a (Compact, N=1), the turn's next request back on b held, Codex's compaction to a too; a 70k-token compaction skipping a (64k) for rule 2 with Small [a/small]; a group with no compact rule routing a compaction as before; ParseRule/Line/Conditions/Matches for compact. TestRuleCompact failed with the affinity kept from the summary. internal/gui/tests/routing-compact.test.cjs, Chromium and WebKit, en and zh: the compaction in the trace, the rule's words, the toggle on and off with nothing moved, the room note, a compacting-only rule saved as compact:true and not refused. routing-kind/served/signin/steady, click-scroll and old-webkit pass. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against real agents' compactions through the gateway (the prompts are matched as read from their releases) nor in the real Wails window.
2026-09-30 10:24:18 +08:00
gateway: a call Codex makes for itself says so on the Routing page, and goes to a pool account with what Codex says about it (碳碳双键: 为何 codex 桌面选的 5.6sol,但是请求记录…显示的全是 gpt-6-luna) — magpie changes no model: Codex sends its own calls on Luna (the guardian review of an approval, x-openai-subagent: guardian; a thread's title; memories) and, on Luna Reserve once the plan's allowance is used up, every turn with x-openai-codex-luna-reserve. Relayed to Codex's own sign-in these headers went on as they came, but a turn the pool of Codex's accounts served was signed afresh and went without them. Now x-openai-*, x-codex-* (turn metadata, installation, window, parent thread), session_id, conversation_id, x-client-request-id and version go on with it too; never a sign-in (Authorization, chatgpt-account-id, anything with authorization in its name) nor Codex's device attestation, the pool account's own going in its place, nor what holds for Codex's own account alone (x-openai-codex-luna-reserve, as another account's allowance may be left, and x-codex-turn-state, the backend's routing of the turn for that account). The trace, the Recent calls and the usage log keep the call's kind (kind, from x-openai-subagent; memgen; luna_reserve), and the Requests list, a replay and the request's story show it as a grey tag by the model — Approval check / 审批判定, Title / 标题, Memory / 记忆, Subagent / 子代理, Review / 审查, Compaction / 压缩, Luna Reserve / Luna 储备, an unknown one as Codex names it — the story saying Codex made the call itself and picks its model. The Requests list's model column fits its content (up to 36%) so model and tag keep their room, the tag giving way first. Tested with a fake ChatGPT backend on the pool path (acct-1 used up, acct-2 answering: both get the headers and neither the client's sign-in; fails without the change), callKind/codexHeader cases, and a new internal/gui/tests/routing-kind.test.cjs in Chromium and WebKit, en and zh (tags, the model shown whole, the story, a click leaving the row where it is); the other GUI tests, go build -tags nogui, GOOS=windows go build and go test -tags nogui pass
2026-09-29 17:56:43 +08:00
routing: a request answered by another model than the one asked for is marked, from the model the vendor's reply names (v on Discord #feedback: 模型路由情况其实有的时候还是想知道的,比如之前 openai 会把 sol 降智到 luna 这样的,现在只有走 s2a 和 newapi 这些网关服务才能明确看到) — magpie read the tokens off each reply but not the model it says answered, so a vendor serving sol's requests with luna looked, on the Routing page, like sol answering. The reply's model is now kept as it goes by, never delayed or changed: on a relayed reply by the usage sniffer, which already reads every event and JSON body (Chat Completions' model in its JSON and each chunk, the Responses API's response.model in response.created/completed and model in its JSON, Anthropic's message.model in message_start and model in its JSON); on a translated one from the decoders' start event, which already carried it (Chat, Responses, Anthropic, and Gemini Code Assist's modelVersion), streamed or collected; on a Claude subscription's from the CLI's message_start; and on Codex's own models relayed to the ChatGPT backend. It goes in Usage.Served (and so the request log), each try's Served and the route's (the last try's, when it answered), with Swapped when it is another model than the one the try sent the vendor. Vendors mostly echo a dated or pinned name, which is the same model: the names are compared without a maker or path prefix (openai/, models/, Bedrock's us.anthropic.), case, and a version at the end — a date (-2025-08-07, -20250929, -06-05), a build number (-0613, -001), Bedrock's -v1:0, -latest, -preview — so gpt-5 answered as gpt-5-2025-08-07 or claude-3-5-sonnet-latest as claude-3-5-sonnet-20241022 is not marked, while gpt-5 as gpt-5-mini, gpt-5 as gpt-5.1, claude-opus-4 as claude-opus-4-1-… and deepseek-v3 as deepseek-v2 are. A reply that names no model marks nothing. The Routing page's Requests list puts an amber "served luna" (实际 luna) tag by the answer, its title saying what it means, and the request's story a line "requested sol · served luna" (请求 sol · 实际 luna) with the same sentence; no border stripe, and picking the request leaves the page where it is. Cursor, Devin, Kiro and Qoder name the model they were asked for themselves, so they are never marked.
2026-09-29 19:41:29 +08:00
providers: Claude Code is signed in to its next account once the one it is on is spent, as Codex is, and an account keeps its own affinity, backoff and usage across a switch (#208 Cyb3rN8: Claude Code 用自己的模型时,登录账号额度用满不会自动切换 · #209: Smart 选中的账号和 Claude Code 登录账号互不同步…切换登录账号还会把按 id 记的状态串到另一个账号上) — Claude Code on its own model asks Anthropic as the account it is signed in to, never through magpie, so with A spent its sessions got "You've hit your session limit" while the gateway had moved on to B. The Codex watcher (KeepCodexOnAnAccountWithRoom, now KeepOnAnAccountWithRoom, same place: started by the magpie serving the gateway, a minute in and every 5 minutes) now looks at Claude Code too: when the account it is signed in to is spent and another ticked account's allowance is known and has room, it is signed in to that one through SwitchLogin (credentials and .claude.json's oauthAccount), as Use on the Accounts page does; not when the Claude account was removed from magpie. The rule for #209: the sign-in follows Smart, not the other way round — it moves at the share Smart counts an account spent (provider.SpentShare = 98, which the gateway's usedShare now is), for Codex too (it moved at 100%). The Routing field of a Codex or Claude subscription with several accounts says so: routing picks the account for what comes through magpie; the agent on its own uses the one it is signed in to, moved on at 98%. Sessions already running keep the account they started with until restarted, as SwitchLogin says; routing Claude Code's own model through the gateway (the codexFailover way) is not done here. #209's second part was a real bug: the account the agent is on is the candidate "claude" and the others "claude@user", so a switch handed "claude" from A to B — a conversation A answered was kept on B and traced kept=true, and the Least used tokens and the backoff count followed the id. who(), routed.used and routed.failures are now keyed by restKey (provider@user, as rests already were); rest itself (group members, the trace) is unchanged. Tested with new TestClaudeSwitchedWhenSpent (sandbox HOME, keychain off, a fake usage endpoint: 97% stays, 98% signs Claude Code in to b — credentials and .claude.json — a stays ticked; on b it stays; both spent it stays), TestCodexSwitchedWhenUsedUp still passing, new TestLoginSwitchKeepsEachAccountItself (fails on main: the conversation went to b, kept) and new internal/gui/tests/routing-signin.test.cjs in Chromium and WebKit, en and zh; login-import and routing-kind GUI tests pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion timed out at 5s under the full run's load and pass on their own). Not tested against real Claude accounts or a running Claude Code.
2026-09-29 18:56:33 +08:00
routing: requests coming in leave the routing groups where they are on the screen, a group being edited too (Jerell.OvO on Discord #general: on the window's Routing page the request log refreshes live and each refresh scrolls the page up, so the routing groups further down can't be read or edited; a switch to pause it or moving the log to another tab was suggested) — every request redraws what sits above the groups (the stage gains or loses agent and account rows, the story a line, the requests and accounts lists theirs), and app.js's backToReader kept the view's scrollTop as a fixed number, putting it back after any scroll the reader didn't make: WebKit has no scroll anchoring to make up for it and in Chromium backToReader undid the browser's own, so the groups slid up under the reader as the parts above shrank and down as they grew. In WebKit the accounts list, emptied and refilled every second for its countdowns, also made the view scroll on its own. Now a view can name a part of itself to keep in place (keepInView): its position on the screen is taken when the reader leaves it (their own scroll, or a click held), and backToReader follows it wherever the parts above take it, from a ResizeObserver so before paint, with the browser's own anchoring off for that view so the two don't fight; a view at its top stays at its top. The Routing page keeps the groups in place while they're in the upper half of the view or a field in them has the focus, else the lists, and with neither (the stage in sight) nothing moves. The accounts list is patched, keeping each row that's unchanged (rows with a button are always replaced, so their handler is the new one), not emptied and refilled. No live-update switch was needed and no new strings; no border stripe. Tested with a new internal/gui/tests/routing-steady.test.cjs in Chromium and WebKit: with the view wheeled down to the groups, one open in its editor and text typed into its name, six requests streamed in from the trace (new agents and accounts, failures and retries): the part above the groups really changed (more than 50 px), the section's head, the editor and the last group stayed within 1.5 px on the screen on every layout and scroll seen (WebKit scrolls by whole pixels while the parts above are laid out in fractions), the view's scrollTop moved by just what grew above them, and the field kept its focus and its text and went on taking typing. On origin/main it fails in both engines (the head moves 233 → 171 px). go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (126 tests) pass. Not tried in the real window with a live gateway.
2026-09-29 23:52:30 +08:00
davsync: sync can keep its file in an S3-compatible bucket as well as a WebDAV folder (#296) — AWS S3, Cloudflare R2, Backblaze B2, MinIO, Garage or a NAS. Settings › Sync and backup picks WebDAV or S3: endpoint (empty is AWS), bucket, prefix, region (empty is us-east-1, auto for R2), access key ID and secret, path-style; CLI magpie s3 on s3://bucket/prefix access-key-id=… endpoint=… region=… path-style. The bucket holds the same passphrase-sealed file at <prefix>/magpie/magpie.magpie-backup, signed with SigV4 (no SDK). It is read with its ETag and written with If-Match (If-None-Match: * for the first), so two computers syncing at once don't overwrite each other; a server that refuses conditional writes (501, or 400 naming the header) is remembered and gets a HEAD-then-PUT instead. An IP, localhost or a dotted bucket on https goes path-style on its own. Errors say what to do: wrong key/secret, clock skew, bucket in another region (names it), no such bucket, 403 on the first read on AWS without s3:ListBucket. The secret is kept as the WebDAV password is, sent only to the endpoint and key it was typed for, and may not be the passphrase. sigv4_test (AWS's published vectors), s3_test (fake server: conditions, conflict, fallback, regions, errors), s3_cli_test and s3-sync.test.cjs (Chromium, WebKit, en/zh) fail without the change; tested against MinIO. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestGroupAddSetRemove, a TempDir-cleanup flake under load, passes alone); click-scroll, old-webkit pass.
2026-09-30 17:07:47 +08:00
settings: the warm-ups and WorkBuddy's check-in sit under one row of tabs, Codex | Claude Code | WorkBuddy, the picked one's rows alone in the card (#124 Mrhe525: asked for the service groups as tabs in the heading's place instead of three stacked groups) — since v0.1.406 the Settings page ran on after Preferences with three groups under their own headings, CODEX (Warm up on reset, Daily warm-up), CLAUDE CODE (the same two) and WORKBUDDY (Daily check-in). Now one section follows Preferences whose heading row is the tabs, in the headings' small capitals, the picked one darker on a soft pill (a tablist labelled "Warm-up and check-in" · 预热与签到), and the card under it shows only that service's rows, unchanged: same rows, same settings keys and /api/settings posts, the same status lines (last warm-up, today's check-in) and tooltips. The WorkBuddy tab is there under the condition the group had (an account signed in or the check-in on). The tab is remembered per viewer (magpie.warmTab, localStorage in try/catch, like the panel's tab), Codex to begin with and whenever the remembered WorkBuddy tab is not there. A click on a tab moves nothing: the card is its own height, and a shorter one picked at the page's very end gets room kept at the view's foot by the page's "where the reader is" hold; Left/Right/Home/End move along the tabs (roving tabindex), the tab reached clicked so the key is held the same way. A pill drawn while its card was hidden measured nothing, so its thumb is put under the picked option, still, when the card is shown. No border stripes; one new string, en and zh together, none left unused. Tested with internal/gui/tests/settings-groups.test.cjs rewritten, in Chromium and WebKit, en and zh: the tabs in order where a heading would be, after Preferences and before Local network, Codex's rows alone to begin with and each tab its rows alone, the status on the lines, no left border; with the page scrolled, each control posts codexWarmup, codexWarmAt (On, a new time, Off), claudeWarmup, claudeWarmAt and workbuddyCheckin as before and a tab posts nothing, the page left where it was; the pills' thumbs under their picks on a card shown later (fails without the fix); the arrows, Home and End; at the page's very end, WorkBuddy's shorter card and Codex's taller one by click and key leaving the tabs within 1px; the tab remembered across a reload; no WorkBuddy tab with no account, Codex's shown when WorkBuddy's was remembered. Screenshots looked at in light and dark, en and zh, at 900 and the window's narrowest 560. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the GUI suite (128) pass; click-scroll and usage-ledger failed once while go test ran beside them and pass alone. Not tried in the real Wails window.
2026-09-29 23:52:42 +08:00
library: skills in the user-wide ~/.agents/skills are found, one row with the agents that link or junction to them, and brought in by a link that leaves the shared folder as it is (#227 Aimer779: the Library's "In your agents" never looked in ~/.agents/skills, the shared folder many agents read and where the user keeps skills, many as symlinks to D:\aimer-skills; a skill junctioned from there into Pi and ZCode showed as Pi's with "differs in ZCode") — foundSkills walked only each target's own skills folder, and realDir used filepath.EvalSymlinks, which since Go 1.23 leaves a Windows junction (a mount point, reported as irregular, not a symlink) as it is, so two junctions to one folder were two skills, and the Link check (ModeSymlink) took a junction for a folder of its own, one ImportSkill would move. Now foundSkills reads ~/.agents/skills first (hidden entries, ones without a SKILL.md, ones the library has or linked, skipped as for the agents' folders), a skill there carrying shared (its entry) and the agents whose entry resolves to the very same folder; a byte copy in an agent's folder is still another ("differs in"), as copies were. realDir follows links a part at a time itself (symlinks, and on Windows junctions through os.Readlink, relative targets, links to links, a loop or a missing part giving the path back), and a new linked() (symlink, or junction on Windows) replaces the ModeSymlink checks in foundSkills and ImportSkill. ImportSkill links the library to a skill in the shared folder (Source folder), real folder or link alike, and never moves it; the agents' links to it are replaced by the library's as before; an agent's own folder still moves in. No target writes to ~/.agents/skills: targets.go gives no agent a user-wide ~/.agents/skills (only projects.go's project .agents/skills), so it stays a place skills are found in. The page: a found skill in the shared folder says "shared in ~/.agents/skills/<name>" (共享于) above any "linked from", its Bring in title says it stays where it is and can be given to any agent; the shared path is revealable. Tested with TestSharedAgentsSkills (sandbox HOME/USERPROFILE: a symlink in the shared folder to a folder elsewhere, real folders there, Pi and Codex links to one of them, absolute and relative, Gemini's link to the shared link, a Codex copy, a folder with no SKILL.md and a hidden one; one row each with the right agents, shared, link and others; importing all three leaves the shared folder and the folder elsewhere as they were, the library's entries links to them, the agents' links now the library's, Codex's copy untouched, nothing found after), TestRealDir (absolute, relative, a linked parent, .., a loop, a missing path, linked, within) and TestJunction (mklink /J, Windows only, skipped here), and a new internal/gui/tests/shared-skills.test.cjs in Chromium and WebKit, en and zh. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet for windows and go test -tags nogui ./... pass (TestClaudeSignedOut flaked once, passes alone); the GUI suite passes but for login-import (passes alone) and panel-profiles in WebKit, which fails on main the same under this load. Not tried on real Windows, with real junctions, or in the real window.
2026-09-30 02:15:47 +08:00
providers, sign-in, tray, plugins: four of StringKe's reports on Discord — (1) Removing a provider left its editor open and listed, and a second Remove said there was no such provider: the removal had been made, but moving the agents off its models failed on one agent whose config couldn't be written, and Reseat returned that as the change's error; now such an agent is a move with its error ("still on X, not moved: …", a warning in the panel, a line in the CLI and TUI), the others are moved, and the panel, told a delete failed, re-reads the list and closes the editor when the provider is gone. (2) The sign-in waiting sheet shows the sign-in link itself, selected whole on a click, with a copy button, for a browser other than the one that opens. (3) A subscription with several accounts offers "Account in use" among the menu bar's usages, following the account the gateway goes to first. (4) The Antigravity plugin's sign-in never started: magpie called every plugin's authorize with an inputs object, the way OpenCode's CLI does, and opencode-antigravity-auth then asked its Project ID on the terminal — magpie's stdin — until the minute ran out; inputs now go only to a method with prompts, as OpenCode's TUI passes them (checked against the real package under Bun in a sandbox: the Google sign-in URL comes back at once). TestReseatUnmovable, TestTrayInUse, TestInUseOf, TestAuthorizeNoPrompts, provider-remove.test.cjs, signin-link.test.cjs and tray-inuse.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, tray-usages, plugin-signin, signin-callback, factory-sub pass.
2026-09-30 20:45:23 +08:00
settings: a Text size preference (文字大小) that makes everything in magpie's windows larger, 100/110/125/150%, with Ctrl/Cmd + = − 0 in the window and the tray panel, and Windows' text drawn in its own UI faces (Discord, Clarence Dan: the font size is hard on the eyes on Windows) — Settings → Preferences gains a Text size row under Language whose pick posts to a new POST /api/settings/text-size (saved as settings.TextSize, validated against 100/110/125/150, 0 read as 100, and kept by the Settings page's general save, which never sends it). The size is each webview's own zoom, as a browser zooms, not CSS zoom: CSS zoom leaves getBoundingClientRect and clientX in zoomed pixels while offsets, scrollTop and 100vh are not, and app.js, routing.js and library.js place menus, popovers and the panel's height by those measures; under the webview's zoom the page is laid out again in fewer, larger CSS pixels and every measure agrees. Windows and Linux use Wails' SetZoom (WebView2's zoom factor, WebKitGTK's zoom level; the Zoom window option at creation, so the pages open at the saved size), the Mac WKWebView.pageZoom through cgo (macOS 11+), since Wails' Mac zoom is magnification, which lays nothing out again. None is under 100% because Wails clamps SetZoom below 1 on Windows and Linux. The windows grow around the pages: the panel's width and the height its page asks for (still in CSS pixels) are multiplied by the zoom, capped at the screen's work area (textsize.go panelFrame), and resized at once when the size changes; the main window's minimum is 560×420 CSS pixels at the size, capped by the screen, so a window smaller than that grows. Runtime changes wait for the app to be ready (whenReady), and at startup the Mac applies the zoom once its webviews exist. The Mac header, drawn under the native traffic lights which do not grow, divides its height, left padding and the modal's top by --zoom (set from boot.js before the first paint and from the settings), so it stays 50 points tall and level with them. The shortcuts are Cmd on the Mac and Ctrl elsewhere (the other modifier does nothing), step through the sizes and 0 goes back to 100%, with a "Text size 125%" toast; in magpie web the row is hidden and the keys are left to the browser's own zoom. On Windows the page names its faces (html.win): Segoe UI Variable Text, Segoe UI, then Microsoft YaHei UI for Chinese rather than whatever WebView2 falls back to; the Mac and Linux stack is unchanged, and there is no automatic size bump. New strings in en and zh. Tested: internal/gui/tests/text-size.test.cjs in Chromium and WebKit, en and zh, light and dark — a pick posts and does not scroll the settings page, stays picked after a reload with --zoom from boot.js, the Settings page's other saves leave it, the shortcuts step and reset in the window and the panel, and at 150% (the smallest window, 840×630 points, as 560×420 CSS pixels at device scale 1.5) no view, the header or the panel runs off to the side, the Mac header is 50 points; screenshots at 100% and 150% looked at. currency.test.cjs's viewport is 480 tall instead of 420, as settings-groups' is: the new row put the Currency control under the footer, where WebKit's hover could not reach it. Go: TestTextSizeFrames (panel and window sizes) and TestTextSizeSetting (the endpoint saves, zooms the windows, rejects 133, survives the general save, boot.js carries it) in internal/gui, and the settings round trip. go build -tags nogui (and GOOS=linux, GOOS=windows, plain darwin), go test -tags nogui ./... (TestUserPath flaked once under load and passes alone) and the whole GUI suite with --test-concurrency=2 pass. Not tried in the real Wails window on any platform: the Mac pageZoom, WebView2's zoom factor with the resized panel and the Windows font are untested there.
2026-09-30 02:51:17 +08:00
providers, sign-in, tray, plugins: four of StringKe's reports on Discord — (1) Removing a provider left its editor open and listed, and a second Remove said there was no such provider: the removal had been made, but moving the agents off its models failed on one agent whose config couldn't be written, and Reseat returned that as the change's error; now such an agent is a move with its error ("still on X, not moved: …", a warning in the panel, a line in the CLI and TUI), the others are moved, and the panel, told a delete failed, re-reads the list and closes the editor when the provider is gone. (2) The sign-in waiting sheet shows the sign-in link itself, selected whole on a click, with a copy button, for a browser other than the one that opens. (3) A subscription with several accounts offers "Account in use" among the menu bar's usages, following the account the gateway goes to first. (4) The Antigravity plugin's sign-in never started: magpie called every plugin's authorize with an inputs object, the way OpenCode's CLI does, and opencode-antigravity-auth then asked its Project ID on the terminal — magpie's stdin — until the minute ran out; inputs now go only to a method with prompts, as OpenCode's TUI passes them (checked against the real package under Bun in a sandbox: the Google sign-in URL comes back at once). TestReseatUnmovable, TestTrayInUse, TestInUseOf, TestAuthorizeNoPrompts, provider-remove.test.cjs, signin-link.test.cjs and tray-inuse.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, tray-usages, plugin-signin, signin-callback, factory-sub pass.
2026-09-30 20:45:23 +08:00
tray: the menu bar shows several subscriptions' usage side by side (KevinXC on Discord) — Settings → Usage in the menu bar picked one card; its menu now ticks any number (kept open while ticking, saved once when it closes, Off unticks all, a ticked card that has gone stays listed to untick) and the pill says "3 subscriptions". settings.TrayUsages keeps them in order; a file with only the older trayUsage shows that one, and trayUsage is still written as the first for an older magpie. On the Mac the bird and a cell per card are drawn as one image by AppKit on the status item's button: the card's logo (a black glyph tinted in the menu bar's text colour, a coloured one as it is, the name's letter in a ring when there is none) and its two windows stacked, "42%" over "18%", in the bar's colour light or dark and sharp at any scale; the bird's flap goes through it. Arc flags run into numbers (Codex's logo) are spaced out for AppKit's SVG reader. A row longer than 10 characters (a plan's balance read from its page) is cut with an ellipsis in the bar and whole in the tooltip. Windows and Linux show the cards as text, " | " between them. TestTrayUsageCells, TestSVGArcFlags, TestTrayImage (darwin, renders 0-3 cards light and dark at 2x), TestTrayUsagesMigrate and tray-usages.test.cjs (Chromium, WebKit, en/zh) are new. go vet, go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./..., go test ./internal/gui pass; tray-usages, settings-groups, panel-*, click-scroll, old-webkit pass. Not seen in a live menu bar.
2026-09-30 19:45:44 +08:00
zcode: a seat on a team's GLM Coding Plan (团队套餐) is signed in, routed and metered as ZCode does it, on BigModel (智谱) as well as Z.ai, and a Zhipu key on a team plan shows its 5-hour and weekly windows and resets (#236 EZN7L2C3: a BigModel team GLM Coding Plan, the sign-in refused it and the usage was never read) — magpie's ZCode sign-in only went to Z.ai and only looked for the account's own coding-plan key, so a member of a team's plan, whose seat lives in a team project, was refused, and a key on a team plan read nothing from /api/monitor/usage/quota/limit. Read from ZCode.app 3.14.3 (host/index.js and config/provider/zcode-builtin.json): the team rules account:{zai|bigmodel}-team-coding-plan use the individual plans' base URLs with no extra inference headers; getCustomerInfo lists the projects, projectType "2" being a team's (isBigModelTeamCodingPlanProject); fetchTeamCodingPlanEntitlement asks /api/biz/team/subscribe/product/querySubscribeDetail (EFFECTIVE + memberGrantStatus VALID usable, UNASSIGNED no seat, EXPIRED run out); ensureBigModelTeamPlanProjectApiKeyWithStatus / copyBigModelTeamPlanProjectApiKeySecret find or create "zcode-team-api-key" (keyType 2) in the team project and copy its secret; createBigModelBizHeaders sends the access token bare in Authorization with bigmodel-organization / bigmodel-project; resolveAccountTeamPlanRuntimeApiKey takes the stored oauth:{family}:access_token (skipping BigModel's when it equals zcodejwttoken); buildQuotaLimitUrl adds type=2 and createBigModelUsageHeaders sends the key with the org/project headers; ZCode keeps the chosen team in ~/.zcode/v2/setting.json providerFamilyConnectionSelections (legacy modelProviderFamilySelectedKeys "team-plan:builtin:…", migrateLegacyAccountConnectionSettings); pollPendingOAuth/startOAuthWithPolling init with {provider:"zai"|"bigmodel"}, BigModel's redirect param being redirect, its token the poll's bigmodel.access_token. Now: the sign-in first asks where the account is — Z.ai or BigModel (智谱) (POST /api/signin takes a site, StartSignInAt, "zcode:bigmodel" too) — and on its site tries the account's own plan, then a team project with a usable seat (a new zcode_team.go: zcodeTeamSignIn keeps the team key, org, project and the biz token, the plan named by its product, "GLM Coding Team" otherwise), then the Start Plan; a team seat not yet granted or a team plan expired is refused saying so ("…has no seat on it yet — ask the team's admin…"), and an account with none of them names both plans and the site's subscribe page. ZCode's own team login is read from its credentials and setting.json (zcodeOwnTeam), its key minted lazily and kept (zcodeTeamKeyOf, retried after a minute, "sign in to ZCode again" when refused). Team requests go to the family's Anthropic base signed with the team key, no ZCode headers, never the Start Plan. Team usage asks bigmodel.cn (or api.z.ai) /api/monitor/usage/quota/limit?type=2 with the key and Authorization, Bigmodel-Organization, Bigmodel-Project, Set-Language, Accept-Language: CREDIT_LIMIT windows, unit 3 "5 hours" and unit 6 weekly, "current / total" when there is no remaining; /api/biz/customer-package-reset/list?targetType=TEAM gives the resets, fiveHourResets and weekResets counted where available is true, the soonest expireTime as until; the plan's end from querySubscribeDetail, Renew off. ResetCredits gains byWindow/fiveHour/weekly and Words(), "2 five-hour resets · 1 weekly reset" in the CLI, TUI and GUI (en and zh), with no "Use a reset" button for them since they are spent on the vendor's page. The Zhipu GLM key provider (planquota.go) falls back to the team query when the personal one fails or has no windows, adding the team headers when a ZCode team login holds that key. Personal accounts, Coding Plan and Start Plan, are routed, signed and metered as before. Tested with a new internal/provider/zcode_team_test.go against a fake bigmodel.cn/Z.ai in a sandbox HOME, no real tokens: a BigModel team sign-in through StartSignIn("zcode:bigmodel") is added (redirect param, plan "GLM Coding Team Pro", key made with keyType 2, org/project kept), its requests at the BigModel base signed with the team key and no ZCode headers, usage 5 hours 42% "420 / 1000" with a 5h span and weekly 10%, the quota headers sent, resets 2 five-hour and 1 weekly until 2026-10-05, plan until 2026-12-31; an unassigned seat is refused with no key made; ZCode's own team login from credentials + setting.json is found and metered while an individual plan stays as it was; the key provider's team fallback. TestPlanQuotas gains the type=2 case; the zcode Start Plan and account tests pass. A new internal/gui/tests/zcode-site.test.cjs, Chromium and WebKit, en and zh: starting ZCode's sign-in asks the site and posts nothing, BigModel posts {agent:"zcode",site:"bigmodel"} and keeps it, and the by-window resets read in both languages; signin-callback, click-scroll and login-import pass. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath timed out under load and passes alone). Not tried against a real account: BigModel's OAuth sign-in and poll shape, the Set-Language values, customer-package-reset's field formats, querySubscribeDetail on Z.ai, type=2 for a pasted key with no org/project, and the time formats are from ZCode's code, not seen on the wire; not tried in the real Wails window.
2026-09-30 04:15:06 +08:00
zed: a Zed subscription (Pro, Student, Business) signed in from magpie, several accounts, served through Zed's own cloud (yosocoli5 on X: 买了 Zed 订阅,可以加进供应商吗 — yetone: 当然可以!) — magpie had no way to use Zed's hosted models. Zed is now a subscription in the add sheet (Zed's own logo from zed-industries/zed, lobe-icons has none), signed in as the Zed editor signs in (new internal/zed, the protocol): an RSA key is made, the browser opens zed.dev/native_app_signin with its PKCS#1 public half, magpie's loopback port and a system id, and comes back with user_id and the access token encrypted to the key (OAEP SHA-256, PKCS#1 v1.5 also read), answered with a redirect to zed.dev's succeeded page. The account is asked of /client/users/me (signed "<uid> <token>" with x-zed-system-id), kept with its GitHub login, its default organization and plan; an organization with hosted models turned off is refused. Several accounts can be added and switched like the other subscriptions. Requests get an LLM token from /client/llm_tokens (kept in memory, minted again when Zed answers 401 or x-zed-expired-token/x-zed-outdated-token, once; a refused pair marks the account lapsed, "sign in again"). The model list is Zed's /models for the account (disabled and unknown providers left out; context, output, images and effort levels kept). The gateway asks cloud.zed.dev/completions in Zed's envelope {provider, model, provider_request}, the request written in the model's own provider's API as Zed writes it — Anthropic Messages without stream, OpenAI Responses, xAI chat completions with max_completion_tokens, Gemini's request with models/<id> — and reads the NDJSON reply's events with that API's decoder: a failed status is an error with its status (upstream_http_N, rate limit 429, overloaded 529, payment 402), a reply without stream_ended is an error, not a shorter answer, a 402 says the plan doesn't include hosted models or its allowance is used up. relayQoder's status-keeping relay is now relayStatus, shared by Qoder and Zed. Usage shows the plan and its period's end; Zed reports no usage counter, so no windows. Signing in says first, as Antigravity's does, that Zed serves these models to its own editor and may treat magpie's use as third-party use (en and zh); the import box now uses a subscription's own risk note too. Tested in Go against fake zed.dev/cloud.zed.dev servers, no real account: internal/zed TestSignInKey, TestParseModels, TestReadLines; provider TestZedSignIn (the whole sign-in through the loopback callback, what is kept, the account and its models, the token kept and renewed, the quota's plan and end, a refused pair lapsing) and TestZedFreePlanQuota; gateway TestServeZedProviders (each of the four providers, streaming and not), TestServeZedFailures (stale token retried once, 402, upstream 529 with retry_after, a failed 429, cut short → 502), TestZedRequestShapes. New zed-sub.test.cjs in Chromium and WebKit, en and zh: the Zed row with its plans and logo, the risk note in Zed's words with nothing asked, then Sign in anyway asking for zed. add-sheet, add-button, old-webkit, signin-callback and own-signin-remove pass. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Zed account: the sign-in page's real callback, the LLM token and completions endpoints' acceptance of magpie's headers, and each provider's real event shapes are from Zed's source, not seen on the wire.
2026-09-30 03:19:19 +08:00
providers: a Zhipu or Z.ai key on a team's GLM Coding Plan can be given the team's organization and project IDs, and the Usage page then shows the team's 5-hour and weekly windows (#236 EZN7L2C3: the IDs are in the BigModel console; CC Switch asks the team quota with them) — a key pasted alone was asked for the team's quota (type=2) without the Bigmodel-Organization / Bigmodel-Project headers, which magpie can't find from a key, so a team key showed nothing. providers.json keeps zhipuTeam {org, project} for those key providers (trimmed; both empty removes it; a save without it keeps it, {} clears it; carried through from the Zhipu and Z.ai presets). The team query sends the typed IDs, else those of a ZCode team login holding the key, and is now asked at the key's own host (open.bigmodel.cn, as CC Switch asks it) first and bigmodel.cn (where ZCode asks it) only when that gives no windows. It still runs only when the key's personal quota fails or has no windows. The provider editor shows Team org ID / Team project ID (团队组织 ID / 团队项目 ID), optional, for Zhipu and Z.ai key providers alone, adding and editing; one without the other is refused before anything is posted. Tested with internal/provider/zhipu_team_key_test.go against a fake open.bigmodel.cn / bigmodel.cn in a temp HOME: the IDs kept trimmed, the team asked with type=2, the key and both headers at open.bigmodel.cn and read as 5 hours 42% and weekly 10%, a key without them asked with no team headers at both hosts and showing nothing, a pay-as-you-go key with blank IDs no card, Z.ai taking the fields and DeepSeek not. internal/gui/tests/zhipu-team.test.cjs, Chromium and WebKit, en and zh: the fields for a saved Zhipu provider and from the add sheet for Z.ai, none for DeepSeek or a relay, trimmed values posted, one alone refused, both empty posting {}, nothing scrolled on a click. Both fail without the change. click-scroll, old-webkit, provider-proxy and account-proxy pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real team key or in the real Wails window; the team's reset counts are not read for a key (the reset list is asked only with a ZCode sign-in token).
2026-09-30 05:49:19 +08:00

Dropdown browser regression

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.

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.

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-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-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 providers list's "More subscriptions in Plugins" button and the add sheet's row and "look for a plugin" link lead there. English and Chinese, Chromium and WebKit, with the API faked.

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.

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.

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.

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

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.

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 on: true on its own and lights Off, On posts it back, and neither click scrolls the page. 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.

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.

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, after Preferences, before Local network, 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.

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.

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.

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; 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; 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 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; and a dialog opened and closed over the page keeps every logo it drew rather than making them afresh; 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, 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.

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.

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.

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.

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.

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

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 and the fields as saved. The secret field is empty, with "saved" as its placeholder.

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.

tray-inuse.test.cjs checks Settings, Usage 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.

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-resets.test.cjs checks a Claude account's usage-limit resets (a Max subscriber's Opus 5.5 launch reset): the Usage page's card and the menu bar panel's say how many and until when; Auto-use posts settings/claude-auto-reset for that account; "Use a reset" asks first, as a Claude reset (the one Anthropic names next), then posts usage/claude-reset and says what came of it, Anthropic's not_limited in words. The Routing page's story names a Claude reset used by itself as Claude's. No click moves the page, nothing has a left-border accent; 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. 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.

With Node.js and Playwright available:

node --test internal/gui/tests/menu-scroll.test.cjs internal/gui/tests/click-scroll.test.cjs internal/gui/tests/panel-fold.test.cjs internal/gui/tests/panel-routing.test.cjs internal/gui/tests/gateway-fold.test.cjs internal/gui/tests/routing-kind.test.cjs internal/gui/tests/balance-fix.test.cjs internal/gui/tests/cli-update.test.cjs internal/gui/tests/login-import.test.cjs internal/gui/tests/agy-launch.test.cjs internal/gui/tests/currency.test.cjs internal/gui/tests/usage-ledger.test.cjs internal/gui/tests/update-check.test.cjs internal/gui/tests/omarchy.test.cjs internal/gui/tests/brand.test.cjs node --test internal/gui/tests/signin-callback.test.cjs internal/gui/tests/routing-steady.test.cjs internal/gui/tests/old-webkit.test.cjs internal/gui/tests/session-terminal.test.cjs internal/gui/tests/text-size.test.cjs internal/gui/tests/zcode-site.test.cjs internal/gui/tests/rail-tip.test.cjs internal/gui/tests/sync-update.test.cjs internal/gui/tests/plugin-signin.test.cjs internal/gui/tests/plugin-market.test.cjs internal/gui/tests/s3-sync.test.cjs internal/gui/tests/routing-wb-refused.test.cjs internal/gui/tests/routing-shape.test.cjs internal/gui/tests/market-have.test.cjs internal/gui/tests/sessions-today.test.cjs internal/gui/tests/routing-side-calls.test.cjs internal/gui/tests/import-all-skills.test.cjs internal/gui/tests/routing-flood.test.cjs internal/gui/tests/routing-busy.test.cjs internal/gui/tests/own-skill.test.cjs internal/gui/tests/routing-manual.test.cjs internal/gui/tests/tray-usages.test.cjs internal/gui/tests/provider-remove.test.cjs internal/gui/tests/signin-link.test.cjs internal/gui/tests/tray-inuse.test.cjs internal/gui/tests/codex-auto-reset.test.cjs internal/gui/tests/routing-auto-reset.test.cjs internal/gui/tests/claude-resets.test.cjs internal/gui/tests/library-toggle-status.test.cjs internal/gui/tests/library-all.test.cjs internal/gui/tests/routing-reroute-title.test.cjs internal/gui/tests/plain-names.test.cjs internal/gui/tests/panel-effort.test.cjs internal/gui/tests/agent-models-all-hidden.test.cjs internal/gui/tests/omp-roles.test.cjs internal/gui/tests/claude-effort.test.cjs internal/gui/tests/remote-magpie.test.cjs internal/gui/tests/qoder-cn-tile.test.cjs internal/gui/tests/provider-levels-scroll.test.cjs

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

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 separately from make test.