Commit Graph

  • 0f42934c1f fx: a rate fetched behind a page for another home sets nothing in this one, and the gui tests ask no rate of the network (follow-up to #541) — fx.Soon asks for a stale rate behind the page; in the gui tests a state read with no rate cached so reached open.er-api.com, and its answer, landing seconds later, was taken as the rate by whichever test ran then (TestFXStateDoesNotWait saw the live 6.71 where its fake endpoint said 7.31). A fetch whose cache path is no longer the one in force now returns without touching the in-memory rate or the failure time, and internal/gui's TestMain points the rate at a closed local port. go test -tags nogui ./internal/gui ./internal/fx pass three runs of four (the fourth, TestFXInState's TempDir cleanup finding .cache not empty, the flake it had before). yetone 2026-10-02 17:53:41 +08:00
  • da834411b5 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. yetone 2026-10-02 17:46:00 +08:00
  • d072221b0c fx: the state never waits on the dollar-to-yuan rate, a stale one is asked for behind it (#541: placeholders for seconds at start-up) — with CNY chosen, state() and the Settings page asked fx.Get for the rate and, once the one kept was older than its 12 h TTL (every morning's start-up), waited up to 4 s on open.er-api.com before answering. currentFX now takes fx.Soon: the rate known now (this process's, the cache file's, else Fallback, as before), and when that is stale fx.Get behind it, one at a time (a TryLock), with 15 s to answer; the next look shows the new rate. fx.SetRateURL lets another package's test point the rate at a fake endpoint and fx.Settle waits for a fetch under way. TestFXStateDoesNotWait (yesterday's rate cached, an endpoint taking 3 s: the state answers within 1 s with yesterday's, stale, then the new one) fails without the change ("the state waited 3.0s on the rate"). 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. yetone 2026-10-02 17:36:18 +08:00
  • 47741ee31f remote magpie: another computer makes videos with the video models a shared magpie has, as it draws with its image models (#545: 图像生成能否也提供给局域网中其他终端) — drawing from another computer already worked both ways, a plain client with the gateway's key at its /v1/images/generations and a magpie with it as a Remote magpie (since v0.1.555), but a Remote magpie's videos did not: its list never gave them, and /v1/videos turned away any provider but a Grok subscription. A magpie now asks its Remote magpie's list for them too (X-Magpie-Videomakers), the other lists them marked kind:video (a magpie that doesn't ask isn't given them, as it would take grok-imagine-video for an image model), and they are kept apart from the models to chat or draw with (catalog.Model.Films). A video with one is started at the other's /v1/videos, its id there carried, base64, in this magpie's video id, and asked after and fetched at its /v1/videos/{id} and /content. TestRemoteMagpieVideos fails without the change (no video models listed); TestLANDraws covers images from another computer both ways through lanGuard with a caller key. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui of gateway, provider, catalog and imagemcp pass. yetone 2026-10-02 17:31:26 +08:00
  • 6586c556db 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. yetone 2026-10-02 17:31:26 +08:00
  • ff8771ff72 panel: the tray panel's Usage reads again when the panel is opened or focused, and has a Refresh now button (#546: 悬浮窗的用量不会自动刷新) — the panel's Usage was read every 10 s only while the page said it was visible, and a tray panel shown again is not always told it is, so it could show what it read when it was last open. Now a focus or a visibilitychange on the Usage tab reads it at once (not within 2 s of the last read), and the bar has the window's Usage refresh button (an icon, 立即刷新 / Refresh now), spinning while it reads. panel-usage.test.cjs (WebKit, en and zh: the button's label, a click reads once, a focus after 2 s reads once, a second focus at once reads nothing) fails without the change. 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. yetone 2026-10-02 17:25:21 +08:00
  • 472642a6a2 tray: each logo in the menu bar is as large as the next (the user: tray 上的 provider icon 大小不一致,好难受) — a logo was drawn at its file's whole box, so one with a wide margin (Codex's, Alma's) came out a size smaller than one that fills it (Gemini's). Its shape is now cut to its own inked bounds and fitted, aspect kept and centred, to the 14pt box; a solid tile, fuller to the eye, to 90% of it. Black glyphs go the same way. TestTrayImageLogoSizes (ten logos span 24-29 px of the 28 px box at 2x) fails without the change (Codex 22 px, Alma 20 px). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./..., go test ./internal/gui pass. yetone 2026-10-02 17:22:14 +08:00
  • d8ebf2b893 test: one list of the agent folders a sandbox clears, so no sandbox misses an agent (yetone, on #522: A shared list of env vars for the other sandboxes would be a welcome follow-up) — every sandbox kept its own list of the variables that point at an agent's folder, and each had drifted: the gateway's cleared 5 of the 32 magpie reads, the provider package's 2, the tui's 10, the library's 20. A sandbox clearing 2 meets whatever Grok, Kimi, Hermes, MiniMax, Qoder or ZCode the developer has installed, which is how #522 came about — Qoder's two were in the library's list and in none of the four that failed. internal/agentenv is now the one list, and the 21 sandboxes that fake an environment with no agent in it clear it, each keeping beside it the variables that are not an agent's: APPDATA and LOCALAPPDATA, which a sandbox sets to a folder of its own rather than clearing, VISUAL and EDITOR. A name in the list is one magpie reads, and TestVarsAreRead walks the sources to say so, which is how two names a sandbox had been clearing turned out to be read by nothing at all — GEMINI_CLI_HOME and OPENCODE_CONFIG in internal/backup's, while OPENCODE_CONFIG_DIR, the one internal/agent reads (agents.go:545), was not in it. Where a sandbox set an agent's folder to a fixture of its own, the clearing now comes first, since a list of 32 wipes what a list of 5 left standing. Not converted: the omp tests' four variables, which are the ones omp's own lookup consults (a Pi fork) rather than every agent's, and the warm-up's Claude Code traffic flags, which are not folders — CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC stays out of the list for that reason too, its one test setting it on purpose. On Windows 11 the whole suite answers as it did before the change: 31 packages pass, and the same 15 tests in internal/library, internal/gateway and internal/agent fail for what they failed for before (symlinks this machine has no privilege for, and TestSuffixModesReachAgentFiles). go vet ./..., go build -tags nogui (windows, GOOS=linux, GOOS=darwin), and go vet -tags nogui for linux and darwin over every package touched pass. (#543) Lin Huazhong 2026-10-02 17:22:16 +08:00
  • b0dc82c47c 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). yetone 2026-10-02 17:00:51 +08:00
  • d113d7613c claude: an account Claude Code was moved off is never answered, nor counted, as the one it was moved to (nil_1024, v0.1.641, routing Smart: Claude Code signed in to A, B added in magpie and made first, B's 5 hours spent; the Usage page showed A's 5 hours spent too though A was never used, and every Claude request ended 499 until magpie's Claude was turned off and Claude Code signed in to A again; Claude Code's /status said B) — "Make first" on a saved Claude account signs Claude Code itself in to it (SwitchLogin): B's sign-in goes into Claude Code's keychain and ~/.claude.json, A's is saved and from then on run in a config directory of its own. But the Claude Code processes the bridge keeps for a conversation's next turn were found by owner, p.ID + the account's name alone, and A's conversations were kept in Claude Code's own home, from when A was its sign-in. Claude Code reads its keychain again every 30 s (2.1.287, the keychain reader's ASt=30000), so those processes went on as B: A's next turns, routed to the saved A, resumed them and were B's, refused as spent, and the rate_limit_event they printed (B's five_hour, rejected, 100%) was noted under A, so A showed spent and rested until B's reset; with B spent too, every request was turned away with 429, which the agent kept retrying until it gave up (the gateway's 499, "the agent canceled the request"). Signing Claude Code in to A again made those processes A's again, which is why that fixed it. Now a run in Claude Code's own home has "own" in its owner (Account.AgentsOwn: no token of magpie's), so a saved account's turn never resumes one kept from when it was Claude Code's own sign-in, and starts a Claude Code on its own sign-in in its own directory; what such a run says of the account it is on is noted only while Claude Code is still signed in to its owner (ClaudeCodeMovedOff); and Claude Code's /usage reading for the account it is signed in to is kept only when Claude Code was on that account before and after it ran (claudeWindows), so a reading taken across a switch, or asked for an account Claude Code was just moved off, isn't kept as that account's. TestClaudeMadeFirstKeepsTheOtherAccountItsOwn (A's turn after the switch was answered "tok-b turn 2" by the Claude Code kept from before, and A's 5 hours read 100%, without the change; B is still asked as itself and reads 100%) and TestClaudeMadeFirstSpentGoesToTheOther (through the gateway, routing Smart, B refusing as spent: A's conversation was a 429 "You've hit your limit", now answered on A) run a fake Claude Code that answers as the sign-in it reads at each turn, in a temp HOME, with the real SwitchLogin; TestClaudeUsageOfAnotherAccountNotKept (Claude Code moved to B while /usage ran: A read 100% without the change) covers the reading. No real Claude Code, account or keychain was used, and no request was made to Anthropic. 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. yetone 2026-10-02 16:59:34 +08:00
  • 02646a2260 claude: each of Claude Code's tiers (opus, sonnet, haiku, fable) and its subagents can run at a reasoning effort of their own through magpie, picked from the tiers' square under the tier's model (#536: 希望 Claude Code 的 opus/sonnet/haiku/subagent 各自可设置推理强度) — until now one effort was set for Claude Code (modelSettings / effortLevel / CLAUDE_CODE_EFFORT_LEVEL) and every tier ran at what Claude Code asked, so a haiku tier used for small side calls ran at the main model's high. Claude Code takes a tier only as a model id (ANTHROPIC_DEFAULT_<TIER>_MODEL, CLAUDE_CODE_SUBAGENT_MODEL) and has no per-tier effort setting, so the effort travels in that id, the way a routing group's member is fixed at one (#189): a tier at an effort is "<model>:<level>", before any [1m] mark ("v/big:high[1m]"), and the gateway (askedAt) takes the level off before resolving, then asks the model for it at the model's nearest level (withFixedEffort, fitLevel) whatever Claude Code asked, the trace's Try.Fixed saying so; a group member's own effort still wins, and a model whose own id ends in ":<level>" (MemberEffort's isModel) is taken whole. Token counting strips it too. yetone 2026-10-02 16:53:52 +08:00
  • 71ba7150d7 gui: an agent row's grip is there, faint, without the pointer on it, so Hide is found without hovering (#537: 隐藏 agent 的入口再次不可发现:#479 修复的常显把手在 v0.1.619 被改回悬浮才显示) yetone 2026-10-02 16:35:51 +08:00
  • 2f02a767b7 otel: request and response bodies can go with a call's span, as Langfuse's input and output, behind a switch that is off by default (#538: Langfuse 似乎无法看到请求体,如果技术上好实现的话,希望可以加一个开关) — the OTLP export sent only an allowlist of metadata (model, tokens, status, timing), never a body, so a Langfuse trace had no Input or Output. Settings → Observability has a new row, "Include request and response bodies" (otel.bodies, MAGPIE_OTEL_BODIES overriding it as the other MAGPIE_OTEL_* do, and counted in the "environment variables override" note). When it and the export are on, the gateway hands the call's captured request and reply (the Recent-calls capture, first 256 KiB each, "… (cut here by magpie)" added when cut) to its usage record as BodyIn/BodyOut, json:"-" so they never reach usage.jsonl, scrubbed with redact.ScrubJSON as the request archive's are (on top of Mask secrets' placeholders); the span carries them as langfuse.observation.input and langfuse.observation.output. A streamed reply's output is its text put together from the events (Chat's choices[].delta.content, Responses' response.output_text.delta, Anthropic's content_block_delta), a reply that isn't a stream or has no text (tool calls only) going as it came. This is filled for the final record, a locally turned-away request, and an attempt refused before another account was tried (its refusal as output); draw/video/decide and Codex's own-login passthrough still send metadata only. Queued records keep the config they were queued under, so turning bodies off drops any not yet sent, and spans with bodies go four to a request so a batch stays well inside a collector's size limit. The OTLP export row's text now says credentials stay local and prompts and replies too unless bodies are included. Nothing was sent to a real collector or Langfuse. TestOTelExportsBodiesWhenOn (off: no input/output and no prompt in the export; on: the request as input, the reply as output, a streamed reply's text as output, a key in the request scrubbed) fails without the gateway change, and without the scrub; TestBodiesStayOutOfTheLog, TestOTelReplyText, TestOTelSettingsAndOverrides (MAGPIE_OTEL_BODIES) and otel.test.cjs (the row off at first, en and zh, a click saves bodies:true without scrolling, kept through another preference's save; WebKit) 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. yetone 2026-10-02 16:47:34 +08:00
  • 27f0e4ebdd codex: when the ChatGPT account runs out of its allowance after a magpie model was picked, magpie becomes Codex's provider, and goes back beside the sign-in when the allowance is back (#540: once the Codex 5-hour/weekly quota is used up, a third-party model through magpie sends nothing in new or old sessions; Dazzle-sys: the session was on the magpie model all along, the quota went elsewhere) — the Codex desktop app refuses to send for a ChatGPT account that is out of its allowance before any request is made, whichever model the turn is on, so a magpie model beside the sign-in (openai_base_url, ChatGPT mode) is blocked with it; Codex's open-source core and TUI only report a usage limit after a response, the block is the app's, keyed on the account. bd5bf422 made magpie the provider (model_provider = "magpie", model_catalog_json; the app-server's getAuthStatus then answers requires_openai_auth false, the app's API state) only when a model was picked while the account was already out, so an allowance running out later left Codex as it was. KeepOnAnAccountWithRoom now also looks, every 5 minutes, whether the active Codex account is out (CodexUsedUp, now read through LoginUsage's cache, fetched at most once a minute, as the agents' sync asks it on every catalog sync) and tells the agents' sync when that changes (noteCodexUsedUp → catalog.Touched); Codex's Sync then sets the magpie model again, which wires it as the provider while the account is out, marked codex.out in the stash, and puts it back beside the sign-in once the allowance is back, only where it was moved for that (a provider asked for with sign-in "api" stays). Threads started on Codex's own provider and reopened may still be held by the app; the Codex app may need a restart to take the new config. agent's TestMain stubs codexUsedUp so no test asks OpenAI. TestCodexRunsOutAfterPick (ChatGPT mode, then out: provider; out again: stays; back: beside the sign-in; api stays) fails without the change; TestNoteCodexUsedUp checks only a change is told. No request was made to OpenAI. 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. yetone 2026-10-02 16:47:28 +08:00
  • 7911002497 gateway: Codex's model list answers within 3 s even when chatgpt.com is slow, so magpie's models are in it (#539: Codex 0.159.2 in ChatGPT mode on v0.1.641 shows no relay models) — in ChatGPT mode Codex asks openai_base_url (magpie's /backend-api/codex) for GET /models, and magpie answered only after the ChatGPT backend had answered it, waiting as long as the gateway client lets a response's headers take (10 min). Codex's models manager gives that fetch 5 s (MODELS_REFRESH_TIMEOUT) and on a timeout keeps its bundled list, which has none of magpie's models; where chatgpt.com is slow or hangs (in China, through a proxy) that is every time. Both Codex 0.159.2 (from npm) and 0.160.0, run in a sandbox HOME against a fake gateway, list magpie's models from /models in codex debug models and the app-server's model/list, and show only their own when the answer takes 6 s. codexModels now gives the backend 3 s (codexModelsWait); past that it answers from Codex's own models_cache.json and magpie's models, as it does when the backend can't be reached. A 401/403 from the backend is still passed on as it was. TestCodexModelListBackendSlow (a backend that never answers: magpie's and the cached models come back within 3 s) fails without the change. No request was made to OpenAI. 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. yetone 2026-10-02 16:47:16 +08:00
  • ffd3949dcb usage: the token total's line names the cache too, so in and out don't look like they should add up to it (John on Discord: 这个总量和输入+输出不相等,是bug么?) — the Usage card's Tokens total counts input, output, cache reads and cache writes (allTokens), but the line under it named only input and output, so 95M over "6.5M in · 785K out" looked wrong; with a 93% cache hit rate nearly all of it is cache reads. The line now ends "· {n} cached" (缓存 {n}) with the cache's reads and writes when there are any. panel-usage.test.cjs checks the cache is named under the total (en and zh); the extra line pushes the provider ranking down to the footer's edge in the test's 560px panel, where WebKit takes a half-hidden row as in view and clicks the footer, so the test scrolls it in with the wheel first, as a reader would. panel-usage, number-units, click-scroll, usage-ledger pass in WebKit. yetone 2026-10-02 16:42:46 +08:00
  • 93a1e3e7fc fix(davsync): detect a WebDAV write the server kept short (#531) 尔立andClaude Code 2026-10-02 16:42:37 +08:00
  • ba0b2d54bf routing: 新增路由选项「重置前用完」(pace)——按周额度剩余比例 ÷ 距重置小时数排序 (#517) にとりandClaude Fable 5.1 2026-10-02 01:15:20 -07:00
  • 9b1f9ac17e fix(gui): retain expanded usage request text across refreshes (#533) Aliceandraper 2026-10-02 16:15:15 +08:00
  • 55e7df478f codex: a group's effort for the turn keeps Codex's reasoning.context, and Codex's custom tools (code mode's exec, apply_patch) reach a model magpie translates for (#534, #520 follow-up, azir12345: since v0.1.637 a Plus account's gpt-6.1-sol fails with 400 "X-OpenAI-Internal-Codex-Responses-Lite requires reasoning.context to be all_turns", and Codex said it had only the collaboration and wait tools, no terminal) — ChatGPT's catalog marks gpt-6.1-sol (as gpt-6-sol, gpt-6-luna, gpt-5.6-*) use_responses_lite with tool_mode code_mode_only, and Codex asks for the bare slug with its native metadata: the X-OpenAI-Internal-Codex-Responses-Lite header (add_responses_lite_header, which magpie forwards with the other x-openai-* headers), reasoning {"effort","summary","context":"all_turns"} (build_reasoning), and its tools moved into an additional_tools item, every other tool reached through exec, a custom (freeform, lark grammar) tool in the "functions" namespace, beside wait and the collaboration namespace. magpie routes the bare id to the group auto-gpt-6-1-sol, and the effort the group picks for the turn went through withEffort, whose Responses branch rebuilt reasoning as {effort, summary} and dropped context, so ChatGPT refused the Lite request; it now sets only the effort (withBodyEffort), keeping every other field of reasoning as Codex sent it. A member served by translation (Chat, Anthropic, a Responses plugin) had parseResponses drop custom tools and ignore custom_tool_call / custom_tool_call_output items, so exec vanished and the model saw only wait and the collaboration tools; a custom tool (top-level or in a namespace) is now offered as a function taking one string "input", its description its own plus a note on input and its grammar, an earlier custom_tool_call goes to the model as a call with {"input":…} and its output as the call's result, and the model's call goes back to Codex as the custom_tool_call it runs (name and namespace as Codex knows them, input from the "input" argument, else the arguments as given), streamed without function_call_arguments events and in the non-streamed reply alike. This also gives translated models Codex's freeform apply_patch, which was dropped silently before. The Lite header is still forwarded as it was; nothing showed it needs dropping. Codex's base_instructions and identity line are untouched. No request was made to ChatGPT. TestWithEffort's Lite case fails without the decide.go change ("want \"context\":\"all_turns\""); TestCodexCustomToolsTranslated (a code-mode Lite turn through a fake Chat provider: exec offered with input and its grammar, the earlier call and output shown, the model's exec call back as a custom_tool_call with its input and no arguments) and TestCodexCustomToolNamespaced (namespaced and non-JSON calls, renderResponses) cover the tools; TestAdditionalToolsTranslated, TestNamespacedToolsOffered and TestGrokPluginGetsNamespacedTools now expect the custom tools offered. 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 on its wait, passes alone; untouched). yetone 2026-10-02 15:55:20 +08:00
  • 7116b583d8 providers: a deprecated built-in subscription not signed in to offers its plugin when clicked, instead of badging every tile (user: 已弃用的内置 subscription provider,如果没有安装的话,点击后引导安装,而不是一股脑地显示已弃用来污染视觉) — v0.1.628 put a Deprecated badge on every Add-sheet tile with a plugin (Zed, Devin, Qoder…, signed in or not) and a line under the grid explaining it. The badge now stays only where it means something: a subscription signed in through the built-in (its row, its tile, the notice over the list); a tile not signed in to has no badge, its tooltip saying it signs in through a community plugin, and the line under the grid is gone. Clicking that tile no longer starts magpie's own sign-in: a panel in its place (as the risk and site questions are) names the plugin package, says in a line why sign-ins are moving to plugins, and offers Install and sign in, with Use the built-in and Cancel. Install and sign in posts provider/adopt: provider.Adopt installs the plugin (to its mover's minimum version) and puts the subscription onto it as a move does (migrations.json state plugin, Installed when it installed it), with no accounts to carry; one that turns out to have accounts is moved (Move) instead, and a failure takes away a plugin it installed. The subscription is then the plugin's in place (its tile, name and icon as before, no second tile under From plugins, the plugin's provider id the built-in's), and its sign-in goes on through the plugin, the vendor's risk warning first as before; the panel shows a spinner while installing (the first plugin also fetches Bun) and the error, with Try again, if it fails. Moving back works as for a move with no accounts and takes away the plugin it installed. /api/providers carries movesTo (each deprecated built-in's plugin). Nothing was installed or signed in to for real. TestAdoptBeforeSigningIn (the fake plugin: adopted, installed once, provider id kept; back removes it; with an account it moves) and deprecated-subscriptions.test.cjs (Chromium and WebKit, en and zh: no badge on Zed or Claude, Cursor and Kiro badged, the offer, Use the built-in reaching the risk question, Install and sign in → adopt → the plugin's sign-in, no From plugins section) fail without the change; TestMoveOutlivesRequest covers adopt. mobile-web, plugin-move, plugin-move-overflow, plugin-progressive, plugin-movable-review, add-sheet, add-button, plugin-signin, plugin-moved-signin, claude-risk and provider-search pass; old-webkit's one failure (app.css:~388 :focus-visible) is on main already. 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 race, passes on the rerun; untouched). yetone 2026-10-02 15:56:24 +08:00
  • 6835e8d297 sessions: every session of a project folder can be deleted at once, from the folder's row (#527: 有些codex会话多,有些特别老的项目,希望能直接删除整个项目文件夹下所有会话) — the Sessions page lists an agent's sessions by the folder they ran in, but deleting a folder's took picking each one or the per-row trash one by one. Each folder's row now has Delete all (全部删除, titled "Delete every session in this folder" / 删除这个文件夹下的全部会话; the label folds to its icon under 420px) where magpie can delete the agent's sessions. It opens the same dialog of magpie's own as a delete of picked sessions (never confirm()), headed "Delete all {n} sessions in {folder}?" (删除 {folder} 下的全部 {n} 个会话?) with the folder's path under it, the first titles and how many more, and the note that they go to magpie's trash; the confirm posts sessions/delete with every id of the folder, the path a single delete takes, so each goes to magpie's trash as one deleted alone does (Trash restores it) and one written to in the last minute is left and said so. A filter hiding some of the folder's sessions doesn't keep them: the button is the folder's, and the dialog says how many. A folder of one asks as a single delete does; an agent whose sessions magpie can't delete (OpenCode) has no button; the click doesn't fold the folder or move the page. Deleting 200 of 600 sessions one id at a time took about 0.5 s in a sandbox, so the route is unchanged and there is no Go change. The CLI's magpie sessions only lists sessions and what they cost, with no delete, trash or restore, so no folder delete was added there: deleting from a terminal with nothing to put it back would be a new, one-way thing. sessions-folder-delete.test.cjs (new; WebKit, en and zh): the button on every folder in its row, Cancel posting nothing, a filtered folder's Delete all posting all fourteen ids, a folded folder's two posted and the folder gone with the others kept, the running one left and said so, a folder of one asking as one, none for OpenCode, no click moving the page, no left-border accent, every string in Chinese, no browser dialog; it fails without the change (no Delete all). sessions-manage, sessions-purge, click-scroll, sessions-toolbar, session-terminal, sessions-bar-names and sessions-overview pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFXInState and TestUserPath failed once under load, pass on the rerun; untouched). yetone 2026-10-02 15:40:40 +08:00
  • 94af7b6d1d gui: a sign-in for another account left unfinished can be closed, from beside Open again or with the editor (#526: 当我点击添加第二个账号,但最后没添加时,这个请在浏览器中完成登录的窗口状态不会释放,无论我点击底部的移除、取消、保存都没办法。希望在"重新打开"按钮旁边再添加一个"关闭"按钮) — the waiting box did have a Cancel, at its right, but the account list (.accts) is a grid with an auto column, so Qoder's device link, one long unbroken line, widened the box past the list, which clips: the link ran off the edge with its copy button, and Cancel with it, out of sight (as in the issue's screenshot; the "In use" pill of the account above went too). The list's one column is now minmax(0, 1fr), so the link is cut short with an ellipsis inside the box, as .signlink meant it to be, and Cancel and the copy button are in sight. Beside Open again there is now Close (关闭, titled "Stop waiting for this sign-in" / 不再等待这次登录), which drops the sign-in as Cancel does: signin/<id>/cancel is posted, the polling stops, and Add another is back. And the editor's footer no longer leaves a waiting sign-in behind: Cancel, Escape or a click outside (cancelEdit) and a Save or Remove that went through (providerAction) cancel a sign-in of the edited provider's account, so the editor opened again is as it was, not still waiting; an import being read is left to finish, and leaving the Providers page (a tab switch) keeps the sign-in going, as before. signin-close.test.cjs (new; WebKit, en and zh, at the issue's 970x845): the box and its buttons inside the account list, Close beside Open again, Close and the box's Cancel each posting cancel and stopping the polling without moving the page, the editor's Cancel and Save taking the sign-in with them, and no other POST; it fails without the change (Cancel and the copy button outside the list; no Close). signin-link, signin-again, signin-callback, signin-closes-sheet, signin-paste-codex, signin-paste-key, plugin-signin, plugin-moved-signin, routing-signin, login-import, account-return, provider-remove, own-signin-remove, saved-signedout-remove and click-scroll pass in WebKit (one signin-callback case launches Chromium alone, which can't start here without its ICU data). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFXInState, TestUserPath and TestCodexVersion failed once under load, pass on the rerun; untouched). yetone 2026-10-02 15:30:33 +08:00
  • 9e5c778dc1 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. yetone 2026-10-02 15:23:44 +08:00
  • 817a5f50d5 routing: In order keeps sending to the first account until it is used up or refused, and a tick keeps Codex or Claude Code signed in to the first account (#530: an account In order at 98% of its week, its five hours untouched, stopped getting requests; #524: "不允许切换首选账号的登录状态", the agent's own sign-in must stay on the account made first while the gateway still spreads requests) — since #209 magpie signs Codex and Claude Code in to the next ticked account with room once the one they are on reaches 98% (provider.SpentShare, the share Smart counts an account spent at), and since #408 back once the first is under 90%. The account the agent is signed in to is the one every routing tries first (the others stand behind it, AlsoOn), so In order the 98% switch put the second account ahead of the first, which is what #530 saw as the first being dropped; and the gateway counted an account at 98% as full whatever the routing, so a passing failure on it (a 502) rested it until its week renewed (39 hours there), not for the usual backoff. Now SpentShareOf gives 100 In order and 98 otherwise: the login switch moves the agent off an account In order only once a window of it is used up, the gateway's full (the rest after a failure, the quota rest's window) and affinity's "spent" go by the same share, and a vendor's 429 or usage-limit refusal still rests the account until it says it's back, as Smart does; a window used up is out, as before. Smart, In turn and Least used are unchanged. For #524, the subscription keeps KeepLogin (providers.json, beside Routing, kept across an editor Save; POST provider/keeplogin, only for Codex and Claude Code): with it on, magpie never moves the agent off the account it is signed in to, however used up, while the gateway still routes requests through magpie to the other ticked accounts; a pending return to the first (#408) still happens, that being the first. The tick sits at the foot of the Accounts list, under the rows where Make first is, shown only for Codex and Claude Code with more than one account on: "Keep {agent} signed in to the first account" / "{agent} 始终登录首选账号", its tooltip saying requests through magpie still go to the other ticked accounts. The Routing note follows it: kept, it says the agent stays signed in to the first whatever it has left; In order, that magpie moves it on once it is used up; otherwise the 98% wording as before. TestCodexLoginOrderedAndKept (In order stays at 98% and moves at 100%, back once renewed; Smart moves at 98%; kept, it stays at 100% and the setting survives a SetRouting; let go, it moves) and TestOrderedKeepsAnAccountAt98 (In order an account at 98% isn't full and a 502 rests it by backoff, Smart rests it to its week, a refusal rests it either way) fail without the change. keep-login.test.cjs (en and zh, Chromium and WebKit: the tick, its tooltip and Chinese strings, the post, the note when kept and In order, no tick with one account, the page doesn't move) fails without the UI and passes with it (12/12, Playwright 1.61); account-return, routing-account-order and account-arrange pass; click-scroll fails in Chromium here on origin/main too and passes in WebKit. No request was made to any vendor and no live agent config was touched. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves, TestClaudeAccountNamedAsItsLogin and TestClaudeIdentityServedWhileAsked failed once under the full run and pass alone). Turned on after magpie has moved the agent off the first, it goes back to it at once rather than once that has room again. yetone 2026-10-02 15:34:45 +08:00
  • d5322cfbf1 agents: dsh's route is written again while magpie serves, not only when magpie starts (#528) Snowyy 2026-10-02 15:37:07 +08:00
  • f7f6741218 test: a test's sandbox keeps Qoder's folder out, so no test reads the developer's real sessions (#522) Lin Huazhong 2026-10-02 15:34:11 +08:00
  • 9a7219f063 tests: the sessions sandboxes put the zone back after the index is written (#529) Snowyy 2026-10-02 15:34:04 +08:00
  • 64922e1810 provider: moving Devin to its plugin no longer fails over the effort variants picked before Devin's families were one model — the move's check named every pick the plugin doesn't list as one it doesn't serve, and a Devin account picked from before the fold (swe-2-high, claude-opus-5-5-medium, claude-opus-5-5-low-fast…, which the editor shows folded into SWE-2 and Claude Opus 5.5) failed with nineteen of them, "Adaptive" twice. The plugin serves those as the built-in does: the plugin host sends a model it doesn't list under its own id, and the Devin plugin passes a variant's id to Devin as it is (variantFor), as it keeps a picked variant in its list. A mover may now say the plugin serves a pick it doesn't list (served): Devin's is a variant of a model the plugin lists (devinBase strips the tier and effort words: claude-opus-5-5-low-fast → claude-opus-5-5), and Adaptive and Fusion, which the built-in never served either (Devin's API doesn't take them). A variant of a model the plugin doesn't list still stops the move. The names an unserved move gives are each named once. TestDevinMoveKeepsVariants and TestMoveToPlugin's served-variant case fail without the change. No real move was run (it would move the live Devin account). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gui/ ./internal/plugin/ pass (provider flaked once under load, passes again). yetone 2026-10-02 15:29:24 +08:00
  • 901fd87ec0 zcode: a Start Plan request goes as the OpenCode plugin ARNO sent sends it, the one zcode.z.ai still serves (ARNO on Discord: "0.1.633版本还是不行,我给的插件还是成功的") — 11be3f31 followed ZCode 3.14.3's CLI source and some Start Plan requests were still turned away with 405 / code 3012 "request has been blocked due to unusual activity", while ARNO's plugin ("Freeflow for OpenCode and magpie", provider zcode-start), which ARNO sent us, works on the same machine and account. Where the plugin and 11be3f31 differ, the plugin now wins: the stable system block is the identity section and then the "# ZCode Desktop Context" section joined by "\n\n" (11be3f31 had dropped the desktop section; it is back from 31568cb0's text as zcode_prompt.json's desktop); the powered-by line names zai-api / bigmodel-api (was account:zai-start-plan / account:bigmodel-start-plan); the environment's working directory is the one the agent's own prompt names (Claude Code's "Primary working directory:", Codex's <cwd>), else the home directory (was "unknown"), and its shell is the basename of $SHELL or %ComSpec% (cmd.exe on Windows, was "cmd"); the date reminder is a user turn of its own before the agent's turns, always, its tags hugging the text ("<system-reminder>As you answer…task.</system-reminder>", no newlines; was put into the first user turn with newlines, and left out when that turn already had a <system-reminder>, as Claude Code's always does); every cache mark in the turns goes and the last block of the last turn gets one (a string turn becomes a text block), where the agent's last mark used to be kept wherever it was; the agent's system goes as plain text blocks (non-text blocks and empty text dropped); metadata.user_id is {"device_id":<this machine's device id>,"account_uuid":"","session_id":""} as JSON, the rest of metadata kept; X-Os-Version is "<platform> <release> <arch>" (was the release alone); X-Client-Timezone is always sent (TZ, /etc/localtime, else a zone at the local offset, Asia/Shanghai at +8); X-Client-Language falls back to the Mac's AppleLocale or Windows' display language, then en-US as Node's Intl does, so magpie opened from Finder with no LANG no longer says en-US for a zh-CN user; HTTP-Referer is https://zcode.z.ai as written; anthropic-version is 2023-06-01 and the agent's anthropic-beta (claude-code-20250219…) is no longer passed on. Unchanged and already the plugin's: the opening line block, the three cached blocks, User-Agent ZCode/3.14.3 ai-sdk/anthropic/3.0.81, X-ZCode-App-Version, X-Title Z Code@cli, X-ZCode-Agent glm, X-Release-Channel production, X-Platform, X-Os-Category, fresh x-request-id and x-zcode-trace-id, x-zcode-session-type main, the JWT as a Bearer, no x-api-key or X-Device-Mid, tools without cache marks. The prompt texts' SHA-256 (first 8 bytes) all match the plugin's: cliPrefix 46dd360a22c87a92, stableSections[0] 3f21ff9a88a03a76, [1] 39a1e86c93452c01, joined 49bda31511fc9670, beforeEnvironment bbdc66b399d297ff, afterEnvironment 1732b7f098a925d7. The agent's thinking and other body fields go as sent. No request was made to zcode.z.ai. TestZCodeStartPromptIsThePlugins (the hashes) and TestZCodeStartSentAsTheApp (headers, the exact environment section, the context prefix turn, the cache marks, metadata.user_id, BigModel's provider, a string last turn) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 15:09:57 +08:00
  • 7b4065b12a sync: the order the providers were put in on the Providers tab goes with them, so a computer that syncs lists and tries them as the other does (ARNO on Discord: 发现webdav同步的时候没有同步provider的顺序) — since #499 the order is kept apart from the providers, as providers.json's "order" (ids, the built-in signed-in accounts among them), and the list is sorted by it when read; the providers themselves stay in the file in the order they were added. The sync's bundle (backup.Bundle) carried the providers, icons, groups and search APIs but not that order, and the providers part was hashed without it, so arranging the list was not even a change to sync, and a computer bringing the providers in (provider.Mirror) kept its own order, or none. The bundle now has Order (omitempty, from provider.StoredOrder), hashed with the providers part only when there is one, so the hashes of a setup never arranged are as before and an older magpie's file still compares the same; take carries it with the providers, and bring applies it after Mirror with the new provider.MirrorOrder: the other computer's ids first as it has them, then the ones named here alone (an account only this computer has) in the order they have here; none, from a magpie before this or a setup never arranged, leaves the order here as it is. This goes for WebDAV and S3 alike (one path). A backup restored by hand still merges providers without changing the order. TestSyncProviderOrder (a arranges three providers, b joins and lists them in a's order and pushes nothing back, b arranging only is pushed and a follows; against an httptest WebDAV server) fails without the change ("b after joining: [one two three]"); TestMirrorOrder covers the merge. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... (temp HOME) pass. yetone 2026-10-02 15:00:20 +08:00
  • bb5e8a2b61 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. yetone 2026-10-02 14:57:48 +08:00
  • e0121f71b9 Preserve dated Claude usage when allowance is unavailable (#511) SadWood 2026-10-02 14:46:42 +08:00
  • 31d904d938 agents: Pencil (pen.dev) is an agent magpie sets up, its custom models in ~/.pencil/models.json (徐小小 on Discord: Pencil keeps its custom model file at ~/.pencil/models.json, in Pi's format) — Pencil's settings take a custom provider on OpenAI Completions, OpenAI Responses or Anthropic Messages (docs.pen.dev, Authentication), Pi's openai-completions, openai-responses and anthropic-messages, set for the provider as a whole, and its models.json is Pi's: {"providers":{"<id>":{"name","baseUrl","api","models":[{"id","name","reasoning","input","cost":{input,output,cacheRead,cacheWrite},"contextWindow","maxTokens","compat"}]}}}. The new pencil agent (pencil.go) adds magpie there as the provider "magpie": baseUrl the gateway's /v1, api openai-completions, apiKey magpie-pencil (its User-Agent isn't known, so its requests are told by the key), and every model as Pi's entry — Pi's model loop is now piModelJSON, shared — without a model's own api and baseUrl, which Pi reads but Pencil's own editor never writes, so every model goes to the gateway's Chat endpoint whatever Pencil makes of them; each model carries every field Pencil writes for one, Pi's defaults (128K window, 16K out) where magpie knows none, no cost and an empty compat. The model is picked in Pencil's agent composer, not in a file known to magpie, so its field is "provider", as for WorkBuddy and ZCode: whether magpie's models are in Pencil's picker; the user's other providers, their keys and Pencil's other keys stay, off takes only magpie's out, and the catalog sync rewrites it only where it is. It is found by ~/.pencil (the app's and pen.dev's CLI's) or, on a Mac, Pencil.app / pen.dev.app in /Applications or ~/Applications; not by a command, pencil and pen being common names. After a change it asks for Pencil to be restarted when Pencil is running (and always on Windows, where that can't be told): whether it reloads models.json on its own isn't known. No official icon with a licence that allows it was found, so it has the generic one. Nothing of Pencil's was downloaded or run; whether Pencil reads a model's thinkingLevelMap, a settings file naming a default model, and its paths on Windows and Linux (taken as ~/.pencil, as Node's homedir gives) aren't verified. TestPencilModels (the reporter's file with a key and a key of Pencil's: kept through on, sync and off; magpie's provider decoded strictly as Pencil's format, every model's fields of their types, no per-model api or baseUrl; found only with ~/.pencil; among All()) and TestStateListsPencil (the page's state lists Pencil with its provider field once ~/.pencil is there, not before) fail without the change. 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 (TestClaudeSignedOut failed once on its 5 s wait, passes alone; untouched). yetone 2026-10-02 14:39:48 +08:00
  • fff1607a30 portable: a portable magpie on Windows keeps WebView2's profile in data\webview2 and makes no Start-menu shortcut, and an installed one makes the shortcut once and never puts back one the user deleted (#508, suixinex: 便携模式时还是会创建 C:\Users\<username>\AppData\Roaming\magpie.exe\ 文件夹; 开始菜单的快捷方式删除后再打开magpie会重新添加) — the AppData\Roaming\magpie.exe folder is WebView2's user data folder: Wails v3 passes WindowsOptions.WebviewUserDataPath to go-webview2 as its DataPath, and left empty it is %APPDATA%\<exe name>, which portable mode (appdir) never set. The app's Windows options (windowsOptions) now give it appdir.WebView(): data\webview2 when portable, "" (WebView2's default, so the pages' storage and cookies there are kept) when installed. The shortcut came back because shortcut.Ensure ran at every start of the desktop app and wrote Magpie.lnk whenever it was missing or stale, with nothing recording that it had been made. Ensure now does nothing when portable (no shortcut, no App Paths entry); installed, it makes the shortcut on the first start and records that in start-menu-shortcut in magpie's folder (with the exe), keeps one that is still there pointed at the exe (moved exe, update), and makes none once the file is recorded and the shortcut is gone. A user who deleted it under an older magpie isn't given it back on updating either: a missing shortcut with no record but an App Paths\magpie.exe entry already there (every older magpie wrote it at each start) counts as deleted and is recorded as such. The Start menu, the shortcut writer and App Paths are behind function variables, so the decision is tested with fakes on any OS. Checked for other leaks: autostart only refreshes an entry the user turned on (Refresh never enables), the magpie:// link registration is needed for import links and stays, as appdir's doc says, as do temp folders; the update stages beside the exe, caches already follow appdir. Not changed: the Mac's WKWebView and Linux's WebKitGTK keep their own data in the profile (Wails has no option for it), which #508 didn't report. Nothing ran on Windows (the test box isn't reachable): no real WebView2 was started with the data path and no real shortcut was made or deleted. TestEnsureOnce, TestEnsureAfterOlderMagpie, TestEnsurePortable (fake Start menu in a temp folder), TestWebView and TestWindowsOptionsWebViewData fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet ./..., GOOS=windows go test -c for shortcut, gui and appdir, go test -tags nogui -count=1 ./... under a temp HOME pass (TestUsageKeepsTimeToFirstToken and TestUserPath failed once on timing, pass rerun alone; untouched). yetone 2026-10-02 14:39:28 +08:00
  • 0eb16d4228 gateway: log when a Claude Code run is let go because earlier messages changed (#521) geoqiao 2026-10-02 14:32:26 +08:00
  • 87fc534a0a fix(gui): reload requests after clearing unavailable usage filters (#513) Aliceandraper 2026-10-02 14:25:21 +08:00
  • a6b9a7517d 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). yetone 2026-10-02 14:19:41 +08:00
  • dd70a4055a wsl: OpenCode, Kimi Code, omp and the other CLIs magpie sets up through a file in the home are set up in WSL distros too (#515: on Windows only Codex, Pi, OmO and Claude Code were found in a distro) — fourteen agents join wslKinds, each found by its folder under the distro's $HOME or its command on the distro's own PATH, and listed as <name> · WSL <distro> (opencode@wsl:Ubuntu…): OpenCode (~/.config/opencode, opencode), MiMo Code (~/.config/mimocode, mimo), Kimi Code (~/.kimi-code, kimi; the old kimi-cli's ~/.kimi where that is all there is, found by the command), omp (~/.omp/agent, omp), Crush (~/.config/crush/crush.json, Linux's place, not Windows' LOCALAPPDATA), Hermes Agent (~/.hermes), Grok Build (~/.grok, by its folder alone, as on this machine), Droid (~/.factory), fx (~/.fx, by its folder alone: fx is also the JSON viewer), Command Code (~/.commandcode), MiniMax Code's mcode (~/.minimax), Muse Code (~/.config/muse), Qoder and Qoder CN (~/.qoder, ~/.qoder-cn). Each is its host constructor at a place (opencodeIn, kimiIn, ompIn, crushIn, hermesIn, grokIn, droidIn, fxIn, commandCodeIn, miniMaxIn, museIn, qoderIn/qoderCNIn; the host's opencode(home, cfg) and the rest wrap them), so it writes the same keys and schema as on this machine, but the gateway as the distro reaches it (place.gw/v1: 127.0.0.1 when mirrored, Windows' address under NAT) in its provider, its Check and its Sync, and its folder at the default, as the distro's variables (OPENCODE_CONFIG_DIR, KIMI_CODE_HOME, HERMES_HOME, FACTORY_HOME_OVERRIDE, MINIMAX_DATA_DIR, QODER_CONFIG_DIR…) aren't read (place.getenv is "" there); OpenChamber, a Windows app, isn't looked at for a distro's OpenCode, and omp there, whose version isn't known, gets xhigh rather than a max an older omp refuses. Their notices are the restart advice of their own, as for Codex. A stopped distro's agent is built without looking at its files (place.cold: exists/isDir are false, so the defaults are taken), its options read none (wslOwnAsleep, kimiOwnOptions/ompOwnOptions/miniMaxOwnOptions without a file), and a pick, once the distro is started, builds the agent again warm, so it writes the file the agent reads — an opencode.jsonc already there, ~/.kimi for kimi-cli — not the default. Left out: Gemini CLI and Antigravity (a key lent through .env, a launch command), dsh (profiles and .env), Devin (no gateway; its model list from the devin command on this machine), Goose (no magpie routing; its name is pressly's goose too), Cursor and Copilot (no magpie routing), and the apps (Cline, Hanako, WorkBuddy, ZCode, Claude Desktop, OpenChamber, Alma, Cindy); and the library's skills, MCP and instructions for the new WSL agents, which wslTargetOf still gives only Claude Code, Codex, Pi and OmO. Nothing ran on Windows or in a real WSL. wsl_more_test.go, on fake distro roots under temp dirs with the host's variables pointed elsewhere: TestWSLProbeFindsMoreAgents (each folder and command in the probe, none for grok and fx, kimi-cli by its command), TestWSLMoreAgentsDiscovered (all fourteen listed, no command, UA or aliases), TestWSLMoreAgentsPick (each agent, mirrored and NAT: its file in the distro names the distro's gateway, no 127.0.0.1 under NAT, Check passes, Sync keeps it, the notice; this machine's home and the folders its variables name untouched), TestWSLMoreAgentsStopped (nothing asked nor opened, the last value shown, options, a pick starts the distro and writes there) fail without the change; TestWSLStoppedPickFindsItsFiles fails without the warm rebuild (the pick wrote a second opencode.json beside the .jsonc). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet ./internal/..., GOOS=windows go test -c ./internal/agent/ ./internal/library/, and go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup race, passes on the rerun; untouched). yetone 2026-10-02 14:15:13 +08:00
  • cc9aecc7de gui: Claude Code's folded picker (#496) heads Claude Code once, leaves the keys to the filter after a click, shows what a query finds and keeps a starred dated id; a WSL distro's Claude Code folds nothing (#518) hisilingandClaude Opus 5.5 2026-10-02 00:16:14 -06:00
  • 0ccf987880 web: the page asks for its scripts and styles by their content's hash, so a cache in front of magpie web can't run an older version's app.js under a newer page (Docker user on Discord, v0.1.630 behind an HTTPS proxy: tabs shown, page blank, clicks dead; hard refresh and incognito didn't help) — the console's errors were v0.1.582's, not v0.1.630's: "Cannot read properties of null (reading 'firstElementChild') at app.js:6606" is v0.1.582's $("#moreSubs").firstElementChild (#moreSubs left index.html in v0.1.584), the GET /wails/runtime.js 404 is the unconditional import that v0.1.587 dropped in a browser, and "Cannot access 'pinOf' before initialization at keepInView (app.js:10966) at routing.js:2829/2850" is v0.1.582's keepInView and routing.js top-level IIFE, reached after app.js had stopped at its first throw and left pinOf in its TDZ. So the page was new and app.js/routing.js were old; with a private window doing the same, the old files came from a cache between the browser and magpie (the proxy or a CDN), which kept them from before magpie sent no-cache and an ETag (the files had no validators then, so it had nothing to ask again by). The served index.html now names each of its own .js and .css files as <file>?v=<first 6 bytes of its sha256, hex> (boot.js, which the API writes each time, keeps its name and no-store); the page's ETag is of the page as sent. A new version's page therefore asks for addresses no cache has seen. Dev builds serve their own page and are unchanged. Run: TestPageNamesItsFilesByContent (each script/style named by its hash and served at that address, boot.js unchanged, the page 304s on its ETag) fails without the change; TestPageFilesRevalidate still passes. A rebuilt magpie web under a sandbox HOME served the versioned page and loaded with no page errors in WebKit (Playwright). Fresh, populated, zh, non-loopback and web:false states, and the v0.1.633 Docker image, all rendered; the blank page was reproduced only by an app.js that stops at its top level. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gui/ pass. yetone 2026-10-02 14:14:10 +08:00
  • 5d7babcddd claude: a Claude Code /logout takes only the account it was signed in to, and that one is never served in its place (StringKe on Discord: Claude-Code -> Magpie -> Claude Subscription 的这个流程现在顺畅吗,昨天 /logout 之后导致六个账号全部都要重新登录) — before 41d50e15 (v0.1.588) a logout left magpie no Claude account at all and told the user to sign in again; since then the account seen last stands in. That one is the account Claude Code was signed in to, and Claude Code's /logout revokes the refresh token it holds (2.1.287's performLogout: POST <TOKEN_URL>/revoke with claudeAiOauth.refreshToken, best effort, before it deletes its keychain item): magpie's copy of it in logins.json, bookmarked every 30s, is that same sign-in, so the stand-in served first was a revoked one and every request paid a failed Claude Code run before moving on. The saved accounts in config directories of their own are sign-ins of their own and outlive the logout. rememberLogins now marks the account Claude Code holds (savedLogin.Held, cleared on the others); when Claude Code is next seen signed out, that one is marked lapsed ("Claude Code's /logout signed it out (it revokes the sign-in it holds); sign in again") and the stand-in is picked among the others — never the held or logged-out one, one with a saved sign-in, on before off, then the one seen last. A switch in magpie isn't taken for a logout (the account switched off is saved as it was), and a /login in Claude Code to the same account clears the lapse. TestClaudeAccountsOutliveLogout (now: the revoked account lapsed and never put in a config directory, the one on standing in, another on behind it) and TestClaudeSignedOut fail without the change; TestClaudeSwitchIsNoLogout guards the switch. No sign-in was made and nothing was asked of Anthropic. 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. yetone 2026-10-02 14:10:27 +08:00
  • 3b3f5fa726 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). yetone 2026-10-02 14:10:22 +08:00
  • 690c27b8b4 gateway: a ChatGPT account whose plan doesn't take the reasoning effort asked for hands the request to another account, and isn't asked for it again (#520: 在同时登录plus和免费账户时,如果将免费账户的优先级设置到最高,此时会错误的将6-luna的high路由给免费账户,但是免费账户不提供high及以上的思考强度,返回400错误) — accounts of one subscription were picked by their order alone, with nothing of what each plan takes at what reasoning level: perKeyBarred puts an account whose plan lacks the model after the others (Account.Lists), but a Free account that lists gpt-6-luna and turns high and above away was asked first, and its 400 ("Unsupported value: 'high' is not supported with the 'gpt-6-luna' model. Supported values are: …", param reasoning.effort, code unsupported_value) either reached the agent, or — where retryable's unserved words happened to match "invalid_request_error" — set the Free account aside for a minute for every effort. Now (effort_accounts.go): an account candidate known not to take the level it would be sent — it turned that level away within the hour, or its own model list (Account.Levels, the account's supported_reasoning_levels) gives the model levels without it — is moved behind the next account of the same member that may take it; an account whose 4xx is that refusal (effortRefused: the "Unsupported value: '<level>'" form for the level sent, or one naming reasoning.effort; another field's value such as text.verbosity's 'high' isn't one), of a level the model has, is noted, doesn't rest, and the next is asked (failEffort, "effort" in the routing trace, its matesFirst); when no account left takes it, the agent gets a 400 saying which account doesn't take which effort on which model, the levels it takes and the vendor's words, never the effort lowered behind its back. The codex provider's levels for a model are now its on accounts' together (codexPoolLevels), so a Free account signed in to Codex no longer has a Plus one's high lowered to medium. A request at fault otherwise (a 400 over its input) still goes to the agent, unchanged. Codex's prompts are untouched. TestEffortRefusedGoesToAnotherAccount (Free first: high goes Free then Plus, then straight to Plus, xhigh too; medium is the Free one's; a bad input is not sent on), TestEffortTheAccountListsLacks (Free signed in, its list without high: high goes to Plus alone, low to Free, with Plus out of quota the Free one's refusal is told as such) fail without the change; TestEffortRefusedWords. No request was made to OpenAI. 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, except internal/proc's TestUserPath (the login shell's PATH not coming within 5s under the temp HOME; passes under the real one; untouched). yetone 2026-10-02 14:08:23 +08:00
  • bb136e1b70 providers: a Kilo Gateway preset serves Kilo Code's free models with no key (lml on Discord: the community plugin's free models come from OpenCode Zen, KiloCode Gateway and Cline) — Kilo's gateway is OpenRouter's API at https://api.kilo.ai/api/openrouter (KILO_OPENROUTER_BASE in Kilo-Org/kilocode packages/kilo-gateway; /api/gateway is the same), its list GET {base}/models in OpenRouter's shape with an isFree flag, readable with no key, and its docs (kilo-docs gateway/authentication.md) and backend (Kilo-Org/cloud web-shared ai-gateway/handlers/llm-proxy.ts, is-free-model.ts: kilo-auto/free, ":free", openrouter/free, stealth/*-alpha, Kilo's own free models) serve a free model to a caller with no credential, by IP, 200 requests an hour. The new "kilo" preset (Kilo Gateway, NoKey) is Ready with no key: its list is the gateway's own, asked as the Kilo CLI asks it, the free models alone without a key and every model with one, the free ones (isFree, or free by id) marked Free and priced at nothing in ListPrice; a model that says it takes no tools, or that makes no text (Lyria), is left out. Requests to Kilo's host (the gateway's and the Test button's) carry the Kilo CLI 7.8.3's headers (packages/opencode session/llm/request.ts, kilocode/const.ts, kilo-gateway headers.ts): HTTP-Referer https://kilocode.ai, X-Title Kilo Code, User-Agent Kilo-Code/7.8.3, X-KILOCODE-EDITORNAME "Kilo CLI 7.8.3", and on a chat x-kilocode-mode code with the conversation as its task (X-KILOCODE-TASKID, x-session-affinity, X-Session-Id, a ses_ id as for OpenCode), which the gateway keys its prompt cache on; no other provider gets them. Without a key no Authorization is sent: Kilo's clients send "Bearer anonymous", which the gateway reads as no credential only until no released client sends it (tokens.ts ANONYMOUS_BEARER_SENTINEL, "Delete this constant once no released client sends it"), and any other bearer it can't verify is refused with 401 rather than served as anonymous; pi-freeflow measured that a placeholder bearer gets 401 INVALID_TOKEN and no Authorization gets 200 on every free model. A key, when given, goes as a Bearer and reaches the paid models. The key field can now say more than "optional for local servers" (PresetDef.KeyHint: "optional: free models need no key", 可不填:免费模型无需密钥); Kilo's icon is its own (kilocode packages/ui provider/kilo.svg, MIT). Only Chat is set; Kilo's Responses and Anthropic routes are not used. One GET of the public /models was made while looking (399 models, 18 free); no chat request was made to Kilo. TestKiloModels (a fake gateway: asked as the CLI with no Authorization, the free models alone without a key and all with one, a model free by its flag priced at nothing), TestKiloClient and TestKiloClientHeaders (the CLI's headers and no Authorization on a keyless request through the gateway, the key on a keyed one, none of them on another provider) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/... pass (TestClaudeRunEndsWhenTheClientLeaves, TestUserPath and TestClaudeSignedOut failed once under the full run's load, pass on the rerun; none touches Kilo). yetone 2026-10-02 14:02:47 +08:00
  • a72ee0a5fe providers: the concurrency field is styled as the editor's other fields, and its label fits on one line (ARNO on Discord: 最大并发请求的ui样式错了, a screenshot in the dark theme) — the editor styled its text, password and url inputs, and the Max concurrent requests field is a number input, so it was drawn as the browser's own: a white box with spin buttons in the dark theme, and "最大并发请求数" ran to two lines in the editor's 72px label column. A number input in the editor now takes the same rule as a text one (height, padding, radius, border, background) with no spin buttons, the field keeps its 12em width (its selector now outweighs the shared width: 100%), and the label is "Concurrency" / "并发上限" (its error "Concurrency: a whole number from 0 to 1000" / "并发上限:请填写 0 到 1000 之间的整数"); what it does is still the hint under it. provider-concurrency.test.cjs has a case in the dark and the light theme (en and zh, Chromium and WebKit): the field's background, border, radius, height and colour are a text field's, it is wider than 100px, and its label is one line high; the dark case fails without the change (styled as a text field). The file's other cases pass (20/20, Chromium through a temp copy on plain chromium.launch()). yetone 2026-10-02 14:03:31 +08:00
  • a40ed48aea 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. yetone 2026-10-02 13:48:51 +08:00
  • 987d72c133 providers: Max concurrent requests — over it, an agent's requests wait in line and go out in the order they came (Lemon on Discord: 希望 provider 或者插件能够支持一个参数类似 max_concurrency,如果超过上限,放到无界队列里面依序请求; Memo: ChatGPT/Codex subscriptions' risk control trips at about 5-6 requests at once) — the gateway sent every request to its provider the moment it came, so a few agents (or one agent's subagents) on one subscription could have any number out at the vendor at once, and nothing in magpie could hold them to what an account takes. A provider now has maxConcurrency (providers.json; nil or 0 is no limit, a negative one is saved as 0), and a signed-in agent's accounts keep theirs with their picks. The limit is per key or account, as the gateway already tells them apart (candidate.who(): provider#keyid, provider@user, else the provider), so a subscription pooling several accounts lets each one have that many out. Before a try is forwarded it takes a slot in that lane (internal/gateway/concurrency.go); with none free it waits in a FIFO queue with no bound, and is let in, first come first served, as one before it ends. The slot is held for the whole try (attempt reads the vendor's reply to its end or until the agent hangs up, and its body is closed when it returns) and given back once whatever happens (sync.Once), and a lane with nothing out and no one waiting is dropped. An agent that hangs up while waiting leaves the queue at once, is never sent and takes no slot (a slot granted in the same instant is handed on), and goes the way of any canceled request (499, nothing recorded). Waiting is not a failure: a request in line stays for its provider and is not moved on to a fallback or another group member, the account isn't rested, and the time it waited is told in its route's try (Try.Queued, ms). A plugin can say how many each of its accounts takes, as its own field: the auth hook's maxConcurrency, else package.json's magpie.maxConcurrency (a whole number over 0, at most 1000); the user's setting on the provider goes over it. GET /v1/magpie/concurrency (local or shared callers, as quotas) gives each lane's out, waiting and limit. The provider editor (keys, custom, signed-in accounts) has "Max concurrent requests" / 最大并发请求数 under Proxy, "超出时排队,按顺序发出;0 或留空为不限制" below it (with a plugin's own number as its placeholder), refuses anything but a whole number from 0 to 1000 before anything is posted, and an empty field saves none (the plugin's, if it has one); a save that doesn't send the field keeps what the provider had. No coloured left border, and a click in the field leaves the page where it is. TestMaxConcurrency (limit 2, five streams: never more than 2 at the vendor, 3 waiting, let in a..e in order; one canceled while waiting leaves the queue, takes no slot and never reaches the vendor; slots given back when each stream ends and when an agent hangs up mid-stream; the next one served at once) fails without the change (all five reach the vendor at once, no lane), as do TestLanesLimitLifted, TestProviderConcurrency and the updated TestPluginOwnIconAndKeyHint (the auth hook's 3, package.json's 5 when the hook's is 2.5); provider-concurrency.test.cjs (chromium and webkit, en and zh: typed and saved, 2.5 refused with nothing posted, empty saved as null, an account opening on its 5 and saving 0, a plugin's 4 as the placeholder, every string translated, no stripe, no scroll on click) passes. No vendor was asked. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... (under a temp HOME) and go test -race on the new tests pass. provider-proxy.test.cjs's account test fails the same on HEAD without the change (its editor redrawn under it). yetone 2026-10-02 13:36:03 +08:00
  • bb304cc8c0 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. yetone 2026-10-02 13:34:46 +08:00
  • 965846e674 gateway: a DeepSeek provider searches by itself on its Responses API (#512, EthanCaol: api.deepseek.com/v1/responses takes the hosted web_search tool, but magpie dropped it) — searchHosts had no api.deepseek.com, so searchesItself said a DeepSeek provider couldn't search anywhere: a request offering web_search was translated off DeepSeek's Responses API (the preset has Chat and Responses at https://api.deepseek.com/v1, Anthropic at /anthropic) onto /v1/chat/completions, and the tool went with nothing said. api.deepseek.com is now among the hosts that search by themselves on Responses only, so such a request stays on /v1/responses with the tool as the agent sent it. Its Chat API has no search tool and is left as it was; whether its Anthropic API takes web_search_20250305 isn't known, so that route claims nothing either. With a DeepSeek provider and none of Claude, Codex or an Anthropic that searches, searcher() may now ask it to search for other models, at the rank OpenAI's and xAI's Responses have. TestDeepSeekSearchesOnResponses (the preset searches on Responses, not on Chat or Anthropic) fails without the change. No request was made to DeepSeek. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 13:34:35 +08:00
  • 88cccc184e 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). yetone 2026-10-02 13:26:13 +08:00
  • c2947dd6e7 zed: an OpenAI model's request reaches Zed's cloud in the Responses types it reads, so Codex on a Zed GPT model is answered (nico on Discord: "failed to parse OpenAI Responses API request: unknown variant developer, expected one of user, assistant, system, tool", and with it no model switch mid-thread and no auto-review) — Zed's cloud parses the request into its own types (zed-industries/zed crates/open_ai/src/responses.rs: Role user/assistant/system/tool, items message/function_call/function_call_output/custom_tool_call(_output)/reasoning/compaction/compaction_trigger, tools function/custom, include reasoning.encrypted_content only) and refuses the whole request over anything else. The Zed plugin (@magpie-community/opencode-zed-auth) gets Codex's Responses request relayed as Codex sent it, developer messages and all — its permissions message on every request, the one a model switch adds, the auto-review's — so every Codex turn on a Zed OpenAI model failed. The plugin's account now fits the body as grokBody does Grok's (ZedBody): a developer message goes as a system one where it stands (Zed's own client sends system messages too), the shorthand {role, content} gets its type and a string content becomes a list, reasoning's null content goes, an agent_message is the user's turn, an item Zed has no type for (a web_search_call an OpenAI model ran) is left out, namespaced tools go flat and their calls come back under the namespace (as Grok's), other tool types go, include keeps reasoning.encrypted_content alone, a function tool_choice is required, and a reasoning summary Zed doesn't know goes. The built-in Zed account already folded developer messages in; its OpenAI request goes through the same fitting, so a web search offered with no search API no longer leaves a web_search tool and web_search_call.action.sources in it. Codex's prompts and instructions are unchanged. TestZedPluginGetsNoDeveloperRole (a Codex-shaped request through the fake plugin to a fake upstream that parses as Zed's cloud: fails with the exact 400 without the change) and TestZedBuiltInResponsesParse fail without it. No request was made to Zed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 13:21:31 +08:00
  • 20b172e845 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. yetone 2026-10-02 13:21:32 +08:00
  • 11be3f31b0 zcode: a Start Plan request goes as the ZCode CLI sends it, not the desktop app (miaopasi and ARNO on Discord: Start plan还是405 感觉不是IP的问题; an OpenCode plugin sending the CLI's request works on the same plan) — dressed as the desktop app, some Start Plan requests were still turned away with 405 "request has been blocked due to unusual activity", code 3012. Following ZCode 3.14.3's own source (github.com/zai-org/ZCode, apps/zcode-cli/packages/core/src/context/builder.ts and sections/, bootstrap/src/model-config.ts and runtime-platform-headers.ts, adapters/src/model/runner-attribution.ts), the request is now the CLI's: X-Title "Z Code@cli" (was "Z Code@electron", which only the desktop app's agent server sends); the stable system block is the identity section alone, the "ZCode Desktop Context" section being added only for presentationSurface zcode_desktop; X-Platform is <platform>-<arch> and the new X-Os-Version is os.release(), as runtime-platform-headers sets them (Darwin's uname release, Windows' Major.Minor.Build from RtlGetVersion); the environment block's OS Version is "<platform> <release> <arch>" as env-info.ts writes it (was "unknown") and its powered-by line is the provider id and model as ZCode names them (account:zai-start-plan/GLM-5.3-Flash, account:bigmodel-start-plan on BigModel; was lowercased without the provider); the currentDate reminder is wrapped as system-reminder/source.ts wraps a context prefix (a newline after the opening tag, one after the closing). Model requests no longer carry X-Device-Mid (ZCode sends it only to its own API, where the balance still needs it, #282) nor x-api-key: the JWT goes as a Bearer only, as the plugin that works sends it (the official client passes it as the SDK's apiKey too, so this follows the working plugin over the source). The prompt text itself was checked byte for byte against the 3.14.3 sections (rendered with node). The plugin's own source wasn't found (pi-freeflow has no zcode code), so where it and the source might differ (X-Os-Version as release alone, not "<platform> <release> <arch>") the source wins. No request was made to zcode.z.ai. TestZCodeStartSentAsTheApp, TestZCodeStartPlanOwnAccount and TestZCodeSignInStartPlanDeviceMid are updated; with the old prompt, X-Title and x-api-key they fail (X-Title "Z Code@electron", X-Api-Key sent, the desktop section in the system prompt). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass. yetone 2026-10-02 13:11:08 +08:00
  • fbddf45a86 providers: ClinePass lists Cline's real free models and asks for them as Cline's desktop app (lml and ARNO on Discord: the free models didn't match Cline's) — a3962602 listed the Cline API's OpenRouter-style ":free" ids (qwen/qwen3.8-27b:free…), which are not what Cline offers free: Cline's clients take their picker from GET /api/v1/ai/cline/recommended-models (cline/cline sdk/packages/llms/src/catalog/catalog-cline-recommended.ts), whose free bucket is the desktop's FREE section ("Try with limited usage, separate from ClinePass quota": cline-free/deepseek-v4.1-flash, stealth/space-bunny-alpha, cline-free/mimo-v2.6-flash, cline-free/muse-spark-1.3-contributor) and whose clinePass bucket is the plan's. ClinePass now fetches that feed, asked as Cline's desktop app and with no key, as Cline asks it (pi-freeflow measured that X-CLIENT-TYPE cline-desktop is what the feed keys its free list on); the plan's models are its clinePass ones, the free ones are marked Free (the FREE badge) and priced at nothing, and its recommended paid models are left out, as before. When the feed can't be had the API's list is asked as before, and the free models are the desktop's list of today (from Cline's generated fallback, cline-recommended.generated.ts); the ":free" ids are dropped with FreeTag, as Cline's own clients never offer them as free and their cost on a Cline key was never shown. Requests to the Cline API (the gateway's and the Test button's) carry the desktop's headers — HTTP-Referer https://cline.bot, X-Title Cline, X-IS-MULTIROOT false, X-CLIENT-TYPE cline-desktop, X-CLIENT-VERSION 3.5.54, User-Agent Cline/3.5.54 — since the cline-free/ models answer 403 "only available via Cline product surfaces" without them (pi-freeflow, measured live); the key goes as before. ListPrice prices a model free by its cline-free/ id, by the fetched list or by the fallback at zero. No request was made to Cline and nothing was tried with a real Cline key. TestClineFeed (the feed asked as the desktop with no key; plan, free and no paid models listed; a free model the feed alone names priced at zero), TestClineClientHeaders (the headers on a cline-free request to api.cline.bot and on no other provider's) and the updated TestPlanModels fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass (TestClaudeSignInOverStaleCredentials failed once under the full run, passes alone and on the rerun; Cline untouched by it). yetone 2026-10-02 13:04:35 +08:00
  • e413dd8445 factory: another agent's request to Factory's GPT, Grok and open models opens as Droid's does (#506 aohun: Codex on glm-5.3-flash through a Factory account, 403 Forbidden; the user: 只要不是 claude code,都可以尝试伪装一下) — in #242 the same account's GPT, Grok and GLM answered Droid through magpie every time and refused Grok Build and Claude Code every time, on the same models, efforts, endpoint and headers, the body the only difference, and glm-5.3-flash is a model Factory serves (droid 0.231.0's registry: provider factory, generic chat completions, fireworks/baseten/databricks), sent where and as droid sends it. droid 0.231.0 (its binary, read, not run) opens every system prompt with YOU_ARE_DROID_SYSTEM_PROMPT, "You are Droid, an AI software engineering agent built by Factory.": its system blocks [that line, the agent prompt, …] go as Responses' instructions joined with "\n", and on chat completions as one system message put first, joined the same way; its small calls (the latency ping) send the line alone. A request on /api/llm/o now does too: Responses' instructions, or the first system message of chat completions (text parts joined as droid joins them), open with the line and "\n" before the agent's own; with none, the line alone goes first. A request that already opens with it (droid's own) goes on byte for byte, as does everything on /api/llm/a (Claude, MiniMax M2.7), which is left as the agent sent it. The 403 text now says magpie opens other agents' GPT, Grok and open-model requests as Droid's but Factory may still tell them apart, that Claude models from other agents are refused, to use the model from Droid, and that the org's policy or plan is left when Droid is refused too. GLM-5.3-Flash takes images (droid 0.231.0 says images: true; the rest of the table matches), and the requests say droid 0.231.0. Not tried with a real Factory account: whether the line is all Factory checks is not known. TestFactoryOpensAsDroid (Codex-shaped chat and Responses, Claude Code's system parts, no system, droid's own chat and Responses unchanged byte for byte, Anthropic's Messages unchanged) and TestFactoryGLMFlashImages fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gateway/ ./internal/provider/ pass. yetone 2026-10-02 13:00:15 +08:00
  • 0cb6e13d91 gateway: a developer message Codex adds mid-thread stays where it is, so the thread's cached prefix does too (#502, Koohoko: after a memory request the next turn of the same session wrote the whole conversation to the cache again) — Codex adds a developer message to a thread where its context changed (a world-state or skills diff, a settings or model change, and so after its memory pipeline has run) and keeps it there in every later request, which leaves OpenAI's prefix as it was. parseResponses lifted every developer and system message, wherever it stood, into the system prompt, so on a translated route that turn's system changed: Anthropic's system comes before the messages, so the whole conversation was written to the cache again, and a Claude subscription's waiting Claude Code is found by its system (turnKey), so a new one was started and told the whole conversation in one message. Developer messages before the thread's first reply (Codex's initial context) are lifted as before; one after it now stays in place as a user message inside <system-reminder>, which the router's classifier already leaves out of the user's words. The memory request itself (another model, its own prompt, the same session) moves nothing in magpie: affinity is scoped by model or by a group's first words, the Claude bridge's runs by the conversation. TestCodexLaterDeveloperMessageKeepsTheSystem and TestCodexThreadKeepsItsClaudeRunAcrossAMemoryCall (a Codex thread on a Claude subscription, a memory call in the same session between, then a developer message: the same Claude Code goes on) fail without the change. No real vendor request was made. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gateway/ ./internal/provider/ pass. yetone 2026-10-02 12:58:21 +08:00
  • 65f3a6417a settings: portable mode — a data folder beside magpie keeps everything magpie keeps (#508, suixinex) — like VS Code's portable mode, a folder named "data" (or a file named ".portable", the data folder then made beside it) next to magpie makes it keep all its own files there instead of the user's profile. Where magpie keeps its files is now decided in one place, internal/appdir: Config() (~/.config/magpie, $XDG_CONFIG_HOME/magpie; portable: data/), Cache() (~/.cache/magpie, $XDG_CACHE_HOME/magpie; portable: data/cache) and SystemCache() (the OS's cache folder, which the library's market lists and an update staged where magpie may not write used; portable: data/cache). settings.Dir()/Path(), provider.Path(), profile.Path(), the models.dev catalog, the exchange rate and the plugin market cache go through it, and everything built on them follows: providers, logins and saved accounts' folders, usage ledger, routing history and request archive, plugins, library, backups, sync state and lock, caller keys, redact key, stats. "Beside magpie" is the folder holding the executable with links followed; for a Mac app the folder holding Magpie.app (inside the bundle would break its signature), and for an AppImage the folder holding the .AppImage, not where it is mounted. The decision is made once, at start: an update moves the running copy aside (into .magpie-update on a Mac, to magpie.exe.old on Windows) and its files must not move with it; the updated copy replaces the executable or bundle only, so the data folder beside it is left alone. A portable magpie doesn't copy an old dial install's folders in. Not moved, as they aren't magpie's own: the agents' configs (~/.claude, ~/.codex…), the sign-ins agents keep in the system keychain (a saved Claude account's Claude Code keychain item is named after its folder's path on a Mac, so a portable folder moved elsewhere or to another Mac signs that account in again), autostart and link-handler registrations, and temporary folders. The single-instance id already covers the config folder, so a portable and an installed magpie run side by side, but the gateway port is one. Settings shows the config folder as portable. TestResolve (data folder, .portable, a file named data, a Mac bundle), TestResolveSymlinkedExe, TestResolveAppImage (Linux), TestFolders and TestPortableSettings (Path() lands in the data folder, Save writes there, ~/.config/magpie untouched); TestPortableSettings fails without the change (Path() is ~/.config/magpie/settings.json), TestResolveSymlinkedExe without following links. A nogui build run from a folder with data/ under a sandbox HOME wrote profiles.json, cli-identity.json and cache/models.json into data/ and nothing of magpie's into HOME. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./... pass. yetone 2026-10-02 12:57:58 +08:00
  • 739b3bebe3 gateway: a rate limit on the last provider left is waited out even when it doesn't say for how long (#503, ynky007) — a 429 was tried again only when it came with a Retry-After (or a ratelimit-reset header) of 8 s at most, so a relay's 429 that says nothing of when ("Rate limit exceeded, please try again later", "当前分组上游负载已饱和,请稍后再试") went straight to the agent. Where another member, key or account is left a 429 already moves on to it, as before; on the last one left (or the last that may answer, the others out of their allowance) a 429 failure() reads as a rate limit is now asked again up to 3 times (rateRetries; busy 5xx/529 keep lastRetries = 2), after 1, 2 and 4 s or what Retry-After says (each 8 s at most, under half a minute in all), while nothing has been sent to the agent and only until it goes away. A 429 that says the plan is used up (usage limit, resets in hours: failQuota), that the money is gone (creditWords), or a Retry-After past 8 s (ChatGPT's resets_at goes on as one) is passed on at once, as before. passing() now takes the error body and the try count, and the last one's reply is held for as many tries as a rate limit gets. TestRateLimitIsWaitedOut (no Retry-After, a relay's own words, three in a row, then 200; four in a row → 429 after 4 tries; usage limit, 余额不足, Retry-After: 3600 → 429 after one try, at once) fails without the change (429 after 1 try in the first three, 3 tries in the fourth); TestLastOneLeftIsTriedAgain still passes. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gateway/ ./internal/provider/ pass. yetone 2026-10-02 12:54:17 +08:00
  • 22358d5d6e 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. yetone 2026-10-02 11:56:03 +08:00
  • 70a6c72a2c 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. yetone 2026-10-02 11:39:05 +08:00
  • aa42142ea5 update: What's new and the waiting update's notes follow the app's language (freecss on Discord: 更新后的公告的语言是不是应该设置里面的走) — the release notes were English alone. A release's notes are to carry a Simplified Chinese translation below a <!-- lang:zh --> marker (the magpie-releases workflow, changed separately); usemagpie.ai's /api/latest and /api/notes take ?lang=zh for the Chinese where a release has it, and give anything else, an app from before included, the English alone, everything above the marker. The page asks /api/whatsnew, /api/update and /api/update/check in its own language (?lang=); magpie asks the site in it, keeps What's new's notes per language, and asks again for the waiting update's notes when a page's language isn't the one they are held in (at most once a minute); a check by itself asks in the pages' last language, else the app's, as the tray menu has it. Notes that come back whole (a site not yet deployed) are cut in the app too; notes without the marker (every release so far) are English, in either language. The worker must be deployed (npx wrangler deploy in site/) before the workflow change ships, or apps would get both languages. TestInLang, TestFeedsAskInLang, TestAskedLang, TestWhatsNewInPageLang, TestUpdateNotesInPageLang, site/worker.test.mjs (node --test site/worker.test.mjs), whats-new.test.cjs's language check (Chromium and WebKit, en and zh); update-check, update-hide, update-auto, sync-update pass in WebKit. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves flaked once under the full run, passes alone; gateway untouched). yetone 2026-10-02 11:28:51 +08:00
  • 7e3b53b43c providers: a provider can be moved up or down the Providers tab (#499: 供应商 tab 希望添加调整顺序的操作,比如我后面添加的想要挪到前面使用) — they were listed only in the order they were added. A provider that is on now has its logo for a handle, as an agent row has: the grip in the row's margin is drawn only with the pointer on the row, dragging it moves the row, Alt+↑/↓ moves it from the keyboard and keeps the keyboard on it, and a click opens the row as before, without moving the page; one switched off keeps its place. The order is saved in providers.json ("order", POST /api/providers/arrange), follows a rename, and puts a provider added later after the rest. It is the order they are tried in too: the agents' pickers list them in it, a bare model id goes to the first that serves it, and a group magpie finds of a model several serve tries the higher one first, which the handle's tooltip says. dragRows put a row held at the top one place short of it, since the first row is a pixel shorter than the rest (they carry a border); held at either end it now goes there. TestProviderOrder and provider-order.test.cjs (Chromium and WebKit, English and Chinese) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... (but TestClaudeSignedOut, which fails on the base too), go test ./internal/gui, and agent-grip, account-arrange and click-scroll (WebKit) pass. yetone 2026-10-02 11:29:15 +08:00
  • 8dbdcfc8b3 gateway: a Claude subscription's turn at another effort goes on in the same Claude Code, told the level, and reads its cache (#502: 经过意图路由判断改变了推理强度后,好像缓存全部要重新写入) — a router switching Opus 5.5 from high to low and back wrote the whole conversation to the cache each time (427,987 and 429,792 tokens written, 26,107 read, $2.18 and $2.20 a turn) yetone 2026-10-02 11:24:05 +08:00
  • d3308f567f gui: the routing story's lines lead with the provider's logo before the account, key or provider they name (the owner: 这里在前面显示对应的 provider 图标会不会更直观一点) yetone 2026-10-02 11:27:02 +08:00
  • 2d75c4883b gui: the refresh icon ends its last turn on the compositor's clock, so it no longer jumps back when the page is busy drawing the reply (a user on X: 右上角这个刷新按钮点击之后他的旋转似乎有点抽搐) — the spin waited for animationiteration and then dropped its class, and a page busy rendering the synced lists when the turn came round let the icon run on (461ms past the turn in the test: some 180°) and then snap back to 0°; at rest it still lagged a frame or two. The turn the reply catches is now told to be the last (effect.updateTiming iterations), and the class goes once it has finished, at 0°. sync-spin.test.cjs (Chromium and WebKit, en and zh) holds the page busy past the turn and fails without the change. yetone 2026-10-02 11:10:57 +08:00
  • 96367cb8f4 gui: Claude Code's model picker shows each model once (#496) — the rows magpie serves on the very Claude account Claude Code is signed in to fold into one, and models.dev's alias and dated id of one model are one row yetone 2026-10-02 10:55:15 +08:00
  • 7ab91961d2 davsync: an unchanged file on the server costs a request, not a download (Crispin on Discord: 是不是Magpie后台一直在自动同步,导致被坚果云给限制了) — every 3 minutes sync read the whole sealed file even when nothing had changed, and 坚果云's free plan caps monthly downloads and requests per 30 minutes. The sync state now keeps the server's version beside its hash (its ETag, or a Last-Modified at least a second older than the server's Date, so a write in that same second can't go unseen), from the read or from the ETag a write answers with (none said: the next sync reads in full, as before); the next read sends If-None-Match (or If-Modified-Since) and a 304 with nothing changed here ends the sync, while a change here merges with a copy of the server's file kept beside sync-state.json (sync-server.magpie-backup, 0600, checked against the hash; gone or different, the file is read again), so it isn't read twice. A server that ignores the condition sends the file and syncs exactly as today. S3 does the same with If-None-Match on GetObject, signed with the rest (the fake S3 checks every signature). A 429 or 503 (S3's SlowDown) now says the server is limiting how often magpie may reach it, and Run waits as long as Retry-After says (up to 6 h) or else doubles from 6 up to 30 minutes, back to 3 after a sync that goes through; log de-dup is kept, as the message doesn't carry the wait. The 403 paths (there, forbidden) only run when the server answers 403, so a working setup costs one conditional GET per cycle. TestSyncUnchangedIsNotRead (ETag, ETag not on PUT, Last-Modified: three unchanged cycles read 0 bytes, a server change is read once and merged, a change here is pushed without a read, a missing copy is read again), TestSyncRateLimited and TestS3Unchanged fail without the change (with the conditions dropped: 3 full reads of 1932 bytes, 'HTTP 429' with no backoff, unconditional S3 GETs); TestSyncIgnoringConditions guards servers without conditional reads. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./internal/davsync/ ./internal/gui/ pass. yetone 2026-10-02 10:52:36 +08:00
  • a396260204 providers: ClinePass lists the Cline API's free models beside the plan's (lml on Discord: Cline里面的免费模型是否也可以使用呢?) — a Cline key is served the API's ":free" models (qwen/qwen3.8-27b:free, google/gemma-4-31b-it:free, nvidia/nemotron-3-super-120b-a12b:free…) at no cost, so the preset's new FreeTag ":free" keeps the ids of the fetched list ending in it after the plan's cline-pass/ models (the plan's own list when the API names none of them, as before), marked Free, which the model list already badges FREE ("free: it doesn't use the plan's credits") as it does WorkBuddy's and Command Code's free models. Its paid models are still left out, and before the list is fetched only the plan's models show, as before: no free list is written down. ListPrice prices a FreeTag model at zero, where it was priced as the model without its tag (":free" is cut off when matching models.dev), so the usage ledger charges it nothing. The free models were not tried with a real Cline account (no key at hand). TestPlanModels fails without the change (the free models are dropped, none is marked Free, and one has no price). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass. yetone 2026-10-02 10:48:45 +08:00
  • 0f41570859 cursor: a conversation's Runs keep one conversation_id, so Grok's prompt cache on Cursor sticks, and Cursor's usage no longer counts the cache twice (#498, congee949). buildCursorRun put a new UUID in AgentRunRequest's conversation_id (field 5) on every Run, so Cursor's backend had nothing to keep a conversation on the machine holding its cached prefix — the x-grok-conv-id grokSigned sends xAI direct — and a Codex thread on cursor/grok-4.7-fast read about 2% from cache against 94% on the grok provider, its first token growing with the whole prompt. The id is now a UUID (v4-shaped, like the official client's) hashed from what names the session — the client's prompt_cache_key (Codex's thread id), else the agent's own session header (Claude Code's, OpenCode's) — together with the conversation's first user message, which every later request repeats; with neither key nor session header each Run still gets a fresh one, since a first message alone ("hi") would put strangers under one id. On concurrent subagents: Cursor's own client keeps one conversationId per agent session, and whether its backend treats the id as more than a cache-affinity key isn't known, so subagents sharing a session (Claude Code's Task agents share its session id; a Codex collab agent has its own thread and key anyway) get ids of their own through their own first message, never one id sent twice at once. TurnEndedUpdate's input_tokens is the whole prompt, cache read and written included, as Cursor's client has it; decode kept it as Usage.Input, which everywhere else is the uncached rest that prompt() and pricing add the cache to, so a cached turn was counted twice (in=137300 with cache_read=132352 is 4948 uncached). Input is now input_tokens less cache read and write, never below 0, and reasoning_tokens (field 5) is kept as Reasoning. TestCursorConversationID (same Codex key → same id across turns, other key → other id; same Claude Code session → same id, a subagent's first message → another; nothing naming it → a new id each Run), TestCursorUsageTakesTheCacheOut and the updated TestCursorDecodesTheEnd fail without the change. No real Cursor request was made. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gateway/ ./internal/provider/ pass. yetone 2026-10-02 10:48:27 +08:00
  • fc036d1db4 fix(gateway): run Claude Code's /usage without CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC (#438) lainbo 2026-10-02 11:08:04 +08:00
  • 4696d14726 gui: Add provider unrolls its sheet under the list again, and closing it leaves no blank (the owner: 点击「添加供应商」为啥是弹出 modal 而不是像以前一样的交互(展开滚动)· 在添加供应商时会有两层弹窗,很丑,不符合设计原则) yetone 2026-10-02 11:03:29 +08:00
  • 7bc42a79d9 tray: every logo in the menu bar is drawn as a template image is, in the bar's text colour alone (the user: 当在 tray 上显示 agent 图标时,能否是去掉色彩满足 tray icon 样式的图标啊,而不是原样显示) — a coloured logo (Codex's blue tile, Claude's orange, Gemini's gradient, an app's PNG) was drawn as it is. Its shape is now what is opaque, less what is near white when the rest isn't, so a tile with a white glyph (Codex, ZCode, Kiro, WorkBuddy) is a tile of ink with the glyph cut out, light or dark. TestTrayImageMonoLogos (Codex, Claude, Gemini, ZCode, Alma; light and dark) fails without the change. go vet, go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./..., go test ./internal/gui pass. yetone 2026-10-02 11:01:25 +08:00
  • b8daf4013b gui: Take a look on the line naming several subscriptions that can move to plugins opens the Plugins tab, not Cursor's editor (the user: 这里点击查看为什么打开的是 cursor) yetone 2026-10-02 10:44:51 +08:00
  • ec10d3bd98 gui: the header tabs lose their grey track, a Plugins card's move is one word beside the name, and an agent row's grip shows only under the pointer (the user: 这个 tab 栏的背景灰色条有点丑 · 这里的紫色按钮也有点丑,而且还有的变成了两行 · 排序 icon 只在 hover 到这个条目的时候显示,logo hover 的排序 icon 可以删掉) yetone 2026-10-02 10:31:43 +08:00
  • ee5f974f98 providers: PipeLLM is a built-in preset (the owner: 作为内置 provider 加上吧) — one key for OpenAI's, Anthropic's and Gemini's models at api.pipellm.ai. Chat goes to its converter (/openai/v1), which takes every family, as its own OpenCode and OpenClaw guides set it up; GPT goes to its own Responses API (/v1) and Claude to its own Messages API (the root), each of which serves its family alone. A model list's type_target (openai, anthropic, gemini) now says which APIs a model is served on when the list names no endpoints: openai on Chat and Responses (Responses first for GPT, as on OpenAI's API), anthropic on Messages alone, gemini on Chat alone. Keys at console.pipellm.ai; its icon is the site's favicon; README lists it. Not sponsored. No PipeLLM key was at hand, so no real request was made; TestPipeLLMRoutesByTypeTarget (httptest list) fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 10:31:23 +08:00
  • f3ba918491 gui: make magpie web usable on phone screens (#391) Arvak 2026-10-02 10:15:42 +08:00
  • 1eb2640d7e fix: 添加供应商改用遮罩并保持列表位置 (#433) Fanbuzandfanpeng 2026-10-02 10:15:35 +08:00
  • 5ebb053ce1 fix: 统一额度相关的中英文界面文案 (#493) Fanbuz 2026-10-02 09:37:00 +08:00
  • 6b89fb22f4 gateway: an earlier assistant turn's thinking goes to Mistral as the thinking part Mistral itself returns, and a vendor that turns it away inside messages is asked again without it (#494: Mistral's 422 extra_forbidden for OpenCode's reasoning_content, after any tool call) — OpenCode sends an earlier assistant turn back with its thinking in reasoning_content, a tool-call turn too (content null), and magpie relayed it as sent; Mistral refuses any field it doesn't know, loc ["body","messages",2,"assistant","reasoning_content"], so a Mistral reasoning model (GLM on api.mistral.ai) in OpenCode failed as soon as it used a tool. The #393 retry only dropped a fixed list of top-level fields. To Mistral (its preset, or api.mistral.ai) an assistant message's reasoning_content (or reasoning, or reasoning_details' text) now goes as a leading {"type":"thinking","thinking":[{"type":"text","text":…}]} part of its content, its text after it as a text part, its tool_calls as they were, so the thinking stays in context, as the reporter found Mistral takes it; the other messages keep their bytes. Any other Chat upstream answering extra_forbidden located in messages for one of those fields is asked again with it left out of the messages, and once that works it is left out for that provider from then on; DeepSeek, Kimi and every vendor that takes it are sent it as before. Prompts and headers are unchanged. TestMistralReasoningAsThinking (the issue's request, and a tool-call turn) and TestMessageReasoningRefused fail without the change. go vet and the darwin build pass; the full test and cross-build chain could not run, the disk being full. yetone 2026-10-02 09:13:11 +08:00
  • 16e373b924 opencode: Zen's free models answer through magpie from Claude Code, Codex and the model test too (ARNO on Discord: mimo-v2.6-flash-free 没有响应: 403 · OpenCode's free tier can only be used from within OpenCode; the user: 只要不是 claude code,都可以尝试伪装一下) — beyond OpenCode's headers (v0.1.615), Zen's free tier takes only a streamed request offering tools named bash and read. A request to a -free model on opencode.ai now always streams upstream, a client that didn't ask for a stream getting the reply collected; a caller's Bash/Read (any case) go as bash/read, as do earlier calls in the history and a forced tool_choice, and calls in the reply get the caller's names back, streamed or not, in each search round too; when bash or read is missing a stub is offered whose description says never to call it, and a call to a stub never reaches the client (a turn with nothing else ends on its text, or a short note, as a stop). Zen lists its -free models again. Paid Zen models and other providers are untouched. Real requests to opencode.ai/zen/v1 with the anonymous key: Claude Code-style Bash/Read streamed and not, Codex-style shell on /v1/responses, no tools non-stream, and the model test all answered 200. TestZenFree* fail without the change. yetone 2026-10-02 09:23:21 +08:00
  • 3515d99a9c gateway: a request to OpenCode's gateway (Zen or Go) says it is OpenCode's, as OpenCode itself sends it (the user: free Zen models 403 "OpenCode's free tier can only be used from within OpenCode"; mimicking OpenCode is allowed on this route) — magpie sent User-Agent magpie/<version> and its own conversation id (magpie-…) in x-opencode-session. OpenCode (packages/opencode/src/session/llm/request.ts, for a provider whose id starts with "opencode") sends User-Agent opencode/<version>, x-opencode-session (ses_…), x-opencode-request (msg_…), x-opencode-client and x-opencode-project; every request to opencode.ai now carries them: opencode/1.18.34, the conversation as an id of OpenCode's form (one already ses_… goes as it is, any other is made from it, the same for the same conversation), a fresh msg_… id, cli and global. Prompts and body go as the caller sent them; other providers get none of it. Zen's free tier also asks for OpenCode 1.18.0 or newer (426 otherwise), a streamed request, and tools named bash and read: a live request through the gateway with the anonymous key "public" to mimo-v2.6-flash-free went from 403 to 200 for a streamed one offering bash and read (OpenCode's and Pi's), and stays 403 for Claude Code's Bash/Read or no tools, so the free models stay off the list. TestOpenCodeClientHeaders fails without the change; TestOpenCodeGetsConversationSession now expects OpenCode's form. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 08:23:47 +08:00
  • 31568cb0b0 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. yetone 2026-10-02 08:13:04 +08:00
  • 6e858c6641 sessions: a session in magpie's trash can be erased for good, one at a time or the whole trash, after magpie's own dialog (#487: no way to empty the trash but removing its folder by hand) — the Sessions page's Trash only restored, and said magpie never erases what is in it: remove the folder yourself. Each trashed row now has Delete forever and the Trash an Empty trash, with the count of what is in it; both ask in magpie's dialog, as Delete does (never confirm()), naming the sessions, and Cancel sends nothing. They post /api/sessions/purge with the rows' keys, or all for every session the trash lists; each key is checked as Restore checks it (<agent>/<name>, an agent magpie deletes for, no separators, no . or ..), and its folder, and its agent's, must be real folders in <magpie dir>/trash/sessions, not links, with the session's note (session.json) in it; only that folder goes, its note with it, so the trash lists it no more. Nothing is erased by itself: the note under the Trash now says they are kept until erased there, and magpie never erases them by itself. TestPurgeOne, TestPurgeAll, TestPurgeRefusesOutOfTrash, TestSessionsPurgeRoute and sessions-purge.test.cjs (Chromium, WebKit; en, zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFXInState failed once under load, passes alone and in its package); sessions-manage, sessions-toolbar, click-scroll, old-webkit pass. yetone 2026-10-02 08:14:01 +08:00
  • 01af01c7aa usage: the Overview has one refresh, the header's, and it asks for the allowances as their own Refresh did (#486: two refresh controls on one page) — the Usage page's header had its refresh icon and, a little below, the allowances' Refresh button, and the icon on the Overview read the allowances only as the timed reload does, so a Claude account's was not read again by it. The allowances' Refresh is gone; the header's icon on the Overview now loads usage/quotas?asked=1 beside the summary, as that button did (a Claude account's by running Claude Code's own /usage, still at most once in 30s), and turns until both are in; its title says it reads the allowances too. Requests and Sessions are unchanged. usage-refresh.test.cjs (Chromium, WebKit; en, zh) and claude-usage-asked.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; usage-ledger, quota-pools, quota-families, click-scroll, old-webkit pass. yetone 2026-10-02 08:00:51 +08:00
  • cfd67225c8 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. yetone 2026-10-02 08:07:29 +08:00
  • dfc8ea4aea profiles: Apply closes a profile's details, and their × closes them after it (#489, a regression of #478) — on Apply, profileAction dropped profileOpen and then called closeProfiles, whose closeProfileDetail, finding nothing open, returned at once; renderAgents had just drawn the details again, so they stayed in the window and came back with the tray's list, and their ×, which calls the same closeProfileDetail, did nothing until the chip was clicked again. Apply now closes the details itself, and closeProfileDetail closes details on the page whether or not profileOpen still names them. profile-confirm.test.cjs (Chromium, WebKit; en, zh; window and tray) now applies a profile from its details and checks they close, the tray's list opens again on the chips, and × closes them after it; it fails without the change. profile-details, panel-profiles, click-scroll, old-webkit pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass. yetone 2026-10-02 08:02:14 +08:00
  • 480df9bac0 web: magpie web on a phone has a header to read and tap, and a flick's scroll is no longer put back (jiakun_zhao on X: web 页在移动端的显示效果非常不友好诶,甚至滚动会抽搐) — on a phone the tabs were squeezed into the room between the magpie and the icons, too small to read or tap; they now take a row of their own under them, at their size, sliding sideways with the one open kept in sight. A setting's control too wide beside its name goes under it rather than cut the name, and iOS isn't let zoom into a field it focuses (a pinch still zooms). A flick goes on scrolling after the finger lifts with no touch event to say so, and those scrolls weren't counted as the reader's: each was put back, and the page jerked. Each of a flick's scrolls now keeps the next one the reader's till it comes to rest; a tap's click ends it, so what a click redraws still doesn't move the page. A desktop browser and the app's window are unchanged. phone-web fails without the change (the header, and the flick put back at 350 of 711). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass; phone-web, click-scroll, brand, panel-fold, provider-typed, library-loading, old-webkit, header-centre, header-zoom-fit, strip-scrollbar, web-page, text-size, library-row-open-scroll, provider-levels-scroll pass. yetone 2026-10-02 08:17:38 +08:00
  • 7d785c4407 fix: 将订阅设置中的用量文案改为额度 (#484) Fanbuz 2026-10-02 08:09:52 +08:00
  • 5df609e525 usage: an Antigravity account shows each group of models' 5-hour and weekly allowance, one row a group (a user on Discord: 这里显示5h 和一周剩余比较好吧,现在是只显示5h,这3个模型都是一样的,没必要分开…多加个7day 条就好) — fetchAvailableModels gives each model only its 5-hour figure, the same for every model drawing on one quota, so the Usage page, the panel and the account rows showed a figure a family (Gemini, Claude, GPT-OSS) and no week. Antigravity's retrieveUserQuotaSummary reports the quota a group ("Gemini Models"; "Claude and GPT models", Claude and GPT-OSS sharing one), each with a 5h and a weekly bucket; magpie now reads it beside the models' (best-effort: without it the windows are as before) and adds each group's windows, named "5 hours" and "7 days" with their pool (Gemini, Claude & GPT), each model's window naming its pool. The Usage page shows Gemini · 5 hours, Gemini · 7 days, Claude & GPT · 5 hours, Claude & GPT · 7 days, the pool's models in each tooltip; the panel four rings (Gemini 5h/7d, Claude 5h/7d); the account rows each pool with its 5h and 7d meters; the TUI the pools' windows in place of the models'. "Every model" lists the models' own windows. The pools' windows are aside for routing, which reads the models' as before; the tray's text is unchanged. TestAntigravityQuotaPools and quota-pools.test.cjs (Chromium, WebKit; en, zh) fail without the change; quota-families, click-scroll, old-webkit, tray-usages, panel-usage pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 07:02:36 +08:00
  • cb761abdd1 commandcode: a reply is asked for within Command Code's 200000 tokens, and the agents are told no more (Discord: every Command Code model, free or paid, failed with "Too big: expected number to be <=200000 at params.max_tokens") — magpie passed the client's max_tokens to /alpha/generate as it came, and told the agents each Command Code model's reply limit as models.dev gives it: DeepSeek V4's 384000, MiniMax M3's 512000, Space Bunny Alpha's 524288, Kimi K2.x's 262144, so an agent asking for what it was told had the whole request refused. params.max_tokens is now the asked number within the model's own limit as models.dev gives it and within 200000; less is sent as it was, and none asked is still the CLI's 64000. The Command Code subscription's models are listed to the agents (their configs, /v1/models) with a reply limit of at most 200000, a user's own limit included. Prompts and headers are unchanged. TestCommandCodeGoMaxTokens and TestCommandCodeOutputCapped fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 06:57:38 +08:00
  • 134a788821 settings: magpie's own update checks can be turned off, and run every 30 minutes, hour, six hours or day (#472: 更新行为可配置) — the app asked the feed five seconds after start and every six hours, a constant, and downloaded any newer version at once; with releases out most days, a version out just after a check waited up to six hours, and there was no way to stop the checks or the downloads they start (Update button only hides the pill). Settings → About has Automatic updates (on by default) and Check every (30 min, 1 h, 6 h, 24 h; 6 h with nothing set), kept in settings.json as noAutoUpdate and updateEvery, and magpie update auto [on|off] [30m|1h|6h|24h] sets them from the terminal. The updater reads them each minute and asks once the interval since the last check, its own or a click on Check, is up, and never by itself while off; the version row, not asked yet, then says automatic updates are off and offers Check. Check every stays in place, dimmed, while off, so the click doesn't shorten the page under it. TestUpdateDue, TestUpdateAskedAt and TestUpdateAutoCmd fail without the change, as does update-auto.test.cjs (Chromium, WebKit; en, zh). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; update-hide, update-check, whats-new, settings-groups, click-scroll, old-webkit pass. yetone 2026-10-02 06:25:18 +08:00
  • 03ab46dda3 library, plugins: the installed lists are sorted as the reader picks, and remember it (#481: the installed MCP servers, skills and plugins had no order the user could choose; the plugins came in the order they were installed) — the Library's MCP servers and the installed plugins now have A→Z and Z→A beside their list, A→Z until one is picked, so the plugins are no longer in the order they were installed in; the Library's skills have By source (the groups by repository and their parts, as before, and still the default), A→Z and Z→A, the last two one flat list of every skill by name, the filter still finding a skill by its repository. Each list keeps its own pick in the browser's storage (magpie.sort.<list>, read and written in try/catch), and a pick redraws only its list, the control left where it was on the screen. The skills' heading, the fullest, goes on to a second line in a narrow window rather than past the page's side. Sorting by use or by when a thing was installed waits for the library to keep those. list-sort (Chromium, WebKit; en, zh) fails without the change; library-items, library-all, own-skill, every-skill, the plugin tests, click-scroll and old-webkit pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 06:33:08 +08:00
  • e3bf9d05d4 library: one agent's skills or MCP servers are turned on or off at once, and a hidden agent's taken out of it (#475: turning Copilot CLI's 32 skills off was a chip on every row, and once it was hidden on the Agents page it had no chips at all, so what it had been given stayed with no way to take it out) — By agent, beside "In the library" on the Skills and MCP tabs, opens a sheet of the agents that take them, each with how many it has, a Turn all on and a Turn all off; each writes that one agent, every other keeping what it has, and reports what couldn't be written as a row's chips do. An agent hidden on the Agents page is listed while it has any, tagged Hidden and given nothing new; its instructions row is shown while it reads the shared instructions, so it can be switched off. Skills use the existing skills/agents-all; servers get servers/agents-all (EveryServerAgents), which turns on only the servers the agent can reach (no SSE for Codex, no remote one for Claude Desktop). TestEveryServerAgents and by-agent.test.cjs (Chromium, WebKit; en, zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; every-skill, library-all, library-items, library-toggle-status, remove-all-skills, click-scroll, old-webkit pass. yetone 2026-10-02 06:30:15 +08:00
  • fe86fb3a4d gui: the page is first drawn in its saved theme, not the system's (seg-contrast failed in Chromium on the release branch: in dark the view shown read at 3.96:1, the other views at 4.69:1) — the saved theme was put on the page by app.js, at the foot of index.html, so a window kept dark under a light system had its header parsed and styled light first, and its buttons' colour transitions (the switch's views, Sync, Settings) then faded them to dark as the page loaded; the test read them mid-way, the more so as #471 made the page longer to parse. theme.js, run in the head after boot.js and before the styles, sets it from bootPrefs (or ?theme=) for the first paint; app.js still sets it as before. seg-contrast.test.cjs now holds app.js back and checks the page was already drawn in its theme (fails without theme.js, Chromium and WebKit, en and zh, dark), and reads colours once the page's transitions are over. old-webkit lets theme.js (ES5, URLSearchParams) run before compat.js; api-key-theme, from the release branch, expects a text button in --ctl-fg as #477 set. seg-contrast, settings-sections, header-zoom-fit, header-centre, click-scroll, old-webkit, api-key-theme, omarchy, agent-grip, web-page, gateway-fold, sessions-toolbar pass; go vet, go build -tags nogui, go test ./internal/gui pass. yetone 2026-10-02 06:10:49 +08:00
  • 1ea43274b0 gateway: Mistral's thinking, sent as typed parts in a Chat reply's content, reaches a Chat client as reasoning_content and a string content, and a translated one as thinking (#483: OpenCode's 'Invalid magpie/openai-compatible-chat stream event' with GLM on api.mistral.ai) — Mistral streams a reasoning model's thinking as delta.content = [{"type":"thinking","thinking":[{"type":"text","text":…}]},{"type":"text","text":"ok"}], thinking and text sometimes in one delta, and message.content so when not streamed; magpie relayed it as it came, the Chat contract has a string or null there, and the AI SDK OpenCode reads Chat with turned the first such chunk away and gave the request up (499 in the usage log). A relayed chunk or whole reply whose content is parts now has its thinking in reasoning_content, after any already there, and its text as a string content; a delta of thinking alone goes without content. Spoken to Claude Code's API or Codex's, such a chunk wasn't read at all, its thinking and text lost; it is now thinking and then text. Replies of every other upstream pass byte for byte, streamed or not. Prompts and headers are unchanged. TestMistralPartsStreamFlattened, TestMistralPartsWholeFlattened and TestMistralPartsTranslated (Anthropic, Responses) fail without the change; TestChatRelayWithoutPartsUntouched keeps other replies byte for byte. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 06:07:16 +08:00
  • 872e26e2d4 models: a favourite's star is filled, and a model not a favourite shows an outline one on hover (#482: 收藏后切换成实心星) — the model picker's star, in the window and the tray panel, was the same outline star whether the model was a favourite or not, only its colour changed, so which state it was in wasn't clear. A favourite's star is now filled in its own amber, light and dark, and shows always; one not a favourite is still an outline, shown on hover. The button says which with aria-pressed as well as its title. favorite-star (window and tray panel, light and dark, en and zh, Chromium and WebKit) fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass; click-scroll, old-webkit, model-pick pass. yetone 2026-10-02 06:06:56 +08:00
  • 10da2936da 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. yetone 2026-10-02 05:06:53 +08:00
  • 69edf9f6e4 profiles: a chip's ↻ and × act on a second click, and a profile's details close with their own ×, or Escape, without hiding the tray panel (emo172, #478: ↻ overwrote and × deleted a profile on the first click; the details closed only by clicking the chip again, and Escape on them in the tray panel hid the whole panel; closed by hand they came back with the list, closed by Apply they didn't) — a first click on ↻ or × now changes nothing and reads "Overwrite?" or "Delete?" (覆盖?, 删除?) in red, and a second does it; it goes back after 3.5s, on Escape, or when the other one is clicked, with no browser dialog. The details have a × beside Apply, which closes them, the chip where it was. Escape takes back a question, then closes the details, then the panel's list, and only then hides the panel (both the panel's Escape and the list's ran on one press, so it hid the panel with the list open). The details close with the list, so the list opens again on the chips alone, however they were closed, as after Apply. profile-confirm.test.cjs (Chromium, WebKit, en, zh, window and panel) fails without the change; panel-profiles clicks × twice. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; profile-details, panel-profiles, old-webkit, click-scroll pass (click-scroll's routing Replay case flaked once on WebKit, passes alone). yetone 2026-10-02 05:46:30 +08:00
  • 973bf4eb2c profiles: a profile's details list all four of Claude Code's tiers, one left to follow the main model saying so (#480: opus and fable, following it, were missing from the details, so they read as not set) — a tier is a quiet field, empty while it follows the main model, and the details left every empty quiet field out. A field now says which one it follows (Follows, "model" for the tiers), and the details keep such a field, empty, with it; the panel shows it as "follows the main model (<model>)" (跟随主模型(…)), or "follows the main model" when the main is the agent's default. Other quiet fields (Codex's sign-in, subagents) are still left out while empty, and what applying a profile writes is unchanged. TestDetails and profile-details.test.cjs (Chromium, WebKit, en, zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-10-02 05:30:11 +08:00