Commit Graph
1634 Commits
Author SHA1 Message Date
yetone 531dc0aa33 claude: a Claude model runs to its own window in Claude Code, not 272K, and magpie model compact on|off is the Long conversations switch from the terminal (Max on Discord: a [1m] model's context stopped at 272K, magpie wrote CLAUDE_CODE_AUTO_COMPACT_WINDOW back when it was taken out, and there seemed to be no way to change it) — on magpie, Claude Code was told CLAUDE_CODE_AUTO_COMPACT_WINDOW=272000 whatever its model, so a [1m] Claude model compacted at 272K, though Anthropic runs it to 1M (the cap was meant for other vendors' long windows: Chen's DeepSeek turns of ~550K). Now a main model that is one of Anthropic's Claude models, from whichever provider, has the env left out (magpie's own value taken away), and Claude Code compacts at the model's own window; another vendor's model of a longer window keeps the 272K cap unless Settings → Long conversations is on Full window. A value the user writes there is still kept. The switch was only in the app's Settings; magpie model compact [on|off] (alias full-context) shows and sets it, and the Settings row's description says how Claude models are treated (en, zh, ja, de). TestClaudeCompactWindow (a Claude [1m] model gets no 272000, set and synced; another vendor's after it is capped again) and TestModelCompactCmd fail on the old code. The Long conversations row read right in WebKit, en and zh. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestClaudeFailureKeepsClaudeCodesStatus flaked once under load, passes alone). 2026-10-05 00:09:49 +08:00
Fanbuz b93cc1623a fix: 显示 Codex OAuth 账号的 credits 余额 (#800) 2026-10-05 00:05:55 +08:00
yetone e8eb8005f3 gui: one magpie at a time on the Mac, so one started at login twice (its own open at login and a Login Item the user added, or the Mac reopening it after a shutdown it wasn't quit for) ends instead of putting a second bird in the menu bar (Crispin on Discord) — Wails has no single instance on the Mac, and magpie's LaunchAgent runs the binary itself, past the Mac's one-copy-of-an-app, so both starts ran: the second found the gateway served and only watched it, with its own tray icon. A magpie now takes a flock in its config dir, keyed by the config dir and its resolved executable (a sandboxed HOME, and a copy elsewhere such as the owner's magpie-dev, each run on their own); a second one that finds it held ends: a start at login quietly, one for the window or the panel opens the .app, which has the running magpie show its window (the Dock's reopen; at most once in 10 s, so it can't go on), and a magpie:// link is opened, which the Mac hands to the running one. The system lets a flock go when its holder ends however it ends, so a shutdown leaves nothing held behind. Restart to update relaunches only once the old process has gone, so it is unaffected. TestOneMagpieAtATime (a second process holds the lock, this one is refused, the holder is killed, the lock is free) fails on the old code. Two GUI launches on the real Mac weren't tried, since a GUI magpie registers its magpie:// handler with the system. go vet, builds (darwin gui and nogui, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 23:43:34 +08:00
TryWorld 05596ef4e2 davsync: scope provider and library credentials during partial sync (#744)
* davsync: scope provider and library credentials during partial sync

* davsync: a sync writes which part carries its credentials, and no blank clears one

yetone's review of the first cut found two losses the per-part markers
caused, and both are fixed by making the markers say exactly which part
they belong to:

1. A keyed computer's library-only upload cleared a keyless computer's
   private MCP token. The blanks in the uploader's library are redactions
   it downloaded and never held a value for, not clears it made; take now
   keeps the server's value for an empty secret whatever the policy is, so
   only a server taken out here is gone. The test that had the loss as
   its expected now expects the token kept.

2. Mixed versions dropped the remote's provider keys. A keyed upload
   didn't write the whole-bundle Keys bit, and an older keyless magpie,
   which reads no per-part marker, uploaded its own providers over the
   server's with nothing to keep. take sets Keys for the providers' again.

The per-part markers are three-state now: a whole collect and a magpie
before them write none, and the whole-bundle bit says it then; a sync
writes one for every part it merges — true when the upload carried that
part's credentials, false when it didn't — and that bit, not Keys, is
what a reader goes by. That is what keeps 1 and 2 apart: the providers'
whole-bundle bit is set for older readers, while the settings' and the
library's own false is what tells a new reader they came without keys and
their local values stay. take marks its merged bundle as a sync one
(BundleVersion), so a sync's readers never fall back to Keys for a part
it wrote.

The two regression tests of the first cut that encoded the losses as
expected are updated: library/clear keeps the server's secret, and the
keyed providers upload writes the whole-bundle bit while leaving the
other parts' contents alone.

A remote that main already flipped to Keys=true over redacted settings or
library isn't repaired by this: the fix keeps new syncs from flipping it,
and a keyed computer syncing those parts repairs them by writing them
whole.

Validation on Windows: go test ./internal/... under a temp HOME passes
whole; the davsync, backup, access and settings packages pass with -race;
stashing the change fails TestTakeScopedPartCredentials (8 cases), the
scoped-sync tests that cover both losses, and the balance-token and
restore-scope tests that read the markers. go vet ./internal/... and the
darwin, GOOS=linux and GOOS=windows builds pass.

* davsync: preserve local secrets when applying partial uploads
2026-10-04 23:42:12 +08:00
yetone 258eae592d agent: Cursor Private Inference, which magpie finds by its product.json and starts on magpie with a command its row copies, and whose /models gives each model's api_types and capabilities (#299, mamba on Discord) — the build (Cursor 3.23.x, the same app name, bundle id and ~/.cursor as Cursor) runs its agent locally and asks the endpoint in CURSOR_LOCAL_AGENT_BASE_URL / CURSOR_LOCAL_AGENT_API_KEY when its Open configuration has none; mamba confirmed it works with any account pointed at magpie. magpie writes nothing of Cursor's (its settings share Cursor's state.vscdb): the Agents page lists it when an app's product.json says "Cursor Private Inference" (/Applications, ~/Applications; %LOCALAPPDATA%\Programs on Windows; /opt and the like on Linux), and its row's one control, Copy launch command (复制启动命令), copies CURSOR_LOCAL_AGENT_BASE_URL=<gateway>/v1 CURSOR_LOCAL_AGENT_API_KEY=magpie-cursor-local '<app>' (PowerShell on Windows); in the tray panel the row copies it. Its notice says to quit it first (a second start hands over to the running one) and that a base URL in Open configuration comes first. /v1/models asked with its key gives each model api_types (the APIs its provider serves it on, all three for a group or a translated one; it asks Anthropic Messages only when no OpenAI one is listed) and capabilities (context_length, max_output_tokens, supports_vision); any other client's list is unchanged. usage.AgentOf matches a name before a UA prefix, so cursor-local isn't counted as Cursor. TestCursorLocal, TestCursorLocalModels and cursor-local-launch.test.cjs (WebKit, en and zh) fail on the old code; agy-launch and agent-* GUI tests pass on WebKit. Not tried with the real app (not installed here). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 23:38:19 +08:00
yetone 80441c2c29 gui: a provider's editor has a Max output beside its Context window, so a model whose reply limit magpie has wrong can be set right there (ARNO on Discord: 上下文长度有问题还可以手动覆盖,maxtoken错了如何解决呢) — the reply limits the user sets (ModelOutputs, "provider/model" or "provider/*") could only be set with magpie model output; the editor now shows them in a Max output box written as the Context window box is (32k for all its models, or gpt-6=128k, comma separated), and a Save sends them: the limits not in the box are dropped, a model the provider doesn't list is refused, and the catalog is touched so /v1/models, the agents' max tokens and the gateway's per-member clamp take the new limit. zh and ja strings added. TestModelOutputsSetFromTheEditor, TestProviderSaveOutputs and provider-max-output.test.cjs (WebKit, en and zh) fail on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestClientGoneRecordedAsCanceled flaked once under load, passes alone). 2026-10-04 23:37:35 +08:00
yetone 3f5cd1f3cc tray: the Mac's menu bar icon can be Command-dragged again, so Hidden Bar and the like can move it (#792, BennettChina: after v0.1.865 a Command-click no longer opened the panel, but the icon still didn't move) — magpie's own monitor of mouse-downs on the icon (#629, which keeps it from flickering) took every left click on it and handed it to the action, so the button never saw a Command-held mouse-down, and that is what starts the system's drag. One with Command held is now left to the button. On this Mac (macOS 26.2) with a GUI build in a sandbox HOME: a synthetic Command-drag of its icon 60 px right did nothing on origin/main and swapped it with its neighbour with the change; the same drag moved a bare NSStatusItem, and one set up as Wails does (sendActionOn mouse-down, its local monitor) in both cases; a plain click still opened and closed the panel. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows), go test ./internal/gui/ and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 23:37:07 +08:00
tobitege 21702d01ff Add German translation; harden paths against unset env vars (#770)
* Add German translation and harden paths against unset env vars

Add a German locale to the GUI, tray menu, usage alerts and website home page, and refuse to start when the home folder is unset or relative so nothing is written under the working folder; the pi plugin no longer adopts the user's PI_CODING_AGENT_DIR, and an inherited APPIMAGE no longer selects portable mode. Scripts refuse to run from the wrong folder or empty a shared out folder, and a new internal/testenv sandboxes every test package from the user's real files, which also fixes the Claude picker and Termux installer tests on Windows.

* Makefile: know magpie's folder by its go.mod, so paths with spaces work

The check compared $(CURDIR) with $(dir $(abspath …)), which make splits on spaces, so a checkout under 'My Projects' or 'John Doe' refused every target. It now reads go.mod relative to the working folder and wants magpie's module there: make -C and a symlinked Makefile work, make -f from another folder still stops before any recipe runs.

* i18n: German for the strings main added since, with a parity test

75 strings main added (group order, patterns, slow-start wait, usage caps, RTK savings …) get their German, and two that main dropped go. gui-de.test.cjs checks, as gui-ja does, that de has every zh string with its placeholders and tags, and that the window reads in German when picked or on a German system.

* appdir: ignore relative folder variables when reading, keep them for children

CheckEnv no longer unsets anything. magpie reads the XDG folders, APPDATA/LOCALAPPDATA and every agent's folder variable through appdir.Getenv/LookupEnv, which pass over a relative value; the environment stays as it is, so the agents magpie starts get the variable as the user set it. That covers what proc.UserPath lends from the login shell too, which is set after main's check and reached the desktop app unchecked. On Windows a path rooted on the current drive (\Users\x, Git Bash's /c/x) counts as usable, and magpie version and help answer without a home, before run makes any folder.

* Sandbox the tests main added since

internal/wslrun gets testenv's TestMain, and four tests main added that set HOME alone set their home with testenv.SetHome, so on Windows they don't read the runner's profile through USERPROFILE.

* agentenv: PI_CONFIG_DIR and OPENCODE_DB take relative names on purpose

omp puts $PI_CONFIG_DIR under the home and OpenCode a relative $OPENCODE_DB in its data folder, so neither depends on the working folder: appdir no longer passes them over.

* i18n: German for the Library's multi-select and System One's decision API

The 19 strings main added since (#791's skill multi-select, the decision API snippets) get their German; the one main replaced (four APIs → five APIs) goes.

* appdir: tidy the import into magpie's group; say what Go makes of /c/x

The appdir import joins the group of magpie's own imports where a file has one. rooted's comment now says that Go takes a Git Bash path like /c/x as \c\x on the current drive, not C:\x.
2026-10-04 23:30:56 +08:00
Zhizhen He 689b819294 budget: keep reservations in their original window (#796)
A request still in flight across a calendar reset could reserve the next
window's entire budget even though that window had no usage. Scope held
reservations by the usage file, gateway key and window start, and use the
same key for admission, status and release.

Cover day, week and month boundaries with token and cost caps, including a
late check for the old window and settlement while the new window has an
active reservation. All six cases fail before the change. The key-limit
tests pass normally and under the race detector; access tests and vet of
the affected packages also pass.
2026-10-04 23:30:42 +08:00
Zhu Wang c1f919ca09 fix(routing): simplify the request purpose filter (#795) 2026-10-04 23:30:01 +08:00
yetone 0e7a1f6b6a cli: magpie <agent> default takes magpie out and puts back what the agent had before, as the Agents page's Disconnect does (__jingling on X: magpie claude default took the model they had set away with magpie's) — the agent-wide default set its first field to the agent's own default, which for Claude Code is Claude Code as installed: settings.json's model was deleted with magpie's endpoint and tiers, so a user on opus before magpie was left on no model at all. On an agent magpie is wired into it now runs Disconnect (Unwire, then each field magpie set back to its default), which puts back the model, endpoint and token stashed when magpie was wired in; Codex goes back to its own model the same way. magpie <agent> <field> default is as it was, the field to the agent's own default. The README and usemagpie.ai said under 15 MB for the desktop app and 7 MB for the terminal build; v0.1.869's are 27–30 MB (CLI) and 31–35 MB (desktop on Linux and Windows), a 13–16 MB download on a Mac, and say so now (en, zh, ja). In a sandbox HOME: settings.json with // mine, model opus and theme dark, magpie claude deepseek/deepseek-chat, then magpie claude default: model opus back, the env cleared, the comment and theme kept (on origin/main: no model). TestAgentDefaultPutsBackTheModelBefore fails on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 23:25:03 +08:00
yetone 0fd1c5c8f5 agent: an omp in WSL has its version asked in the distro, so its models offer max from 16.4.0 (whqtian on Discord: a WSL omp still had max made xhigh) — the omp there isn't the one on Windows' PATH, so its version wasn't known and was taken for an older omp's, whose models.yml refuses max. The distro's probe now asks the distro's own omp (not one on Windows' drives) its --version, with a 10 s timeout, and the WSL omp's entry is built for that version; one that says none is taken for an older omp as before. The version is kept with the distro's other probe results, and asked again when the distro is next started. TestWSLOmpVersionProbed; the probe script run under sh with a fake omp printed ver:omp omp/16.5.1. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 23:24:02 +08:00
yetone 7909de22ac usage: magpie usage and the TUI's Usage page show the agents' own calls, read from their session files, beside the gateway's, and a Codex-based client's calls aren't counted twice (Kumo31 on Discord: Codex used directly, not through magpie, was on the window's Usage tab but not in magpie usage) — magpie usage and the TUI only ever summed the gateway's log (usage.Summarize); the calls the agents' session files record that the gateway never saw (Codex on the user's own ChatGPT sign-in, Claude Code, Claude Desktop…) were merged only into the ledger the window's Requests tab and the tray panel read (#239). usage.Direct sums those rows of the ledger (Source "log", this computer's, rejected ones left out) by agent, model, account and session, priced as the ledger prices them, and magpie usage prints them after the gateway's tables under "not through magpie · the agents' own requests, read from their session files" (a model whose provider the file doesn't tell reads "local session/<model>"); the TUI's Usage page shows the same block under the gateway's, each with its totals and agents' and models' tables. The two parts are separate, so nothing is counted in both; a period with only the agents' own calls shows them under "no calls through magpie". All of magpie usage's output now goes to the writer it is given. Checking it against the owner's real data showed the ledger counted a Codex-based client's calls twice: miyi-helper's requests through the gateway are logged under its own name while its Codex session file names Codex, and the gateway/session-file matcher keyed on the agent, so they never matched; the matcher now keys on the session, tokens and time alone (a session id names one conversation), which also corrects the Requests tab. With read-only copies of the owner's ~/.config/magpie and four days of ~/.codex/sessions in a sandbox HOME: magpie usage week printed 177 calls not through magpie, in 1.3M, out 52.4K, cache read 3.9M, gpt-6-astra 154 and gpt-6-sol 23 — the same rows, to the token, as usage.LedgerOf (what /api/usage/requests serves) gave for the period; before the matcher fix, today's 7 miyi-helper calls showed in both parts. TestDirectCountsOnlySessionFileCalls (the miyi-helper case included), TestUsageShowsCallsNotThroughMagpie and TestUsagePageShowsCallsNotThroughMagpie fail on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 23:03:15 +08:00
yetone 2f5ff69d3d gui: the Gateway page's Connect speaks System One too, with a decision model (ARNO on Discord: gateway添加system one api 有想法吗) — magpie's gateway has served POST /v1/systemone, but Connect showed only OpenAI, Responses, Anthropic and Gemini, so nothing on the page said so or how to ask it. Connect has a System One tab: base …/v1, curl, Python (requests) and Node (fetch) snippets that POST …/v1/systemone with Authorization: Bearer, a state and a choice question, printing the answer's choice and confidence; the note says Clef also reads images ("images": [data URLs]). No SDK speaks it, so it has no Shell tab, and the base URL and key hints say where the request goes and how the key is sent instead of naming env variables. Its model is a decision model only (decidesModel: a decision-only provider's, or a Jev), and the other tabs no longer take one; a click on a decision model in the list switches Connect to System One, a click on any other model back to OpenAI, each tab keeping its own pick (magpie.decideModel). The head says five APIs; Connect's tab strips scroll in themselves on a narrow window, as the region strip does, rather than widening the card. zh and ja strings. gateway-systemone.test.cjs (WebKit, en and zh, light and dark) fails on the old code; gateway-connect-keys, -model-info, -fold, -older, -caller-keys, -key-limit and gui-ja pass on WebKit; Chromium doesn't launch in this sandbox. The snippet's request, sent to the gateway's handler, reached a Workers AI Jev and Clef in their own shapes, and with the owner's real TypeSafe key in a sandbox HOME, jev-latest answered 200 with choice billing, confidence 0.55. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 22:59:23 +08:00
yetone 26706c3371 library: skills are picked to turn on or off together, and a part of a list (lark-*) has its own chips (#791, mintonight: 资源库中local中的技能,能不能支持多选分组,或者支持多选来开启或关闭 … lark-* 我希望把他们归到一起统一来管理) — the skills on this computer are one list with no heading when they're all from there, so #787's group chips never showed for them, and Feishu's lark-* skills were turned on or off one row at a time. A part of a big list (its skills' shared name start, or their folder) now has chips on its heading, as a repository's heading does: lit when an agent has them all, half lit when it has some, a click gives it every one or takes them all, All does it for every agent; a click doesn't fold the part. Select, beside Turn all on, puts a box on every skill's row and on every heading (ticked when all under it are picked, half with some), a row's click picks it instead of opening its SKILL.md, and a bar that stays in view under the list says how many are picked, has a box for every skill the filter shows ("All 4 that match"), the same chips for the picked ones, Clear and Done; the picks stay through a change and Done takes the boxes away. A pick doesn't scroll the page. zh and ja strings added. pick-skills.test.cjs (WebKit, en and zh): the lark part's Codex chip posts the four lark skills, rows and headings pick, the bar's chips post the picked ones, the filter's All picks what it shows, Done ends it; it fails on the old code. Looked at in light and dark. group-skill-chips, every-skill, library-all, library-items, library-toggle-status, library-row-open-scroll, own-skill, remove-all-skills, shared-skills, import-all-skills, library-own-folder and library-loading pass on WebKit; Chromium doesn't launch in this sandbox. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./internal/gui/ under a temp HOME pass. 2026-10-04 22:52:01 +08:00
AaronYiandyizhuolin e8cd4bac8c fix: 透传 Claude 订阅审批上下文与服务端结果 (#794)
Co-authored-by: yizhuolin <yizhuolin@i-calorie.com>
2026-10-04 22:51:45 +08:00
yetone 7df9345903 routing: a group tells agents the longest reply a member of it gives, and each member is asked for no more than its own (ARNO on Discord: 发现group 模型的maxTokens是按照最小的来设定的,导致agent经常跑一半发生错误 "Response was truncated before completion.")
A group's catalog entry took the smallest member's output limit, so /v1/models and the agents' catalogs told agents that, and every reply through the group was capped at it, even from the members that write far longer ones. The group's Output is now its largest member's (as its context window already is, #700), and the gateway, for a group request, lowers the agent's max tokens to each member's own known limit (its catalog Output: the user's per-model setting, else the vendor's list or models.dev) only when the request asks for more: max_tokens / max_completion_tokens (Chat), max_output_tokens (Responses), generationConfig.maxOutputTokens (Gemini), and for Anthropic max_tokens with the thinking budget kept under it (budget = limit-1, or thinking off when no 1024+ budget fits). A request asking for less, or a member with no known limit, goes as before.

TestGroupOutputPerMember (members at 4096 and 32000: the group says 32000; asked for 20000, the 4096 one gets 4096 and the 32000 one 20000; asked for 1000, 1000) fails on the old code at either half; TestWithMaxOutputAnthropicThinking. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn, TestUserPath and TestClaudeSignedOut failed under load and pass alone).
2026-10-04 22:48:06 +08:00
yetone 3cdb7fb3a5 gui: a Command-click on the Mac's menu bar icon is left to the system, which drags the icon with it (#792, BennettChina) — Hidden Bar's users move icons with Command-drag, and magpie's icon opened its panel instead. The tray's click handler now reads the held modifiers ([NSEvent modifierFlags]) and does nothing when Command is down; other systems are unchanged. go vet, builds (darwin with the GUI, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./internal/gui/ under a temp HOME pass; the drag itself wasn't exercised here (it would move icons in the owner's live menu bar). 2026-10-04 22:45:28 +08:00
Junjie Zhou 28a830fc5c aside: magpie picks Aside's model, its thinking level, its four task models and its picture model (#789)
* aside: magpie picks Aside's model, its thinking level and its four task models (X: junjie — Aside Settings → Models has a Default model row magpie could not touch, and Fast/Standard/Deep/Visual rows it could not reach at all)

Aside reads the same provider list Pi does, so magpie hands it the whole
catalogue in a provider block of its own, carrying the magpie-aside token so
its requests are told apart in usage. What differs is the default: Aside keeps
it as one object in ~/.aside/u/0/settings.json naming the provider, the model,
the thinking level and fast mode together, so the model field reads and writes
two keys where Pi's are two strings, and both go through Aside's own settings
API — the running daemon holds its default and writing the file behind it
leaves the daemon on what it already had, read back the same way with the old
model still named after magpie had written the new one. The API is reached
through the repl, the settings command having no set of its own.

The four task roles are the same shape as the default and are wired the same
way, each a Quiet field following the model, so a role the user has not set is
not a line of its own and follows the default. A role is written whole:
thinkingLevel is required by Aside's schema and a role without one is refused,
so a role keeps the level and fast mode it had, a new one takes the default's,
and where neither is set it takes the level Aside gives its own roles. Taking
magpie out puts every role back, the user's own model where there was one and
nothing at all where magpie was the only thing that set it, and a role the user
moved onto a model of their own is left alone.

Each change checks the value that came back: Aside answers a set with the
settings as they now are and drops what it will not take without a word — a
role naming a model it has not resolved yet is the one that happens — so
without the check a change that went nowhere read as one that landed. Caught,
it is written to the file, which is what Aside reads at its next start, and
the Agents page says so.

The account magpie wires is 0: the folder is ~/.aside/u/0 while the CLI names
it u0, and TestMain stands in for the binary so a test can never change the
real Aside's default. A models.json carrying a magpie provider of someone
else's is refused rather than written over, and one that will not parse is the
user's to fix.

With the owner's real Aside under a sandbox HOME: a pick reached the gateway
and was attributed to a client named Aside, the default put the user's own
minimax model back and took the magpie provider out, and a role set through
magpie was read back by the running daemon the moment it had a magpie model to
resolve. Not tried: a change written to the file taking hold at Aside's next
start, which needs a restart. go vet, builds (darwin, GOOS=linux,
GOOS=windows) and go test -tags nogui -count=1 ./... pass (TestBunThroughBridge
fails, reproduced on a pristine worktree of the parent; internal/plugin passes
alone and times out only under the full run's load).

* aside: the row appears for an Aside that is installed and not yet used (the CI preview of #789 could not find .row.agent[data-id="aside"])

Detected() took ~/.aside/u/0, the account folder, and that only exists once
someone has signed in. The binary was the other look and the CLI installer
links it into ~/.local/bin, which a magpie launched from Finder has no PATH
entry for, so an Aside that was installed and never used showed no row at all —
the preview installed one, drove the UI and could not find it in either the
installed list or the row selector. The installer writes ~/.aside/cli and the
daemon writes the rest, so ~/.aside is the folder that is there from the
install, and it is the one Detected() should ask about. Path stays the
account's settings.json, which is the file magpie edits and the only one
worth reading a model out of.

TestAsideIsDetectedBeforeAnyAccount fails on the old Dir: it takes PATH away
so the binary cannot be found either, makes the installer's folder and no
account, and expects the row.

* aside: a wedged repl gives up, and the model comes off through Aside (yetone on #789)

Three things about the way out of an agent and into one, all found on a real
Aside.

The repl had no timeout. proc.Command(...).Output() waits as long as the
command does, so a daemon that has wedged or one waiting on input leaves a pick
in the Agents page or the CLI hanging on it, and switching off asks up to five
times over — the default and four roles. proc.CommandContext with ten seconds:
Aside answers in milliseconds, and a run that does not is written to the file
instead, which is what Aside reads at its next start, with the notice that says
so. TestAsideGivesUpOnAReplThatHangs runs the real command against an aside
that never returns: it passes in the timeout, and without CommandContext it
hangs until the test's own timeout kills it.

Switching off with no model of the user's behind magpie's wrote settings.json
directly. By then the provider block was gone, so the running daemon went on
using magpie/… with no way to reach it, and asideStale was never set, so
nothing said the change needed a restart. It goes through asideApply like
every other change, with the value that came back checked.

The provider block now goes last, after the default and the four roles are put
back. A step that fails on the way out otherwise leaves the settings naming a
provider that is no longer there. TestAsideDefaultTakesTheProviderOutLast
watches the block from inside the settings step and fails on the old order.

The icon is Aside's own brand-embossed-light.svg with the embossing's
transparency taken off, so the mask is solid and takes the row's text colour
like every other agent. As it was, the white fills at 8–20% opacity the dark
and light brand files are built with left the mark almost invisible on a light
row, and it vanished entirely in dark mode. Same paths, same shape, now solid;
checked in the app next to Pi's.

Tested against Aside 1.0.928.1 (daemon 1.26.928, CLI 1.26.916.1741) on
darwin/arm64. go vet, builds (darwin, GOOS=linux, GOOS=windows) and
go test -tags nogui -count=1 ./internal/agent pass (408).

* aside: the model Aside draws with is magpie's to set too (X: Aside Settings → Images had no row for it)

Aside keeps a fifth model setting beside the default and the four task roles:
imageGenerationModel, the provider and the model it draws pictures with and
nothing else — no thinking level, no fast mode, since neither means anything for
a picture — and no key at all on an account that has never picked one.

It is a magpie field like the roles: Quiet, so it is not a line of its own on an
account that has none, and taking the model after while it is unset. A model of
magpie's needs the provider block to reach it through, and what it named before
is remembered the same way a role's is, so `magpie aside model default` puts the
user's own picture model back and takes a picture model of magpie's off rather
than leaving it naming a provider that is no longer there.

Not wired: customModels, which is the user's own model definitions with their own
credentials, and asideModelCatalog, a list of model ids to pin or hide. Neither
is a model magpie picked, so neither is a setting of magpie's to own.

Aside 1.0.928.1, daemon 1.26.928. go vet, the agent/gui/profile tests, the three
CLI cross-builds and the CLI verification under a temp HOME all pass.
2026-10-04 22:45:16 +08:00
yetone 23eb6c5f32 gateway: a Clef asked through magpie's System One API gets the images the request embeds (ARNO on Discord: clef模型是支持图像输入的) — /v1/systemone already passed a request's fields to the Clef as they were, images among them, but refused any request over 1 MiB with a 413, so a single embedded screenshot didn't fit. The limit is now 16 MiB (Workers AI takes up to four images, 13 MiB of request in all). TestSystemOneClefImages (a 3 MiB data-URL image to cloudflare-jev/@cf/cloudflare/clef: answered, asked at the Clef's own path with model clef, the image reaching it unchanged) fails on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 22:36:56 +08:00
yetone 12acf07a30 gui: checkboxes are drawn as magpie's own, not the system's (ARNO on Discord: 会话面板下的checkbox的样式和软件整体的设计风格有点割裂) — the Sessions page's boxes, and every other checkbox (Library picks, import apps, caller-key limits), were the system's: white squares on the dark page. They are now a 14 px rounded box on the card's colour with a faint border, filled with the accent and a check in its foreground when ticked, a bar when partly ticked (a folder with some of its sessions picked), a ring on keyboard focus; pages keep their own margins. checkbox-look.test.cjs (WebKit, en and zh, light and dark: unticked on the card, ticked and partly ticked filled with the accent, a click still ticks and unticks) fails on the old code; Chromium doesn't launch in this sandbox. Looked at in light and dark on WebKit. sessions-manage, -folder-delete, -toolbar, -purge, -bar-names, gateway-caller-keys, gateway-key-limit, library-items, -all, -desktop-skills pass on WebKit; go test ./internal/gui/ passes. 2026-10-04 22:32:19 +08:00
yetone a94cc3c950 provider: Workers AI's decision models (Jev, Clef) are tested from their right-click as System One's are (ARNO on Discord: cloudflare-jev provider模型无法右击测试) — a model test of a cloudflare-jev provider said its models couldn't be tested, since the test sent System One's question as it is and Workers AI wants it wrapped. DecideAsk and DecideAnswer move from the gateway to the provider package, and AskSystemOne (which TestModels uses) now wraps the question for the provider's API and reads the answer out of Cloudflare's envelope, as routing's own decisions do: Jev is asked {model, input}, a Clef at /run/@cf/cloudflare/<clef> with its own model selector. TestCloudflareDecideModelsTested (an httptest Workers AI: both models pass, the bodies are each one's, a refused key fails) fails on the old code; TestModelTest's cf case now expects testable. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn flaked once, passes alone). 2026-10-04 22:22:23 +08:00
yetone 25826ea19f gui: the Usage chart's days are written in the page's language (the owner: 用量图表的日期也按语言格式化) — the backend labels a day "Sep 5" in every language, and the Overview chart put that label under its bars and in their titles in Chinese and Japanese too. The chart now writes each point's time as the page's language does (toLocaleDateString with intlLang, month short, day): "Sep 5" in English as before, "9月5日" in Chinese and Japanese, and a week's bar "9月12日 起的一周" / "9月12日 の週". An hour's bar keeps "15:00". usage-chart-dates.test.cjs (Chromium and WebKit, en, zh and ja: the days under the bars, a day's, a week's and an hour's title) fails for zh and ja on the old code. The usage-*, number-units, gui-ja and panel-usage GUI tests pass, but for usage-refresh-flight's two Chromium cases, which fail the same on origin/main. go build (darwin, GOOS=linux nogui) passes. 2026-10-04 22:13:12 +08:00
yetone 2dc8e249cb agent: dsh on a DeepSeek model picked in its own /model stays connected while magpie's route is in its list (Fate on Discord: DSH went back to Not connected) — magpie connects dsh by putting its route among dsh's custom providers and starting it on one of magpie's models; a model of DeepSeek's own picked in dsh is saved in its settings.yaml over the patch list, and the Agents page, which called an agent connected only when its model field was magpie's, showed it Not connected, though the route, its key and magpie's models in dsh's /model were all still there. dsh now has Joined (the first profile's patch list has magpie's route), as Codex signed in with ChatGPT does, so it reads Connected · pick magpie's models with /model, and a pick off magpie while joined isn't drift ("changed outside magpie"). Disconnect in that state has an Unwire that takes the route, the start magpie wrote and the key out of every profile and .env and leaves the model picked in dsh; on one of magpie's models it does nothing and the model set back does it all, as before. TestDshOwnPickStaysConnected (connect, a DeepSeek pick in settings.yaml: wired, no drift; Disconnect: the template as it was, no key, the pick kept) fails on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 22:03:41 +08:00
yetone ee3fca6375 decide: Cloudflare's Clef and Clef Flash are offered on Workers AI beside Jev (ARNO on Discord: cloudflare workers-ai 现在有了新的 decision 模型) — Workers AI now serves Cloudflare's own decision models, @cf/cloudflare/clef (27B) and @cf/cloudflare/clef-flash, which answer System One's questions as Jev does. The cloudflare-jev provider (now named Cloudflare Workers AI · Jev, Clef) lists them beside typesafe/jev, so a routing group can pick either as its classifier (cloudflare-jev/@cf/cloudflare/clef), and magpie's System One API routes to them by name. Each is asked at its own run (…/ai/run/@cf/cloudflare/clef), with the request as it is and its model named clef or clef-flash, as Cloudflare's docs give it; Jev is still a run of typesafe/jev with the questions as its input, and stays the default. TestCloudflareClef (offered, routed, each one's URL, a Clef id outside Workers AI no decider) and TestClefOnCloudflare (a group classified by Clef Flash through Workers AI) fail on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against Cloudflare: no Workers AI key in the owner's config. 2026-10-04 21:52:11 +08:00
yetone 56d5341636 routing: an open group folds back into its row from its editor's heading (ARNO on Discord: 路由组详情现在可以单击展开,是否可以再加上单击收起) — a group's row opened its editor on a click, and only Cancel closed it. The editor's heading of an existing group is now a button (pointer, "Click to fold it", aria-expanded, Enter or Space from the keyboard) that folds it back while nothing in it has changed; with a change it stays open and says to Save or Cancel first, so a stray click never throws an edit away. Folding saves nothing. zh and ja strings. group-fold.test.cjs (WebKit, en and zh: click, Enter, a change kept with the warning, Cancel, nothing posted) fails on the old code. group-first-token, group-arrange, group-member-off, group-patterns and gui-ja pass on WebKit; group-from-model-add's 600x420 case fails the same on origin/main. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./internal/gui/ under a temp HOME pass. 2026-10-04 21:50:16 +08:00
yetone 0b8fca7380 gateway: a tool schema's oneOf goes to Cline, Kilo and OpenRouter as anyOf (H20 on Discord: Cline answered 400 "unsupported schema keyword: oneOf" for Codex's automation_update tool) — some of the models those routers serve sit behind a grammar compiler (xgrammar; OpenRouter's ModelRun) that refuses oneOf "with overlapping or non-provably-disjoint branches" and takes anyOf, which to a model choosing what to call says the same. Every oneOf key in a tool's parameters/input_schema (through properties, $defs, items, allOf/anyOf and the other schema keywords) is renamed in place, the same length, so nothing else of the body changes and the prompt cache keeps its prefix; a property called oneOf and a description that says it are left alone, and every other provider is sent oneOf as before. With the owner's real OpenRouter key in a sandbox gateway, qwen/qwen3.8-27b:free pinned to ModelRun with a oneOf tool: before, 400 from xgrammar; now a tool call {"id":"7","change":{"paused":true}}. TestToolOneOfAsAnyOf (Cline, OpenRouter renamed; nested, items; a property named oneOf and the description kept; another provider unchanged) fails on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 21:38:58 +08:00
TryWorld 64a4e1014b fix(gui): a reply that broke off is told as failed everywhere (#772)
* fix(gui): a reply that broke off is told as failed everywhere

733's tryOk rule — a try with a status under 400 and no fail didn't
answer — was put through the Routing page's log and its try tally, but
every place that judges a request as a whole still read the status alone:
a request whose reply had begun and then broke off (its 200 sent, the
vendor's error after it) kept an answerer it never had. Its row was a
green dot reading "provider · model" as if it answered, the replay and
the Agents panel told the same, and the "errors your agent saw" stat,
which counts the request, picked nothing when clicked since it only knew
statuses of 400 and up. The log of the very request said it began
answering and then broke off, so the page disagreed with itself.

outcome() asks tryOk for who answered, and a route with an error is bad
even under 400; the swap and routed marks, on the row, the seat and the
log, only show on a try tryOk answers; the stat's click knows an error;
and the two play animations send a try with a fail back as failed. The
row, the replay, the Agents panel, the stats and the animations now tell
the same story the log does.

routing-broke-off.test.cjs — Chromium and WebKit, English and Chinese —
fails on the old page with the row's class "rt-req ok" and passes: the
row is bad, the errors stat picks the request, and its story says it
began answering and broke off.

* fix(gui): a 200 with a note of its own is still an answer

yetone's review of the first cut: outcome() read a route's error field
for "bad", but a 200 whose error is magpie's own note answered — Codex's
titles off in Settings writes one, as a title reply with no title in it
does, and their tries carry no fail. Every titled thread's first request
turned red. Reads the tries now, as the rest of the rule does: a route
under 400 is bad only when it has tries and none answered.

The Agents panel's row said so too: its class came from the outcome, but
its "where it went" text still branched on the status alone, so a
broken-off request showed red with "→ provider" and no failure. It
branches on the outcome now.

routing-broke-off.test.cjs gains the note case (200, an error, a try with
no fail): its row is "rt-req bad" on the first cut and "ok" with this.
The broke-off case's rows, the errors stat and the story still hold, on
Chromium and WebKit, in English and Chinese.

Validation on Windows: node --test routing-broke-off.test.cjs — 8/8 on
Chromium and WebKit in both languages; both cases fail on the code
before this commit's routing.js (the broke-off row as "rt-req ok", the
noted one as "rt-req bad").

* fix(gui): the errors stat picks a broken-off reply, not a noted 200

yetone's second round: the stat's click looked for a status of 400 or an
error, which a 200 whose error is a note of magpie's own — Codex's titles
off in Settings, a title reply with no title — answers: every titled
thread's first request was a newer "error", and "show the latest request
that failed" jumped to a green row. It asks the same question the rows
and the story do now — outcome(r)[1] === "bad" — so the stat, the rows and
the story name the same request.

routing-broke-off.test.cjs puts the noted 200 above the broken-off one:
clicking the stat picks the broken-off request under it, and the noted row
is not picked. That fails on the previous commit (the noted 200, newer and
with an error, was picked) and passes. Two test fixes go with it: the
unpicked row's aria-pressed is "false", not absent, and the day list is
waited for before it is clicked, which webkit needed mid-run.
2026-10-04 21:27:46 +08:00
yetone 13742b1cb4 library: a repository's skills are turned on or off for an agent at once from its group's heading (#787, mintonight: 我记得昨天还能直接一次性开关一个作者的所有技能包,怎么今天不行了) — the heading of a repository's group (owner/repo, N skills) showed which agents had its skills as plain icons, which looked like the rows' chips but did nothing, so its skills could only be switched one row at a time (or every skill in the library at once). The icons are now chips, as on a row, with All ahead of them: a chip is lit when its agent has every one of the group's skills, half lit (dashed, the icon dimmed) when it has some, and a click gives that agent every skill of the group or, when lit, takes them all; All gives them to every agent shown, or takes them from every one when all have them. Other groups' skills and agents not clicked keep what they have; a click doesn't fold the group. The page sends the group's skill names to a new library/skills/agents-some, library.SomeSkillsAgents, which is EverySkillAgents for the skills named alone (one write; a name not in the library is refused). zh and ja strings. TestSomeSkillsAgents (on and off for named skills only, the agents' folders follow, an unknown or empty name list refused) and group-skill-chips.test.cjs (WebKit, en and zh: lit/half-lit, a click on a half-lit chip, a lit chip, All on and off, the requests sent, the group not folded) — the latter fails on the old code. gui-ja, list-sort, own-skill, shared-skills, every-skill, library-all, library-toggle-status, library-items, remove-all-skills, import-all-skills, library-row-open-scroll and click-scroll pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 21:23:31 +08:00
Zhizhen He c744a097b6 agent: avoid panics in disconnect preview masking (#788)
Check for a key/value match before indexing its value boundary. Compact
JSON arrays and TOML table headers can contain secret-related words
without matching that pattern.

Cover both cases in TestMask.
2026-10-04 21:13:31 +08:00
yetone 30c40e2f4a library: Droid's and Grok Build's own AGENTS.md take the shared instructions (frankittee on Discord: Droid, Grok Build 没有用户级指令文件, 但其实是有的) — the Library's Instructions said both keep no user-wide instructions file, so they couldn't be switched on or edited there. Droid reads ~/.factory/AGENTS.md as personal instructions that project files override (docs.factory.com/cli/configuration/agents-md), and Grok Build loads $GROK_HOME/AGENTS.md (else ~/.grok) first, as its global base; both are now their instructions file, magpie's block written after what the user has. TestDroidGrokInstructions (GROK_HOME honoured, the user's text kept) fails on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 21:04:49 +08:00
yetone 1d404db710 site: the Japanese docs show magpie in Japanese (the owner: 日文文档也要有日文截图) — the ja pages of Getting started, Intent routing and Import showed the English window. Each of their nine shots is taken again with the GUI in Japanese (v0.1.851): once in magpie's own dark and light look (/img/<name>-ja-dark|light.png, drawn by Chromium on the Mac like the en and zh ones) and once for every one of the 22 Omarchy themes (/img/omarchy/<theme>/<name>-ja.webp, lossless, in the VM's Omarchy). omarchy.js takes -ja as it takes -zh, with the sizes of the Japanese shots that differ. Alt texts name the tabs as the Japanese UI does. Locally, with the light look and three Omarchy themes, all nine images on the three pages are the Japanese ones, loaded, at their own ratio. 2026-10-04 20:51:05 +08:00
yetone 9d3d68eb05 i18n: the purpose filter (#771) speaks Japanese — the Usage and Routing pages' purpose picker landed with its zh strings only, written before 日本語 was added, so gui-ja failed on main (six strings missing in ja). They are added: すべての用途, 用途, 未記録, 不明な用途, its tooltip and the picker's title. gui-ja and purpose-filter pass on WebKit; go test ./internal/gui under a temp HOME passes. 2026-10-04 20:47:47 +08:00
zzzpyyy 57e80efbf4 feat: filter usage and routing requests by purpose (#771) 2026-10-04 20:46:46 +08:00
yetone 8026e3ce15 claude: Claude Code's auto mode checks read their prompt from the cache, and a request with thinking off runs with it off (0xAncientTwo on X: Claude 额度在 magpie 下掉得特别快) — behind a gateway Claude Code's auto mode classifier is a request of its own for each action it checks, billed to the subscription (on Claude Code's own sign-in Anthropic runs it server-side), with a ~37k-token security-monitor prompt. Its system starts with x-anthropic-billing-header: cc_version=….<hash>, the hash taken from the request's first user message, which for the classifier differs on every check; the bridge put the system, header first, into the cached instructions block, so every check wrote all ~37k tokens to the cache again (2x the input price at the 1h TTL) and read 2303. renderClaudePrompt now leaves the header out (Claude Code's runs send their own). Claude Code's small asks and its classifier also send thinking disabled, and the bridge ran them at Claude Code's default, which on Haiku thought 1.8k–5k output tokens before a 64-token verdict or a title; a request with thinking off now runs with --thinking disabled, and that is part of the run's key, so it never resumes a run that thinks or the other way round. With the owner's real Claude subscription, the captured classifier request replayed twice with two hashes: on origin/main each check wrote 36482 and read 0; now the second wrote 343 and read 36086 (Sonnet 4.6, 10 tokens out). On Haiku 4.5 a check had 2599 reasoning tokens before and 0 now. TestClaudeInstructionsWithoutBillingHeader and TestAutoModeClassifierOnTheClaudeBridge's --thinking disabled check fail on the old code; the gateway's TestClaude* and TestAutoMode* pass. 2026-10-04 20:33:12 +08:00
yetone 7ec7c8e0cd agent: Claude Code's Haiku tier stays on Haiku when its model comes through magpie (0xAncientTwo on X: Claude 额度在 magpie 下掉得特别快,跟直接用 CLI 相比体感明显) — Claude Code sends its titles, its Explore subagents and other small asks to the haiku tier (ANTHROPIC_DEFAULT_HAIKU_MODEL, ANTHROPIC_SMALL_FAST_MODEL), which on its own sign-in is Haiku. Wired to magpie, every tier followed the main model, so with Opus or Sonnet picked all of those ran on Opus or Sonnet and spent the subscription at its rate. Now when the main model is a Claude one, the haiku tier (and SMALL_FAST) follows it to the newest Haiku served where it is: the same provider, the same group/auto-claude-… group, or OpenRouter's anthropic/claude-haiku-…, the undated id first. A model that isn't Claude, a Haiku main model or a place with no Haiku leaves the tier on the main model as before. Picking the main model for the haiku tier yourself is kept (claude.tier_own.haiku) across model changes; clearing it goes back to Haiku. Follow() moves a wiring an older magpie left with the haiku tier on the main model. TestClaudeHaikuTierOnClaude (opus and sonnet get anth/claude-haiku-4-5, deepseek follows, an own pick stays, Set("") returns, effort field, an older wiring moved) fails on the old code; TestClaudeLight covers the group, OpenRouter and none cases. With the owner's real Claude account in a sandbox Claude Code session on Sonnet, its title request went to claude/claude-haiku-4-5. 2026-10-04 20:33:12 +08:00
yetone 7f6e2c99ed routing: a group's stack of logos stays in its handle, inside its card and clear of its name (ARNO on Discord: the new routing groups' icons spilled past the left edge, over the text) — the handle #779 made of a group's logos had width auto in .rt-group .rt-ghandle, but body.window .ag-handle (28px, from the Agents page) outweighs it, so in the app's window a stack of two or more logos was held to 28px and centred over its edges. The selector now has .ag-handle too. The narrow web layout's 20px first column is auto, for the same reason. group-arrange.test.cjs (WebKit, en and zh) checks a two-logo stack lies inside its handle, the handle in its row and left of the name; it fails on the old CSS. agent-grip, click-scroll and the group-*/routing-* GUI tests pass in WebKit, but for group-from-model-add's two narrow-window cases, which fail on main as well. 2026-10-04 20:31:39 +08:00
yetone 81625282f5 gui: magpie speaks Japanese — Settings › Language offers 日本語, and a system in Japanese is followed (the owner: 日文文档也要有日文截图; the ja docs showed the English window for want of a Japanese one)
Every string the window, the panel and the menus have in Chinese now has a Japanese one too: 2917 entries in I18N.ja, translated to one glossary (プロバイダ, エージェント, ゲートウェイ, ルーティンググループ, 使用量, サインイン…), each keeping its {placeholders}, tags and leading spaces. Language takes "ja" (settings.Langs); System picks ja when navigator.language starts with ja, and <html lang> is ja-JP so CJK text draws in Japanese glyphs. Dates and numbers format through intlLang() (zh-CN or ja-JP) where they said zh-CN only. The tray menu, the quota alerts (7 日枠を 80% 使用、明日 14:30 にリセット) and the restart-to-update item have Japanese of their own (trayLang reads ja from the setting or a ja_JP/ja-JP system). Release notes stay English for ja, as the site has them in en and zh. The 万/亿 units option stays Chinese only.

Looked at in the Omarchy VM (magpie web, tokyo-night, ?locale=ja): Agents, Providers' Add sheet, a link's import, Routing, Usage, Gateway, Settings and the panel read in Japanese with nothing cut or overflowing. gui-ja.test.cjs (every zh key has a ja one with the same placeholders and tags; ja picked, a ja-JP system, en and zh systems, on WebKit and Chromium) and the ja cases in TestTrayMenuLabels and TestAlertText fail on the old code. On WebKit the other GUI tests fail as on the base but mobile-web's desktop comparison with origin/main, whose Settings now has a fourth language. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 20:26:07 +08:00
yetone 5a6b90f74f gui: the only account on, at its usage cap, says Claude Code or Codex signed in to it still uses it (𝕏 on Discord: a Claude account capped at 90% ran its five hours to 100%) — the cap holds back what goes through magpie and moves the agent's sign-in to another account (NextLogin), but Claude Code with its own models (or Codex) asks Anthropic (OpenAI) itself, and with no other account on to move it to it went on past the cap while the row said At its cap beside In use. That row's note now reads At its cap · Claude Code still uses it, in amber, and its title says requests through magpie are refused until the window renews while the agent signed in to it goes on, and what holds it: another account, or the agent's models via magpie. The cap pill's title says the same before the cap is reached. With another account on, nothing changes. zh strings added. account-cap.test.cjs (WebKit, en and zh): the lone account's note and its title, the pill's title, the warning colour, and none of it with the spare on; the new case fails on the old code. account-arrange, -mask, -models, -proxy, -return, keep-login and login-import pass on WebKit; go test -tags nogui ./internal/gui/ under a temp HOME passes. 2026-10-04 20:17:18 +08:00
yetone 14f59920f0 zcode: moved accounts are brought to the plugin's 0.1.8, which spends an account's ZCode gift plans before its GLM Coding Plan (magpie-community/plugins#19, erlinerd) — an account with both a coding plan and a gift (a Trust Build grant, say) sent every request to the coding plan, and the gift lapsed unspent. 0.1.8 sends a model the gift serves to it first, replays to the coding plan on any refusal (a quota answer keeps that model off the gift for a minute), and shows the gift's buckets on the coding card. With the owner's real GLM Coding Lite account (no gift) the card and a glm-4.6 request were as before; the gift route is covered by the plugin's tests only. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 20:09:16 +08:00
yetone 9aa7a3dca6 tray: the Mac menu bar can show the allowances without magpie's icon (KevinXC on Discord: 能否支持隐藏状态栏里的图标,只显示各个供应商的剩余额度?) — Settings → Usage has a Menu bar magpie icon row (On by default) under Menu bar logos, on a Mac with a card in the menu bar. Off (settings.TrayNoBird) draws the cards alone: the image starts at the first card, with no room left for the bird; a click on a card still opens the panel at it, a modified click opens it as before, and the flap leaves the image alone. With no cards up the bird is shown as before, so the item never goes blank. It is the computer's own, like the other menu-bar settings (KeepOwn), and a change redraws at once. Menu bar logos is 菜单栏订阅图标 in Chinese now, so the two rows read apart. TestTrayImageNoBird (the image 48 px narrower at 2x, the rest the same cells, ink at its left edge, logos and plain, light and dark) fails on the old code; tray-bird.test.cjs (WebKit, en and zh) and tray-logos pass; the birdless image was looked at in light and dark. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestClientGoneRecordedAsCanceled failed once under the full run's load and passes alone, five times, and in the gateway package's own run). Not tried in the real menu bar. 2026-10-04 20:01:57 +08:00
TryWorld f24ee8b06d fix(agent): installs without Node ask for it the way that works (#774)
* fix(agent): installs without Node ask for it the way that works

The commands that install Node first, for a machine without it (#727),
failed in ways that say nothing about the real problem.

On a Mac or Linux, `curl -o- … | bash` keeps going when the download
fails: a pipe's status is the last command's, so blocked or 404'd
(raw.githubusercontent.com is commonly unreachable), bash reads nothing,
still exits 0, and the user is told `nvm.sh: No such file or directory`
— the network's fault told as a missing file. The vendor commands beside
it all curl with -f (install.go's own scripts do); this one now does too,
so a failed download stops the line with curl's own words.

On Windows, winget asks to accept the msstore source's agreements the
first time it runs, which waits for input a copy-pasted command can't
give: Node isn't installed, and npm.cmd isn't found. The command now
carries --accept-source-agreements and --accept-package-agreements, the
flags Microsoft documents for unattended installs; they are idempotent
where the source is already accepted.

TestInstallCommandsWithoutNode expects the two commands as they now
stand, and fails on the old ones ("curl -o- …" and the winget line
without the flags, on darwin, linux and windows alike);
TestInstallWithoutNodeParses still reads each line with `bash -n`.
Validation on Windows: go test ./internal/agent under a temp HOME leaves
the three Unix-only failures the base commit leaves (TestCodexStaleOnly
ForMagpiesChange, TestSuffixModesReachAgentFiles, TestWSLProbeScript
SaysWhere); go vet ./internal/... and the darwin, linux and windows
builds pass.

* fix(agent): a failed Node download stops the line where it failed

yetone's review of the first cut: `curl -fsSL … | bash` doesn't stop at a
failed download either, and my claim that -f alone fixes it was wrong — a
pipe's status is the shell's, bash reading nothing exits 0, and the &&
chain goes on to source nvm.sh, which reports the missing file exactly as
before. Reproduced against a 404 server on this machine: `curl: (22) …
404` then `bash: …/.nvm/nvm.sh: No such file or directory`.

The command downloads to a file now and runs that: a failed download ends
the chain at curl, with curl's own words about why. pipefail isn't an
option — the line is pasted into the user's own shell, and not every one
of them sets it.

TestInstallWithoutNodeStopsWhenTheDownloadFails runs the real line,
against a local 404 server with a temporary HOME, on each POSIX shell
here: the old line failed with the download's error *and* nvm.sh missing,
the new one stops at curl's own error and never mentions nvm.sh. It is
skipped where there is no curl. TestInstallCommandsWithoutNode expects
the line as it now stands.

Validation on Windows: go test ./internal/agent under a temp HOME leaves
the three Unix-only failures the base commit leaves (TestCodexStaleOnly
ForMagpiesChange, TestSuffixModesReachAgentFiles, TestWSLProbeScript
SaysWhere); go vet ./internal/..., the darwin, GOOS=linux and GOOS=windows
builds pass.
2026-10-04 19:49:29 +08:00
yetone 25f67437c2 claude: a conversation whose run was let go goes on from its saved session, the prompt cache read, not written again (X, AncientTwo: a Claude subscription's limits went much faster through magpie than in Claude Code itself; Codex didn't) — the bridge keeps at most idleMost (6) Claude Code runs waiting for their conversations' next turns. With more conversations going (subagents, several agents), the one waiting longest was let go, and its next turn started a new run told the whole conversation in one message: a prefix Anthropic had never seen, so all of it was written to the cache again at 2x the input price (1h TTL), every turn, where a run kept reads it at 0.1x. In usage.jsonl three Sonnet 5.5 requests each wrote 273823 tokens and read none. Runs now keep their Claude Code session on disk (no --no-session-persistence where they work in the shared folder, claudeWorkDir; not in WSL or a run's own folder). A run let go past idleMost leaves its session on the bridge's shelf (savedSession, keyed by turnKey as the idle runs are) for as long as it would have waited (idleLongest, the cache's hour), at most 64. The conversation's next turn starts Claude Code with --resume on it and tells it only the messages since (renderClaudeTurn), so Anthropic is asked the same prefix the run would have asked. A session's file goes with its run when nothing will resume it (a one-off ask, a failed or ended run, a rewound one), when its turn takes it and that run ends, when it expires, or when its model changed (retire); a gateway that stopped leaves them to the next one's sweep (sweepSessions: files untouched for over an hour, so another gateway's on the same account stay). With the owner's real Claude subscription through the bridge (claude-haiku-4-5, a 9000-word system prompt, then 6 other conversations): conversation A's run was let go, and its next turn resumed it, writing 318 and reading 13881 (before, a new run wrote it all again); turn 3 wrote 106 and read 14199; no session file was left after. TestClaudeLetGoConversationResumesItsSession, TestClaudeSessionFilesAreRemoved and TestClaudeOldSessionsAreSwept (a script standing in for Claude Code, keeping sessions as it does) fail on the old code; the Claude tests pass -race. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 19:44:41 +08:00
yetone 209dc69b59 providers: Tencent Cloud is one preset, its regions Plan · China, Pay as you go · China and Pay as you go · Global (Discord, Jorben: Tencent Cloud's Token Plan, TokenHub and TokenHub China were three tiles of one vendor) — tencent-token-plan, tencent-tokenhub-cn and tencent-tokenhub become tencent-cloud's regions plan, cn and intl, as Xiaomi MiMo's are, each made as its own preset made it: the plan's /plan endpoints with no Responses, its own models and no catalog; TokenHub's hosts with Responses, models.dev's tencent-tokenhub and its own keys page and docs. A region now carries its Website, Catalog and Models, and a saved provider's region is found by its endpoints (regionOf, by exact URL before path, since TokenHub China and global share their paths): normalize gives it that region's keys page, docs and catalog (a catalog the user picked is kept), and planModels its models. The old ids are aliases, as qianfan-token-plan is of baidu-qianfan: Preset and FromPreset take them, FromPreset builds at their region, normalize rewrites a saved provider's preset to tencent-cloud, and a provider's own id never changes, so its key, models, routing groups, usage and prices stay where they were; magpie://import links by an old id land at its region, and region= now also gives a region's docs and catalog. Imports from other apps match a region's endpoints. The editor's docs link follows the region picked with the segmented control, en and zh ("按量 · 中国", "按量 · 国际"); the add sheet has one Tencent Cloud tile; the site's import docs list tencent-cloud with its regions and import.html still names the old ids. TestTencentCloud (each region, old ids, FromPreset, imports, import links) and TestTencentCloudOldPresetsKeepWorking (a providers.json saved by an older magpie under all three old presets, with a routing group over them, read, re-saved as the editor saves and switched between regions) fail on the old code; TestTencentTokenHubRoutes routes through old ids and tencent-cloud alike. tencent-cloud.test.cjs (WebKit en and zh: one tile, the three regions, no select, the docs and Get a key links following the region, an old id's provider opening at its region and saved under its id) fails on the old app.js; add-sheet, zhipu-team, system-one, strip-scrollbar and click-scroll pass (Chromium doesn't launch in this sandbox). Not tried with a Tencent account. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 19:38:14 +08:00
yetone 6f64956f27 routing: a group can let a member go that hasn't begun answering after a set time and ask the next, and a request's detail splits its first token into magpie's time and the vendor's (Chen on X: answers through magpie felt slow, with no way to tell whether magpie or the vendor was the wait) — a group's editor has a Slow to start row (Wait, 30 s, 1 min, 2 min; Wait, the default, keeps today's behaviour). With a time set, a streamed request to a member that has sent no text, reasoning or tool call by then is cancelled before any of its bytes reach the agent (the held start is dropped; keepalives go on meanwhile) and the next member is asked; the try is marked slow ("slow to start", narrated in the route), and the member doesn't rest, since nothing is wrong with it. The last member left is always waited for. The card says "Next after 1 min without a first token". Each request now records when it went to the vendor (httptrace WroteRequest, sent_ms), and its detail shows In magpie and Vendor's first token beside First token; after a failover In magpie includes the time spent on the members let go. With the owner's real DeepSeek key in a sandbox, a group [a local member that sends its role chunk then stalls 60 s, deepseek-flash] with 3 s: the answer came from DeepSeek in 4.4 s, none of the stalled member's stream reached the client, its try read slow / "no first token in 3s" with no rest, and the ledger had ttft 3622, sent 3003. TestGroupFirstTokenFailsOver (failover, the last one waited for, one in time kept; passes -race) and group-first-token.test.cjs and usage-ledger-detail.test.cjs (WebKit, en and zh) fail on the old code. group-from-model-add's narrow-window case fails the same on origin/main; Chromium doesn't launch in this sandbox. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn flaked once, passes alone). 2026-10-04 19:33:55 +08:00
yetone 855e7985f2 routing: the groups are put in an order of the user's, as the Agents page's rows are, and /v1/models lists them so (#779)
The Routing page's group list followed the order the groups were made in, found ones after, so a group used every day couldn't be put first, though /v1/models lists the groups in that order and agents' pickers (Claude Desktop's 3P list among them) show it and some clients probe its first model. A group row's logos are now its handle, as an agent row's logo is (#57): dragged (dragRows, the Agents page's sorter) it moves the row, clicked, or the row right-clicked, it opens Move up and Move down (openRowMenu, with the Agents menu's arrows, now MOVE_UP/MOVE_DOWN), and Alt+↑/↓ moves it from the keyboard, which stays on it. The grip in the row's margin is drawn only while the row is under the pointer, dragged, or its handle is focused or its menu open, as on the Agents page. The list is drawn in its new order at once and put back if the save fails; a later move wins over an earlier one still saving.

The order is kept in providers.json as groupOrder (provider.SetGroupOrder, POST /api/groups/arrange), beside the providers' own order (#499), and applied in groupsIn, which every list of the groups is made from: the Routing page, Catalog and so /v1/models, magpie group ls and the TUI's routing page. A found group, which isn't stored until changed, can be put among the user's this way, which reordering the groups array couldn't do. A group not named (one made or found after) goes last; a renamed one keeps its place. Backup and sync carry the order as they do the providers' (MirrorGroupOrder).

TestGroupOrder (Groups and the catalog follow SetGroupOrder, providers.json has it, a new group last, a rename in place, bad orders refused, a synced order first) fails on the old code. group-arrange.test.cjs (WebKit, en and zh): the grip hidden with the pointer away and shown on the hovered row only, in its margin; right-click Move down with Move up off on the first; Alt+↓ and Alt+↑ with focus kept; the handle's click opens the menu, not the editor; a drag by the grip to the top; each posting every group's id with the removed one last; no click moving the page. It fails on the old code. agent-grip, click-scroll, account-arrange, usage-arrange, provider-order and the group-*/routing-* GUI tests pass in WebKit, but for group-from-model-add's 600x420 cases, which fail on main as well. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 19:27:51 +08:00
kevinandClaude Opus 5.5 4127787182 agent: Crush's models picked in magpie go to its data file, which it reads after crush.json (#172)
* agent: Crush's models picked in magpie go to its data file, which it reads after crush.json

Crush saves what its own model picker chooses to its data file,
$XDG_DATA_HOME/crush/crush.json or ~/.local/share/crush/crush.json
(%LOCALAPPDATA%\crush\crush.json on Windows), and loads that file after
~/.config/crush/crush.json, so a models.large or models.small there wins.
On Linux and macOS magpie wrote its picks to ~/.config/crush/crush.json
only. Once somebody had picked a model inside Crush (its first run asks
for one), a model set in magpie never took effect, while magpie read it
back from the file it wrote and showed it as set.

The picks and the large model's effort are now read as Crush merges the
two files, the data file over crush.json, and written to the data file.
Unsetting one takes it out of both, and magpie's provider still goes
with the last pick on it. The provider stays in crush.json, beside the
library's MCP servers. On Windows the two paths are one file, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* agent: a model magpie picks for Crush replaces Crush's whole model object

Crush's picker saves the whole models.<type> object in its data file,
with its per-model settings (max_tokens, think, reasoning_effort,
sampling, provider_options), and applies them to whatever model the
object names. setPick only rewrote provider and model, so a max_tokens
picked for another model went upstream with magpie's, and magpie's
effort row showed an effort it never set.

Write the whole object instead. Only an effort magpie set itself is
kept: one on a large pick that is already magpie's and carries nothing
but provider, model and reasoning_effort. When magpie is first to make
Crush's data file, which holds API keys, make it 0600.

* agents: Crush's data file reaches crushAt, so WSL's Crush gets its picks where Crush reads them too

Main moved Crush's wiring into crushAt(at, path); the data-file path the
pick fields read and write is now passed in beside path: crush() works it
out as before (XDG_DATA_HOME or ~/.local/share/crush, LOCALAPPDATA's crush
on Windows), crushIn uses the WSL home's ~/.local/share/crush. The effort
test sandboxes XDG_DATA_HOME as it does the other XDG dirs, since the
package's TestMain shares one.

* agents: test that Crush in WSL picks in the distro's data file

A pick magpie makes for Crush in a WSL distro is read from and written to
<distro home>/.local/share/crush/crush.json, never this machine's data
file, whatever XDG_DATA_HOME says on it. The test fails when crushIn's
data path is the config file or follows the host's XDG_DATA_HOME.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:27:23 +08:00
yetone 77860f7e45 claude: a turn stopped mid-reply is taken back in the same Claude Code, so the resend reads the cache it wrote (#780, Kenoia) — a 499 (the user pressed Esc and reworded the message) ended the Claude Code run, and the resend found no run waiting, started a new one and told it the whole conversation in one message: a prefix the cache had never seen, written again whole (~119k tokens in the report). A turn resumed in a run kept for it now sends its user line with a uuid; when the client goes away, magpie tells that Claude Code a rewind_conversation control request to that uuid with interrupt_if_running (as its SDK's rewindConversation does), which stops the reply and leaves its conversation as it was before the turn, and the run waits again where it waited before, so the resend goes on in it with only its new message. A run started for the turn, one with tool calls in the client's hands, a continuation after tool results (the turn's id is dropped when its reply ends), or a Claude Code that refuses or doesn't answer in 10s is ended as before; a reply that ended as the client went away is left to the rewind. With the owner's real Claude account in a sandbox (Claude Code 2.1.289, haiku 4.5): two turns, a third streamed and dropped after 3 deltas, then the resend reworded: the same Claude Code pids throughout, the log said "the turn the client gave up on was taken back", and the resend read 6667 tokens from the cache (the whole prefix before) and wrote 91. TestClaudeTurnGivenUpOnIsRewound, TestClaudeTurnGivenUpOnNotRewound (refuse, silent) and TestClaudeFirstTurnGivenUpOnEnds; the Claude tests pass under -race. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 19:22:30 +08:00
yetone 1e271896d1 gui: a model added by id has its own Add model button, and Built-in icons offers 92 more vendors' icons, found by name (Tom on X: 希望在 provider 的 models 里面增加手动添加模型选项;另外就是 Built-in icons 太少了) — a model the vendor's list leaves out could be added by typing its id in the box under the list and pressing Enter, but the box alone said nothing of that and Tom didn't find it. It now has an Add model button beside it, off until an id is typed, with a title saying what it does (several ids, comma separated); a click picks the typed model as Enter does and a Save sends it. Built-in icons listed only the icons some preset uses, about 60; it now also offers MORE_ICONS, 92 vendors' and platforms' marks from lobehub/icons 1.95.1 (MIT, where magpie's other icons come from): Alibaba Cloud, Bailian, Doubao, Hunyuan, Wenxin, Hugging Face, Perplexity, Cohere, Cerebras, SambaNova, Novita, PPIO, Together's peers, AWS, Vertex AI and the like. A colour icon that draws part of itself in currentColor (black in an <img>, so lost on a dark tile: aws, cerebras, longcat, poe, yi, zeroone, llmapi) is taken in its monochrome version, drawn as a mask in the text colour. The grid, now well over a hundred, scrolls in its own box (196 px) under a Find an icon… box that filters by name. zh strings added. models-add-and-icons.test.cjs (Chromium and WebKit, en and zh): Add model off/on, a click and Enter pick the id, every built-in icon draws (none falls back to the cube, no broken image), find narrows to one, and Save sends the models and the icon picked; it fails on the old code. Looked at in dark mode on WebKit. model-test-why, model-test-one, provider-detect(-models), provider-levels-save, provider-same-as, provider-typed and provider-picks-stay pass on WebKit; model-api-preset (2) and provider-levels-scroll (1) fail the same on origin/main. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./internal/gui/ under a temp HOME pass. 2026-10-04 19:19:57 +08:00
Zhu Wang a13923a1e3 fix(routing): automatically group Codex title requests with their chats (#768)
* fix(routing): associate Codex title requests with applied chat titles

* fix(routing): address automatic title grouping review
2026-10-04 18:56:31 +08:00