Files
magpie/internal/gui/tests
yetone 58edc00bba gui: a provider with no model picked opens with none ticked, so Select none and Save stays none (#578, #614: Sun1090 — 我点了全不选,并且点击了保存,但是还是显示24个。重新打开供应商,里面也是勾选了24个free模型; 我没点保存,点了只选free 那个按钮,关闭的时候已经生效了)
The editor took its picks from the models marked on, and with no pick saved magpie marks on the ones agents are served: the vendor's first 24 (Exposed). So a provider with none picked opened with those 24 ticked as if picked, never dashed as v0.1.689 draws the served ones, and the next Save wrote them as picks. Select none and Save did write none, but opening the editor again showed 24 ticked, and a Save from there made them picks. That is also why Free only looked applied without a Save: the buttons only change the editor's draft, and Cancel, Escape or a click outside drop it, but what opened afterwards was the 24 served, ticked, not what was saved. Now the editor starts from the saved picks (the provider's chosen, those of them still served), so none saved opens with none ticked and the served 24 dashed, with the hint saying agents see the vendor's list. Since a fresh provider now opens dashed rather than ticked, a click on a served chip leaves it out as a click on a ticked one does, the rest of what is served picked in its place; a chip not served is picked as before.

provider-draft-picks.test.cjs (30 models, 3 free, none saved: opens with none ticked and 24 served; Free only then Cancel or Escape writes nothing and opens as before; Free only and Save writes the 3; Select none and Save writes [] and opens with none ticked; saved again untouched it stays []) and provider-none-picked.test.cjs (now: opened with none picked, both drawn as served) fail on the old code; model-rate.test.cjs's chip click now leaves the served chip out. go build -tags nogui ./... passes; the provider and model GUI tests under WebKit pass but provider-levels-scroll, which fails on main as well. Chromium doesn't launch on this machine, so WebKit only, and not tried in the app with a real provider.
2026-10-03 20:57:39 +08:00
..
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
2026-10-03 00:18:16 +08:00
providers: a provider's Routing is picked in its editor and made with its Save, and its Stays (会话保持) is beside it (01huadalang on Discord, a WorkBuddy AI subscription with several accounts: 在这里切换会自动保存,没有手动点击就给我保存了,这样切换来看会很卡; 没有看到会话选择) — each click of Smart / In order / In turn / Least used first / Weekly pace posted provider/route, saved, and redrew the providers page with the editor in it, so looking through what each option says lagged and saved a pick only looked at. Routing is now staged in the editor's draft as Names & levels is (b0dc82c4): a click shows the option and its description, says "unsaved" (未保存) beside it, sends and redraws nothing, Cancel drops it, and both editors' Save (a signed-in account's and a key's) carry it. How long a conversation stays with the account or key that answered it (Auto / Session / Within a turn / Off, provider.Affinity, which the gateway already honours for a provider's own accounts) could be set only on the Routing page's "Several accounts or keys" row; it is now in the editor under Routing, staged the same way. The save action takes routing and affinity when sent and keeps them when not: before, every Save of the editor dropped the Stays set on the Routing page, since the editor never sent it and in.Affinity came in empty. routing-save.test.cjs (WebKit and Chromium, en and zh, a subscription with three accounts and a provider with two keys: every option clicked shows its description and unsaved with nothing sent, redrawn or scrolled; Cancel drops them; one Save carries routing "rotate" and affinity "session") fails without the change and passes with it; account-arrange.test.cjs now saves the routing it picks. TestProviderSaveRouting (the Save's picks made, a Save without them keeps them, the Routing page's route and affinity survive the next Save, Smart and Auto sent as "") fails without the change (routing lost). keep-login, provider-levels-save, provider-levels-scroll, click-scroll, editor-short-window, provider-proxy, provider-concurrency, account-proxy, account-models, plugin-move, deprecated-subscriptions, routing-account-order pass in WebKit (redact-rule-row's 640px case fails as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 23:46:00 +08:00
providers: each account of a subscription, and each key, can be kept for some of the provider's models only (#474: 增加账号级别的模型选项, a low-quota but verified account kept for the cheap models) — every account or key a provider had served every model it did, so one couldn't be spared for some. An account's or key's row in its provider's editor now has a models badge: a click opens the provider's models as chips, Save keeps the account for those picked, All models gives it every one again; one kept for some says how many, always shown. magpie provider account-models <id> [account|key [models…|all]] does the same and lists them. It is kept in providers.json as accountModels, an account by its name in lower case, a key by its id; one not in it serves every model, as before. The gateway never sends a model to an account or key whose list leaves it out, in every routing mode, in drag order, in a group and as a fallback, and never falls back to it all the same; the trace says it was left out as set to serve other models. When every account or key there is leaves the model out, the request is refused with a 403 saying so and how to change it, not "none of X's models is ready". TestKeyModelsNarrowCandidates, TestAccountModelsNeverChosen, TestAccountModelsCmd and account-models.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; account-models, click-scroll, old-webkit, account-arrange, account-proxy, account-return, routing-account-order, panel-routing, unlisted-models pass.
2026-10-02 05:50:23 +08:00
providers: 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
agents: Disconnect from magpie takes out everything magpie wrote into an agent's config and puts back what it had before (Fate on Discord: 移除由Magpie写入的配置,也就是重置功能,这个好像不是很明显。初步体验好像是选择默认就会清除写入的配置吗?希望能有一个更直接的交互动作,来代表移除由Magpie写入的配置。) — there was no one action for it, and picking Default isn't it: Default is the agent as installed, and for Codex, Claude Code and Gemini CLI it also forgets the stash, so the user's own provider or relay (model_provider + its table, ANTHROPIC_BASE_URL/AUTH_TOKEN, GOOGLE_GEMINI_BASE_URL/GEMINI_API_KEY and sign-in) was lost rather than put back. agent.Disconnect reuses each agent's own restore path: a new Agent.Unwire, for those three, runs the unroute their own-model pick already used (codex.go and claude.go have it pulled out of set as unroute; gemini.go's was there) and then forgets the stash, and every other field magpie set — each one reading a magpie value, or the value applied.json recorded and Unwire left — is set to "" through its own Set, which for every other agent unstashes what it had. applied.json forgets the agent (Keep), so no drift says it was switched off magpie. Agent.Wired (magpie or a magpie model in any field) goes to the GUI as agentJSON.wired; an agent magpie isn't in is left byte for byte. POST /api/agents/disconnect/{id} runs it under edit.Atomically as Sync and the fields' Sets are.
2026-10-03 02:42:09 +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
agents: Claude Code's alias reads as the model it stands for, and a model it asks Anthropic for itself says it isn't through magpie (StringKe on Discord: "model": "sonnet" showed as a bare sonnet; no ANTHROPIC_BASE_URL in settings.json read as magpie having failed to set it up) — with "model": "sonnet" (or opus[1m], best, haiku, opusplan, default) in Claude Code's settings.json the row showed the word with no logo, matching none of the models offered. The alias is now offered first in the Claude Code group as the model Claude Code takes it for: "sonnet · claude-sonnet-5-5", the newest of its family in the catalog's Anthropic list (no compiled-in table), or the model the user's own env gives that tier (ANTHROPIC_DEFAULT_SONNET_MODEL); [1m] keeps its mark, best is the newest Opus, opusplan names both, default is left to Claude Code's plan. Claude's logo with it; the file is only read, never rewritten, and a model picked in magpie is written as before. Each of Claude Code's own models now carries who it goes to (Anthropic, or the host of the user's own ANTHROPIC_BASE_URL): the row's tooltip says it isn't through magpie, so settings.json names no magpie endpoint and that is expected, and picking one says "straight to Anthropic, not through magpie"; a magpie model says neither. TestClaudeAliasShown and claude-direct.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath and TestClaudeAccountNamedAsItsLogin, load flakes, pass alone); claude-direct, model-pick, claude-effort, agent-model-groups, plain-names, old-webkit pass; click-scroll's WebKit run fails as it does on main without the change.
2026-10-01 19:38:36 +08:00
claude: magpie no longer asks Anthropic for a Claude account's usage, and adding a Claude account warns of a ban (the user: 不要伪造任何的 Claude 请求,能否通过 claude cli 获取 / 我害怕被封; 添加账号时要像其他的 Subscription Provider 一样显示风险提示) — the usage was read by magpie itself calling Anthropic's /api/oauth/usage with the account's token, on a timer, for every saved account, and Claude's usage-limit resets (cedar_ember) were listed, spent and auto-spent the same way. Now the usage is what Claude Code's own /usage prints (claude -p /usage, a command Claude Code answers itself without asking a model), run only when Usage is opened or Refresh pressed (GUI, magpie quota, accounts, TUI), for the account Claude Code is signed in to, at most once in 30s; a saved account shows only what Claude Code reported while using it, or the last reading kept. The Claude resets, Auto-use and their settings are gone (Codex's stay). Session jsonl files hold no usage figures, so they aren't read. Starting a Claude sign-in or import (GUI and CLI) first says Anthropic may suspend or ban the account and that it is at the user's own risk. TestClaudeWindowsAsked, TestParseClaudeUsage, TestClaudeUsageRunsClaudeCode, claude-usage-asked.test.cjs and claude-risk.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; routing-auto-reset, codex-auto-reset, quota-families, tray-usages, click-scroll, old-webkit, plugin-signin, usage-alert, usage-ledger pass.
2026-10-01 12:38:25 +08:00
provider: a Codex reset about to run out unused is spent shortly before it does (#624, jyuuroku16: 快到重置卡到期时间前,自动使用重置卡) — Auto-use only spent a reset once the account's week was used up and no other account could answer, so a reset the account never got that far for ran out unused. For the accounts with Auto-use on, the gateway now goes over them every 10 minutes (KeepResetsFromRunningOut): within 3 hours of the soonest reset running out (the credits' expires_at, already read for the card), if any of the account's windows has been used, it spends that reset — UseCodexReset spends the one that runs out first, so a longer-lasting one is kept. Nothing used means nothing to start again, and it is left to run out. An account is read only when there may be something to do: at most every 6 hours, at 3 hours before a reset runs out, every 10 minutes inside those 3 hours, 30 minutes after a failure. It doesn't count against the week's one reset (a reset lost anyway takes none of it) and spends under the same lock. Auto-use's tooltips (en, zh) say so. TestResetSpentBeforeItRunsOut, TestResetExpiryRetriesLater, TestSpendExpiringCodexResets (a fake ChatGPT: off → never read; on → the soon one spent, not the later one; the next pass doesn't read again); dropping the windows-used or the lead check fails them. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass but for the known flakes (each passes alone).
2026-10-03 11:11:47 +08:00
usage: Hide accounts hides every account, not only an email address, and a WorkBuddy account's card says how many credits it used each day (#568: 默认情况下启动邮箱打码时,workbuddy的用户名那里不会被打码;用量统计能不能统计一下每天用了workbuddy多少积分) — the mask matched email addresses only, so a WorkBuddy account named by its nickname or phone number (and a Qoder, Kiro, Devin, Cursor, CodeBuddy, ZCode or plugin account named so) stayed in sight on a screenshot taken with 邮箱已打码. The page now learns the accounts' names from what magpie answers it (an allowance's user, a provider's account and its logins, a route's account: noteAccounts over every API answer) and hides them as it hides an address, in a row, a sentence and a tooltip, on Usage and Routing; a card's own name (WorkBuddy, ZCode signed in with no name of its own), a provider's or an agent's is never taken for an account, nor is a name inside a longer word. The tray panel hides them too, following the window's setting as it is turned (a storage event); the button is now Hide accounts (账号打码). WorkBuddy's meter tells only what the cycle has used against what it grants, the built-in's card and its plugin's alike, so magpie keeps each reading beside the one before in credits-daily.json (by provider and account: the two figures and the days' sums, nothing else) and counts what was used in between on the local day of the later reading; a reading that has used less (a cycle started again, a pack gone) counts nothing and counting goes on from it, credits granted count nothing, an error or a reading kept from before isn't one. The card, under its Credits meter, says the period's credits (今天 n / 7 天共 n) and over more than a day draws a bar a day, each day's figure in its tooltip, the days before magpie began counting an empty dashed slot saying so, and "Counted since <day>" while that falls in the period. TestDailyCredits (days, a reset, a check-in's grant, a gap, both builds, error and kept readings, another vendor, the file's size, a year on) is new; account-mask.test.cjs and credit-days.test.cjs (WebKit, en and zh) fail without the change and pass with it; mask-emails, click-scroll and balance-parts pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState, TestProvidersAnswerWhileListsComeIn, TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin timed out once under load and pass alone).
2026-10-02 22:21:03 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
gui: the Add sheet shows a new user only magpie's own subscriptions; a deprecated one is installed from Plugins (the owner: 对于新用户来说,这里应该只显示内置的 provider 啊,更多不是要点击那个链接跳转到插件页面让用户去主动安装吗?) — a fresh magpie listed all 18 subscription tiles, the deprecated built-ins (Cursor, Kiro, Zed, Qoder…) among them, each offering its plugin when clicked. A deprecated built-in with no account in magpie (unusedSub) is now left out of the Subscriptions section, which for a new user is Claude, ChatGPT, Copilot, Gemini CLI and Antigravity, then More in Plugins; a search naming a left-out one shows More in Plugins, which opens the Plugins page on that search, where its plugin is installed. One with an account stays, badged Deprecated; one moved to its plugin stays as before; one whose plugin is installed shows the plugin's tile (replacedSub). The tile's plugin offer (Install and sign in / Use the built-in), unreachable now, is removed with its CSS and zh strings. AGENTS.md says so. deprecated-subscriptions.test.cjs now checks Zed (no account) isn't in the sheet, a search for it shows More in Plugins and opens Plugins on "zed" with Zed's card, and nothing signs in or installs by itself; plugin-replaced.test.cjs expects no tile for an unused deprecated built-in and one for a signed-in one. Both pass in Chromium and WebKit (en/zh), as do add-sheet, provider-search, qoder-cn-tile, mobile-web, deprecation-notice, click-scroll, plugin-move-overflow, plugin-movable-review, plugin-move-unserved, plugin-progressive and plugin-check-updates; plugin-move's editor-scroll check in Chromium flakes the same on origin/main.
2026-10-03 00:52:41 +08:00
gui: a short window's provider editor scrolls past its models, and a routing group made from a model is added (悠悠哥 on Discord: in a small window the WorkBuddy editor's model list was cut off and the wheel took it no further until the window was maximized; the new group "Hy4 preview" did nothing on 添加, its 推理档位 label at the right and 成员共有/自定 under the labels) — the model chips (.mchips) scroll on their own, 132px high, with overscroll-behavior: contain; in a short window the editor's body shows little more than the list, so the pointer was nearly always over it, and at its ends the held wheel never took the body on to the fields below (the body stopped at 539 of 660px at 800x500). Now the chips are at most 28vh high, and in a window up to 640px tall their ends pass the wheel on to the body; Names & levels is held to 40vh there. The dialog, head and Save already fit any window. A group opened from a model (newGroupWith: the picker's and the provider editor's "Make a routing group of it", the tray panel's ?newgroup=) had no fast in its draft, so Add threw on d.fast.filter and posted nothing, since 10b94023 (unlisted-models.test.cjs has failed since); the editor now fills a draft's members, fast and rules whoever opened it, a throw in Add (or Enter in the name) is said in the status line as "Couldn't save the group: …" (分组没能保存:…), and Add leaves busy (pointer-events: none) once the save is answered, so a refused save can be pressed again. The classifier row's grid cell was left in the grid when it was hidden, so every row after it slipped one cell; it is hidden with its label now. editor-short-window.test.cjs (12) and group-from-model-add.test.cjs (8) fail without the change. Chromium and WebKit, en/zh: editor-short-window, group-from-model-add, unlisted-models, group-member-fast, routing-effort, routing-effort-row, routing-levels, routing-manual, routing-time, routing-compact, settings-groups, agent-model-groups, add-button, add-sheet, library-all, model-test-one, plugin-market, plugin-move, plugin-signin, provider-levels-scroll, saved-signedout-remove, nokey-editor, model-pick, old-webkit pass; click-scroll and plugin-moved-signin timed out under load (avg 239) and pass alone.
2026-10-01 19:38:36 +08:00
factory: the Gateway page says when an older magpie serves the gateway, and a Factory account can be added by its API key (#506, 黄瑞 and aohun: Factory 403 "Factory refused this account the request; check that droid… sign in to it again" on v0.1.630; aohun: support FACTORY_API_KEY as droid2api does) — the 403's wording quoted in #506 was removed in 5523eb17 (first in v0.1.551; git grep finds it in v0.1.550, not in v0.1.630), so those requests were served by a magpie at v0.1.550 or older still holding :3425 (a magpie serve, a second copy, one that didn't quit): serveGateway leaves the port to any magpie answering GET / and the page said only "running · served by another magpie", so none of the later Factory fixes (droid's opening line on /api/llm/o in v0.1.630 among them) reached their requests. gateway.ServedBy reads the serving magpie's version from its GET / answer (Serving wraps it); the providers state carries it with older when it is below this one's, the Gateway card then says "running · served by magpie <v>, older than this one" with an amber dot and that agents' requests go through it without this version's fixes until it quits, when this one takes over within 15 s (zh too, wrapped, never cut off), and the log says so. A Factory API key (fk-…) is added from the import box (Factory's accounts list "Add an account by API key…", and "Use an API key instead…" on the warning before a sign-in): as droid's om() takes FACTORY_API_KEY, the key is the bearer with droid's headers, never renewed, and sends no X-Factory-Org-Id (droid's active org comes from a sign-in, st() is null for a key); whoami asked with the key says whose it is and its region/host, as droid's yP checks a key ("invalid-api-key" when refused), and a refused key isn't added. factoryFresh hands a key account on as it is (no WorkOS, no whoami for an org) and factoryMendOrg leaves it alone; when Factory has moved to its plugin the key isn't offered, the built-in being what keeps it. No request was made to Factory, no real key or account was tried, and whether Factory serves a key's requests from agents other than droid is not known. TestGatewayServedByAnOlderMagpie (fake magpie answering 0.1.550/0.1.630/0.1.640/no version), TestFactoryAPIKey (fake Factory: import, bearer and headers, no org, no WorkOS, refused key, "exists" again; fails with a 403 without the key handling in factoryFresh) and gateway-older.test.cjs, factory-key.test.cjs (Chromium and WebKit, en and zh) pass; login-import, factory-sub, plugin-moved-signin, gateway-fold pass in WebKit (Chromium's channel launch lacks ICU here). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 14:58:49 +08:00
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
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
factory: the Gateway page says when an older magpie serves the gateway, and a Factory account can be added by its API key (#506, 黄瑞 and aohun: Factory 403 "Factory refused this account the request; check that droid… sign in to it again" on v0.1.630; aohun: support FACTORY_API_KEY as droid2api does) — the 403's wording quoted in #506 was removed in 5523eb17 (first in v0.1.551; git grep finds it in v0.1.550, not in v0.1.630), so those requests were served by a magpie at v0.1.550 or older still holding :3425 (a magpie serve, a second copy, one that didn't quit): serveGateway leaves the port to any magpie answering GET / and the page said only "running · served by another magpie", so none of the later Factory fixes (droid's opening line on /api/llm/o in v0.1.630 among them) reached their requests. gateway.ServedBy reads the serving magpie's version from its GET / answer (Serving wraps it); the providers state carries it with older when it is below this one's, the Gateway card then says "running · served by magpie <v>, older than this one" with an amber dot and that agents' requests go through it without this version's fixes until it quits, when this one takes over within 15 s (zh too, wrapped, never cut off), and the log says so. A Factory API key (fk-…) is added from the import box (Factory's accounts list "Add an account by API key…", and "Use an API key instead…" on the warning before a sign-in): as droid's om() takes FACTORY_API_KEY, the key is the bearer with droid's headers, never renewed, and sends no X-Factory-Org-Id (droid's active org comes from a sign-in, st() is null for a key); whoami asked with the key says whose it is and its region/host, as droid's yP checks a key ("invalid-api-key" when refused), and a refused key isn't added. factoryFresh hands a key account on as it is (no WorkOS, no whoami for an org) and factoryMendOrg leaves it alone; when Factory has moved to its plugin the key isn't offered, the built-in being what keeps it. No request was made to Factory, no real key or account was tried, and whether Factory serves a key's requests from agents other than droid is not known. TestGatewayServedByAnOlderMagpie (fake magpie answering 0.1.550/0.1.630/0.1.640/no version), TestFactoryAPIKey (fake Factory: import, bearer and headers, no org, no WorkOS, refused key, "exists" again; fails with a 403 without the key handling in factoryFresh) and gateway-older.test.cjs, factory-key.test.cjs (Chromium and WebKit, en and zh) pass; login-import, factory-sub, plugin-moved-signin, gateway-fold pass in WebKit (Chromium's channel launch lacks ICU here). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 14:58:49 +08:00
library: checking skills for updates asks GitHub with a GitHub token, set in Settings → Network and sharing or taken from GITHUB_TOKEN / GH_TOKEN, and a check GitHub limited says until when and that a token raises it (Player on Discord: 资源库检查技能更新时提示 github is limiting request,希望能设置 GitHub token) — the check asks GitHub's API for the last commit to each skill's folder, which GitHub answers 60 times an hour for an address without a token, so a library of a few repositories, checked twice, or a network shared with others, used it up, and the check said only "GitHub is limiting requests from here until 15:04", with nothing to do about it. Now the API's requests carry Authorization: Bearer <token> (5,000 an hour) when there is one: the GitHub token set in Settings → Network and sharing (a new GitHub row: typed masked, saved on its own with POST /api/settings/github-token, then shown as ghp_…0123 with Remove; one with a space is refused in the row, what was typed kept), else GITHUB_TOKEN, else GH_TOKEN, the row saying when one of those is used. gh auth token is not run: it would borrow another app's credential without asking. The token goes to GitHub's API's host alone (withGitHubToken checks the request's host against githubAPI's), never to codeload, raw.githubusercontent or anything else, and Go's client drops it on a redirect to another host; rtk's latest-release lookup on api.github.com carries it too. It is kept in settings.json as the LAN key is: the page is told only the masked one and where it is from (githubTokenMask, githubTokenFrom), a save of the Settings page keeps it, a backup or sync without keys leaves it out, and restoring one without keys keeps this machine's. Each folder's answer is kept with its ETag and asked for again with If-None-Match, so an unchanged one is a 304, which GitHub doesn't count against the limit. A rate limit (403 with the limit spent, a secondary limit's Retry-After, or 429) now says it plainly: without a token, "GitHub's rate limit for requests without a token (60 an hour) is used up until 15:04. Add a GitHub token in Settings → Network and sharing to raise it to 5,000 an hour"; with one, that the token's limit is used up until then; and the check carries limited {until, token} for the page, which says the same in its own words, in English and Chinese, in the status and on each Not checked tag. A token GitHub refuses (401) says which: the one in Settings, or GITHUB_TOKEN / GH_TOKEN. The library's test sandbox clears GITHUB_TOKEN and GH_TOKEN, so no developer's token reaches a fake GitHub. TestGitHubTokenSent (no token: none sent; GH_TOKEN; GITHUB_TOKEN before it; Settings before both; codeload never gets it), TestGitHubTokenOnlyToTheAPI (api.github.com only, not codeload, raw, a look-alike host or the gateway; dropped on a redirect), TestGitHubRateLimitSaid (until when and the token advice without one, not with one, the token never in the error, a secondary limit, a refused token from Settings and from GH_TOKEN), TestGitHubConditionalRequests (If-None-Match, and the 304 answered as the 200 was), TestGitHubTokenBackup (left out without keys, fails without the change; kept on a keyless restore), TestGitHubTokenSetting (set, masked, kept by a Settings save, refused with a space, removed with the environment's then said) and github-token.test.cjs (WebKit, en and zh: the row, a refusal keeping what was typed, saved masked with the token nowhere on the page, removed with GH_TOKEN said, nothing scrolls; a limited check's status and tag say until when, and the token advice only without one) are new; TestCheckSkills, TestSettingsSaveKeepsWhatItDoesNotSend (now with the token) pass. GitHub was never called: httptest stands in for it. settings-sections, search-api, click-scroll, library-toggle-status, library-all, otel, lan-docker, every-skill pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed on its TempDir cleanup, the known flake).
2026-10-02 21:16:50 +08:00
gui: a short window's provider editor scrolls past its models, and a routing group made from a model is added (悠悠哥 on Discord: in a small window the WorkBuddy editor's model list was cut off and the wheel took it no further until the window was maximized; the new group "Hy4 preview" did nothing on 添加, its 推理档位 label at the right and 成员共有/自定 under the labels) — the model chips (.mchips) scroll on their own, 132px high, with overscroll-behavior: contain; in a short window the editor's body shows little more than the list, so the pointer was nearly always over it, and at its ends the held wheel never took the body on to the fields below (the body stopped at 539 of 660px at 800x500). Now the chips are at most 28vh high, and in a window up to 640px tall their ends pass the wheel on to the body; Names & levels is held to 40vh there. The dialog, head and Save already fit any window. A group opened from a model (newGroupWith: the picker's and the provider editor's "Make a routing group of it", the tray panel's ?newgroup=) had no fast in its draft, so Add threw on d.fast.filter and posted nothing, since 10b94023 (unlisted-models.test.cjs has failed since); the editor now fills a draft's members, fast and rules whoever opened it, a throw in Add (or Enter in the name) is said in the status line as "Couldn't save the group: …" (分组没能保存:…), and Add leaves busy (pointer-events: none) once the save is answered, so a refused save can be pressed again. The classifier row's grid cell was left in the grid when it was hidden, so every row after it slipped one cell; it is hidden with its label now. editor-short-window.test.cjs (12) and group-from-model-add.test.cjs (8) fail without the change. Chromium and WebKit, en/zh: editor-short-window, group-from-model-add, unlisted-models, group-member-fast, routing-effort, routing-effort-row, routing-levels, routing-manual, routing-time, routing-compact, settings-groups, agent-model-groups, add-button, add-sheet, library-all, model-test-one, plugin-market, plugin-move, plugin-signin, provider-levels-scroll, saved-signedout-remove, nokey-editor, model-pick, old-webkit pass; click-scroll and plugin-moved-signin timed out under load (avg 239) and pass alone.
2026-10-01 19:38:36 +08:00
routing: a group's member can be sent in its vendor's fast mode, beside its own reasoning (vs on Discord: a member "gpt-6.1 sol · high · fast") — a member's effort was already its own (#189, "provider/model:effort") but there was no way to ask for speed, so a group could not hold "gpt-6.1-sol at high, priority" next to slower members. A group now lists in fast the members it sends fast, as Members spells them, kept beside Members rather than in their ids so an older version still routes to them, only not fast (checked: HEAD's build on the same providers.json sends the same members at their effort, no tier, and leaves the file as it was). When the gateway sends to such a member it asks in the upstream's own words, only where the model has a fast mode magpie can ask for (provider.CanFast): service_tier "priority" for a ChatGPT account's GPT models and an OpenAI key's GPT/o-series models on its Responses API, Cursor's -fast variant, and speed "fast" with the fast-mode-2026-02-01 beta for Claude Opus 4.8/5/5.5 on an Anthropic key (api.anthropic.com); a relay, Bedrock, a Claude subscription, a model without one, or a provider since moved off those hosts is sent untouched. It is applied in the agent's own API and carried through translation (parseAnthropic now reads speed), so Chat, Responses and Anthropic agents all get it. A translated request for OpenAI's Chat API is still told no tier (TestCursorPluginFast), hence the Responses requirement. The trace marks the try, the order and the group (fast), shown as "fast" beside the reasoning in Requests and the order. Typed, a member is provider/model[:effort]:fast: magpie group add/set models=… takes it, fast=<m1>,… sets them, models-= takes either spelling, and the group lists and shows members as typed with "· fast"; a model with no fast mode, or a group in the group, is refused. The GUI group editor shows a "Standard speed"/"Fast" toggle (标准速度/快速) beside a member's reasoning only for a model that has one, the card reads "· fast" after the reasoning, and fast follows a member's reasoning change and leaves with a removed member; the TUI's group view takes E to step a member's effort and F to toggle fast. Provider renames carry it. TestFastMemberOpenAI, TestFastMemberAnthropic, TestWithFastWire (gateway, per wire format, a fake vendor recording body and anthropic-beta), TestCanFast, TestSaveGroupFast, TestGroupMembersFast, TestGroupMemberEffortAndFast, TestGroupSaveFast and group-member-fast.test.cjs fail without the change. A sandbox magpie (own HOME, gateway on 3966) with providers at http://api.openai.com and http://api.anthropic.com through a local recording proxy sent priority+high to OpenAI and speed fast + the beta to Anthropic from Chat, Responses and Anthropic clients, and nothing extra for Sonnet or a non-fast member. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; group-member-fast, click-scroll, old-webkit, routing-levels, routing-manual, routing-effort, settings-groups pass (Chromium, WebKit, en/zh).
2026-10-01 17:13:34 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
library: Claude Desktop takes skills, into Cowork's skills-plugin with their manifest entries (#638) — Desktop had MCP servers only and showed as "has no skills folder". Every account's local-agent-mode-sessions/skills-plugin/<org>/<account> (one with a manifest.json or .claude-plugin/plugin.json, in Claude and Claude-3p alike, no id hard-coded) gets a copy of each skill it is given — a real folder, as Cowork won't read a linked skill's other files from outside the plugin — and an entry in manifest.json (skillId, name, description from SKILL.md, creatorType user, syncManaged false, updatedAt, enabled), which Desktop lists from. The other entries and keys, and an entry's enabled and other fields, stay; times are written as the manifest's are (ms or ISO). A name Desktop has of its own (built in, or made there) is left and reported. The copy carries magpie's mark with the hash of what was copied: it is made again when the library's skill changes, left as it is once changed in Desktop (reported), and set aside into the backups rather than deleted when the library stops giving it; only magpie's copies and entries are ever taken out, and magpie's entry whose folder went gets it back. Desktop's own skills aren't offered to bring in. The Skills tab says Desktop shows changes once its window is reloaded (⌘R / Ctrl+R). TestClaudeDesktopSkills (two accounts, Claude and Claude-3p) and library-desktop-skills.test.cjs (WebKit, en/zh) cover it. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real Claude Desktop.
2026-10-03 13:51:37 +08:00
library: RTK says when rtk isn't on the PATH the agents get, and puts it there when asked (#601, Moody-Sin: magpie好像没有正确安装RTK — Pi said "RTK disabled: rtk binary not found in PATH" while the Library showed RTK working) — rtk's hooks run it by name (Pi's extension probes rtk --version and turns itself off when that fails; Claude Code's and Codex's are rtk hook …; the commands they rewrite to are rtk git status and so on), but magpie looked for rtk in more places than an agent's PATH: ~/.local/bin and ~/.cargo/bin, which the app adds to its own PATH whether the user's shell has them or not (rtk's own install.sh puts it in ~/.local/bin and only warns when that isn't on PATH), and WinGet\Links on Windows, which winget doesn't always link into. So rtk was "installed", the agents were switched on with it by its whole path, and none of them could run it. Now the PATH a terminal opened now has is read — the login shell's (proc.LoginPath, the one UserPath already asks for, kept a minute) or on Windows the registry's (moved from provider's registryPath) — and when rtk isn't in it the RTK card says so, with what Put RTK on PATH will do, and each agent that has rtk is tagged Not on PATH (不在 PATH 中). Put RTK on PATH (magpie library rtk path) links rtk into the first of ~/.local/bin, ~/bin, /opt/homebrew/bin, /usr/local/bin, linuxbrew's bin, ~/.cargo/bin that is on that PATH and writable, or on Windows adds rtk's own folder to the user's PATH in the registry (a link needs Developer Mode, and a copy is left behind when winget upgrades rtk; winget's package folder doesn't change across upgrades) and tells Explorer; with no such folder it says which folder to add to PATH in the shell profile — shell profiles are never edited. rtk is also found in winget's package folder (WinGet\Packages\rtk-ai.rtk_*), and rtk's script upgrades a linked rtk where the link points, not over the link. TestRTKOffPath and library-rtk-path.test.cjs (en, zh) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet, go test -tags nogui -count=1 ./... under a temp HOME pass but the known TestFXInState flake. The Windows registry path is built, not run on Windows.
2026-10-03 02:38:17 +08:00
gui: lightweight mode, off by default, lets a closed panel's or window's webview go and makes it again when it is opened (somls on #580: GUI版可否增加轻量模式,关闭面板退出webview,减少资源占用) — closing the tray panel or the main window only hid it, so each kept its page and, on a Mac, its WebKit WebContent process (about 65 MB each at start, 95–100 MB after use) for as long as magpie ran. Settings → General now has Lightweight mode (轻量模式), shown in the app only (not on a browser's page) and kept as this computer's own through a sync or a restored backup (KeepOwn): with it on, a window found closed on two looks in a row 30 s apart (lightStep) is let go 30–60 s after it was closed, so a panel opened again soon after is still the same one at once. Letting go happens on the main thread under a new h.winMu: the window field is set to nil, the panel is unhooked from the tray icon (AttachWindow(nil); left click goes through magpie's own OnClick), and the main window is marked gone so its closing hook, which otherwise cancels a close and hides, lets it close; on the Mac the WKWebView is taken out of the window first (stopLoading, delegates and script handlers cleared, removed from its superview), since Wails' NSWindow close alone left the WebContent process alive for minutes. Opening it again (the tray icon, ShowMain, an import link, the Dock) makes the window anew with the options it was started with (makeMain/makePanel, moved out of Run with their hooks), its text size and title bar or Hyprland name (madeAgain), on Windows shown only once its page has loaded; every other path that reached h.main or h.panel (fit, tint, zoom, hide, glide, the Windows title-bar tint, the Hyprland click watch, the folder picker) now goes through the main thread and does nothing to a window let go; Linux keeps running with no window left (DisableQuitOnLastWindowClosed). Measured in a sandbox build under a temp HOME on other ports: started in the tray with the setting on, two WebContent processes of ~65 MB each; about a minute later both windows were let go and both processes ended (GPU and Networking, ~56 MB, are shared and stay; magpie's own process is unchanged), so the saving is ~130 MB idle and up to ~200 MB after use; the main window and the panel were then opened again with their pages, tinted and at the tray icon, closed, let go a second time, and magpie quit cleanly, with fit/tint/zoom calls every 0.2 s through a release crashing nothing. Windows and Linux build but were not run. TestLightStep and TestLightweightKeptAndOwn are new; lightweight.test.cjs (WebKit, en and zh: the row, off by default, On/Off posted as lightweight true/false, another setting's save keeping it, no scroll, no left-border accent, no row on a browser's page) is new and passes, as do click-scroll, settings-sections and searcher-pick. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 00:24:49 +08:00
plugins: the Plugins page is drawn in parts, Installed at once and npm's figures after (#488: the page showed skeletons for seconds, even on Installed) — the page waited on /api/plugins/market, which answered only once the plugin host, the registry fetch and an npm request for every package (two each, 8s apiece, all at once, kept an hour in memory only) had all come back, so the slowest package decided when anything was drawn. The page now asks for each part on its own, as the Library page does: Installed is drawn from GET /api/plugins as soon as it answers; Discover's cards from the new GET /api/plugins/listings (the registry, with its cached and built-in copies), each with what npm said last; and versions, weekly downloads and an installed plugin's "v… out" fill in from the new GET /api/plugins/npm?names=…, never waited on (a card's button waits, disabled, until npm has said whether it has the package). npm is asked at most six packages at a time and for no more than 8s in all; a package it answers late is what it said last, and its answer is kept for the next time. What npm said is kept on disk (~/.cache/magpie/plugin-npm.json) beside the registry's copy, so a magpie just started shows it before asking, and GET /api/plugins gives each plugin's newest version from it. /api/plugins/market still answers the whole market as before. Once loaded the page shows what it did. TestNPMInfoKeptOnDisk, TestNPMInfoDeadline, TestNPMInfoAtOnce, TestPluginMarketInParts and plugin-progressive.test.cjs (Chromium, WebKit; en, zh; Installed while the market is held pending) fail without the change; plugin-local, plugin-updates, plugin-market, plugin-move, plugin-move-overflow, plugin-release, plugin-git, plugin-key-hint, plugin-plan, list-sort, click-scroll, old-webkit pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 08:20:32 +08:00
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
library: a remote MCP server signed in once in magpie is used by every agent without signing in again (#615) — a streamable-HTTP server behind OAuth (Neon, Notion, Supabase) had each agent run its own sign-in. The server's editor now has Sign in: magpie finds the authorization server from the server's 401 (RFC 9728 protected-resource metadata, RFC 8414/OIDC metadata, else the 2025-03 /authorize, /token, /register at its origin), registers itself (RFC 7591, a 127.0.0.1 redirect), opens the browser with PKCE S256 and the resource, and keeps the tokens in mcp-signins.json beside its other account secrets (0600, never the keychain). While signed in, the agents are given the gateway's /mcp/<name> for that server, without the Authorization the library has for it; the gateway passes POST (JSON or event stream), the GET stream and DELETE on with the Mcp-Session-Id and magpie's token, renews it 2 minutes before it runs out and once on a 401, one renewal per server at a time, and a sign-in the server won't renew is answered 502 "sign in to it again" (never a 401 the agent would act on). Signing out, a new URL or an SSE server gives the agents the server's own address as before; a rename keeps the sign-in and removing the server drops it. /mcp/ is local-only or LAN-shared with the key, and refuses a request with an Origin or a Host not naming this computer, so a web page can't borrow the sign-in through the browser (form posts, DNS rebinding), and isn't counted as busy or key-limited. WSL agents are given their distro's gateway address. The row says Signed in or Sign-in ran out; zh strings added. Tests: TestSignIn, TestSignInWithoutMetadata, TestSignInPage, TestNothingToSignIn, TestTokenRenewsBeforeItRunsOut (8 calls, 1 renewal), TestRenewRefused, TestRenewRefusedForGood, TestMCPProxy, TestMCPProxyStaysHere, TestMCPProxyNotForWebPages (fails without the Origin/Host check), TestSignedInServerGoesThroughMagpie against a local fake OAuth+MCP server; mcp-signin.test.cjs (webkit, en and zh). Not tried against a real Neon/Notion/Supabase sign-in. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 09:37:39 +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
provider: OpenCode Zen's Jev routes groups on System One, and a preset's or a subscription's model can be asked on the API picked (Discord, 01huadalang: Zen's "Jev free" was asked as a chat model and failed with no way to switch; OpenCode Go's DeepSeek V4.1 Flash answers on Responses too, and magpie asked it on chat alone, while a custom provider could pick) — Jev isn't an Anthropic model: OpenCode's docs (anomalyco/opencode packages/web/src/content/docs/zen.mdx) list jev-1.13 and jev-1.13-free at https://opencode.ai/zen/v1/systemone alone, and models.dev has no entry for it; asked keyless, Zen answers it there and errors on chat, messages and responses. The opencode-zen preset now has Decide (https://opencode.ai/zen/v1), making Zen a mixed decision provider (#609): Jev is left out of agents' models and is a routing groups' classifier, the free one when Zen is used without a key; a Zen provider saved before gets Decide back in Normalize. Zen's and Go's other models already take their API from models.dev's npm package (catalog.APIOf), which matches the docs. The per-model API custom providers have (settings ModelAPIs, the gateway's Provider.APIs) now reaches presets and subscriptions with more than one API: ModelAPI no longer refuses an account, APIs() is the pick or ListedAPIs (what the vendor's list says, sent to the editor as a model's auto); the editor's model chip has "Asked on: Auto…" on its right-click, opening the app's menu (Auto says what the vendor's list says, then each API the provider has), tagging the chip with the pick, staged and sent with Save as modelPrefs, the page not moved; Names & levels' API row shows for subscriptions too. English and Chinese. TestOpenCodeZenJevDecides and TestPresetAndSubscriptionModelAPI fail on the old code and pass; internal/gui/tests/model-api-preset.test.cjs (WebKit, en/zh) covers the menu, no <select>, no left-border accent, no scroll, the Save's modelPrefs, a subscription's tag and a single-API provider with nothing to pick, and fails on the old assets. A sandbox magpie (keyless Zen) answered POST /v1/systemone with opencode-zen/jev-1.13-free (noul 0.44) and chat on mimo-v2.6-flash-free, Jev not in /v1/models. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, a timing test that passes alone; provider-levels-scroll.test.cjs fails the same on origin/main. Not tried with a paid Zen/Go key.
2026-10-03 16:03:32 +08:00
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
models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
2026-10-01 23:20:57 +08:00
providers: a ClinePass model's test is asked as Cline asks, and every provider's model chips say on a right-click whether they can be tested (ARNO on Discord: clinepass的free模型在右击检测的时候会报错 cline-free/xxxxx 没有响应: 500·empty response content; 有些provider里的模型可以右击检测,有些却不可以,为什么?) — the test already carried Cline's desktop headers (probe sets ClineClient), but its body was the generic smallest one: not streamed, max_tokens 16. Cline's free models (deepseek-v4.1-flash, mimo…) reason first, so 16 tokens left nothing to say and the Cline API answered 500 "empty response content", while agents' requests (streamed, with room) were answered (lml). A Cline probe (clineProbe) is now asked as Cline's clients ask: stream true, max_tokens 1024; and probe reads any streamed answer only to the model's first word, thought or tool call (Chat, Responses and Anthropic events), so the room isn't spent, and an error the stream gives after its 200 (an error chunk, Anthropic's error event, response.failed) fails the test with its message instead of passing it — this also applies to the probes that already streamed (stream-only accounts, OpenCode Zen's free tier); a stream that ends with neither is answered, as a 200 was. Which models could be tested was !decide && !account in app.js, with no reason: every signed-in account lost the right-click and Test models, even those magpie sends requests at an endpoint (Claude, Codex, Copilot, Grok, WorkBuddy, ZCode, plugins), whose endpoint Test was offered and which TestModels already handled. provider.ModelTest now says why a provider's models can't each be tested — "decide" for a classifier, "own-api" for a sign-in reached only through its agent's own API (Cursor, Devin, Kiro, Zed, Qoder, a Google sign-in), whose requests the gateway translates and which has no endpoint to probe — and providerJSON carries it as modelTest. The chip's right-click and Test models are offered wherever it is empty; elsewhere the right-click still opens the menu, its Test this model off with the reason under it and as its title (openRowMenu items take a why), and Test models is shown off with the reason as its title; en and zh, no coloured left border, nothing scrolls. TestModels refuses a classifier's model rather than probe its endpoint. No request was made to Cline. TestClineModelTestStreams (a fake Cline API that answers 500 "empty response content" to a non-streamed 16-token request and a stream to Cline's), TestProbeReadsTheStream and TestModelTest fail without the change; model-test-why.test.cjs (WebKit, and Chromium through a temp copy on plain chromium.launch(); en and zh: a Codex account's chip tests its model and Test models is on; a Kiro account's right-click shows the item off with the reason in the menu and its title, the menu fits, Test models off with the reason, nothing posted, nothing moved) fails without it. model-test-one, click-scroll, account-models, provider-typed and models-free-only pass in WebKit and Chromium. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup race, passes on the rerun; untouched).
2026-10-02 14:20:08 +08:00
providers: a provider's model list can be set to its free models alone, with Free only beside Select all and Select none (Discord: 是否可以支持只选择免费模型?现在只有"全选"和"全不选"两个按键) — the provider editor's model list (renderModels: keys, custom, signed-in accounts) could pick every model or none, so keeping just the free ones (ClinePass's cline-free/, WorkBuddy's x0.00, OpenCode Zen's, a vendor's foo:free) meant clicking each by hand. A third button, "Free only" / 只选免费, picks exactly the models the list marks free (m.free, or a name that says free, as the FREE badge has it), those the filter shows as Select all does, and unpicks the others, models added by hand among them; it sets the editor's picks as the other two do, so a Save sends them. It is offered only when the list has both free and paid models: with none free it would pick nothing, with all free it is Select all. Same text-action style as the other two, no coloured left border, and a click leaves the page and the editor where they are. That is the only model list with Select all / Select none in app.js (the import's "Select all" is a checkbox of providers, not models). models-free-only.test.cjs (WebKit, and Chromium through a temp copy on plain chromium.launch(), as channel chromium has no ICU data here; en and zh: not offered on an all-paid or all-free list, offered on a mixed one, styled as Select all, the filter's free models alone picked under a filter, exactly the three free ones picked with the editor's body scrolled and nothing moving, a Save posting them, both strings translated) fails without the change. provider-picks-stay, account-models, model-test-one, provider-typed, editor-short-window and click-scroll pass in WebKit and Chromium, except account-models' chromium "Codex account is kept for some models" case, which fails the same on origin/main without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./internal/gui/ (under a temp HOME) pass.
2026-10-02 13:50:37 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
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
models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
2026-10-01 23:20:57 +08:00
plugins: Check for updates asks npm now for each installed plugin's newest version, and each row says what it found (the owner: 插件的检查更新按钮还是得做) — the community's plugins update by themselves a little after magpie starts and every 6 hours, and the page knew of anyone else's new version only from what npm said within the hour (Info's cache), so a reader who heard a plugin had a new version had no way to have magpie look. The foot of Installed (beside Update all) now has Check for updates, shown when a plugin is npm's (a folder's or a git one's has no versions there; a git one keeps its own Update, which fetches it again): it says Checking… with a spinner while POST /api/plugins/check asks npm, a few at a time, past the hour's cache (plugin.CheckNow, through npmAsk, which is npmInfo telling why npm said nothing), and nothing is installed. Each row then says it: "v0.0.9 out" titled "v0.0.7 installed, v0.0.9 on npm" with its Update (the existing upgrade path, Upgrade → Add), "Up to date", or an amber "Couldn't check" chip with a dot, titled why — npm not reached (check your connection or proxy), npm turning requests away (429, try again in a few minutes), npm answering with an error, or npm not having the package (fetchJSON's answers other than 200/404 are now a statusError, so a 429 is told apart). The status line says how many have updates, or that every plugin is up to date, and how many couldn't be checked and why; npm not reached at all is "Couldn't check for updates: npm couldn't be reached — check your connection or proxy". A community plugin's Update is titled that magpie would update it by itself within a few hours and Update does it now. What npm said is kept as Info keeps it, so the rows and the market read it, and an update found for the reader (someone else's, or pinned) waits as the background look's do, putting the dot on Plugins; one npm didn't answer for keeps what waited before. Every new string has its Chinese; the click never scrolls the page. TestCheckNow (an httptest npm registry: the community's and someone else's updates found past an hour-old cache saying otherwise, up to date, 404 missing, 429 limited, 500 registry, git and folder not asked, nothing installed, the cache and the waiting list updated) and TestCheckNowOffline (npm not reached: offline, what waited still waits) are new, as is TestPluginsCheckRoute; plugin-check-updates.test.cjs (WebKit and Chromium, en and zh: Checking… while npm is asked, the rows' out / up to date / couldn't check chips and titles, the status line, Update after the check, offline said plainly, the view's scroll unmoved) fails without the change and passes with it. plugin-updates, plugin-market, plugin-progressive, plugin-git, plugin-local and click-scroll pass in WebKit and Chromium. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 21:58:22 +08:00
plugins: the Plugins page is drawn in parts, Installed at once and npm's figures after (#488: the page showed skeletons for seconds, even on Installed) — the page waited on /api/plugins/market, which answered only once the plugin host, the registry fetch and an npm request for every package (two each, 8s apiece, all at once, kept an hour in memory only) had all come back, so the slowest package decided when anything was drawn. The page now asks for each part on its own, as the Library page does: Installed is drawn from GET /api/plugins as soon as it answers; Discover's cards from the new GET /api/plugins/listings (the registry, with its cached and built-in copies), each with what npm said last; and versions, weekly downloads and an installed plugin's "v… out" fill in from the new GET /api/plugins/npm?names=…, never waited on (a card's button waits, disabled, until npm has said whether it has the package). npm is asked at most six packages at a time and for no more than 8s in all; a package it answers late is what it said last, and its answer is kept for the next time. What npm said is kept on disk (~/.cache/magpie/plugin-npm.json) beside the registry's copy, so a magpie just started shows it before asking, and GET /api/plugins gives each plugin's newest version from it. /api/plugins/market still answers the whole market as before. Once loaded the page shows what it did. TestNPMInfoKeptOnDisk, TestNPMInfoDeadline, TestNPMInfoAtOnce, TestPluginMarketInParts and plugin-progressive.test.cjs (Chromium, WebKit; en, zh; Installed while the market is held pending) fail without the change; plugin-local, plugin-updates, plugin-market, plugin-move, plugin-move-overflow, plugin-release, plugin-git, plugin-key-hint, plugin-plan, list-sort, click-scroll, old-webkit pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 08:20:32 +08:00
plugins: the Plugins page is drawn in parts, Installed at once and npm's figures after (#488: the page showed skeletons for seconds, even on Installed) — the page waited on /api/plugins/market, which answered only once the plugin host, the registry fetch and an npm request for every package (two each, 8s apiece, all at once, kept an hour in memory only) had all come back, so the slowest package decided when anything was drawn. The page now asks for each part on its own, as the Library page does: Installed is drawn from GET /api/plugins as soon as it answers; Discover's cards from the new GET /api/plugins/listings (the registry, with its cached and built-in copies), each with what npm said last; and versions, weekly downloads and an installed plugin's "v… out" fill in from the new GET /api/plugins/npm?names=…, never waited on (a card's button waits, disabled, until npm has said whether it has the package). npm is asked at most six packages at a time and for no more than 8s in all; a package it answers late is what it said last, and its answer is kept for the next time. What npm said is kept on disk (~/.cache/magpie/plugin-npm.json) beside the registry's copy, so a magpie just started shows it before asking, and GET /api/plugins gives each plugin's newest version from it. /api/plugins/market still answers the whole market as before. Once loaded the page shows what it did. TestNPMInfoKeptOnDisk, TestNPMInfoDeadline, TestNPMInfoAtOnce, TestPluginMarketInParts and plugin-progressive.test.cjs (Chromium, WebKit; en, zh; Installed while the market is held pending) fail without the change; plugin-local, plugin-updates, plugin-market, plugin-move, plugin-move-overflow, plugin-release, plugin-git, plugin-key-hint, plugin-plan, list-sort, click-scroll, old-webkit pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 08:20:32 +08:00
plugins: the Plugins page is drawn in parts, Installed at once and npm's figures after (#488: the page showed skeletons for seconds, even on Installed) — the page waited on /api/plugins/market, which answered only once the plugin host, the registry fetch and an npm request for every package (two each, 8s apiece, all at once, kept an hour in memory only) had all come back, so the slowest package decided when anything was drawn. The page now asks for each part on its own, as the Library page does: Installed is drawn from GET /api/plugins as soon as it answers; Discover's cards from the new GET /api/plugins/listings (the registry, with its cached and built-in copies), each with what npm said last; and versions, weekly downloads and an installed plugin's "v… out" fill in from the new GET /api/plugins/npm?names=…, never waited on (a card's button waits, disabled, until npm has said whether it has the package). npm is asked at most six packages at a time and for no more than 8s in all; a package it answers late is what it said last, and its answer is kept for the next time. What npm said is kept on disk (~/.cache/magpie/plugin-npm.json) beside the registry's copy, so a magpie just started shows it before asking, and GET /api/plugins gives each plugin's newest version from it. /api/plugins/market still answers the whole market as before. Once loaded the page shows what it did. TestNPMInfoKeptOnDisk, TestNPMInfoDeadline, TestNPMInfoAtOnce, TestPluginMarketInParts and plugin-progressive.test.cjs (Chromium, WebKit; en, zh; Installed while the market is held pending) fail without the change; plugin-local, plugin-updates, plugin-market, plugin-move, plugin-move-overflow, plugin-release, plugin-git, plugin-key-hint, plugin-plan, list-sort, click-scroll, old-webkit pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 08:20:32 +08:00
gui: built-in subscriptions that have a community plugin are marked Deprecated, with the reason — some subscriptions are reached in ways their vendors' terms may not allow, so to keep magpie itself from being banned they are being decoupled from magpie and run by community plugins instead. The provider list's quiet "can run on community plugins" line is now a deprecation notice (an amber card with the plugin mark: the deprecated signed-in subscriptions, the reason, that moving keeps accounts, models and agents, and Review in Plugins — or Review the move for one — beside Not now, which hides it until another deprecated subscription signs in). Their rows, their Add tiles (signed in or not; /api/providers' new movable lists every built-in with a plugin, from provider.MovableIDs) and the editor's Runs on field carry an outlined Deprecated badge whose tooltip gives the reason; the Add sheet's Subscriptions end with a line saying why, with Browse plugins. A subscription already on its plugin, or one with no plugin, carries none. deprecated-subscriptions.test.cjs (Chromium and WebKit, en and zh: the badges, their reason, none on a moved or keyed provider, the notice, Not now staying hidden), plugin-move and plugin-movable-review updated to the notice. add-button, add-sheet, mobile-web, old-webkit, plugin-move-overflow, plugin-moved-signin, plugin-market, plugin-signin, plugin-plan, provider-search, provider-picks-stay, providers-off-fold pass (click-scroll's 5 failures fail on main as well). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gui/ pass.
2026-10-02 11:39:05 +08:00
zcode: a Start Plan account neither lists nor offers GLM-5.3, and the move to the plugin doesn't count it lost (悠悠哥 on Discord: "仍使用内置:插件不提供 GLM-5.3" moving ZCode onto its plugin) — GLM-5.3 is the Coding Plan's alone (ZCode's builtin config lists GLM-5.3-Flash, GLM-5.2 and GLM-5-Turbo for account:zai-start-plan), and the plugin serves a Start Plan account what ZCode does. But a Z.ai account with a key made and no Coding Plan bought, whose requests go to the Start Plan, was taken for a Coding Plan account until a request found its route (zcodeOnStart with no context said the Coding Plan), and afterwards was listed whatever list was fetched last (the Coding Plan's, another account's or its own before), so GLM-5.3 was listed, could be ticked, and the move found the plugin "short" of it. Now: before a route is found, the plan the account showed at sign-in says it (zcodeOnStartAs, zcodeStartNamed: "Start Plan"); on the Start Plan, a model the plan doesn't serve (zcodeStartServes: ZCode's config on the machine, else zcodeStartModels) is unusable, taken out of a fetched list and out of the picks, with a list fetched or not (Exposed); and a mover may say which models the built-in served on an account (mover.builtin), ZCode's saying a Start Plan account was never served GLM-5.3, so one the built-in didn't serve either is no loss. A model really lost is named with the account and its plan: "the plugin doesn't serve GLM-5.3 for a@x (GLM Coding Pro), though the built-in does" (插件没有为 … 提供 …(内置提供)), the why's args carrying user. TestZCodeStartPlanHasNoGLM53 (a Start Plan account before and after the Coding Plan's list was fetched, GLM-5.3 ticked or not; a Coding Plan account still has it; the mover's builtin) fails without the change (it listed GLM-5.3 GLM-5.3-Flash GLM-5.2 GLM-5-Turbo); TestMoveToPlugin's new case (a model the built-in didn't serve moves; one it did is named with "a@fake (Fake Start)"); plugin-move-unserved.test.cjs (WebKit, en and zh, with an account and without) fails without the app.js change and passes with it, plugin-move.test.cjs passes. No ZCode request or sign-in was made. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass.
2026-10-02 22:00:34 +08:00
gui: built-in subscriptions that have a community plugin are marked Deprecated, with the reason — some subscriptions are reached in ways their vendors' terms may not allow, so to keep magpie itself from being banned they are being decoupled from magpie and run by community plugins instead. The provider list's quiet "can run on community plugins" line is now a deprecation notice (an amber card with the plugin mark: the deprecated signed-in subscriptions, the reason, that moving keeps accounts, models and agents, and Review in Plugins — or Review the move for one — beside Not now, which hides it until another deprecated subscription signs in). Their rows, their Add tiles (signed in or not; /api/providers' new movable lists every built-in with a plugin, from provider.MovableIDs) and the editor's Runs on field carry an outlined Deprecated badge whose tooltip gives the reason; the Add sheet's Subscriptions end with a line saying why, with Browse plugins. A subscription already on its plugin, or one with no plugin, carries none. deprecated-subscriptions.test.cjs (Chromium and WebKit, en and zh: the badges, their reason, none on a moved or keyed provider, the notice, Not now staying hidden), plugin-move and plugin-movable-review updated to the notice. add-button, add-sheet, mobile-web, old-webkit, plugin-move-overflow, plugin-moved-signin, plugin-market, plugin-signin, plugin-plan, provider-search, provider-picks-stay, providers-off-fold pass (click-scroll's 5 failures fail on main as well). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gui/ pass.
2026-10-02 11:39:05 +08:00
plugins: the Plugins page is drawn in parts, Installed at once and npm's figures after (#488: the page showed skeletons for seconds, even on Installed) — the page waited on /api/plugins/market, which answered only once the plugin host, the registry fetch and an npm request for every package (two each, 8s apiece, all at once, kept an hour in memory only) had all come back, so the slowest package decided when anything was drawn. The page now asks for each part on its own, as the Library page does: Installed is drawn from GET /api/plugins as soon as it answers; Discover's cards from the new GET /api/plugins/listings (the registry, with its cached and built-in copies), each with what npm said last; and versions, weekly downloads and an installed plugin's "v… out" fill in from the new GET /api/plugins/npm?names=…, never waited on (a card's button waits, disabled, until npm has said whether it has the package). npm is asked at most six packages at a time and for no more than 8s in all; a package it answers late is what it said last, and its answer is kept for the next time. What npm said is kept on disk (~/.cache/magpie/plugin-npm.json) beside the registry's copy, so a magpie just started shows it before asking, and GET /api/plugins gives each plugin's newest version from it. /api/plugins/market still answers the whole market as before. Once loaded the page shows what it did. TestNPMInfoKeptOnDisk, TestNPMInfoDeadline, TestNPMInfoAtOnce, TestPluginMarketInParts and plugin-progressive.test.cjs (Chromium, WebKit; en, zh; Installed while the market is held pending) fail without the change; plugin-local, plugin-updates, plugin-market, plugin-move, plugin-move-overflow, plugin-release, plugin-git, plugin-key-hint, plugin-plan, list-sort, click-scroll, old-webkit pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 08:20:32 +08:00
plugins: the Plugins page is drawn in parts, Installed at once and npm's figures after (#488: the page showed skeletons for seconds, even on Installed) — the page waited on /api/plugins/market, which answered only once the plugin host, the registry fetch and an npm request for every package (two each, 8s apiece, all at once, kept an hour in memory only) had all come back, so the slowest package decided when anything was drawn. The page now asks for each part on its own, as the Library page does: Installed is drawn from GET /api/plugins as soon as it answers; Discover's cards from the new GET /api/plugins/listings (the registry, with its cached and built-in copies), each with what npm said last; and versions, weekly downloads and an installed plugin's "v… out" fill in from the new GET /api/plugins/npm?names=…, never waited on (a card's button waits, disabled, until npm has said whether it has the package). npm is asked at most six packages at a time and for no more than 8s in all; a package it answers late is what it said last, and its answer is kept for the next time. What npm said is kept on disk (~/.cache/magpie/plugin-npm.json) beside the registry's copy, so a magpie just started shows it before asking, and GET /api/plugins gives each plugin's newest version from it. /api/plugins/market still answers the whole market as before. Once loaded the page shows what it did. TestNPMInfoKeptOnDisk, TestNPMInfoDeadline, TestNPMInfoAtOnce, TestPluginMarketInParts and plugin-progressive.test.cjs (Chromium, WebKit; en, zh; Installed while the market is held pending) fail without the change; plugin-local, plugin-updates, plugin-market, plugin-move, plugin-move-overflow, plugin-release, plugin-git, plugin-key-hint, plugin-plan, list-sort, click-scroll, old-webkit pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 08:20:32 +08:00
gui: the Add sheet shows a new user only magpie's own subscriptions; a deprecated one is installed from Plugins (the owner: 对于新用户来说,这里应该只显示内置的 provider 啊,更多不是要点击那个链接跳转到插件页面让用户去主动安装吗?) — a fresh magpie listed all 18 subscription tiles, the deprecated built-ins (Cursor, Kiro, Zed, Qoder…) among them, each offering its plugin when clicked. A deprecated built-in with no account in magpie (unusedSub) is now left out of the Subscriptions section, which for a new user is Claude, ChatGPT, Copilot, Gemini CLI and Antigravity, then More in Plugins; a search naming a left-out one shows More in Plugins, which opens the Plugins page on that search, where its plugin is installed. One with an account stays, badged Deprecated; one moved to its plugin stays as before; one whose plugin is installed shows the plugin's tile (replacedSub). The tile's plugin offer (Install and sign in / Use the built-in), unreachable now, is removed with its CSS and zh strings. AGENTS.md says so. deprecated-subscriptions.test.cjs now checks Zed (no account) isn't in the sheet, a search for it shows More in Plugins and opens Plugins on "zed" with Zed's card, and nothing signs in or installs by itself; plugin-replaced.test.cjs expects no tile for an unused deprecated built-in and one for a signed-in one. Both pass in Chromium and WebKit (en/zh), as do add-sheet, provider-search, qoder-cn-tile, mobile-web, deprecation-notice, click-scroll, plugin-move-overflow, plugin-movable-review, plugin-move-unserved, plugin-progressive and plugin-check-updates; plugin-move's editor-scroll check in Chromium flakes the same on origin/main.
2026-10-03 00:52:41 +08:00
plugins: 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
plugins: the Plugins page is drawn in parts, Installed at once and npm's figures after (#488: the page showed skeletons for seconds, even on Installed) — the page waited on /api/plugins/market, which answered only once the plugin host, the registry fetch and an npm request for every package (two each, 8s apiece, all at once, kept an hour in memory only) had all come back, so the slowest package decided when anything was drawn. The page now asks for each part on its own, as the Library page does: Installed is drawn from GET /api/plugins as soon as it answers; Discover's cards from the new GET /api/plugins/listings (the registry, with its cached and built-in copies), each with what npm said last; and versions, weekly downloads and an installed plugin's "v… out" fill in from the new GET /api/plugins/npm?names=…, never waited on (a card's button waits, disabled, until npm has said whether it has the package). npm is asked at most six packages at a time and for no more than 8s in all; a package it answers late is what it said last, and its answer is kept for the next time. What npm said is kept on disk (~/.cache/magpie/plugin-npm.json) beside the registry's copy, so a magpie just started shows it before asking, and GET /api/plugins gives each plugin's newest version from it. /api/plugins/market still answers the whole market as before. Once loaded the page shows what it did. TestNPMInfoKeptOnDisk, TestNPMInfoDeadline, TestNPMInfoAtOnce, TestPluginMarketInParts and plugin-progressive.test.cjs (Chromium, WebKit; en, zh; Installed while the market is held pending) fail without the change; plugin-local, plugin-updates, plugin-market, plugin-move, plugin-move-overflow, plugin-release, plugin-git, plugin-key-hint, plugin-plan, list-sort, click-scroll, old-webkit pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 08:20:32 +08:00
library: ZCode and Pi get a project's MCP servers too, and a project can be removed keeping what magpie wrote into it (#514, follow-up to #500: zcode支持项目级mcp添加 / pi也支持项目级mcp / 删除项目但保留mcp/skill) — a project's servers went only to Claude Code, Codex, Gemini CLI, Cursor and OpenCode, and Remove took every skill and server magpie had put in the folder away, so a project given some once had to stay in magpie for good. ZCode's project file is <project>/.zcode/config.json, servers under mcp.servers as in its user-wide ~/.zcode/cli/config.json (zcode.z.ai/en/docs/mcp-services, "Configuration Files and Default Load Paths": workspace scope, read before the user's, "enable": false the user's to set), written with fmtZCode (type, command/args/env or url/headers); Pi's is <project>/.pi/mcp.json, mcpServers as Pi's own MCP reads them once the project is trusted (pi.dev/docs/latest/mcp, "Configure servers"), written with fmtPiNative (command/args/env, url/headers, no type; SSE refused, as Pi refuses it), whatever extension the user's own Pi file is written for. Both are merged as the other agents' are: the user's keys, servers and comments stay, a server of the same name that isn't magpie's is left alone and said, a file magpie made (.pi/mcp.json) goes in the .gitignore and goes again once empty, and ZCode's user-wide file isn't touched. The page learns a project file's own SSE limit (projectNoSSE), so Pi's chip is greyed for an SSE server in a project even when its user-wide file (pi-mcp-adapter's) takes SSE; the trust note now names Pi beside Codex. The Remove dialog of a project magpie put anything in has "Keep its skills and MCP servers" / 保留项目里的技能和 MCP 服务器 (a checkbox row in the library's pick style, no coloured left border): checked, the text says what stays and Remove posts projects/remove with keep, and RemoveProject(dir, keep) only takes the project off the list — skills (links still following the library's), servers in the agents' files and magpie's .gitignore lines stay as they are, and nothing magpie does later (a sync, the server removed from the library) touches them; unchecked it removes as before. A one-off add without a project first (pick a folder from a server or skill) is not in this change. TestProjectServersZCodePi (ZCode's file with the user's model, server, enable:false and key kept beside magpie's three; Pi's file made in its shape, the SSE server refused with its reason, listed in .gitignore; ZCode's user-wide file unchanged; removing takes out only magpie's and the file it made) and TestRemoveProjectKeep (every file byte for byte after the removal, after a sync and after the server leaves the library) fail without the change, as does project-remove-keep.test.cjs (WebKit, and Chromium through a temp copy on plain chromium.launch(); en and zh: the box unchecked, its text, no stripe, pressing it scrolls nothing and swaps the text, Remove posts keep: true and says they were kept; a project with nothing written has no box and posts as before) against HEAD's assets. project-servers, click-scroll, library-items, library-all pass; menu-scroll and old-webkit fail the same on HEAD. No live config was read or written. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... (under a temp HOME) pass (TestFXInState failed on a TempDir cleanup race twice in the full gui run, passes alone and on the rerun; untouched).
2026-10-02 14:10:55 +08:00
library: a project gets its own MCP servers, agent by agent (#500) — the library's servers could only be given to agents user-wide, while its skills could already go into a project. The MCP tab now lists the same projects as Skills under "In projects" (add one by its folder, or the folder picker in the app), and a project's card opens to each server with chips for the agents that read a project's own servers: Claude Code .mcp.json, Codex .codex/config.toml, Gemini CLI .gemini/settings.json, Cursor .cursor/mcp.json, OpenCode opencode.json (opencode.jsonc when that is the project's file), each as its docs give it. Other agents have no chip and are named under the card; the project files of Copilot, Crush, Droid, Kimi and the rest weren't confirmed, so none is written. The server is written with the same encoders and edit helpers as the user-wide files, so the rest of a file (its comments, other keys, the team's own servers) is kept; magpie records what it wrote in the project (Project.Servers, Wrote, MadeFiles) and only ever takes that away again, a same-named server that isn't magpie's is left as it is and named as a problem, as is an SSE server for Codex. A file magpie made is listed in the project's .gitignore between magpie's lines (a server's env and headers can hold keys) and removed with its folders once nothing of magpie's is left in it, unless the user wrote something else into it; a file that was there already is never ignored or removed. Renaming a library server renames it in its projects, removing it takes it out of them, and removing a project takes out its servers as well as its skills. A project whose MCP file is an agent's user-wide file (the home folder) is refused, as for skills. The chips of a project row (servers and skills) now light at once and send one write at a time, the last click winning: two quick clicks posted the first's stale list before. Claude Code still asks before it starts a project's servers and Codex reads .codex/config.toml only in a trusted project; the card says so. library-all.test.cjs gets a 800px-high window: the MCP tab's new "In projects" made it overflow 700 by a pixel, and Chromium's scroll anchoring then moved it when the problems card came in above. TestProjectServers (merge into a user's .mcp.json keeping its comment, key and server; Codex TOML; .gitignore; rename; removal down to the made folders; RemoveProject keeping the user's own), TestProjectServerNotOurs, TestProjectServerFileKept, TestProjectServerChecks and TestProjectServerOpenCodeJSONC run in a temp HOME; project-servers.test.cjs (en, zh; chromium and webkit) checks the chips, the greyed Codex chip for SSE, two clicks before magpie answers, and that nothing scrolls. No real agent was started on a written project. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; the library's Playwright tests pass in webkit, and in chromium except list-sort, shared-skills and import-all-skills, which fail the same way at HEAD.
2026-10-02 12:37:07 +08:00
providers: a custom provider's models can be picked from its vendor's list while it is added, before its first Save (Sun1090 on #578: 添加供应商的时候,希望添加可以获取全模型的按钮或者下拉框?像cc-switch那样,可以选) — the vendor's list, its chips and Refresh were only in a saved provider's editor; the custom provider's add form had a comma-separated Models field and the hint "magpie asks the vendor for its list after saving", so a user adding one had to save it, open it again and pick there, which nothing in the form said could be done sooner. The add form now has Fetch models (获取模型) beside its Models field: it posts the new provider/list with the form as it stands (the URL and key typed, its headers, models URL and proxy; no id), which builds the draft as detect does (from its preset when it has one, then typed) and asks the vendor with the new Provider.List — Fetch's own path (a NoList preset's models, Cline's feed, Kilo's list, fetchOne) without SaveLive, so nothing is kept — answering the models agents chat with, a model's name where it has one. The list shows as chips (a long one with a filter), each ticked one in the Models field and an id typed there ticking its chip, with "{n} of {all} picked", Pick all (Pick those shown under a filter) and Pick none above the chips, so a pick that reflows them moves no button; one typed by hand stays through Pick none. A vendor refusing says why under the field; no URL asks nothing. The Add sends the picks as the provider's models, as the field did. TestProviderListBeforeSave (the list asked with the key typed, an embedding model left out, no provider saved and no list kept, a 401 told, no URL refused) is new; provider-add-models.test.cjs (WebKit, en and zh: nothing asked without a URL, the post carries URL and key and no id, the refusal shown, chips with names, tick and type in sync, filter and Pick those shown / none / all, the Add's models, no click moving the page, no left-border accent) is new and passes; add-sheet, provider-detect, provider-detect-models, provider-typed and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
2026-10-02 23:48:00 +08:00
providers: Detect APIs asks every picked model on each API and shows model by API, and a model's right-click Test says the API it answered on (01huadalang on Discord: 右键选择测试只能测试出是不是通的,不能说什么协议是通的。检测协议的结果看不出对应的模型支持协议情况…有的仅支持 response 有的双协议) — Detect APIs asked one model (typed, or one picked from the vendor's list for each API), so a relay serving some models on Responses only and others on both read as "chat works, responses doesn't", and the right-click Test said only that a model answered. Detect APIs now has "Each picked model" (逐个检测已选模型): provider/detect with detectModels asks each picked model (the first provider.DetectMax, 30; an image model left out, it is asked on the images API) on Chat, Responses and Anthropic at the URLs Detect asks, provider.DetectWide (8) requests at a time, each waiting 12 s, the whole within 2 minutes (a probe not started by then is "not asked: out of time" and sent nowhere), and answers model by model plus, by API, the first model it served. The editor shows a model × API grid, each cell a ✓ with the time or a ✗ with the status and why (the whole in its title). Its "Use these" sets the URLs of the APIs that answered and gives each model that answered on one API only (of two or more asked) that API, staged in Names & levels as draft.modelPrefs like the rest of b0dc82c4 — "unsaved", nothing sent until the Save, Cancel drops it — while a model answering on two stays Auto; the hint under the grid names them ("m-resp: Responses only"). Detect for one model is as it was (its body now Provider.detecting and detectOne, shared). A chip's right-click Test now says the API it was asked on: "gpt-5.5 answered via Responses in 123 ms", the dot's title "Answered via Responses in …", a failure "didn't answer via OpenAI: …". TestDetectModels (a fake relay serving gpt-both on Chat and Responses, gpt-resp on Responses, glm-chat on Chat, claude-only on Anthropic: each model's ✓/✗ on each API, 12 requests for 4 models, the image model not asked, by API the first served; 35 models asked as 30, never more than 8 at once) and TestDetectModelsOutOfTime are new; provider-detect-models.test.cjs (WebKit, en and zh: the models posted, the grid, Use these staging m-resp on Responses and m-chat on OpenAI with m-both left Auto and nothing sent, the Save's modelPrefs, the right-click Test's "via Responses", no click scrolling, no left-border accent, every string's Chinese). provider-detect, provider-levels-save, provider-levels-scroll, model-test-one, model-test-why and click-scroll pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside).
2026-10-02 23:10:11 +08:00
providers: a model a vendor names its own way is merged with the same model from other providers — by its id as Volcengine Ark spells it, or by the model the user says it is the same as in Names & levels (kyzhouxu, #583: 火山引擎获取到的默认 ds 4.1 flash 模型的名称跟其他供应商的名称不一样,没有自动合并到同一个模型分组) — the groups magpie finds merged a model by its id lowercased, its vendor prefix and an eight-digit date taken off and a version's dot made a dash, so Ark's deepseek-v4-1-flash-260910 (a six-digit YYMMDD snapshot) was a group of its own beside ali-tp's deepseek-v4.1-flash. sameModel now also takes off Ark's YYMMDD (a year from 23 to 39, a month and a day that are one), Vertex's "@YYYYMMDD", reads Fireworks' "p" between digits (deepseek-v4p1) as the dot and "_" as "-"; -flash, -thinking, ":batch", a four-digit release (qwen's -2507, kimi-k2-0905) and a number that is no date (-128000, -261301) stay apart. For an id no rule matches (an Ark endpoint id, a relay's own name), each model in a provider's Names & levels now has a "Same as" (等同于) box, its placeholder the name it is merged by now: the model it is the same as, as any vendor spells it, staged in draft.modelPrefs and made with the provider's Save like the other prefs (Cancel drops it, Restore default empties it, nothing sent or redrawn or scrolled before the Save). It is kept in settings' ModelSameAs by "<provider>/<model>" (renamed with the provider like every Model* map), set by provider.SetModelSame (its own id however spelt keeps nothing; a model the provider doesn't serve, or a name of no model, is refused) and taken by autoGroups, AutoStandIn and the agents' reseat (AutoGroupOf); the agents are told once. TestSameModelVendorSpellings (Ark, Fireworks, Vertex, underscores, and pairs that must stay apart) and TestModelSameAsMergesFoundGroup (c/ep-sol-x joins auto-sol once said, GroupFor("sol") finds it, back out when cleared, refusals) are new; TestProviderSaveModelPrefs covers a Save's same; provider-same-as.test.cjs (WebKit, en and zh: placeholder, staging, unsaved, Restore default, Cancel, one Save carrying exactly the changes, no scroll, Chinese for each string) fails without the change and passes with it; provider-levels-save and provider-detect pass. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 23:39:40 +08:00
provider: a providers.json that can't be read is said where the providers are listed, never backed up or synced as none, and is replaced through a rename (PR #415's review) — #415 made edits refuse a file they couldn't read or decode, but every read for the user still took it as empty: magpie providers said "no providers yet", magpie models "no models yet", the Providers page opened the Add sheet as for a first use, and backup.Collect, so an export and a WebDAV/S3 sync, carried it as no providers, which another computer's Mirror would then take as every provider removed. magpie providers and magpie models now list what is left (the signed-in accounts) and end in the error, the TUI says it over its list, and the Providers page shows a "Your providers file can't be read" box (无法读取供应商配置文件) over the list with magpie's reason and no Add sheet; start-up, the gateway and the catalog still go on with it as empty. Stored and StoredGroups return the read error, so a backup and a sync stop on it with nothing pushed. The error, for reads and refused writes alike, names the file, says magpie left it unchanged and to fix it or move it aside (errors.Is ErrUnreadable). store writes through writePrivate, a 0600 temp file renamed over it (steady.Rename waits out a Windows reader holding it, a symlink keeps its target), instead of os.WriteFile's truncate-then-write, and the read goes through steady.ReadFile. TestFileErrorSaysUnreadable, TestWriteErrorsSayLeftUnchanged, TestStoredRefusesUnreadable, TestStoreReplacesAtomically, TestSyncStopsOnUnreadableProviders, TestProvidersCmdSaysUnreadable and provider-file-error.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; provider-file-error, click-scroll, old-webkit, providers-off-fold, add-sheet pass.
2026-10-01 19:45:24 +08:00
providers: a model's reasoning levels, name and images in the editor's Names & levels are made with the provider's Save, not at each tick (ARNO on Discord: setting one model's reasoning effort applies the config at each click rather than at the provider's OK/Save, so picking levels lags) — each tick of a level (and each name typed, each Accepts images, each Restore default) was its own POST /api/provider/efforts (name, images), which saved settings and ran catalog.Changed, agent.SyncCatalog, rewriting every agent's files, then fetched the providers and the state and redrew the providers page, the editor's model list with it, before the next tick took: picking levels lagged a click behind, and Cancel had nothing to drop. Names & levels now stage in the editor's draft (draft.modelPrefs, by model id: name, efforts, images or ownImages for the vendor's answer), nothing sent and nothing redrawn on a tick; each row changed says "unsaved" (未保存), a tick back to what is saved stages nothing, and Restore default stages its own name, every level and the vendor's answer for images (the provider list's model now carries ownImages so the tick shows it). Both editors' Save (a key's provider and a signed-in account's) send modelPrefs with the rest, and Cancel drops them. The save action makes them after the provider is saved (and renamed) with the new provider.SetModelPrefs, which makes each as SetModelName, SetModelEfforts and SetModelImage do (their bodies now setModel*, telling nothing, the three Set* telling the agents when they changed something) and tells the agents once for all of them; a refused one fails the Save saying why. The per-model endpoints stay for the CLI and anything else that calls them. provider-levels-save.test.cjs (WebKit and Chromium, en and zh: ticks, a name, Accepts images and Restore default send nothing, redraw nothing, scroll nothing and say unsaved; Cancel drops them; one Save carries exactly the changed models' levels) fails without the change ("nothing is sent before the Save") and passes with it; provider-levels-scroll.test.cjs now checks that a tick leaves the list in place with nothing sent. TestSetModelPrefs (all set, agents told once, nothing changed told nothing, Restore default, a refused level) and TestProviderSaveModelPrefs (a Save's modelPrefs made; a refused level fails it) are new. click-scroll (one flake of "picking request 7", then 3/3), model-pick, models-free-only, model-test-one, panel-effort, claude-effort, editor-short-window pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup, passes alone and in a second full run).
2026-10-02 17:02:42 +08:00
providers: a model's reasoning levels, name and images in the editor's Names & levels are made with the provider's Save, not at each tick (ARNO on Discord: setting one model's reasoning effort applies the config at each click rather than at the provider's OK/Save, so picking levels lags) — each tick of a level (and each name typed, each Accepts images, each Restore default) was its own POST /api/provider/efforts (name, images), which saved settings and ran catalog.Changed, agent.SyncCatalog, rewriting every agent's files, then fetched the providers and the state and redrew the providers page, the editor's model list with it, before the next tick took: picking levels lagged a click behind, and Cancel had nothing to drop. Names & levels now stage in the editor's draft (draft.modelPrefs, by model id: name, efforts, images or ownImages for the vendor's answer), nothing sent and nothing redrawn on a tick; each row changed says "unsaved" (未保存), a tick back to what is saved stages nothing, and Restore default stages its own name, every level and the vendor's answer for images (the provider list's model now carries ownImages so the tick shows it). Both editors' Save (a key's provider and a signed-in account's) send modelPrefs with the rest, and Cancel drops them. The save action makes them after the provider is saved (and renamed) with the new provider.SetModelPrefs, which makes each as SetModelName, SetModelEfforts and SetModelImage do (their bodies now setModel*, telling nothing, the three Set* telling the agents when they changed something) and tells the agents once for all of them; a refused one fails the Save saying why. The per-model endpoints stay for the CLI and anything else that calls them. provider-levels-save.test.cjs (WebKit and Chromium, en and zh: ticks, a name, Accepts images and Restore default send nothing, redraw nothing, scroll nothing and say unsaved; Cancel drops them; one Save carries exactly the changed models' levels) fails without the change ("nothing is sent before the Save") and passes with it; provider-levels-scroll.test.cjs now checks that a tick leaves the list in place with nothing sent. TestSetModelPrefs (all set, agents told once, nothing changed told nothing, Restore default, a refused level) and TestProviderSaveModelPrefs (a Save's modelPrefs made; a refused level fails it) are new. click-scroll (one flake of "picking request 7", then 3/3), model-pick, models-free-only, model-test-one, panel-effort, claude-effort, editor-short-window pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup, passes alone and in a second full run).
2026-10-02 17:02:42 +08:00
2026-10-02 11:31:11 +08:00
providers: each provider, a signed-in account like Codex's among them, goes through its own proxy — the one in Settings, none, or its own address (#237 xosi: 目前似乎是全局设置代理的,codex和国模供应商无法单独区分设置代理) — magpie had one proxy, Settings' (or the environment's, or the system's), for every request it made, so Codex, which needs a proxy from China, and a vendor at home, which is slower or refused through one, couldn't both be right. A provider now has a proxy, "" (following the global one, as before and the default), "direct" (none, whatever Settings says) or an http://, https:// or socks5:// address (host:port meaning http), kept as proxy in providers.json only when set, and for an account provider in its picks entry beside its models. It is checked on save by the same rule Settings' proxy is (settings.CheckProxy, now shared), so ftp:// and the like are refused. netproxy.With puts a provider's choice on a request's context and netproxy.Func honours it; netproxy.Dispatch sends such a request through a clone of the transport kept for that proxy alone (at most 32, cleared past that), since a transport shares an HTTP/2 connection by host whatever proxy dialled it; loopback is never proxied. The gateway's client and http.DefaultClient go through Dispatch, and every request made on a provider's behalf carries its choice: the gateway's turns (attempt, forward, draw, decide, the Codex backend), and the model lists, balances, plan and subscription usage, tests, sign-in, Codex's warm-up and reset, while the Claude subscription's and warm-up's CLI runs get the account's proxy in their *_PROXY (netproxy.EnvWith). The provider editor, a key provider's and a signed-in account's alike, has a Proxy row after Fallback: Global proxy | Direct | Custom (全局代理 | 直连 | 自定义), the address field beside the options only for Custom, its room kept otherwise so a pick never changes the centred dialog's height and moves nothing, and a line under it saying what the pick does; Save posts proxy ("" , "direct" or the trimmed address), a save without it keeps the old one, and Custom with no address is refused before anything is posted ("Proxy: type its address, like http://127.0.0.1:7890"). No border stripes; en and zh strings together. Tested with a new internal/gateway/proxy_test.go in a sandbox HOME: with a global proxy set, provider A's turns and model list go through its own fake proxy, B's (direct) straight to the vendor, C's (following) through the global one, none through another, and A set back to following goes through the global one; ftp:// is refused (fails with With neutered). netproxy_test.go's TestWith covers Func, Dispatch's per-proxy transports and EnvWith. A new internal/gui/tests/provider-proxy.test.cjs in Chromium and WebKit, en and zh: a key provider opens on Global proxy with no address, Custom unfilled is refused and nothing posted, a padded socks5 address saved trimmed, Direct saved as "direct" and Global as ""; Codex's account opens on Custom with its address and saves Direct with its models; every pick leaves the option within 1px; no left border. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./..., provider-proxy, click-scroll, old-webkit, balance-fix and add-sheet pass (click-scroll's WebKit case failed once alongside the others and passes alone, twice). Not tried against a real proxy or the real Wails window.
2026-09-30 02:15:50 +08:00
providers, 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 model a vendor names its own way is merged with the same model from other providers — by its id as Volcengine Ark spells it, or by the model the user says it is the same as in Names & levels (kyzhouxu, #583: 火山引擎获取到的默认 ds 4.1 flash 模型的名称跟其他供应商的名称不一样,没有自动合并到同一个模型分组) — the groups magpie finds merged a model by its id lowercased, its vendor prefix and an eight-digit date taken off and a version's dot made a dash, so Ark's deepseek-v4-1-flash-260910 (a six-digit YYMMDD snapshot) was a group of its own beside ali-tp's deepseek-v4.1-flash. sameModel now also takes off Ark's YYMMDD (a year from 23 to 39, a month and a day that are one), Vertex's "@YYYYMMDD", reads Fireworks' "p" between digits (deepseek-v4p1) as the dot and "_" as "-"; -flash, -thinking, ":batch", a four-digit release (qwen's -2507, kimi-k2-0905) and a number that is no date (-128000, -261301) stay apart. For an id no rule matches (an Ark endpoint id, a relay's own name), each model in a provider's Names & levels now has a "Same as" (等同于) box, its placeholder the name it is merged by now: the model it is the same as, as any vendor spells it, staged in draft.modelPrefs and made with the provider's Save like the other prefs (Cancel drops it, Restore default empties it, nothing sent or redrawn or scrolled before the Save). It is kept in settings' ModelSameAs by "<provider>/<model>" (renamed with the provider like every Model* map), set by provider.SetModelSame (its own id however spelt keeps nothing; a model the provider doesn't serve, or a name of no model, is refused) and taken by autoGroups, AutoStandIn and the agents' reseat (AutoGroupOf); the agents are told once. TestSameModelVendorSpellings (Ark, Fireworks, Vertex, underscores, and pairs that must stay apart) and TestModelSameAsMergesFoundGroup (c/ep-sol-x joins auto-sol once said, GroupFor("sol") finds it, back out when cleared, refusals) are new; TestProviderSaveModelPrefs covers a Save's same; provider-same-as.test.cjs (WebKit, en and zh: placeholder, staging, unsaved, Restore default, Cancel, one Save carrying exactly the changes, no scroll, Chinese for each string) fails without the change and passes with it; provider-levels-save and provider-detect pass. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 23:39:40 +08:00
providers: the Providers tab answers at once, the accounts' lists asked for behind it (#541: with 200-odd models the Agents and Providers tabs showed placeholders for ten seconds and more at start-up) — GET /api/providers ran provider.FetchNew(8s) before answering, which asks each signed-in account without its vendor's list yet, one after another, up to 8 s each, under newFetches' lock; at start-up that lock is held by startBackend's own FetchNew(20s), run after the models.dev sync, so the page waited for every account start-up was asking, each up to 20 s, and an account whose list can't be had (cursor-agent hanging, a vendor out of reach) was waited on again a minute later. The page now starts FetchNew in the background (provider.FetchNewBehind, at once unless one it started still runs) and is told fetching (provider.FetchingNew, any FetchNew under way); while it is, the page asks again every 2.5 s, draws the answer only when it changed, never under an open editor, and stops once the lists are in. A sign-in done in magpie still waits for the new account's list (GET /api/providers?wait, #204), as the editor opens on it. TestProvidersAnswerWhileListsComeIn (a cursor-agent taking 4 s over its models: the page answers within 2 s, told fetching, and lists them once in) fails without the change ("the page waited 4.02s"); providers-fetching.test.cjs (WebKit) fails without the page's asking again. The Agents tab's wait (GET /api/state) was not reproduced: with 240 models over 13 agents state() takes ~50 ms here; currentFX can hold it up to 4 s when the CNY rate is stale, left as it is. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass; Chromium's launch fails here, WebKit only.
2026-10-02 17:31:54 +08:00
2026-10-01 17:13:34 +08:00
gui: the quota curves can be turned off, picked once in the allowances' head, and the cards drawing them are calmer (ARNO on Discord: 新版本加上了额度走势后,额度面板变得非常混乱和难以观看了) — since 9ccabdfb every account on the Usage page had its own "2 days | Cycle" segmented control over its curve, so a card with two accounts carried two, there was no way to have none, and the tray's cards drew a thin line each. The range is now one pick in the allowances' head, beside Hide accounts, in the app's own menu (Trends: Off / 2 days / Cycle; 走势:关闭 / 2 天 / 本周期), remembered in magpie.quotaRange as the range was. Off draws no curve on any card, the tray panel's thin lines included; the panel follows a pick made in the window at once (storage event) and keeps it after a restart. The cards themselves: an account after a curve's legend wasn't set apart by the rule every other section gets (the next email sat right under the legend), now it is; the plot is 52px rather than 64; a window far shorter than the span drawn (a 5-hour one over a week's cycle, an all but upright comb) is drawn faint and without its upright even burn; a legend key never breaks inside (a long name ends in …); the curve's head names its range in small print instead of holding the control. quota-curve.test.cjs fails on the old code (no pick in the head, a control on every card, no Off, the 5-hour window's even burn over the week, no rule before the second account) and passes: Off removes every curve without moving the page or the pick, survives a reload, and the tray's line goes when Off is set from another window and stays gone after its reload. In WebKit: quota-curve, click-scroll, panel-usage, usage-fold, account-mask and mobile-web pass; before/after screenshots with seven subscriptions (two Claude, two Codex, Antigravity's five windows, GLM, Kimi) in en and zh, window, tray and a 390px web view. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, TestUserPath and TestClaudeSignedOut, timing tests that pass alone.
2026-10-03 19:18:46 +08:00
gui: a subscription with several accounts shows one of them on Usage and in the tray panel, the others behind "Show N more accounts" (whqtian on Discord: 我这个workbuddy只有4个账号,其实大多数情况下,我不需要显示全部的账号信息,你只要显示一个账号信息即可,其他的可以点击展开再看) — every provider's card listed each account's meters, credits and curve one under another, so four WorkBuddy accounts took the page. Now a card with several accounts shows the one that answered through the gateway last (lastServedAt), else the first, with "Show 3 more accounts" / 展开其余 3 个账号 under it; opened, all are shown in their order with "Show fewer accounts" / 收起其余账号. Opened is remembered by provider (localStorage magpie.usageOpen), for the window and the tray panel alike, each following the other as it is turned; a provider with one account has no button. The folded accounts stay in the page, hidden, so Hide accounts masks them as before; the line between accounts is drawn only after one in sight. The click keeps the page where it is (backToReader). usage-fold.test.cjs (WebKit, en and zh: 4 accounts show the last-served one and the button, opening shows all without moving the page, remembered after a reload, one account has no button, masking covers them, the panel folds the same and the window follows it) fails on the old code and passes; quota-families and quota-pools open the panel's second Antigravity account first. account-mask, quota-curve, click-scroll, mask-emails, credit-days, balance-parts, usage-arrange, panel-usage, tray-usages and usage-accounts pass; the GUI tests failing here fail the same on origin/main. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 16:50:31 +08:00
gui: a subscription with several accounts shows one of them on Usage and in the tray panel, the others behind "Show N more accounts" (whqtian on Discord: 我这个workbuddy只有4个账号,其实大多数情况下,我不需要显示全部的账号信息,你只要显示一个账号信息即可,其他的可以点击展开再看) — every provider's card listed each account's meters, credits and curve one under another, so four WorkBuddy accounts took the page. Now a card with several accounts shows the one that answered through the gateway last (lastServedAt), else the first, with "Show 3 more accounts" / 展开其余 3 个账号 under it; opened, all are shown in their order with "Show fewer accounts" / 收起其余账号. Opened is remembered by provider (localStorage magpie.usageOpen), for the window and the tray panel alike, each following the other as it is turned; a provider with one account has no button. The folded accounts stay in the page, hidden, so Hide accounts masks them as before; the line between accounts is drawn only after one in sight. The click keeps the page where it is (backToReader). usage-fold.test.cjs (WebKit, en and zh: 4 accounts show the last-served one and the button, opening shows all without moving the page, remembered after a reload, one account has no button, masking covers them, the panel folds the same and the window follows it) fails on the old code and passes; quota-families and quota-pools open the panel's second Antigravity account first. account-mask, quota-curve, click-scroll, mask-emails, credit-days, balance-parts, usage-arrange, panel-usage, tray-usages and usage-accounts pass; the GUI tests failing here fail the same on origin/main. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 16:50:31 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
gateway: a remote magpie's routing group answering with the member it routed to isn't a swap (莫 on Discord: 发送模型 group/auto-deepseek-v4-1-flash, 实际模型 deepseek/deepseek-v4.1-flash shown amber) — a request for one of another magpie's groups ("group/<id>", the name only a magpie answers to) comes back with the model its member's vendor named, and the Usage page's Requests and the Routing page marked that as the vendor serving another model. Now it is routed, not swapped (usage.GroupRouted): the try, the route and the ledger row say so, the served member shows plain, muted on the Usage page, with a title (and a line in the request's story) saying the remote magpie's group routed the request to it, in English and Chinese. Rows recorded before are judged again as the ledger lists them, and routes kept in the Routing page's history as they are read. A real swap through a remote magpie, and a swap within a group of this magpie's own, are still marked; a swap behind the remote's group shows on that magpie's own Routing page. Checked end to end with two sandbox magpies over a fake vendor: the local ledger and routing history mark group/… false (swapped before the change) and fake/sol served as luna true. TestRemoteMagpieGroupNotSwapped, TestRemoteMagpieGroupLedgerRow, TestRemoteMagpieGroupHistory and remote-group-served.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (load flakes pass alone); routing-served, usage-ledger, panel-routing, remote-magpie, click-scroll, old-webkit pass.
2026-10-01 13:46:14 +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
archive: a request the archive kept is opened and downloaded from the Usage page, with its bodies whole (jorben, #447: 请求存档 reachable from usage history, full bodies, download) — the archive's name was only on the Gateway page's recent calls, gone once a call left the list or the gateway restarted, and the archive kept the first 256 KB of each body, the recent list's cut. The usage ledger's row now names the call's archive (archive in usage.jsonl, kept through the Requests page's packed rows), and its details on Usage → Requests show the Request archive panel, Fetch from archive and Download; with the archive on, a request it has no copy of says "Not archived". The bodies are kept whole up to requestArchiveMaxMB in settings.json (32 MB unless set, at most 1024): the response is teed to a temp file as it is sent, the request body written to one when the call ends, so the upload queue holds file handles, not bodies, and one upload at a time reads them; a body past the limit is kept to it and marked cut, with its whole size. What goes upstream is unchanged, the archive stays off unless turned on, headers and bodies are scrubbed as before, and the temp files are removed after the upload or when the queue is full. The page is sent a body to show only when it is 256 KB or less (a longer one is said by its size), an archive over 80 MB is only said to be; Download streams the object whole, to Downloads in the app (archive/export, -2, -3… for a name taken) and as the browser's download in magpie web (archive/file). TestArchiveWholeBodies, TestArchiveLimit, TestRowKeepsArchive and TestArchivePreviewAndDownload fail without the change; TestArchiveOffUsage, TestArchiveTooLargeToPreview, TestArchiveOldPreview keep the rest. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; request-archive-usage (new), request-archive, click-scroll, old-webkit, usage-ledger, usage-ledger-detail, usage-ledger-content, web-page, sse-body pass.
2026-10-02 00:57:51 +08:00
gui: the request archive's switch sits over the Usage page's requests, and the Gateway page's recent calls no longer fetch from the archive (Jorben on Discord: 网关页的最近请求下遗留了取回归档和预览的逻辑,和最近请求的预览能力重叠了,建议移除;以及把是否启用请求归档开关也挪到用量页请求列表的右上方) — since #447 a request's archive is read back and previewed from its row on the Usage page, so the Gateway page's recent calls showed two previews of one call: their own bodies, and under them the archive's copy with "Fetch from archive" and "Download". A recent call now shows only its own request and response bodies; archivePanel and what it uses stay for the Usage page. The archive's Off/On row over the recent calls (#archiveList) is gone, and its switch is now a compact one — "Request archive" and a switch, role=switch — at the right of the line over the Usage page's request list, beside the requests' sum. It posts settings/archive as before; what it keeps and where, or that Sync needs an s3:// address first, is its tooltip; a refusal still goes to the footer and leaves it off; a failed last upload says "Upload failed" beside it (上传失败, the error in its tooltip). It waits for the gateway's state rather than say Off before it is in, and is changed in place on a redraw, so a switch being pressed is never swapped out (the ledger and providers both redraw it). request-archive.test.cjs now checks the Gateway page has no switch and a kept call no archive panel and no Fetch from archive, and the switch at the list's top right on the sum's line: refused with no bucket, on with one (bucket in its tooltip), off, and a failed upload said; web-page.test.cjs finds the switch on the Usage page. Playwright in Chromium and WebKit: request-archive, request-archive-usage, web-page, click-scroll, usage-ledger, usage-ledger-detail, gateway-fold, sse-body pass (72/72). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gui/ pass.
2026-10-02 13:23:41 +08:00
gateway: ZCode's count_tokens is estimated, and a vendor's firewall block is said in plain words (ZCode Start Plan: HTTP 405 "request has been blocked due to unusual activity") — Claude Code asks count_tokens before every turn, and magpie sent each to zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages/count_tokens, which answers it with an error every time and which ZCode itself never asks; a ZCode account's count, built-in or on its plugin, is now estimated as Cursor's, Grok's and Kiro's are, and zcode.z.ai is not asked. The 405 is Alibaba Cloud's edge firewall (ESA) in front of zcode.z.ai blocking the address: its HTML page was shown as "405 Method Not Allowed" and, a 405 not passing as a provider's failure, no other member of a group was asked. A block page (request has been blocked, errors.aliyun.com) now reads "the provider's network firewall blocked requests from this IP; wait a while, or switch to another network or proxy" to the agent and in the trace, where the Routing page gives it on its own line in English or Chinese, and the group goes on to its next member, the blocked one resting as for any other failure. No request header, user agent or device id changes. TestZCodeCountTokensEstimated, TestMovedZCodeCountTokensEstimated, TestEdgeBlockedSaidPlainly and routing-blocked.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; routing-blocked, routing-wb-refused, routing-proxy, routing-shape, click-scroll, old-webkit pass.
2026-10-01 15:04:16 +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
providers: a provider's Routing is picked in its editor and made with its Save, and its Stays (会话保持) is beside it (01huadalang on Discord, a WorkBuddy AI subscription with several accounts: 在这里切换会自动保存,没有手动点击就给我保存了,这样切换来看会很卡; 没有看到会话选择) — each click of Smart / In order / In turn / Least used first / Weekly pace posted provider/route, saved, and redrew the providers page with the editor in it, so looking through what each option says lagged and saved a pick only looked at. Routing is now staged in the editor's draft as Names & levels is (b0dc82c4): a click shows the option and its description, says "unsaved" (未保存) beside it, sends and redraws nothing, Cancel drops it, and both editors' Save (a signed-in account's and a key's) carry it. How long a conversation stays with the account or key that answered it (Auto / Session / Within a turn / Off, provider.Affinity, which the gateway already honours for a provider's own accounts) could be set only on the Routing page's "Several accounts or keys" row; it is now in the editor under Routing, staged the same way. The save action takes routing and affinity when sent and keeps them when not: before, every Save of the editor dropped the Stays set on the Routing page, since the editor never sent it and in.Affinity came in empty. routing-save.test.cjs (WebKit and Chromium, en and zh, a subscription with three accounts and a provider with two keys: every option clicked shows its description and unsaved with nothing sent, redrawn or scrolled; Cancel drops them; one Save carries routing "rotate" and affinity "session") fails without the change and passes with it; account-arrange.test.cjs now saves the routing it picks. TestProviderSaveRouting (the Save's picks made, a Save without them keeps them, the Routing page's route and affinity survive the next Save, Smart and Auto sent as "") fails without the change (routing lost). keep-login, provider-levels-save, provider-levels-scroll, click-scroll, editor-short-window, provider-proxy, provider-concurrency, account-proxy, account-models, plugin-move, deprecated-subscriptions, routing-account-order pass in WebKit (redact-rule-row's 640px case fails as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 23:46:00 +08:00
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
zcode: a Start Plan request goes as the ZCode app sends it, its system prompt and headers (#425; the user allowed mimicking ZCode's client, for this vendor only) — zcode.z.ai turns a Start Plan (体验套餐) request without ZCode's own system prompt away with 405 "request has been blocked due to unusual activity", code 3012, as zcode2api found (a137460387/zcode2api src/upstream/system-prompt.js, zcode-plan.js; dengyie/zcode2api app/body_transform.py), and magpie sent the agent's request as it was. A Start Plan /v1/messages request now carries ZCode 3.11's three system blocks, each cached (its opening line; its identity, harness and desktop context; "\n\n" and its dynamic sections around an environment section that leaves the working directory and OS version unknown), the agent's own system after them, and the day in a <system-reminder> before the first user turn unless one is there; ZCode's blocks take three of the four cache breakpoints, so the agent's system and tools give up theirs and only its last in the messages is kept. Model, max_tokens and the rest of the body go as sent (numbers as written). Its headers are ZCode's as zcode2api's headers.js has them: User-Agent ZCode/3.14.3 ai-sdk/anthropic/3.0.81, X-ZCode-Agent glm, X-Release-Channel production, X-Os-Category, X-Client-Language and X-Client-Timezone from the machine, a fresh X-Request-Id and X-ZCode-Trace-Id, X-ZCode-Session-Type main, no X-Session-Id or X-Query-Id, beside the X-Title, X-Platform, HTTP-Referer and X-Device-Mid sent before. The GLM Coding Plan and team seats are unchanged. The 405 hint no longer says magpie won't pretend to be ZCode: a block that still happens is the network blocking the IP or ZCode checking something new. No ZCode account was signed in; TestZCodeStartSentAsTheApp (httptest) fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; routing-zcode-blocked, click-scroll, old-webkit pass.
2026-10-02 08:20:33 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
search: which provider searches the web for a model that can't is picked in Settings → Web search, Automatic as before (01huadalang on Discord: 能不能在供应商联网搜索里加一个入口,能用自己能联网搜索的地方来替代不能联网搜索的供应商) — magpie picked it by itself (a Claude account, then a Codex account, then a provider that searches by itself on Anthropic's API, the Responses API, Chat, each by its small model) and Settings only said who. A "Searches for other models" row now comes first in the section: its picker offers Automatic (magpie's pick, named), and each provider able to search — on, and searching by itself or a Claude/Codex account, in the order magpie would pick them — by its small model or by any model of it. The pick is saved as settings' searcher ("<provider>" or "<provider>/<model>"), sent with every other setting saved (prefsKeep), and refused when the provider can't search. searcher() takes it while it can (chosenSearcher); one gone, turned off or no longer able to search gives way to magpie's pick, and the row says which and why, showing Automatic; a model it no longer lists gives way to its small model. A relay said to search is still never asked for other models (#359: it would spend the relay's quota, and one that serves only Claude Code refuses magpie's own request), so it isn't offered, and the row names such relays and says why. TestSearcherChosen (the automatic order, a provider, a model of it, a model gone, one that can't search, a relay said to search, one gone, one turned off and back on, Automatic again) and searcher-pick.test.cjs (WebKit and Chromium, en and zh: Automatic shown with magpie's pick, the relay not offered and named, a model and a provider picked and saved, another setting saved keeping the pick, one turned off said and Automatic shown, Automatic sent back, no click moving the page) are new; it passes in WebKit (this machine's Playwright Chromium fails to launch, icudtl.dat missing, for every test). search-api, settings-sections, provider-search, click-scroll pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once, passes 3/3 alone).
2026-10-02 23:33:31 +08:00
sessions: each project folder's row starts with a box, so several folders are picked and deleted together with the bar's Delete, the count still in sessions (mintonight, on #527: 现在这样一次只能删一个项目…在项目文件夹那一行的字首加一个复选框…多选同时删除多个项目,并且还能复用已选 x 项那一行右侧的删除按钮,这个数量 x 的计数逻辑还是按会话数量来,不要把文件夹这个复选框当做一个数) — #527 gave each folder's row a Delete all, which deleted one folder at a time beside the box-and-bar way sessions are deleted. Each folder's row now starts with a box, as each session's row does, where magpie can delete the agent's sessions: ticked, it picks every session of the folder shown, folded or not, and the bar says "{n} selected" (已选 {n} 个) counting those sessions, the folder not one of them; unticked, it lets them go; a session's own box ticked or not part-ticks or ticks its folder's at once; under a filter it picks the ones the filter shows, as the bar's select-all does. Folders and single sessions picked together go with the bar's Delete, which asks in magpie's own dialog as before: a pick that is one whole folder is headed "Delete all {n} sessions in {folder}?" with its path, as Delete all's was; several whole folders "Delete all {n} sessions in {k} folders?" (删除 {k} 个文件夹下的全部 {n} 个会话?); anything else "Delete {n} sessions?". They go to magpie's trash through sessions/delete as before, and one written to in the last minute is left and said so. Delete all is gone from the folder's row: it did what the box and the bar's Delete do, two ways for one thing (the lesson of #526's Close). The box's label is "Select every session in {folder}" (选中 {folder} 下的全部会话); the box doesn't fold the folder or move the page; no left-border accent. sessions-folder-delete.test.cjs now covers the boxes (first in their row, none for OpenCode), a folded folder picked and asked about by name, Cancel posting nothing, two folders and a mixed pick, the part-tick, a filtered pick, the whole of app with its running session refused; it fails without the change (no folder box). sessions-manage, sessions-purge, click-scroll, sessions-toolbar, session-terminal, sessions-bar-names and sessions-overview pass in WebKit.
2026-10-02 20:22:00 +08:00
2026-10-02 08:20:33 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
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
workbuddy: an added account is told from the others by its uid, and the one WorkBuddy is signed in to stays once WorkBuddy moves on (#413: 添加已提前登录的 WorkBuddy 账号会顶掉默认第一个账号) — magpie matched a new sign-in to a listed account by its name, the nickname: another account of the same nickname replaced the one listed, or was taken for WorkBuddy's own and let go with "signed in". And WorkBuddy's own account is only read from WorkBuddy's file, so signing WorkBuddy in to the next account (to pick it on WorkBuddy's page from magpie) renamed that row, the last account gone, and the one picked, being WorkBuddy's own, was let go: one account at a time. A WorkBuddy or WorkBuddy AI sign-in is now matched by its uid (one of the same name whose uid isn't known is still taken to be it, #155); another account of the same name is added as "name (end of uid)"; the same account renews its sign-in where it is listed, and the one WorkBuddy is signed in to becomes magpie's own sign-in of it, first as it was, so it stays when WorkBuddy signs in to another. The sign-in says so: "该账号已在列表中,已更新登录" in the app, "is already listed — its sign-in was renewed" from magpie accounts add. On the plugin, the host no longer takes two sign-ins of one account id for one account when their uids differ. No request header or user agent changes. TestWorkBuddyAddSameNicknameAsOwn, TestWorkBuddyAddSameNicknameTwice, TestWorkBuddyAddWhatWorkBuddyIsSignedInTo, TestPluginSameNameOtherUID and signin-again.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; signin-again, click-scroll, old-webkit pass.
2026-10-01 19:38:36 +08:00
gui: a waiting sign-in has one button that puts it away, Cancel, and no Close beside Open again doing the same (mintonight, on #526: 那你这样改「取消」和「关闭」功能不是重复了吗,只保留一个就行了) — the fix for #526 had two causes to answer: the box's Cancel was pushed out of the account list by Qoder's long device link, and the reporter asked for a Close beside Open again. The first was fixed in the layout (the list's column is minmax(0, 1fr), the link cut short), which brought Cancel back in sight; the Close added beside Open again then did exactly what Cancel does (cancelSignIn: signin/<id>/cancel posted, the polling stopped, Add another back), two buttons for one thing in the same box. Close is gone; Cancel stays, as it is the button every other state of the sign-in box has (failed, installing, paste a code, the risk notice), at the box's right, and now carries Close's title, "Stop waiting for this sign-in" (不再等待这次登录). Open again stands alone in its row, with the import and key ways beside it where a subscription has them. The editor's Cancel, Save and Remove still put a waiting sign-in away. signin-close.test.cjs now checks that Open again stands alone, that no Close is in the box and that the one Cancel, titled so, posts the cancel, stops the polling and brings Add another back without moving the page; it fails without the change (Close still beside Open again). signin-link, signin-again, plugin-signin, routing-signin and signin-closes-sheet pass in WebKit (signin-callback's one case launches Chromium alone, which can't start here without its ICU data).
2026-10-02 20:22:00 +08:00
providers: a subscription signed in from the Add sheet leaves no blank page (#509, Sun1090: 登录这俩个之后,供应商显示空白 — WorkBuddy AI and Qoder CN) — a sign-in done opens the account's editor and sets adding = false, so the redraw hides the Add sheet at once instead of rolling it up through closeAddSheet, which is the only place that let go of the page's hold on a click in the sheet and of the room kept for it (#433). A sign-in that finishes within the four seconds a click is held (Qoder CN's "Sign in anyway", an app already signed in) kept the page held where the sheet had been: hold() made room under a list that now ended far above, and closing the editor uncovered an empty page, Add provider and the list scrolled out of sight above it, until the reader scrolled back. renderAdd now does what closeAddSheet does whenever a redraw puts away a sheet that was open (a sign-in done, an import of an account, Import apps, a provider action): the hold on the providers view goes and its room with it. signin-closes-sheet.test.cjs (ten keyed providers, Qoder CN signed in from the sheet with its risk accepted and done at the first poll, the editor closed: no .view-room is left, the new row is in sight, nothing empty under Add provider; Chromium and WebKit, English and Chinese) fails without the change (a .view-room left, 414px in a probe). add-button, add-sheet, rollup-short, deprecated-subscriptions, account-return, factory-sub, claude-risk and click-scroll .test.cjs pass. Not seen with the reporter's own data: their screenshot is consistent with this, not proven to be it. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 13:35:16 +08:00
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
signin: Grok and Command Code sign in from magpie in Docker (xugui on Discord: docker版登不上grok和command code) — Grok's sign-in first installs Grok Build with its installer, curl … | bash, and the image has neither bash nor curl, so it stopped at "installing Grok Build failed (exec: "bash": executable file not found)"; the community Grok plugin, beside the built-in rather than moved onto, wasn't given the CLI at all and said "install Grok Build first". Where no shell and curl/wget can run, Grok Build is now installed as its installer does, from the same x.ai/cli (then its storage bucket) address: the stable version, its binary for the platform, checked with --version, linked as ~/.grok/bin/grok and agent, installer = "internal" noted in its config.toml; the plugin's sign-in installs it first too. The device sign-in itself needs nothing back from the browser and was fine. Command Code's Studio posts the key to magpie's 127.0.0.1 port in the background, which a browser outside the container can't reach and no address carries; while it waits, its sign-in now takes an API key made on commandcode.ai/settings/keys and pasted (as Command Code's own CLI offers), checked with whoami and its plan as Studio's was, and a plugin's browser sign-in offers "Use an API key instead", its own api way (the Command Code plugin has one). No identity, header or prompt changes. TestGrokInstallsWithoutShell, TestCommandCodeSignInPastedKey and TestGrokPluginSignInInstallsCLI fail without the change, signin-paste-key.test.cjs too. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; signin-paste-key, signin-paste-codex, plugin-signin, signin-callback, commandcode-risk, plugin-key-hint, signin-link, click-scroll, old-webkit pass.
2026-10-01 22:25:39 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
provider: Bailian's decision model as a routing preset, and System One for custom providers (#647, Aimer779: Alibaba Cloud Bailian's decision-model-preview speaks TypeSafe's System One at POST <root>/systemone, and magpie only took it from TypeSafe's Jev) — a new preset, Bailian Decision Model (qwen icon, decide-only like TypeSafe Jev), asks decision-model-preview at its plan's root: Workspace · Beijing / Workspace · Singapore (https://{WorkspaceId}.<region>.maas.aliyuncs.com/compatible-mode/v1, the editor asks for the workspace ID and fills it in) or Token Plan · Beijing (token-plan.cn-beijing…, sk-sp- keys). Saving with the workspace still unfilled is refused, both in the editor and in provider.Add. A Bailian host has no /models for it, so its list is the one model, checked by a System One ask. A custom provider can now be a System One API: the CLI's decide=<root> (with models=… named anything, not only jev-*), workspace= and region=/plan= for a preset's plan; the editor's API seg has a fourth option, System One, whose Detect APIs posts a test question to <root>/systemone with the model typed, and a decide-only custom provider passes the "base URL is needed" check. A decide-only provider's first model is what it is asked as (Jev() no longer assumes jev-*), and its models stay routing groups' classifiers, never agents'. TestBailianDecisionPreset, TestCustomSystemOne, TestProviderSystemOne and TestProviderDecidePairs fail on the old code (undefined BailianDecision / DetectDecide, unknown decide=); internal/gui/tests/system-one.test.cjs (webkit, en/zh) covers the workspace, the Singapore and Token Plan plans, TypeSafe's unchanged editor and a custom System One detected and added. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes alone. Not tried against a real Bailian key.
2026-10-03 15:21:15 +08:00
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
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
update: on Windows, an exe something holds open for a moment still goes aside for the new version, and one held for good says why in full, with the release page a click away (freecss on Discord: "0.1.628 已下载 · couldn't move magpie-windows-amd64.exe aside to put the new version in: rename C:\Us…") — InstallBinary renamed the running exe to .old once and gave up on the first error, which Go prints as "rename <exe> <exe>.old: <reason>", two full paths ahead of the reason, and the version row cut it off on one line, so what failed couldn't be read. A leftover .old was already handled (RemoveOld, then .old-2…, TestInstallBinaryBesideARunningOld), and Windows renames a running image (it is mapped with FILE_SHARE_DELETE), so the likely cause is another program holding the exe open without delete sharing — an antivirus scan, OneDrive or a similar sync client — or a folder that refuses the change (access denied); which one isn't known until the full message is read. moveAside now tries again for about 3 s (0.1, 0.2, 0.4, 0.8, 1.5 s apart), clearing a read-only attribute after the first failure, with a fresh .old name each try; a missing exe isn't retried. When it still can't, the message is the reason without the paths and what to do: ERROR_SHARING_VIOLATION/ERROR_LOCK_VIOLATION says another program has it open (often an antivirus or OneDrive), access denied names the folder Windows won't let magpie change, anything else says to download the new version; the download is kept for another try, as before. The version row wraps a failed install's or download's reason (.sub.wraps) with the whole text as its tooltip, and a ready-but-failed install offers Download, which opens the release page. The Windows test box wasn't reachable from this session (ssh key refused), so nothing ran on Windows; TestInstallBinaryHeldOpen (windows-only: an exe held open without FILE_SHARE_DELETE for 0.5 s still installs; held for good, the error says another program has it open and the download is kept) was cross-compiled with GOOS=windows go test -c but not run. TestMoveAsideTriesAgain and TestMoveAsideSaysWhy (portable, with a fake rename) and the new update-check.test.cjs case (webkit, en and zh: the reason is not cut off, its tooltip is the full text, Download opens the release page, a later state is one line again) fail without the change; Chromium couldn't launch here (its Playwright cache has no ICU data). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet, go test -tags nogui ./... pass (TestFXInState failed once on a TempDir cleanup race, passes on the rerun; untouched).
2026-10-02 13:26:54 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
update: a restart to update waits until the gateway is idle instead of cutting off what the agents have in flight (#577: 正式版更新时保持网关请求与工具链不中断 — a Codex reply streaming through magpie, or a tool chain under way, was cut off by Restart to update) — the app serves the gateway in its own process and a restart to update is install, quit, relaunch, so whatever was in flight ended with it: a stream mid-reply, and a Claude Code turn of the subscription bridge waiting on its caller's tool results, whose Claude Code lives only in this process. The gateway now says what a restart would cut short (Server.Busy): its requests in flight (all but GETs and the bridge's own MCP callbacks), the bridge's runs parked on tool results, and when the last request ended. Restart to update (Settings → Version, the header's Update pill, the tray's item) restarts at once only when nothing is in flight, no turn waits on tools and nothing ended in the last 10 s (a tool chain's gap, the agent running a tool between two requests); otherwise it waits, saying what for: Settings shows "restarts once the gateway is idle · 2 in flight · 1 awaiting tool results" (网关空闲后自动重启 · 2 个请求进行中 · 1 个工具调用待返回) with Restart now and Cancel, the pill reads "Waiting to update" (等待更新) and its next click (not a double click's second half) restarts now, the tray item becomes "Restart Now to Update (3 in flight)" (立即重启以更新(3 个进行中)). The wait asks the feed again once idle, so versions out meanwhile collapse into the newest, downloaded before the restart; it gives up after an hour, saying so, the update then going in at the next restart or quit. A gateway served by another magpie (magpie serve, an older one) is not this one's to wait for. Handing the gateway over to the new version instead (the dev handover, e0c6892) is not used for releases: it rests on SO_REUSEPORT, which Windows lacks, and the new app would have to start beside the old one (single-instance, the Mac's bundle swapped under it). TestBusy (a stream counts until it ends, a GET never, a parked bridge run until it is answered or ends), TestRestartToUpdateWaitsForTheGateway (fails without the change: the click restarted with 2 requests in flight), TestRestartWhenIdle (not while streaming, parked or in a tool chain's gap; then once; an hour's wait gives up), TestRestartWhenIdleIntoTheNewest (0.1.11 staged, 0.1.12 and 0.1.13 out while waiting: restarted into 0.1.13) and TestTrayRestartNow are new; update-wait.test.cjs (WebKit, en and zh: what is in flight, waiting with Restart now and Cancel, counts following the gateway, the hour's give-up, the pill's wait and restart now, an idle click restarting at once, no click scrolling, no left border) fails without the change and passes with it. update-hide, update-check, sync-update, update-auto and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside; passes on rerun).
2026-10-02 23:36:43 +08:00
usage: each subscription account's tokens and cost, by the account that answered (#557: 希望用量账本能按订阅账号统计,每个 Codex / Claude 账号各用了多少 token、多少钱) — a call's record named the upstream key it went out on (ProviderKeyID/Name, #500) but never the subscription account, so with two Codex accounts signed in, or a Claude account that took over after a failover, the ledger could say what Codex cost and not what each account did. A record now carries providerAccount, the account that answered as the Routing trace names it (Account.User: its email or login, never a token), set by the gateway at the try that went out (the candidate loop's last try, so a failover's answer is the account that took over and the refused try stays the first account's; an X-Magpie-Account pin is the pinned one), and on the decide, draw and video paths too. Record.Account() gives it, or for a record written before the field the account its host named (Where() has long written "chatgpt.com as dee@example.com" for an account), and otherwise "" — shown as "account not recorded", never inferred from today's sign-in. The summary groups calls by provider and account (Summary.Accounts, the same email on Codex and on Claude being two accounts), priced as every other group; the ledger's Filter has Account, the search finds a call by its account, the CSV has a provider_account column beside provider_key_name (the provider's identity columns together, the caller's still last), and the Requests page's packed rows keep it and offer the period's accounts. magpie usage prints an accounts table after the provider keys, and magpie usage --csv --account <name> keeps one account's calls. The app's Usage page lists Accounts on the Overview (tokens and cost per account, the provider under its name, an unrecorded one in the faint colour, no stripe down a row's side) and has an Account filter on Requests, each account noting the providers it answered for, which asks for and exports that account's calls without moving the page. OTLP export is an allowlist and does not carry the account. TestUsageNamesPinnedAccount (both pinned accounts on magpie's endpoint and on Codex's own, the trace's Who and the record agreeing, no token in usage.jsonl) and TestUsageNamesAccountThatTookOver (Ordered routing, the first account refusing: the answer recorded as the spare, the refusal as the first) fail without the gateway change; TestSummaryByAccount, TestLedgerByAccount and TestRequestPageByAccount cover the grouping, the host fallback, "not recorded", keyed providers kept out, the filter, the search, the CSV column and the packed rows; TestLedger and TestLedgerWithSessionLogCalls take the new column. usage-accounts.test.cjs (WebKit and Chromium, en and zh: the Overview's accounts and costs, the Account picker's notes, its request and export, All accounts clearing it, scrollTop unchanged) fails without the asset change; api-key-usage, click-scroll, panel-usage, request-archive-usage, tray-usages, claude-usage-asked and every usage-* test pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup, passes alone).
2026-10-02 20:15:33 +08:00
usage: with WebDAV or S3 sync, each computer shares its usage and sees the others', the total and each computer's part (#542) — sync carried keys, agents and the library, and each computer's Usage page counted only the calls made on it. Sync now has a Usage tick (off unless picked, usage=yes in magpie webdav/magpie s3): a computer with it on puts its own calls up as one file a day, magpie/usage/<id>-<day>.magpie-usage beside the bundle (under the prefix's usage/ on S3), each a gzipped list of times, agents, providers, models, tokens, costs and statuses, sealed with the sync passphrase (backup.SealData, the key cached per passphrase so a sync derives it once); no request body, session, archive name or key goes, a call another computer passed on over the LAN (Via) isn't put up again, and an error is cut to 300 characters. The id is a random one kept in usage-computer.json, which no backup carries, with the host name beside it. A computer only ever writes and deletes its own names, so none overwrites another's; the files are not in the bundle and its hash, so sharing usage never makes a sync or a conflict of the bundle; the first sync puts up the last 90 days, after that only yesterday and today, at most every 15 minutes (a Save or Sync now at once), and a day past 90 is deleted by the computer it is of. The others' files, listed (PROPFIND Depth 1; ListObjectsV2) and fetched only when changed, are kept in usage-others and read with the ledger, as Record.Computer; a failure is said in the sync row ("Couldn't share usage") and never fails the sync. The Requests page and the panel's Usage tab get a Computer pick (this computer, other computers when more than one, each by name) sending computer=this|others|<id>; a row from another computer says "· on <name>" beside its agent, and its details name the computer and say what was said stays there, nothing asked of this computer's session files for it. With no other computer's usage there is no pick and nothing new on either page, and an older magpie never lists or reads usage/. TestUsageSharedBetweenComputers (computers a and b over a fake WebDAV under httptest: each sees 4 calls, the totals and its own and the other's part, nothing counted twice over repeated syncs, the bundle not put again for usage, a new call reaching the other, a day past the window deleted), TestUsageSharedThroughS3 (the same through a fake S3, signed list and delete, a prefix with a space and a plus) and TestUsageOffAsksNothing are new and don't build without the change; usage-computers.test.cjs (WebKit, en and zh: no pick without other computers, the pick and computer=this, another computer's row and details with no /content asked, the panel's pick, the sync form's tick posting usage: true, no click moving the page) fails without it on all but the no-other-computers case. s3-sync, sync-other-kind, panel-usage, request-archive-usage, click-scroll and usage-ledger-chart pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup, passes alone). Turning the Usage tick off, or sync off, lets the other computers' usage kept here go (DropAllShared), so the Usage page counts this computer's alone again: TestUsageSharedBetweenComputers checks both, and fails without it.
2026-10-02 17:47:36 +08:00
gui: the request archive's switch sits over the Usage page's requests, and the Gateway page's recent calls no longer fetch from the archive (Jorben on Discord: 网关页的最近请求下遗留了取回归档和预览的逻辑,和最近请求的预览能力重叠了,建议移除;以及把是否启用请求归档开关也挪到用量页请求列表的右上方) — since #447 a request's archive is read back and previewed from its row on the Usage page, so the Gateway page's recent calls showed two previews of one call: their own bodies, and under them the archive's copy with "Fetch from archive" and "Download". A recent call now shows only its own request and response bodies; archivePanel and what it uses stay for the Usage page. The archive's Off/On row over the recent calls (#archiveList) is gone, and its switch is now a compact one — "Request archive" and a switch, role=switch — at the right of the line over the Usage page's request list, beside the requests' sum. It posts settings/archive as before; what it keeps and where, or that Sync needs an s3:// address first, is its tooltip; a refusal still goes to the footer and leaves it off; a failed last upload says "Upload failed" beside it (上传失败, the error in its tooltip). It waits for the gateway's state rather than say Off before it is in, and is changed in place on a redraw, so a switch being pressed is never swapped out (the ledger and providers both redraw it). request-archive.test.cjs now checks the Gateway page has no switch and a kept call no archive panel and no Fetch from archive, and the switch at the list's top right on the sum's line: refused with no bucket, on with one (bucket in its tooltip), off, and a failed upload said; web-page.test.cjs finds the switch on the Usage page. Playwright in Chromium and WebKit: request-archive, request-archive-usage, web-page, click-scroll, usage-ledger, usage-ledger-detail, gateway-fold, sse-body pass (72/72). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gui/ pass.
2026-10-02 13:23:41 +08:00
gui: Settings is a page per part, under a tab each, the part kept in the address (#471: Settings is one long scroll of some 45 rows in 9 sections) — the page now opens with a tab list in the warm-ups' look (role=tablist, a tab each with aria-controls/aria-labelledby): General (appearance, language, text size, the icons, model names, login, Dock, session terminal), Usage (the menu bar's usage, refresh, logos, what usage shows, currency, number units, the two alerts, then the warm-up and check-in tabs), Network and sharing (proxy, local network), Models (images, web search), Privacy, Observability, Sync and backup, About; the part picked shows its rows alone. Every row, its id and what it saves are as they were; the duplicate heading of a one-section part and the Preferences heading are gone. The part is kept as ?view=settings&tab=<part> (dropped on any other view), so a reload or a link lands there, and remembered (magpie.settingsTab) for Settings opened again without one, as the tray's window/main?view=settings; an unknown one falls back to it. A part whose rows are all hidden on this system has no tab, and one picked falls back to the first shown. The arrows, Home and End move along the tabs, one in the Tab order; a click on a tab moves nothing on the page, the tabs staying where they were; a pill drawn while its part was hidden has its thumb put under its pick when shown (the warm-ups' fix, now shared). The tabs wrap to a line more when they don't fit (English in the 560x420 window, at 110 and 125%, a phone), no left-border stripe. The click hold also holds a click not yet held before taking the next (keys pressed within a frame on a tab list whose part shrank to the window: the second click was taken at the clamped top and the page stayed there). New strings General and Network and sharing are in i18n.js (常规, 网络与共享). Tests that reach a row now open or click its part: api-key-theme, currency, gateway-caller-keys, gateway-connect-keys, lan-docker, number-units, otel, redact-rule-row, s3-sync, search-api, settings-groups, sync-other-kind, tray-inuse, tray-logos, tray-usages, update-check, update-hide, usage-alert, webdav-settings-refresh, whats-new; those needing a scrolled page wheel a shorter window or in smaller steps, as a part is shorter than the whole page was. settings-sections.test.cjs (Chromium, WebKit, en, zh) fails without the change (10 of 10). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy as on main); settings-sections, settings-groups, click-scroll, old-webkit, text-size and every settings test listed above pass.
2026-10-02 05:50:24 +08:00
site, gui: release notes survive GitHub limiting usemagpie.ai, and a failure to get them says so, apart from a release without any (#661) — /api/notes answered 503 {"error":"no releases"} with Cache-Control public, max-age=300 whenever GitHub's API limited the worker's shared IP, and /api/latest fell back to the release page's redirect with notes "" (kept 300 s too), so the update window had nothing to show. The worker now: never keeps an error (503 is no-store); takes the notes from GitHub's releases feed (releases.atom, not API-limited) when the API says no, turning its HTML back into the markdown the app draws and putting back the <!-- lang:zh --> marker GitHub strips (before the first Chinese heading after an English one); keeps the last whole answer for 30 days in the edge cache and serves it (merged with the feed's newer releases) when GitHub fails; keeps a degraded answer 60 s instead of 300; and sends a GITHUB_TOKEN secret to the API where one is set (none is yet). In the app, /api/whatsnew says error (and the release page's url) when the notes couldn't be had, and Settings' What's new opens a dialog saying "Couldn't load the release notes" with Try again and "Open the release page" (through the app), or "No release notes provided." with the release page and no retry for a version with none, instead of a passing toast; en/zh. Tests that fail on the old code and pass: site/worker.test.mjs (API limited: notes from the feed in either language, briefly kept; all of GitHub down: 503 no-store, then the last whole answer; the token sent), TestWhatsNewTellsFailureFromNone, and whats-new-failed.test.cjs (WebKit, en/zh: failure with retry and link, retry showing the notes, none without retry, no click moves the page). The feed parser was tried on the real releases.atom (v0.1.730–739: English and Chinese split as the API's bodies are). go vet, builds (darwin, GOOS=linux, GOOS=windows), go test -tags nogui -count=1 ./... under a temp HOME, node --test site/ and whats-new.test.cjs pass.
2026-10-03 17:12:48 +08:00
gui: What's new no longer comes up by itself after "Restart to update", and its dialog has "Don't show again today" (#525: 现在只要升级过,下一次打开窗口就会先弹一屏 What's new……点"重启以更新"之后也一样……今天到现在已经发了 22 个版本……希望改成:只在首次启动时展示更新内容;打开应用、点"重启以更新"之后,默认不弹;弹窗上加一个"今天不再弹出"的勾选) — the dialog came up on the first window of every version newer than last-version, the one the user had just restarted into from magpie included, so with releases many a day it was in the way of almost every open. A restart to update (restartToUpdate, once the new version is installed) now leaves the version it installed in magpie's folder (updated-in-app); that version's start finds it, removes it, and keeps its notes to Settings' What's new row, which still shows everything since the version last run. An update that came some other way (installed on quitting, or by hand) still shows the dialog once, on its first start; a note for another version is no reason to keep quiet, and a build from source leaves it alone. The dialog shown by itself has a "Don't show again today" box (今天不再弹出), posted to the new POST /api/whatsnew/today as it is ticked or unticked, so Close, Escape and a click outside all keep it; the local day it was ticked on is kept in whatsnew-quiet-day, and until the day turns nothing comes up by itself; Settings' notes have no such box. TestWhatsNewQuietAfterRestartToUpdate (a restart to update through restartToUpdate notes the version, its start shows nothing while Settings has the notes, an outside update shows, another version's note doesn't count, quiet today and not the next day, unticked) and TestWhatsNewTodayRoute fail without the change; whats-new.test.cjs (WebKit, en and zh) now ticks, unticks and ticks the box and checks each is posted without moving the page, and that Settings' notes have none, and fails without it. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; whats-new, update-check, update-auto, update-hide pass in WebKit.
2026-10-02 15:48:17 +08:00
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

Gateway Caller Keys

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

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

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

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

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

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

Other Browser Regressions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

balance-fix.test.cjs opens a custom provider whose balance token sits beside new-api's /api/usage/token (and a new one with a token and no Balance URL): the editor says so, one click moves it to /api/user/self with the quota as its field, the New-Api-User header is asked for until it is typed, Check balance asks as the form has it and says the fix plainly, and the Usage page's card does too.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

sync-update.test.cjs checks the header's refresh button looks for a newer magpie too: one click, one check, and the Update pill shows what it found. Its tooltip says so. English and Chinese.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

searcher-pick.test.cjs checks the Web search section's "Searches for other models" row (01huadalang on Discord: pick which provider searches for a model that can't). With none named it shows Automatic and magpie's own pick; its picker offers Automatic first, each provider that can search by its small model, and each of its models, but never a relay said to search, whose names the row gives with why. A model picked is saved as searcher: "<provider>/<model>", a provider by its small model as searcher: "<provider>", and shown; another setting saved still sends the pick; one named that magpie can't use (turned off) is said in the row with magpie's pick shown instead; Automatic sends searcher: "". No click moves the page. English and Chinese, Chromium and WebKit, with the API faked.

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

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

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

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

With Node.js and Playwright available:

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/rollup-short.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/routing-log-icons.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 internal/gui/tests/library-loading.test.cjs internal/gui/tests/provider-typed.test.cjs internal/gui/tests/phone-web.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-blocked.test.cjs internal/gui/tests/routing-zcode-blocked.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-usage-asked.test.cjs internal/gui/tests/claude-risk.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 internal/gui/tests/signin-paste-codex.test.cjs internal/gui/tests/signin-paste-key.test.cjs internal/gui/tests/usage-alert.test.cjs internal/gui/tests/quota-families.test.cjs internal/gui/tests/provider-search.test.cjs internal/gui/tests/webdav-settings-refresh.test.cjs internal/gui/tests/lan-docker.test.cjs internal/gui/tests/plugin-key-hint.test.cjs internal/gui/tests/update-hide.test.cjs internal/gui/tests/tray-logos.test.cjs internal/gui/tests/routing-proxy.test.cjs internal/gui/tests/sse-body.test.cjs internal/gui/tests/remote-group-served.test.cjs internal/gui/tests/plugin-move-overflow.test.cjs internal/gui/tests/plugin-movable-review.test.cjs internal/gui/tests/deprecation-notice.test.cjs internal/gui/tests/routing-time.test.cjs internal/gui/tests/unlisted-models.test.cjs internal/gui/tests/plugin-updates.test.cjs internal/gui/tests/request-archive.test.cjs internal/gui/tests/group-member-fast.test.cjs internal/gui/tests/providers-off-fold.test.cjs internal/gui/tests/account-return.test.cjs internal/gui/tests/usage-ledger-detail.test.cjs internal/gui/tests/usage-ledger-chart.test.cjs internal/gui/tests/usage-chart-axis.test.cjs internal/gui/tests/panel-usage.test.cjs internal/gui/tests/usage-refresh.test.cjs internal/gui/tests/usage-ledger-content.test.cjs internal/gui/tests/claude-direct.test.cjs internal/gui/tests/signin-again.test.cjs internal/gui/tests/sessions-calendar-fill.test.cjs internal/gui/tests/editor-short-window.test.cjs internal/gui/tests/group-from-model-add.test.cjs internal/gui/tests/sync-other-kind.test.cjs internal/gui/tests/web-page.test.cjs internal/gui/tests/balance-parts.test.cjs internal/gui/tests/search-api.test.cjs internal/gui/tests/searcher-pick.test.cjs internal/gui/tests/provider-file-error.test.cjs internal/gui/tests/sessions-manage.test.cjs internal/gui/tests/number-units.test.cjs internal/gui/tests/sessions-bar-names.test.cjs internal/gui/tests/model-suffix-own.test.cjs internal/gui/tests/routing-effort-change.test.cjs internal/gui/tests/every-skill.test.cjs internal/gui/tests/header-centre.test.cjs internal/gui/tests/public-url.test.cjs internal/gui/tests/strip-scrollbar.test.cjs internal/gui/tests/remove-all-skills.test.cjs internal/gui/tests/request-archive-usage.test.cjs internal/gui/tests/agent-layout.test.cjs internal/gui/tests/group-found-off.test.cjs internal/gui/tests/provider-picks-stay.test.cjs internal/gui/tests/subagent-effort.test.cjs internal/gui/tests/tier-effort.test.cjs internal/gui/tests/header-zoom-fit.test.cjs internal/gui/tests/library-row-open-scroll.test.cjs internal/gui/tests/whats-new.test.cjs internal/gui/tests/by-agent.test.cjs internal/gui/tests/sessions-purge.test.cjs internal/gui/tests/sessions-folder-delete.test.cjs internal/gui/tests/quota-curve.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.62.1.

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

Set BROWSER=chromium or BROWSER=webkit for one engine. Set ARTIFACT_DIR to an external directory to retain screenshots and Playwright traces, including failed assertions. These browser checks run separately from make test.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Mobile Web

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

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

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