Commit Graph
1329 Commits
Author SHA1 Message Date
yetone d50651a1b6 gateway: a Codex subagent's sealed task goes to the routing group's ChatGPT account that answered its lead, not turned away (#619)
Koohoko: on a routing group in Ultra or normal mode, every subagent Codex spawned sometimes failed with "An OpenAI lead sent a sealed subagent task that a Magpie-served model cannot read". A group with a Codex (ChatGPT) account in it has that member's turns passed through as they came, encrypted marks on spawn_agent's message and all, so when it answered the lead the ChatGPT backend sealed the task (gAAAAA…), and the subagent's request on the same group was refused before routing although the group had an account able to read it: sometimes, as it depended on which member answered the lead. The check moves from codexBackend into serve: a request carrying a sealed agent task goes only to ChatGPT accounts (sealedReader, p.Account.Agent == codex) of the group or model, and the account that answered the lead (its affinity under the parent thread, x-codex-parent-thread-id or the turn metadata's parent_thread_id) goes first while the subagent's own conversation isn't kept anywhere yet, as another account may not open what it sealed, as it doesn't another's reasoning. With none in the group or model it is turned away as before, before any rule or classifier, now saying a group with a Codex account in it would do. TestCodexSealedAgentHandoffGoesToGroupsChatGPTAccount (group xai/m then codex/gpt-5.5, lead pinned to the spare account) fails without the change (400) and without the lead-first part (acct-1 asked). Whether another ChatGPT account than the sealing one can read the task isn't known; not tried with a real account. 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:51:56 +08:00
YAYOI27 cb82822f3d source: add opt-in China mirror fallback (#566) 2026-10-03 09:50:59 +08:00
yetone bf0f3e3f73 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
yetone 05bc2ffb92 provider: Cursor's plugin is kept at 0.1.8 or newer (plugins #11) — catalog entries are written as CallDynamicTool calls the model answers in, and a thinking-only turn is retried as an empty reply instead of ending the session. keepMovedCurrent updates moved users. 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 08:45:55 +08:00
yetone 7c583b32dd provider: a Grok subscription is given every function's parameters as an object at the root (Fate on Discord: Codex through Grok fails "[invalid_client_tool_schema] mcp__codex_app__automation_update: tool parameter root must be an object type (root schema is an anyOf/oneOf union with a non-object branch)") — grokBody, which the Grok plugin's requests go through too, merges an anyOf/oneOf root's object branches (local $refs read): their properties, the fields all of them require; the other branches go. TestGrokBodyObjectRoot fails without it. 2026-10-03 07:31:35 +08:00
yetone 290062679d gui: Show on a hidden agent nothing is set on says it stays under Not set up (Fate on Discord: the Agents panel's hide, show and restore aren't intuitive) — it moved from Hidden to Not set up, still folded, and the status said "Idle shown" while it stayed out of sight. It now says it stays under Not set up until a model is picked for it; one with a model set still says shown. show-unset-agent.test.cjs fails without it (WebKit, en and zh). 2026-10-03 07:26:29 +08:00
Luis Araya 49ee7d82d9 fix(agent/codex): recognize CC Switch official OpenAI mirror for grouping and failover (#504) (#579)
* fix(agent/codex): recognize CC Switch official OpenAI mirror for grouping and failover (#504)

CC Switch writes Codex's built-in OpenAI provider as model_provider = "custom"
with a [model_providers.custom] table having name = "OpenAI", requires_openai_auth = true,
and no base_url. Because model_provider was not empty or "openai", Codex's models
were filed under a group named "custom" instead of "OpenAI", and account failover was
skipped.

Recognize a model_provider table without a base_url and named OpenAI as the official
provider for model option grouping and account failover.

* fix(agent/codex): route CC Switch official mirror failover through table base_url (#504)

Codex ignores openai_base_url when model_provider = "custom" is set, so
setting openai_base_url during account failover caused requests to bypass
magpie and fall back directly to chatgpt.com.

Instead, point the mirror table itself at magpie (base_url = at.codexURL())
during failover, and remove base_url when failover turns off, returning the
config byte-identical to the original. Exclude the official mirror from
takeTables so relays remain distinct, and preserve the original picker
grouping logic for magpieID and openai.

* fix(agent/codex): clear mirror failover marker when switching table and preserve relay base_url
2026-10-03 06:48:49 +08:00
yetone 0ebce8b4ed gateway: a translated Responses request doesn't ask the upstream to seal a tool's arguments (#613) — Codex marks spawn_agent's message "encrypted"; its calls go back to Codex as unsealed (encrypted_function_args []), so an upstream honouring the mark would hand the subagent ciphertext as its message. The marks are left out of the tools sent (unsealed). TestNamespacedCallKeepsArgsWhenSearchTakesOver checks the upstream isn't asked to seal. 2026-10-03 06:10:29 +08:00
yetone 71f9884ee7 gateway: a call from a Responses upstream keeps the arguments its finished call holds (#613) — Codex offers web_search beside its namespaced tools; on a provider not set to search by itself the request is translated rather than relayed, and spawn_agent (every namespaced tool) came back with arguments {}: the decoder took the argument deltas once any came, and dropped what output_item.done / function_call_arguments.done held. A call's arguments are now given once it is done, the fuller of the deltas and its done events (JSON first); a stream that ends with a call open gives what it has. TestNamespacedCallKeepsArgsWhenSearchTakesOver (the issue's request, stream and not: arguments {} without the fix), TestResponsesCallArgsFromDone. 2026-10-03 06:09:11 +08:00
yetone 77cc49543d gui: a provider's models with none picked are drawn as served (#614: the one model unticked and saved was still "1 model" on the card) — with no pick agents are served the vendor's list, up to 24 of it, while the editor drew those chips plain, as if left out. They are now drawn as served (dashed, .auto), their title says why, and the hint says how to show agents none: tick Only through routing groups, or switch the provider off. provider-none-picked.test.cjs (WebKit, en+zh). 2026-10-03 06:05:06 +08:00
yetone b878f17dc1 groups: a member can be switched off (#feedback on Discord: 希望路由组里的模型可以直接通过checkbox控制是否启用,测试路由规则的时候需要频繁的添加和删除路由组里的模型很麻烦,而且会丢失顺序) — each member in the group editor has a switch; one switched off (Group.Off) keeps its place in the order and its rules but is sent nothing until switched on again, a rule naming it falls to the group's order as for a member not ready. The card says (off) after it. A member whose reasoning changes or that is renamed stays off; a manual group sends to its pick whatever is off; switching every member of a routed group off is refused. TestGroupMemberOff, group-member-off.test.cjs (WebKit, en+zh). 2026-10-03 04:57:01 +08:00
yetone f1ff6ce03a providers: Qwen Token Plan (Jeremy on Discord: 千问的TokenPlan和dashscope是分开的endpoint) — Alibaba Cloud Bailian's Token Plan (personal and team) is a preset of its own at token-plan.maas.qianwenaiapi.com, chat completions at /compatible-mode/v1 and Anthropic messages at /apps/anthropic, on its own sk-sp- key that DashScope's pay-as-you-go hosts don't take. Its models are given as the plan's overview lists its text ones, for when the host lists none. Not tried with a real plan key. TestQwenTokenPlan. 2026-10-03 04:50:46 +08:00
yetone bafdd6954b agents: Empryo (#612) — picking a magpie model in Empryo adds a "magpie" provider at the gateway to ~/.empryo/config.json's providers (baseURL + modelsAPI, no envVar) and sets defaultModel to magpie/<catalog id>; the user's defaultModel is stashed and put back on leaving, magpie's provider goes again, and a file magpie made is removed when empty. The user's own providers and keys are left alone. The gateway now answers GET /v1 (and /v1/) with its info, as Empryo lists a keyless provider only when its baseURL answers. Also on WSL. Built from Empryo's docs and its public v2 code, not run against the Empryo binary. TestEmpryo, TestBaseURLAnswers (GET /v1 was 404 before). 2026-10-03 04:44:37 +08:00
XYenonandAmp 564f8782c7 provider: keep conversation models on mixed decision providers (#609)
Distinguish decision-only providers from gateways that also serve conversations. Only Jev models use the decision API; ordinary models remain in the catalog, automatic groups and conversation routes. Keep the full model cache when probing a mixed provider’s decision endpoint, and allow conversation model tests in its editor.

Verified with go vet -tags gtk3 ./..., go test -tags gtk3 ./..., node --check internal/gui/assets/app.js, and an isolated browser fixture.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0fe05-afd7-70df-97cf-fdb69c4f4680

Co-authored-by: Amp <amp@ampcode.com>
2026-10-03 04:06:02 +08:00
yetone f55747ce82 agents: Claude Desktop's tiers each take a model of their own (WilianWeng on Discord: desktop中调用子agent时,claude无法识别模型角色) — Desktop's Code tab runs Claude Code with every ANTHROPIC_DEFAULT_<TIER>_MODEL set to "" unless the gateway's /v1/models tags a model with anthropic_family_tier (shortnameIdentityOverrides in its app.asar, 2.7032), so a subagent on sonnet or haiku asked for Claude Code's own claude-sonnet-…, which magpie doesn't serve, and ran on the chat's model. Claude Desktop now has opus, sonnet, haiku and fable pickers (the tiers' square, as Claude Code's), kept in claude-desktop.tiers.json: the model picked is tagged with its tier and is_family_default, a Claude model magpie serves is tagged with its own tier while that tier has none picked, and a request for Claude Code's own id of a tier no row is tagged with (a model picked for two tiers carries the first) goes to the tier's pick. The tiers' square and picker work without a model field to follow (Not set: a Claude model of the tier, else the chat's model). Desktop reads /v1/models at start-up. TestClaudeDesktopTiers (gateway, agent) and desktop-tiers.test.cjs (fails without the app.js change); not tried against a real Claude Desktop. 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 03:57:24 +08:00
Davy 94cadbc34d fix(factory): open other agents' Factory requests as Droid (#519)
* fix(factory): open Anthropic requests with Droid's line

Factory only accepts a subscription request whose body opens as Droid
(#242, #506). Responses and chat completions already do. Anthropic
Messages now puts that line in the first system block, as droid2api
does, and keeps the agent's own prompt after it.

* fix(factory): serve Gemini on Factory's generate route

Every agent already reaches Factory's Claude, GPT and open models:
the gateway translates its own API into the model's, and the body
opens with Droid's line. Gemini was the wire still missing.
droid2api posts those models to /api/llm/g/v1/generate and prepends
the same line to systemInstruction. Factory now does that too.

The models are the ones droid 0.232.0's CLI registry still offers
(gemini-3.1-pro-preview, 3.8/3.7/3.6/3.5 Flash, gemini-3-flash-preview).
2.5 and Gemini 3 Pro Image are availableInCLI false there and stay out.
They are not Droid Core. A streamed Gemini reply comes back in the
client's own API.

TestFactoryFromEveryAgentWire posts Claude Code, Codex, Pi, OpenCode
and Gemini CLI at Claude, GPT, GLM and Gemini. TestFactoryGeminiStreamsToChat
reads a Gemini SSE chunk back as chat completions. Not tried against
a real Factory account.

* fix(factory): open other agents with droid2api's system prompt

droid2api's config.json system_prompt is Droid's line and a blank line,
written in front of the agent's own prompt on Responses, chat, Messages,
and Gemini generate. A body that already opens with the line is unchanged.

* fix(factory): answer non-streaming Gemini clients with JSON

Factory's generate route always returns SSE and droid sends no stream
field. A generateContent call is translated into JSON; a relayed
streamGenerateContent drops stream before it goes upstream. The provider
Test probes generate instead of failing the protocol, and a Gemini model
is tested on that route. The system prompt is joined into the one
systemInstruction part droid sends.

* fix(factory): join Droid's opening with one newline

droid writes the line and one newline in front of the agent's prompt.
The extra blank line from droid2api's system_prompt is dropped.
Anthropic still keeps the line as its own first block.

* fix(factory): send Droid's line alone when there is no prompt

droid joins blocks with "\n", so a request with no system prompt is
the line and nothing after it. Responses, chat and Gemini were
sending the line plus a trailing newline. Anthropic already sent
the line alone.
2026-10-03 03:39:54 +08:00
yetone f0e4a79c06 plugins: auth.refresh — magpie renews a plugin's sign-ins before they run out, once per account at a time (owner: 按你建议的优先级,先做 1 和 2, from the pi plugin study) — each community plugin renewed its own tokens inside fetch, deduping by hand (factory's "two refreshes would spend it twice", minimax's renewing, cline's REFRESH_LEAD, workbuddy's EARLY_MS). A plugin can now give auth.refresh(auth, provider), returning the fields to keep over the old sign-in, and optionally auth.refreshLead (ms, 5 minutes by default). Before an account's loader options, provider.models and auth.usage, the host renews an OAuth account whose expires is that close: requests that find it due share one renewal (vendors that spend a refresh token once never see it twice), waiting up to 15s, and a renewal that takes longer is still saved when it ends (pending keeps the host up). What it gives is merged over the account and saved through setAuth, which clears the loader caches here and in Go, and the account is said renewed; a sign-in replaced or removed meanwhile stands. A throw leaves the account as it was and the request goes with the old token; signIn: "expired" on it marks the account and it isn't tried again until signed in again, any other error is retried after 30s. OpenCode ignores the hook, so plugins keep their in-fetch check for it. Documented in /docs/plugins (#renew, en and zh). TestPluginRenewsSignIns fails without the host change (no renewal). 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 03:08:15 +08:00
尔立 90d9d40dcf gui: a menu-bar quota picker taller than the window is clamped and scrolls (#603)
The base .pop.proto-menu set max-height:none with no overflow, so a
proto menu grew to its full height. The menu-bar quota picker lists one
row per account, so a subscription with several accounts made its menu
taller than the window: it ran past the bottom edge, the last
subscriptions were unreachable, and scrolling did nothing because the
box itself never scrolled — openProtoMenu's flip-up only fires when
there is room above, and near the top of Settings there is not.

Clamp the base proto menu to min(360px,70vh) with overflow-y:auto and
overscroll-behavior:contain, as its siblings (.sess-menu,
.lan-address-menu) already are, so a long list scrolls within the
window instead of overflowing it. This covers every openProtoMenu
caller, including the Search-API vendor picker, which had the same
latent overflow.

proto-menu-overflow.test.cjs opens the quota picker with enough
accounts to overflow a 600px window and checks the menu stays inside
the viewport and scrolls to the last row; it fails without the clamp
(the menu's bottom lands at 1071px) and passes with it.
2026-10-03 03:03:25 +08:00
Snowyy 8c7791c071 fix(provider): use message-array input for Responses probes (#605) 2026-10-03 03:02:46 +08:00
yetone 20c5fa6fd9 agents: Grok Build's model picker folds magpie's rows for the account Grok Build is signed in to (Fate on Discord: grokbuild 在登录态下会加载重复的模型) — signed in, the picker listed Grok Build's own models and then the same models again as the Grok (SuperGrok) subscription via magpie. They are marked Same, as Claude Code's are (#496), so the picker folds them into one row a click opens. viaMagpie also counts as the agent's own a plugin's account for the agent's vendor (Grok moved onto its plugin) when it is the user the agent is signed in to (provider.AgentUser: Grok Build's auth.json). TestGrokFoldsItsOwnAccount fails without it (magpie/grok/grok-4.7 unfolded); the plugin path is not covered by a test. 2026-10-03 02:58:44 +08:00
yetone 1e80a84fe3 provider: a Codex warm-up asks the account's cheapest model (#604) — warmModel took a model with mini in its id or else the first listed, and a GPT-6 account lists no mini and starts with the flagship gpt-6.1-sol. warmPick takes the cheapest by list price (the model's own, else models.dev's openai price), and with no price known a mini, then a luna (GPT-6's cheap one), then the first. TestWarmPick. 2026-10-03 02:53:20 +08:00
yetone 5154df624f gui: the Routing page's accounts say how much is left when Settings' allowance display is Left (#602) — the stage's rows and the list of what each account did said "{n} used" whatever quotaLeft was, while the Usage page, panel and menu bar followed it. routing.js now words a window, and fills its bar, with quotaLeft as app.js's quotaText/quotaFill do ("{n} left · kept for last", "剩余 {n} · {d}后重置", …); the routing explanations ("the most of its allowance left — {n} used") are unchanged. routing-quota-left.test.cjs (en/zh, Left and Used) passes in WebKit. 2026-10-03 02:53:20 +08:00
yetone dfc074afdb 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.
In the GUI, an agent magpie is in has "Disconnect from magpie" (断开 magpie(还原配置)) in its row's menu (its logo, or a right-click; beside Apply again), with a tooltip saying what it does, and in its model picker right under Default, so the two aren't taken for each other; the logo's title says the menu has it. It asks first in the app's own dialog (askDisconnect, as askReset and askForgetSaved do; no native confirm), and the row is then drawn from the answer: the model the agent had, no longer wired, and the status line says so. Every new string has its Chinese. The drag grip stays hover-only; no click moves the page.

What it leaves: Codex keeps magpie's [model_providers.magpie] table, as its own-model pick does, for threads started on it; with several ChatGPT accounts in magpie, Codex failover may write openai_base_url again on a later sync. A relay's own ANTHROPIC_MODEL in Claude Code's env was never stashed, so it doesn't come back; modelSettings effort entries for magpie models may stay in settings.json.

Tests: TestCodexDisconnect (own provider, own provider signed in, nothing set: the top keys as before, the user's provider table untouched, no subagent model, no catalog file, no stash, not wired, no drift), TestClaudeDisconnect (own relay and nothing set: settings.json as before), TestGeminiDisconnect, TestDisconnectUnwiredLeavesConfig, TestAgentDisconnectAPI; internal/gui/tests/agent-disconnect.test.cjs (WebKit, English and Chinese: only a wired agent has it in the menu and the picker, the in-app dialog, Cancel posts nothing, Disconnect posts and the row shows the old model, no native dialog, the page doesn't move). click-scroll, agent-grip, agent-layout, agent-models, agent-model-groups, model-pick, favorite-star, claude-picker-fold and panel-effort pass under WebKit; menu-scroll fails on /api/plugins/updates unmocked, as it does 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. Not run against a live agent config.
2026-10-03 02:42:09 +08:00
yetone 9848a9062a 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
yetone 4b5d6d725f site: the home page in Japanese at /ja/ too, picked from a language menu (the owner: 官网的日文也加上) — i18n.js gains a ja dictionary for every marked string, LANGS saying which languages have docs of their own (zh; /ja/ links the English docs); / sends a browser whose Accept-Language prefers Japanese to /ja/ as it does Chinese to /zh/, until a language is picked. The nav's 中文/EN link is now a globe button opening a menu of English, 中文 and 日本語, the current one checked (Escape or a click outside closes it), with hreflang ja alternates in the head; the page's scripts take T(en, zh, ja), so the routing demo, copy button, download label and theme titles speak Japanese on it, and Japanese text is set in Japanese faces (Hiragino Sans, Yu Gothic, Noto Sans CJK JP) rather than the Chinese ones. Also fixes the routing demo's stat labels in every language: since 8efd7d8 wrapped each in its own span, .rt-stats span matched the inner ones too and the labels were written into the wrong places ("errors your agent saw" twice); it now takes .rt-stats > span. Checked in WebKit: every demo string has three languages, each tab's labels are right in en/zh/ja, the menu marks and switches the language and sets the cookie, and /, /zh/ and /ja/ don't scroll sideways at 320, 390 or 768. worker.test.mjs covers the ja dictionary and the ja redirects. 2026-10-03 02:35:59 +08:00
Snowyy 5422306fac usage: guard cache transitions and Windows long-path reads (#598) 2026-10-03 02:19:11 +08:00
Zhizhen He dab594714f sessions: preserve symlinks when moving across volumes (#600)
The copy fallback skipped symbolic links and then removed the source,
permanently losing them when a session moved to or from the trash across
volumes. Copy each link with its original target and propagate link errors
so a failed copy leaves the source in place.

Cover relative, absolute, directory, dangling and nested links, a root link,
and destination conflicts. The copy regressions fail without this change.
2026-10-03 02:17:57 +08:00
yetone 4160f464aa groups: a routing group whose member's reasoning levels nothing knows offers its other members' levels, and Pi's thinking field offers and shows what Pi runs the model at (Moody-Sin, #597: 路由组设了推理强度最高是否正确应用?) — Pi on group/deepseek (腾讯DeepSeek) at magpie's 最高 listed off alone in its /thinking, and sent no reasoning_effort: Pi clamps defaultThinkingLevel max to the off it offers, so the group's members were asked at the vendor's default, not max.
The group offered no levels: its levels were what every member has, and a member whose levels magpie doesn't know (a Token Plan's deepseek-v4-pro-202606, which models.dev doesn't list) has none, so the intersection was empty and models.json had the group "reasoning": false. A model a provider was given that its vendor's models.dev list leaves out (TokenHub's deepseek-v4-pro, its list holding the Hy models alone) also had none in the catalog, where the gateway (Provider.Efforts) fits efforts to DeepSeek's low, high, max. Magpie's Pi field offered all seven levels whatever the model.

- provider.Exposed: a picked model the vendor's list doesn't have takes the levels Provider.Known gives it (knownElsewhere), not none.
- groupEntries: a member levelsUnknown (no levels, no account list, its vendor and maker don't list it, models.dev knows no model of its id — catalog.Knows) doesn't narrow the group's levels; the gateway sends it the effort as asked (fitEffort). A member listed with none still narrows them.
- Pi/OmO thinking field: for a model of magpie's, Options are the levels Pi offers for the entry magpie writes (piSupported, as pi-ai's getSupportedThinkingLevels), and Get shows a level it doesn't offer as Pi's clamp of it (piClamp).

Now Pi at max on that group sends reasoning_effort "max", relayed as max to both members (DeepSeek's levels have max; the unknown one is sent it as asked).

New tests: TestGroupLevelsWithUnknownMember (provider), TestPiGroupThinkingIsWhatPiRuns and TestPiClamp (agent); each fails without the change (group entry "reasoning": false; catalog levels []; field offering all seven). TestMoveCursorEfforts gives its model levels, since Pi is offered off alone for one without. go vet, nogui/linux/windows builds and the full test suite pass but the known flake TestFXInState.
2026-10-03 02:08:47 +08:00
yetone 7547dfb3c9 library: a skill in the library's own folder is never copied onto itself, is shown and brought in where it is, and one kept in ~/.agents/skills isn't linked a second time into the agents that read that folder (#595)
"Add from folder" pointed at a skill already in ~/.config/magpie/library/skills emptied every file of it: InstallSkills' symlink failed (it was there) and its fallback copied the folder onto itself, O_TRUNC first. A folder from elsewhere by a name the library's folder already had was copied into it the same way. Such a folder is now refused ("…is in the library's folder already: bring it in from the skills found there"), anything by that name in the library's folder is never written into, the copy fallback runs only when the symlink failed for another reason, and copyDir refuses a destination that is its source or inside it.

A skill put in the library's folder by hand, not in library.json, was shown nowhere: foundSkills now scans the library's folder too, first, and such a one gets FoundSkill.Library (magpie's links to it in the agents count as the agents having it). Bringing it in lists it where it is: nothing moved or linked, Source nil for a folder of its own, {folder, its real path} for a link. The found row says "in the library's folder, not listed" with its path, and Bring in that it is listed where it is (en, zh).

Codex, Gemini CLI and the others that read ~/.agents/skills themselves got a ~/.codex/skills/X link to a library skill whose real folder is in ~/.agents/skills, and saw it twice. A user's own entry there (not magpie's link) that is the very folder the library's is now counts as the skill being in the shared folder: no link of their own is made, magpie's made before are taken away, and a byte copy of their own is kept aside with the backups as before. Nothing in ~/.agents/skills is moved or removed. SkillView.Always names the agents that read that folder, and their chips are lit, disabled, and say "Codex reads ~/.agents/skills itself, where this skill is kept — it has it whatever is ticked here" (en, zh); a click on another chip or All neither sends nor darkens them.

A skill's kind and "linked from" are now what its library entry is, not what was written when it came in: a linked folder moved in by hand is the library's own, shown without "linked from", and its Remove says the folder goes to the backups, as removeSkill does (which now also counts a junction as a link, as the page does).

TestAddFromLibraryFolderKeepsIt (the skill's folder and the library's skills folder as the source, another folder by that name, copyDir onto and into itself: bytes unchanged), TestFoundInLibraryFolder, TestSharedSkillNotLinkedTwice and TestSkillKindIsWhatTheFolderIs are new and fail without the change (the first with SKILL.md and run.sh emptied); TestSharedAgentsSkills now asserts Codex and Gemini CLI get no second link (Pi keeps its) and are Always. library-own-folder.test.cjs (WebKit, en and zh) is new and fails without the change but for its Remove case; library-all, library-items, library-loading, library-row-open-scroll, library-toggle-status, click-scroll, import-all-skills, own-skill, every-skill, remove-all-skills, by-agent and market-have 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.
2026-10-03 02:03:12 +08:00
yetone a8309d5ca4 site: a plugin developer guide at /docs/plugins and /docs/zh/plugins — how magpie runs OpenCode plugins, the hooks it calls (config, auth methods and prompts, loader and fetch, provider.models, chat.headers, magpie's auth.usage), the API each AI SDK package maps to, accounts and their scope, X-Magpie-Sign-In, provider ids, testing in a sandbox, publishing and the registry, and the host protocol. The Acme example on it was run against v0.1.676 with a stand-in API (plugin add, login, provider test, quota). Every docs page links it; inline code wraps on phones so no page scrolls sideways. 2026-10-03 01:59:45 +08:00
XYenonandAmp 6e6338f8d6 agent: configure Zed models and gateway credentials automatically (#584)
* agent: configure Zed models and gateway credentials automatically

Add Zed to the CLI and Agents page with an OpenAI-compatible magpie provider, a synced model catalog and default model selection. Preserve existing JSONC settings and restore the previous provider and default model on reset.

Store the gateway credential in Secret Service, macOS Keychain or Windows Credential Manager so Zed needs no manual API key entry. Cover config preservation, catalog sync, model limits, Flatpak paths and credential failures.

Verified with the full nogui test suite, go vet, macOS and Windows cross compilation, and a local Zed conversation through the gateway.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd03-00d7-75fd-956f-c505e07ba60d
Co-authored-by: Amp <amp@ampcode.com>

* agent: store Zed credentials directly through Secret Service D-Bus

Replace the Linux and FreeBSD secret-tool subprocess with the existing godbus client. Open a session, create the default collection when needed, unlock it and replace the matching Zed credential. Handle collection, unlock and item prompts, dismissal and timeout.

Verify the protocol on isolated D-Bus instances with no helper executables on PATH, including immediate completion signals and failed writes. The full nogui suite, targeted vet and race checks, macOS and Windows cross compilation pass; local credential readback confirms replacement without duplication.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd03-00d7-75fd-956f-c505e07ba60d
Co-authored-by: Amp <amp@ampcode.com>

* docs: remove the separate Zed configuration paragraph

Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd03-00d7-75fd-956f-c505e07ba60d
Co-authored-by: Amp <amp@ampcode.com>

* agent: fix Zed reset, keychain access and translated notices

Avoid restoring magpie’s own provider after a model change in Zed, and cover reselection followed by reset. Remove the unsupported Flatpak config path.

Authorize the installed Zed app and security command to access the macOS gateway credential. Translate Zed’s notice and apply translation when selecting or reapplying an agent model.

Verified with the full nogui test suite, go vet, Zed race tests, cross compilation and browser checks. macOS keychain behavior still needs runtime verification on a Mac.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd70-98a1-737f-93bd-77e181f904c1
Co-authored-by: XYenon <register@xyenon.bid>

* agent: preserve Zed model picks when resetting magpie

Restore the stashed default model only while Zed still selects magpie. Preserve a model chosen in Zed itself and discard the stale model stash, while restoring the previous provider.

Find Zed Preview in Applications folders and Spotlight without requiring a CLI, and use any in the isolated D-Bus tests.

Verified the reset regression fails before the fix and passes afterward. The full nogui suite, go vet, Zed race tests and macOS/Windows test cross compilation pass.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd70-98a1-737f-93bd-77e181f904c1
Co-authored-by: XYenon <register@xyenon.bid>

* agent: retain Zed provider backup when reselecting magpie

When Zed’s provider already points at the gateway, update only the model stash and preserve the original provider backup. Reset can then restore the user’s provider after a native model pick followed by another magpie pick.

Add a regression covering the full sequence and checking the restored provider, model catalog, custom headers and latest native model. Confirm it fails before the fix and passes afterward; all Zed tests, the full nogui suite and go vet pass.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd70-98a1-737f-93bd-77e181f904c1
Co-authored-by: XYenon <register@xyenon.bid>

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-10-03 01:51:53 +08:00
yetone 12cc2089e4 agents: each magpie model in T3 Code has a Reasoning pick of the levels it takes (KevinXC on Discord: 加进去的那些模型没有任何 reason 和 thinking 的设置,导致现在所有的模型都默认是 medium) — magpie's custom models in T3 Code's magpie instance were a slug and a name, and T3 shows a custom model with no capabilities with no options and sends Claude Code no effort. T3 Code's custom model entry takes capabilities.optionDescriptors (packages/contracts model.ts CustomModelEntry), which replace the driver's default in its picker, and its Claude adapter hands the effort chosen to the Agent SDK as it is for a custom model (no effortMap), which Claude Code sends to the gateway. Each model now gets an "effort" select labelled Reasoning, as T3's own Claude models have it, of the levels it takes that Claude Code can send (low, medium, high, xhigh, max, within claudeEffortsFor for Claude's own models), medium chosen first, else the lowest; a model with none (or only none/minimal) gets no pick. TestT3CodeEfforts is new, and t3Format now decodes a custom model's capabilities strictly as T3's schema has them (one default per select). Not run against a live T3 Code. 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 01:47:42 +08:00
mofelee 38b4a9d984 feat: export gateway and agent conversation waterfalls to Langfuse (#581) 2026-10-03 01:43:32 +08:00
Gabi 24bd8aa1ae gateway: DashScope's glm-5.3 cannot think with it off either, in its own words, so the request magpie turned thinking off for is asked again with it left to the model (#596)
DashScope (qwen-cn, qwen) answers 400 "InternalError.Algo.InvalidParameter:
The value of the enable_thinking parameter is restricted to True." for the
models of its own that always think (glm-5.3, glm-5.3-prime). Claude Code's
session title and its auto-mode classifier ask without thinking, on the
session's model (the title request carries max_tokens 32000 and no thinking
field), and magpie's Anthropic passthrough turns thinking off for a
non-Claude model unless the request says otherwise (#250) — so both calls
came back the 400, and the retry that asks again with thinking left to the
model knew only Z.ai's ("…always engages in thinking…") and OpenRouter's
("reasoning is mandatory…") words for that refusal, not DashScope's. The
400 reached the agent as it was: Claude Code reads a failed title call as
no title and a failed classifier call as no verdict, so a session on
DashScope's glm-5.3 never got its title (Lody, which dispatches Claude Code
through magpie, showed it as every child session keeping its draft title)
and auto mode blocked every action it meant to check.

alwaysThinks learns DashScope's words ("enable_thinking … restricted to
true"); the request is asked again with its thinking left to the model,
once, as for Z.ai. Another 400 is the agent's to see.

Tested with TestAnthropicPassthroughDashScopeThinkingRestricted, a fake
upstream answering 400 exactly as DashScope does when the request says
thinking disabled and 200 for the one with it left to the model, with the
unrelated 400 still not asked again; TestAnthropicPassthroughModelThatAlwaysThinks
and TestOpenRouterMandatoryReasoningRetries pass as they were. go vet,
go test -tags nogui ./internal/gateway/, GOOS=linux and GOOS=windows builds
pass (internal/netproxy's TestBunThroughBridge fails the same on origin/main
here, its SOCKS check). Against the real DashScope: through the stock
gateway Claude Code's title request (Agent SDK 0.3.284, claude-code
2.1.284, qwen-cn glm-5.3) returned null — no title; through the patched one
the session was titled. glm-5.3-prime answers the thinking-left request as
well, its thinking inside the title call's own 32000 tokens.
2026-10-03 01:40:40 +08:00
yetone d49d889cd7 gui: a provider's Fallback picker says how many more models match past the dozen it draws, and that typing narrows them (01huadalang on Discord: 这个备用模型的来源是哪里,怎么只有这么几个,是否可以选择供应商里的别的模型,例如 workbuddy 的模型) — focusing the picker drew the first 12 of every on model of every ready provider and nothing else, so the rest (a WorkBuddy model among them) looked missing. Past 12 it now ends with "{n} more — type to narrow" (还有 {n} 项 — 输入以筛选), the string the other pickers use. fallback-more.test.cjs is new (a provider beside one with 20 models: 12 chips and "8 more", a typed filter finds the 17th, the note gone; en and zh): it fails without the change and passes with it in WebKit; click-scroll passes. 2026-10-03 01:40:30 +08:00
Snowyy 3d8c6f24c5 usage: capture Windows change time for cached logs (#594)
* usage: capture Windows change time for cached logs

* ci: show native Windows usage test outcomes
2026-10-03 01:15:29 +08:00
Zhizhen He 6d7bd11d07 edit: validate nested YAML input and output (#591)
Reject duplicate keys and multiple documents instead of silently editing
them. Validate rendered output before writing so edits cannot leave
dangling aliases or introduce duplicate keys. Errors leave files untouched.
Apply the same validation to YAMLText.

JSONToYAML now rejects duplicate keys without creating or replacing the
target, including omp's models.json conversion. Treat a document containing
only --- as empty, matching the top-level YAML editor.

Add regression tests for document loss, dangling aliases and duplicate
keys, alongside existing-behavior coverage for malformed input.
2026-10-03 01:15:14 +08:00
yetone 8efd7d8027 site: the home page in Chinese at /zh/, and made for phones (the owner: magpie官网加上多语言支持,另外官网要对移动端友好) — index.html stays the one English source, each string marked data-i18n / data-i18n-attr; the worker serves /zh/ from it with i18n.js's dictionary put in by HTMLRewriter (<html lang>, docs links to /docs/zh/…, canonical and hreflang alternates), and / sends a browser whose Accept-Language prefers Chinese there until a language is picked with the nav's 中文/EN link (the lang cookie; Vary: Accept-Language, Cookie). The routing demo, copy button, download label and theme titles speak Chinese on it; the zh docs' home links go to /zh/. On phones the nav's links fold into a menu (≤860px; Escape, a click outside or on a link closes it, aria-expanded kept), Discord and GitHub move into it under 560px, the footer wraps, the routing tabs wrap, and nothing scrolls sideways at 320, 390, 768 or 1280 in either language (WebKit, light and dark). worker.test.mjs checks every marked key has its Chinese and the dictionary has none unused, and the / redirect by Accept-Language and cookie. 2026-10-03 01:09:21 +08:00
yetone b683e531d9 gui: a window lightweight mode made again isn't shown before its page has come, however it is asked for (found smoke-testing #580's lightweight mode on Windows 11) — on Windows a remade window is shown only once WebView2 says its page has come (whenLoaded), but a second ask in that time — two magpie app at once, a tray click while the panel was being made — went to openMain's or togglePanelNow's ordinary path and showed it at once: the main window was drawn black for about half a second, the early Show markReady's comment warns can crash WebView2 on a slow start. A window waiting on whenLoaded is now marked loading (h.loading, main thread only) until its page comes; meanwhile openMain only sets its URL and togglePanelNow does nothing, the waiting callback showing it, and the lightweight loop doesn't let it go. Other systems are unchanged (whenLoaded runs at once there). go vet, GOOS=windows go build and go test ./internal/gui pass; the race itself was not run again on the Windows box. 2026-10-03 01:05:14 +08:00
yetone eddc298e92 gui: a gateway key's limit window and the session terminal are picked from the app's own menu too, so no native select is left (the owner, on the account picker: 这里为啥是原生的 selector 而不是我们抽象的 ComboBox?) — after the keep-signed-in account picker, two <select>s remained: the Window of a gateway key's limit editor (#585) and Settings → Session terminal. Each is now a sess-pick button showing what is picked, with a chevron, opening the app's menu (openProtoMenu) headed Limit window / Session terminal (限额周期 / 会话终端) with the current one ticked. The limit window's pick is staged in the editor as before (nothing sent until Save) and fills its column with the chevron at its end; the terminal's is drawn at once and saved as before. gateway-key-limit.test.cjs and session-terminal.test.cjs now check there is no native select, the menu's head and items, the button's value and text, a pick closing the menu (and leaving the limit editor open), a second click closing it, and picking through the menu, posting as before; they, keep-login, click-scroll and settings-sections pass in WebKit, en and zh. 2026-10-03 01:02:53 +08:00
yetone 53b60764d4 gui: the account Codex or Claude Code is kept signed in to is picked from the app's own menu, not a native select (the owner: 这里为啥是原生的 selector 而不是我们抽象的 ComboBox?) — the Keep {agent} signed in to row (#524) used a <select>, so it drew and dropped the system's menu while every other pick in magpie is a pill opening its own (openProtoMenu). It is now a sess-pick button showing the account (or the first account) with a chevron, opening the app's menu headed Account to stay signed in to (始终登录的账号) with the first account and each account that hasn't lapsed, the current one ticked; picking one ticks Keep and saves it as the select did, a second click or Escape closes it. keep-login.test.cjs now checks there is no native select, the button's value and text, the menu's head and items in order, a second click closing it, and picking through the menu (spare, then back to the first) posting as before; it and click-scroll pass in WebKit, en and zh. 2026-10-03 00:59:48 +08:00
yetone ad93293885 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
yetone f1ca6139b5 usage: a Codex call through the gateway that wrote to its upstream's cache is no longer listed a second time as a local session call (laowangba233 on #589: 用量中的统计是不是重复了 … 为啥会一个是提供商的一个是本地会话的) — the Usage page drops a Codex rollout's call when it matches a gateway record by session, end time within 2s and equal tokens. Codex reads input_tokens_details.cache_write_tokens from a Responses reply and keeps it as cache_write_input_tokens; the gateway's rUsage read only cached_tokens, so for a call whose upstream told a write the record had CacheWrite 0 and the rollout didn't, the tokens never matched, and the call stayed listed under Local session beside its provider's row. Calls without a write matched, so only some calls were counted twice (Maru Code 92 calls plus 22 "本地会话" in the report). Also, sessions' spent took only the cache read out of Codex's input_tokens, which holds the write too, so such a call's write was counted twice in its own input. Now: the gateway reads cache_write_tokens (input = input_tokens - cached - written) and tells it to Codex when it translates a reply; spent takes the write out of the input as well; and the matcher compares a call's input plus cache writes, its output and its cache reads, so a record and a rollout that split the write differently — an older Codex without the field, or records already in usage.jsonl from before this change — still match, and the duplicates already on a user's page go too. The session caches' versions go up (sessions.json 11, calls-v4) so kept parses are read again, and older calls-v* folders are removed. Under a provider filter the provider ranking still lists the other providers, as it is a picker; the duplicates were what made it read as double counting. TestCodexCacheWriteMatchesGatewayCall (a Codex rollout and gateway records for two calls, one with a 9000-token cache write; with the rollout naming the write and the record not, both naming it, and an older Codex without it: two rows, no Local session row, the provider ranking maru-code alone, unfiltered and filtered to the provider, by QueryPage and LedgerOf; fails without the change with a third row from the log), TestCodexCacheWriteNotInInput and TestResponsesUsageCacheWrite (parse and round trip through what Codex is told) are new and fail without the change; TestCallsCodex's call with a write now expects input 200 (2500 - 2000 read - 300 written). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./internal/... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside, which passes alone). 2026-10-03 00:47:33 +08:00
尔立andClaude Code 93fc561f17 feat(gui): a provider switched off no longer lists its models (#588)
The model picker (gatewayModels) and the fallback picker
(renderFallback) both iterated every provider's models regardless of
its Off flag. The backend already refuses an off provider's models
(providerEntries skips !p.On(), so /v1/models never lists them), and
the routing page's own providerEntries and the fallback picker's
ready check already honour the off gate - but the two pickers above
still offered ids the gateway would turn away, showing a model the
picker says works but a request to it cannot use.

Both now skip a provider whose Off is set, matching the backend's
behaviour so the picker and the gateway agree on what is usable.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-03 00:44:49 +08:00
尔立andClaude Code 2c81af6983 fix(provider): InUseLogin follows the gateway when the signed-in account is spent (#587)
The menu bar's account-in-use card (trayInUseCard via InUseLogin)
picked the account by sign-in state alone: Active or first, unless
paused. The gateway moves its requests as soon as an account's
allowance is spent (Allowances, SpentShare), but the sign-in follows
only every few minutes (SwitchWhenSpent, codex/claude only). Between
those - and for every other multi-account provider - the card showed
the exhausted account while traffic flowed through another.

InUseLogin now weighs the leading account against the same allowances
the gateway routes by (Allowances + loginSwitching's SpentShareOf).
When it is known spent and another account is on with room, that one
is reported instead. An allowance not known is never taken for spent,
so a single account, an unread one, or a failed read keeps the
signed-in account as before.

Tests: the existing inUseOf table gains a room parameter (nil-safe);
new cases pin the follow-the-gateway behaviour and its guards;
TestLoginRoom verifies the wiring against injected allowances.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-03 00:44:43 +08:00
yetone 4bc7d6c00d provider: moved Factory users get opencode-factory-auth 0.1.10 (magpie-community/plugins#10) — a tool result quoting Claude Code's fixed phrases is sent so Factory's check takes it, and Claude Code's # Environment update carrying a model is adapted as its model-switch updates are. The mover's min is raised from 0.1.9, so keepMovedCurrent updates every moved user's plugin. go build -tags nogui and go test ./internal/provider/... under a temp HOME pass. 2026-10-03 00:42:16 +08:00
Lin Huazhong 71365192d9 test: the agent folder variable check also scans gateway, so a new variable there is caught (#586)
* test: 环境变量清单检查覆盖 gateway 的读取

* docs: 恢复环境变量检查的英文注释并补充扫描依据
2026-10-03 00:37:02 +08:00
lunar-meandlunar-me 99b1865494 docs: add missing relative pronoun in README.md (#593)
Co-authored-by: lunar-me <lunar-me@users.noreply.github.com>
2026-10-03 00:31:25 +08:00
yetone 4a2f538c61 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
yetone 2e340f7bb6 gui: an agent row's grip is drawn only with the pointer on the row, as ec10d3b had it (the owner: 这个为什么又加回来了啊?不是说 hover 的时候才出现吗?一直出现太丑了) — 71ba715 (#537) drew the grip in every row's left margin with the pointer away, so the Agents list showed a column of dots beside its logos at all times. The grip is again drawn only on the row under the pointer, the one dragged, or a handle with its menu open or the keyboard on it, as the Providers list's is; dragging and clicking it do what they did, and Hide stays in the handle's menu and the row's right-click menu. agent-grip now checks no row's grip is drawn with the pointer away, the hovered row's alone is, and it goes when the pointer leaves; it fails without the change ("alpha's grip is drawn with the pointer away") and passes with it in WebKit (en/zh, window/panel). click-scroll and agent-layout pass in WebKit. 2026-10-03 00:19:52 +08:00