Commit Graph
863 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
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
Zhu Wang c1f919ca09 fix(routing): simplify the request purpose filter (#795) 2026-10-04 23:30:01 +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
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 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 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 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
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
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 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 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
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
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
yetone 71d96e28e8 system one: a decision provider's own models-list URL is asked as given, and its models can each be tested (Discord, ARNO: OpenRouter added as a System One provider with the list https://openrouter.ai/api/v1/models?limit=500&output_modalities=decisions didn't get its models, and a model typed in by hand couldn't be tested from its right-click) — a System One-only provider went to fetchDecide before its ModelsURL was read, so magpie asked …/models, which at OpenRouter lists the 466 chat models, and kept only Jev's ids of those: typesafe/jev-router alone. With a ModelsURL, fetchDecide now asks it as written, query included, and keeps every id in it, since that list is of decision models already (OpenRouter's answers 13: pplx-decider, liquid/d1, cloudflare/clef, respan/span-01, typesafe/jev-1.13 …). Without one nothing changes, Bailian included. Testing a decision model was refused outright (the chip's Test this model off, Test models leaving them out); where the API takes System One's own body (System One, TypeSafe, OpenRouter, Vercel's TypeSafe API) each model is now sent AskSystemOne's yes-or-no question, the one Detect already asks, and the editor offers Test for it (providerJSON.decideTest); Vercel's evaluation models and Workers AI, which wrap it their own way, still say why not. TestDecideModelsURLAndModelTest (a fake OpenRouter: the plain list, the decisions list, a hand-added model through /systemone, an unknown one failing) and TestModelTest's Workers AI case fail on the old code; model-test-why.test.cjs gains a hand-added System One model tested from its chip and Test models, and a Workers AI one still off (WebKit en and zh pass, with model-test-one and model-copy-id; Chromium doesn't launch in this sandbox). OpenRouter's live decisions list returns the 13 models. Not tried with an OpenRouter key: the /systemone question itself. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 18:40:13 +08:00
yetone e950eb080a codex, claude: a model with a window above 272K is compacted at 272K, as OpenAI's and Anthropic's own are (X: Chen — through magpie on DeepSeek flash, Codex and Claude Code both "wait" before every answer, and more the longer the task) — magpie told both agents a 1M model's whole window, so neither compacted until near 1M and every turn sent the whole conversation again: the live ledger had turns of ~550K tokens waiting 70–90s for a first token, while magpie's own time before the vendor stays under 0.2s. OpenAI runs its own 1M-capable models at 272K in Codex (context_window 272000, max_context_window 872000) and Anthropic its at 200K unless a [1m] one is picked; magpie now does the same by default. Codex's catalog gives such a model context_window 272000 and its whole window as max_context_window; Claude Code gets CLAUDE_CODE_AUTO_COMPACT_WINDOW=272000 while it runs on magpie (it compacts at the smaller of that and the model's window, a [1m] model's too), kept apart from a value the user set and taken out with magpie. Settings → Long conversations ("Compact at 272K" / "Full window", en and zh) turns it off (settings.FullContext), and both agents' lists are written again at once. In a sandbox with the owner's real accounts: Codex's /models listed group/test and group/auto-glm-5-3-flash at 272000 with max 1000000 (stepfun's 32K and 16K as they were, gpt-6-* still 272000/872000), Claude Code on deepseek/deepseek-flash[1m] answered and its /context said 16.3k / 272k, and Full window turned both back to 1M (/context 16.3k / 1m). TestClaudeCompactWindow and the extended TestCodexCatalogToolSearch fail on the old code. go vet (darwin, GOOS=windows), builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 18:27:09 +08:00
yetone ee508d0ce9 claude: on Windows with no Claude Code of its own, the gateway runs the one in WSL (X: kyotiger, Claude Code installed in WSL only, magpie on Windows found no claude) — claudeBinary and the sign-in's findClaudeCLI now fall back, when no claude is on Windows, to one in a running WSL distro (internal/wslrun: the default distro first, Docker Desktop's left out, a stopped one never started; found by a login shell, then an interactive one for nvm's node, then ~/.local/bin and /usr/local/bin, never Windows' own under /mnt). A run is wsl.exe -d <distro> --exec /usr/bin/env PATH=<its PATH> <claude> …, so no shell rc runs and Claude Code there uses its own sign-in and quota; what the bridge sets goes in through WSLENV, CLAUDE_CONFIG_DIR (a saved account's folder) and the sign-in's BROWSER as paths (/p), the proxy variables with a loopback proxy pointed at the Windows host under NAT networking. The MCP tools server stays magpie.exe on Windows, started from WSL by its /mnt path. Killing wsl.exe leaves the Linux process running, so each run is marked (MAGPIE_WSL_RUN) and a canceled one is ended inside the distro by that mark. The Claude provider's editor says "Runs through WSL · <distro>" under Accounts (en, zh); the page never waits on WSL for it. A claude on Windows itself is still used first. With the owner's real Claude account on the Windows test box (Claude Code 2.1.289 in WSL only, magpie in a sandbox): /v1/models listed the Claude models, a haiku request answered from WSL with the MCP server at /mnt/c/…/magpie.exe, a tool_use round trip worked, the quotas came from its headers (5h 13%, 7d 37%), "Add another Claude account" opened claude.com's OAuth page through the BROWSER hand-off with its localhost callback reachable from Windows, canceling it ended claude auth login in WSL, and the editor showed the line in en and zh. Not tried: finishing a browser sign-in (needs a person). Tests: internal/wslrun (the probe's parsing, path and WSLENV translation, the proxy rewrite, Find's distro order and cache, a run's argv). go vet (darwin, GOOS=windows), builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (the gateway package flaked once under load and passes alone). 2026-10-04 17:49:53 +08:00
yetone 58dbd06289 linux: the window no longer flickers under Wayland — WebKitGTK draws the pages without accelerated compositing there (myxxts on Discord: Fedora 44, KDE, Wayland, AMD Radeon 780M, the UI flickered on every interaction). Wails turns DMA-BUF off for NVIDIA only; the reporter tried both WEBKIT_DISABLE_DMABUF_RENDERER=1 and WEBKIT_DISABLE_COMPOSITING_MODE=1, both stopped it and the second was smoother. Run now sets WEBKIT_DISABLE_COMPOSITING_MODE=1 before the first webview when the session is Wayland (XDG_SESSION_TYPE or WAYLAND_DISPLAY) and the user set neither variable; X11 is left as it was, and WEBKIT_DISABLE_COMPOSITING_MODE=0 turns compositing back on. TestWebkitDefaults (Wayland, X11, a user's own setting) passes in a linux/arm64 container. Not tried on a Linux desktop here; the fix is the reporter's own tested setting. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn, TestUserPath and TestClaudeSignedOut flaked under load and pass alone). 2026-10-04 17:48:54 +08:00
yetone 47eb27296f settings: a search API's saved key can be shown, hidden and copied in its row, as a provider's can (OnurBen on Discord: the Providers page shows a key, Settings → Models → Search APIs only listed Firecrawl/Tavily/Exa keys masked, so copying one meant going back to the vendor's site)
Each saved search API with a key gets Show and Copy next to Remove. Show asks the new POST /api/settings/search-key for the key (served on loopback, as provider/key is) and puts it in the row's line in place of the masked one, selectable; Hide puts the masked one back. Copy copies the key without showing it. The settings still list keys masked. search-api.test.cjs checks Show/Hide/Copy (fails on the old app.js) and TestSearchKeyShown the endpoint. GUI: search-api, searcher-pick, click-scroll pass on WebKit; go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 17:29:11 +08:00
yetone 5569275465 group: a routing group's members can be patterns, so a group follows a provider's catalog (#766: magpie group add my-free-pool models='openrouter/*:free,opencode-zen/*-free') — a group names its models one by one, so a pool of a vendor's free models went stale as the vendor added and dropped them. A group now keeps patterns apart from the models it names (Group.Match): a glob, * any run of characters (/ too) over the provider/model id in any case, or re:<regexp> over the whole id. groupsIn, which every reader goes through (Groups, FindGroup, /v1/models, MemberGroups, GroupsWith, RenameGroup, rules, Off, Pick, the GUI, TUI and CLI), puts after the named models every enabled provider's model a pattern matches, in catalog order, deduplicated, the named one keeping its place; a pattern never matches a group. What was matched is derived (Group.Matched) and never stored: store() and SaveGroup keep only the named members, so a save never freezes matches. Off and rules may name a matched model. A group with only patterns is allowed; one matching nothing routes nowhere and says so: magpie group <id> lists each pattern with how many models it matches now ("matches nothing now" in amber), the group list and the TUI too, and the GUI card shows a line per pattern with its count. CLI: models= takes patterns among the ids, models+= adds them (naming a matched model moves it to where it is named), models-=<pattern> drops one, models-= of a matched model is refused naming its pattern; the help documents patterns. GUI: the editor has a Patterns box (Add pattern / Remove, validated as the server does), matched models follow those named marked "by pattern" with their off switch and no Remove, and saves send members without the matched ones plus match; zh strings added. Tests: TestGroupPatterns, TestGroupPatternMembersOffAndRules, TestGroupPatternMatchingNothing, TestPatternSyntax (provider), TestPatternGroupRoutes (gateway: a request reaches a matched model, a model a provider adds joins, Off on a matched one, /v1/models lists the group), TestGroupPatternsCLI, and group-patterns.test.cjs (WebKit, en+zh); they fail on the old code (the Go ones don't build, the GUI one finds no pattern lines). GUI: group-patterns, group-found-off, group-member-fast, group-member-off, settings-groups, agent-model-groups, remote-group-served, routing-manual pass on WebKit; group-from-model-add has the same 2 failures (600x420) as origin/main. A request to a group whose patterns match nothing now is a 404 naming the group and its patterns (emptyGroupError), not 'magpie knows no model' and every model; TestEmptyPatternGroupSaysSo. With the owner's real config under a sandbox HOME: group/ds-pool of models='deepseek/*' listed deepseek-flash and deepseek-v4-pro by pattern, stored only the pattern, was in /v1/models and answered from deepseek-flash; none-pool of 'nobody/*' said its pattern matches nothing. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 16:52:15 +08:00
yetone a7cd091ac6 agents: an agent's model list has its providers down the left, one picked showing its models alone (蓝猫 on Discord: picking models in the list meant scrolling down one at a time; they asked for providers on the left, then their models) — with three or more providers (routing groups count as one) and room for it, the list widens to 580px and gains a rail: All providers, then each provider with its icon and how many of its models are shown. One picked shows only its group, open even if long; All brings back the folded list as before; a search still looks in every provider; the counts follow each click. Narrow windows and lists of one or two providers stay as they were. agent-models.test.cjs checks the rail's entries and counts, picking OpenRouter (24 rows, open), a click updating its count, a search past the picked one, All, and that the rail sits left of the list; its click-away now clicks outside the wider box. GUI: agent-models, agent-models-loading, agent-model-groups, agent-models-all-hidden, desktop-model-menu and click-scroll pass on WebKit; go test ./internal/gui passes. 2026-10-04 16:03:00 +08:00
yetone 0e409a04b7 quota: the trend chart says what its dashes are (John on Discord asked what the dashed line under an account's meters is) — each window's dashed line is an even pace, full at the cycle's start to empty at its reset, but nothing on the card said so. Hovering the chart now says: solid is what was left, dashed the even pace; a line above its dashes lasts the cycle, one below runs out before the reset (zh too). quota-curve.test.cjs checks the hover text in English and Chinese; it fails on the old code. GUI: quota-curve and panel-usage pass on WebKit; go test ./internal/gui passes. 2026-10-04 15:50:51 +08:00
yetone 6fa7a8c30b routing: Codex's home-page suggestions are named, and the story says where Codex turns them off (#705, DuGuaiYA: ambient_suggestions calls on gpt-6-luna every few seconds, 58.5k tokens each, shown by their raw name) — the Codex app drafts the suggested prompts on its home page by itself (thread_source ambient_suggestions, on gpt-6-luna, searching the project's files and connected apps) and checks them with a second call (ambient_suggestion_safety); magpie's Routing and Usage pages showed both by their raw names with the generic "made this call itself" story. Both now read Suggestions (提示词建议), and the story says what the calls are and that Codex's Settings › Configuration › Suggested prompts (设置 › 配置 › 提示词建议) turns them off, as the Codex app 26.x bundle names the setting (settings.agent.ambientSuggestions, key ambient-suggestions-enabled). routing-kind.test.cjs checks both tags and the story in English and Chinese; it fails on the old code. GUI: routing-kind, routing-side-calls and the routing/account tests pass on WebKit (group-from-model-add's two failures are origin/main's); go test ./internal/gui passes. 2026-10-04 15:43:05 +08:00
TryWorldandyetone 92ebdb4104 fix(provider): mark a window one model's own as capped, as routing holds it (#760)
* fix(provider): mark a window one model's own as capped, as routing holds it

Routing held an account at its usage cap when any window that counts the
model was at or past it, Opus's own window included: CapHeld walks the
allowance with Limit.applies, and a window naming one model counts every
request whose id holds that word. WithCapped, which tells the GUI which
windows a cap counts, left those windows out — it marked a window only
when it named no model. So a Claude account kept at 70% whose Opus week
was at 90% was barred, and then answered 429 with nothing left when it was
the only account, while the Providers page showed the account in use, no
"At its cap" note and no tick on that window's meter. The rows said the
opposite of what routing was doing, and the cap look broken.

WithCapped now marks a window as capping as CapHeld holds it: not Aside,
not Unlimited, one model's own window like the rest. The GUI needs no
change: capHeldOf, capHeldNote and capMark already read Capped, and a
mark on a per-model window says the cap holds that window's model — the
window's row already names its model. The 429-when-every-account-capped
path is untouched: capReached counts only the windows that stop an
account for every model, as its comment says, and is not what WithCapped
marks.

TestWithCappedMarksPerModelWindows fails on the old rule with "the window
one model's own is not marked" and passes: the account-wide window, a
window naming "opus", on-demand spending (Aside) and an unlimited one are
marked, not marked, not marked and not marked, and the windows handed in
are left as they were. TestCapHeld, which spells out that Opus's window
counts for Opus alone, and the rest of the cap tests are unchanged.

Validation on Windows: go test ./internal/provider passes whole;
TestWithCappedMarksPerModelWindows fails without the change. go vet
./internal/... and the darwin, GOOS=linux and GOOS=windows builds pass.
The full go test -tags nogui -count=1 ./... under a temp HOME leaves the
same Unix-only failures as the base commit does on Windows (Keychain,
mode bits, shell-script CLIs, skills' folders); the provider package
passes whole. account-cap.test.cjs's fixtures carry capped on
account-wide windows, so the data change does not touch them; Playwright
is not installed in this worktree, so it was not run.

* gui: an account held at its cap for some models says which, not that it is held

A window that counts some models only (Opus's week, a pool's) is now
marked Capped, so the Providers page said "At its cap" and "sends
requests to the other accounts" for an account routing still used for
every other model. WithCapped also sets CapsSome on such a window;
capHeldOf tells those from the account's own windows, and an account
held by them alone reads "Opus week at its cap · back in 3h", its
tooltip saying other models still use it. When an account-wide window
holds it, that window's renewal is the time it is back.

account-cap.test.cjs: a Codex account whose five hours and week are
under its 70% cap but whose Opus week is at 90% names the Opus week, in
English and Chinese; it fails on the PR's head.

---------

Co-authored-by: yetone <yetoneful@gmail.com>
2026-10-04 15:43:00 +08:00
yetone 4e0bf88370 gateway: Gemini on a Google sign-in searches with Google Search and draws with its image models (#757, on Antigravity: gemini-3.8-flash's searches went to the Codex account and spent its allowance, and neither the searcher picker nor the image model picker offered Antigravity's models) — a Google sign-in (Gemini CLI's or Antigravity's, not a plugin's) was never said to search by itself, and buildCodeAssistSent sent no googleSearch, so a Claude Code WebSearch on an Antigravity model was handed to magpie's search, which the searcher magpie picked (Codex first) answered.
Search: Code Assist runs googleSearch for Gemini, but Antigravity turns it down beside function tools (400 "Please enable tool_config.include_server_side_tool_invocations to use Built-in tools with Function calling", tried with that flag in toolConfig too), so searchesFor (searchesItself for one request) says a Google sign-in's Gemini searches by itself for a request without tools; buildCodeAssistSent then sends tools [{"googleSearch":{}}], and codeAssistDecoder reads the candidate's groundingMetadata (webSearchQueries, groundingChunks[].web) as a search the client is told of (Anthropic's server_tool_use + web_search_tool_result), once. A request with tools — every agent's — is given magpie's search tool as before, and its searches go to the conversation's own account first (ownSearcher, as a Kimi Code plan's do): searchWith on its Gemini without tools, so googleSearch runs there and no other subscription is spent. A Google sign-in is also a searcher for other models, last of those magpie picks by itself (rank 5; relays named by hand move to 6), and is offered in Settings → 代搜供应商, with its Gemini models only; the row says such sign-ins search for their own models first. smallModel took every Gemini for a small model ("gemini" has "mini" in it), so the first listed, gemini-pro-agent, searched; it now picks a Flash.

Images: Drawers lists the image models a Google sign-in has (gemini-3.1-flash-image), and draw asks them on Code Assist through the account (drawCodeAssist, askTranslated with the conversation's envelope); buildCodeAssistSent asks an image model for responseModalities ["TEXT","IMAGE"], which a chat with one now gets too. AutoDrawer doesn't pick them by itself (no known price), so nobody's automatic drawer changes.

With the owner's real Antigravity account (a copy of the config, a sandbox gateway): a web_search with no other tool on gemini-3.8-flash-low came back with Google's grounding as web_search_tool_result; with a bash tool beside it the model called magpie's search, which went to antigravity/gemini-3-flash (not Codex) and gave pages; /v1/images/generations with antigravity/gemini-3.1-flash-image drew a 1024x1024 JPEG. TestCodeAssistSearchesWithoutTools, TestCodeAssistDecodesGrounding, TestCodeAssistImageModelAsksForImages and TestSmallModelOfGemini are new; searcher-pick.test.cjs checks the note. go vet, builds (darwin, GOOS=linux, GOOS=windows), go test -tags nogui -count=1 ./... under a temp HOME and the searcher GUI tests (webkit) pass. Not tried with a Gemini CLI sign-in, which takes the same path.
2026-10-04 15:31:23 +08:00
Liucwandyetone c28924091e fix(gui): open Help Learn More in the system browser (#759)
* fix(gui): open Help Learn More in the system browser

* gui: set the Help menu on macOS only — on Linux a set application menu becomes a menu bar in every window, the tray panel's too (Wails' linux window takes globalApplication.applicationMenu when it has no menu of its own); Windows shows it only with UseApplicationMenu, which magpie doesn't set

---------

Co-authored-by: yetone <yetoneful@gmail.com>
2026-10-04 15:01:01 +08:00
yetone 1565bb9ce1 usage: the cache hit rate is of the whole prompt, what was written to the cache in it too (Chapin on Discord: with Claude Code started anew at every AmpCode tool round, writing its prompt to the cache again each time, the Usage page still showed a hit rate near 100%) — the Usage page's Overview divided cache_read by input + cache_read, as did the Sessions tab's totals, the TUI's Sessions page and magpie sessions' hitRate; input is what was neither read from the cache nor written to it (the gateway's Usage.prompt() is input + cache_read + cache_write, and the sessions readers subtract both, as alma's and cline's do), so a prompt mostly re-written showed as mostly read. Each now divides by input + cache_read + cache_write, as the tray panel's Usage tab and the Requests tab's strip and rows already did. number-units.test.cjs checks the Overview's rate on its fake ledger (2B read of 1B + 2B + 30M written: 66%, where the old formula said 67%; fails on the old code), TestHitRateCountsCacheWrites checks hitRate (20% of 10 + 20 + 70), and the TUI Sessions page's fixture, which has writes, now says 67% where it said 69%. GUI: number-units, panel-usage, usage-ledger-chart, usage-refresh and mobile-web pass on WebKit; go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 14:14:43 +08:00
yetone 775612f6eb gui: one list of sessions — the Sessions page shows what each session spent, and Usage → Sessions' latest sessions open there (#752, hisiling on v0.1.814: the top bar's 会话 listed every session by agent and folder with resume, delete and the trash but no tokens or cost, while 用量 → 会话's 最近的会话 had folder, model, tokens and cost and resumed too but couldn't delete or group by folder; both opened to the same time, folder, session id and resume command, so the same list was drawn twice with half missing from each). /api/sessions/manage already carried each session's tokens, cost and models (sessions.Managed embeds Session, and ListAgent assembles it priced), the page just never drew them; its rows now end in the same two columns Usage's list has (sessSpent: tokens with in/out/cached under them, ≈cost, "—" with its reason where there is no known price), the sub line names the models and the gateway's route (sessModelsText, shared with Usage), the details gain the per-model breakdown and the Routed lines (sessUsageDetail, which sessionDetail now uses too, so the top-sessions card is as before), and its filter also matches a model. manage now also sends each session's via (usage.Vias since the agent's oldest session, as /api/sessions does), so routing isn't lost; a null models list is read as empty. Usage → Sessions keeps its statistics, charts, top sessions and its latest-sessions list, as that list follows the range, agent, model and folder filters the Sessions page doesn't have: its rows keep title, folder, models, tokens and cost, lose Resume, Open in session terminal and the in-place details, and open on the Sessions page instead (role=link, Enter/Space too, a chevron, title "Open in Sessions" / 在会话页打开): the session's agent is picked, its folder unfolded, the row opened, and once the list is drawn the row is brought into sight — a scroll the reader's click asked for (scrollOnPurpose(e) at the click, then readerScrolls and readerLeaves when the row arrives, as the quota cards' focus does). An All sessions / 全部会话 button by the list's search opens the page on the agent picked there. At 560px and below a Sessions page row drops its tokens column, keeping the cost, so the title keeps room beside Resume, terminal and delete. The other way round (deleting and the trash moved into Usage) was not taken: the Sessions page's per-agent, per-folder tree with picking and the trash is the page made for it, and Usage's list is a drill-down of its filters. No <select>, no left-border accent, no click scrolls on its own, the Agents grip untouched. The GUI has en and zh tables only; both have the new strings. TestSessionsManageCarriesUsage (a Claude session from testdata plus a gateway record: tokens, models and via in sessions/manage) fails on the old handler; sessions-usage-open.test.cjs (WebKit, en and zh) fails on the old assets and passes, and sessions-*, click-scroll, usage-fold and routing-sessions pass; Chromium doesn't launch on this machine. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real session files beyond the fixtures. 2026-10-04 14:14:43 +08:00
Leslieandgoogya 8031edc78b agent: AtomCode support — one provider account, one model table per catalog model, as its own sign-in writes (#736)
* agent: AtomCode is wired like its own CodingPlan sign-in — one provider account, one model table per catalog model

AtomCode (atomgit.com/atomgit_atomcode/atomcode, AtomGit's open Rust terminal agent) keeps its settings in ~/.atomcode/config.toml ($ATOMCODE_HOME). Its account schema, what its own CodingPlan sign-in writes, is one [provider_accounts."magpie"] table at the gateway's /v1 (protocol openai, gateway.TokenFor("atomcode") as the key, so a request is its agent's even when the UA is overridden) and one [models."magpie/<provider>/<model>"] table per catalog model: the model string goes to the gateway as it is to resolve, with the model's context window (AtomCode compacts as it nears it; a 128000 one is told when the model's isn't known, and an output above the told window is cut to it), its output limit, vision and reasoning_effort_levels; reasoning_effort rides on the table once the user picks one. Picking a model points default_provider and default_model at it — default_model wins when both are set, as atomcode reads it — and stashes the defaults the user had; stepping out puts them back. The picker offers the user's own model tables and the catalog via magpie; the Library gives AtomCode its ATOMCODE.md, its mcp.json (omp's shape) and skills/; its UA is atomcode/<version>.

Found against a real atomcode 5.2.1: the documented legacy [providers.*] tables work, but its own sign-in writes the account schema, so magpie writes that; reasoning_effort, max_tokens and supports_vision were verified to reach the request from the model table, and default_model to win over default_provider. Verified end to end on a real install: atomcode --provider "magpie/…" answered through the gateway, and the Agents page shows the tile, its picker and the Usage attribution. TestAtomcodeWiring, TestAtomcodeRestoresOwnDefaults and TestAtomcodeSync fail on the old code; TestDriftUnwiredEveryAgent and TestUnsetAgentsReadEmpty cover it from All(). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/agent ./internal/library ./internal/agentenv pass; the plugin-host and timing failures of the full ./... run fail on main without this change too.

* tweak

* agent: AtomCode's config is matched by its own spellings, and Sync keeps the effort — AtomCode rewrites its config through serde on every /model and /cd, and that writes the account header bare ([provider_accounts.magpie]) where magpie wrote it quoted, so after the first rewrite Check read the wiring as gone, Sync and every pick appended a second account table (edited TOML is invalid: table magpie already exists) and clearing left the unquoted table and its api_key behind. The account is now matched by exact name in its three spellings (quoted, bare, literal — magpie-2 stays out) and the model tables by their decoded keys (kimiModelKey, replacing atomcodeModelName), through a new SetTOMLTablesMatching that takes exact names beside prefixes; SetTOMLTables delegates with none, unchanged. Sync carries each magpie model table's reasoning_effort over when rewriting — it ran at every gateway start and catalog change and dropped both an effort picked in magpie and one picked with AtomCode's /effort, and picking another model cleared every table's. The effort Set validates against the levels AtomCode knows (low, medium, high, xhigh, max — its clamp_effort_to_levels drops none, minimal and ultra), a model without levels offers none now, and the low/medium/high fallback is gone with it. supports_vision is written explicitly false, where AtomCode otherwise guesses from the model name. The user's legacy [providers.*] entries join the picker, a stashed default_model naming one comes back on Disconnect, and picking one sets default_provider and takes default_model out, as the legacy shape reads. site docs (en, zh, ja) list AtomCode. TestAtomcodeSync rewrites the config into the bare and single-quoted spellings before Sync and again before clearing and fails on the old code (drift reported, effort dropped, unquoted table left behind); TestAtomcodeLegacyProvidersArePickableAndRestored and TestAtomcodeEffortLevelsMatchAgentSupport are new. Reported by yetone on #736. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/agent ./internal/edit pass.

* agent: TestAtomcodeWiring picks an effort through the field too — the write side of the validation the last commit added (a level AtomCode knows lands as reasoning_effort on the model table, one it does not know is refused) had no coverage left: the test only read back what AtomCode's own /effort had written. provider.SetModelEfforts gives fake/m1 levels to offer.

* agent: Disconnect puts AtomCode's default_provider back only where AtomCode still has it — the restore checked the stash's value was not magpie's but not that the table it names was still there, so a table the user had deleted in between came back as a dangling default (the account schema's default_provider names a model table, the legacy shape a [providers.*] one; own answers for both). The default_model branch checked already. TestAtomcodeRestoresOwnDefaults deletes the stashed provider's model table before clearing and fails on the old code.

* agent: AtomCode joins the CLI updates — npm @atomgit.com/atomcode, the brew cask atomcode, and the curl script's ~/.local/bin/atomcode updating itself with atomcode upgrade, so the Agents page shows its version and a newer one as the other agents' do (#736). TestHowInstalled covers the npm prefix, the cask and the self-updating installer.

* agent: AtomCode's self-updater waits for its install paths to be confirmed — the branch recognized a binary under ~/.local/bin as `atomcode upgrade`, on a docs page, while the README shows another path and the command has not been driven outside the help text; the reviewer asked for it removed until the standalone command and the script's real default are settled. npm and the brew cask stay, with their tests; a comment marks what is pending.

---------

Co-authored-by: googya <118465+googya@users.noreply.github.com>
2026-10-04 14:11:14 +08:00
Zhu Wang 2e77c546d7 routing: label Codex background memory groups (#753) 2026-10-04 14:06:29 +08:00
yetone 81879b90f3 providers: a model chip's right-click menu copies the model's id (Discord, ARNO on v0.1.815: 希望provider的模型右击菜单除了测试模型外,还能添加拷贝模型id的功能) — the menu a chip opens in a provider's editor had Test this model (and Asked on … where the provider has more than one API) and nothing to take the id with; a chip shows the model's name where it has one, so the id (zai/glm-5.3-flash under "GLM 5.3 Flash") had to be read off its tooltip and typed. The menu now has Copy model ID / 复制模型 ID under Test this model, through the app's own menu (openRowMenu) and copy(): magpie's /api/copy, the page's clipboard and the copy command after it, as every other copy in the app; the footer says "Model ID {model} copied" / 已复制模型 ID {model}. It is on where the model can't be tested (an own-API subscription's, a classifier's) too. The app has English and Chinese strings only (i18n.js has no Japanese table). model-copy-id.test.cjs (a relay's prefixed, plain and hand-picked ids and an own-API account's: the id, not the name, is posted to /api/copy, the footer says so, nothing is tested, picked or scrolled, no left-border accent; en and zh) fails on the old code and passes in WebKit; model-test-why expects the second item beside the disabled test one. model-test-one, model-test-why, provider-typed, provider-detect-models and click-scroll pass in WebKit; Chromium doesn't launch here, so not tried there. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in the app's own window (its /api/copy path is the one the other copies use). 2026-10-04 13:36:54 +08:00
yetone a0d41044bb providers: a provider's model list says the context window agents are told — the one set on it, else models.dev's for a prefixed id — not only its vendor's list's (Discord, ARNO on v0.1.815: provider的上下文窗口覆盖功能好像失效了。cline插件的模型都没有默认的上下文窗口大小 … 如zai/glm-5.3-flash,cline-free/mimo-v2.6-flash) — the Providers page's models (providerInfo's modelJSON.Context) took catalog.Model.Context as the vendor's list or the plugin gave it, while the catalog's entries (entryFor: /v1/models, every agent's config, routing) take the window the user set (Provider.ContextOf) over it, and models.dev's for the id (catalog.ContextOf, which already reads zai/glm-5.3-flash as glm-5.3-flash) where the list says none. So a window set in the editor or with magpie model context reached agents but not the page, the agents' model pickers (which read the same list) or a chip's 1M tag, and a model listed with no window — Cline's plugin gives every model 0 (fixed in its 0.1.2), a relay's prefixed ids — showed none. The window is now one function, Provider.WindowOf (the user's, else ListedWindow: the list's, else models.dev's), which entryFor and providerInfo both read; the page is also told the window before the user's (listed), which the context field's "Each model's most" pick compares against, so a model set to its most still offers it. Lookups by a prefixed id were not broken: in the owner's config 14 of 794 models had no window on the page and one from models.dev for agents (Gemini's, Kiro's auto), the same split. TestProviderModelWindowSaid (a relay serving zai/glm-5.3-flash and own-model: the page says 1M before a window is set and 200K/64K after, as Served does) fails on the old code (the page said 0 where agents were told 1000000); TestPluginModelWindow (Cline's plugin with cline-pass/glm-5.3-flash, cline-free/mimo-v2.6-flash: models.dev's window, then the one set) guards the plugin path, which was already right for agents. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass; model-rate and editor-short-window pass in WebKit. Not tried with the reporter's Cline account; an agent already running keeps the window it started with until it rereads its config. 2026-10-04 13:36:54 +08:00
yetone 548774584f gui: a credits count stacked under its window's name breaks at its · in WebKit too, rather than being cut to "剩余 100…" (#694, found beside Hu9956's screenshot of the Usage page's credits rows) — stacked, .quota-n was a block with two nowrap spans and a space between them; WebKit leaves that space out when it checks whether a line fits, so a count a few pixels too wide ("100 / 100 积分 · 剩余 100%" at 139.7px in a 137–139px card) stayed on one line and was cut with the ellipsis, where Chromium breaks it at the ·. Stacked, the count is now a wrapping flex row of its parts with a 4px gap (the gap quotaFit already counts), each part whole or, on a card narrower than one part, cut on its own. quota-count-wrap.test.cjs puts WorkBuddy, WorkBuddy AI and Trae CN credits cards at widths 600–1100 in WebKit and Chromium and checks no count or part is cut; its WebKit half fails on the old CSS at 990px. quota-credits, quota-families, quota-pools and quota-curve pass in WebKit. 2026-10-04 13:10:55 +08:00
yetone d6096e2fb1 routing: a failed request whose route has no order opens when clicked (Discord, mythfish on v0.1.810: on the Routing page's 实时 list, clicking "11:28:08 AM • workbuddy-ai/deepseek-v4.1-flash • 400 · 失败 6.5 秒" did nothing; the request was refused with WorkBuddy AI 400 {"message":"WorkBuddy AI: Invalid request parameters","type":"invalid_request_error"}) — Route.Order has no omitempty and the trace's snapshots (Trace, the saved copy in update, sessionLatest) copied it with append([]Weighed(nil), r.Order...), so a route begun with no seats weighed reached the page as "order": null, while its tries were there. routing.js reads r.order as a list throughout: a row's click runs pick → sync → rebuild, whose setOf(r) is r.order.map(seat), and renderActs runs r.order.forEach, so pick threw (webkit: "null is not an object (evaluating 'r.order.map')") before the story was drawn and the click looked dead. Reproduced with a live trace fixture: a 400 with order null fails to open; the same 400 with order [] or a one-seat order, every failure kind, and an order whose seats don't match the try all open, so it is the null and not the 400 or the WorkBuddy error. The page now fills in a route's lists where routes come in (whole: order, tries and left default to []), for the live trace, a kept day's history and openRoute's fetched route (the tray panel's and Sessions' links), and the gateway writes Order as [] when it is empty in all three snapshots, so an older page reading a newer gateway is fine too. Tests: routing-failed-open.test.cjs (webkit, en and zh, live and a kept day: a request that answered and the report's 400 with "order": null; clicking the 400 presses its row, shows the story "How the request at … was routed" / "… 的请求是怎么路由的" with "It said: WorkBuddy AI: Invalid request parameters" / "原话:…", doesn't move the page, and the answered one opens again after; no page errors) fails all four on the old routing.js and passes; TestTraceOrderIsAList (a route begun with no order and refused with a 400: Trace's JSON has "order":[], sessionLatest's copy a non-nil Order) fails on the old trace.go with "order":null and passes. The routing-*.test.cjs and click-scroll GUI tests pass in webkit (routing-time's zh case failed once in a 4-way parallel run and passes on its own). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not found: which gateway path began the reporter's route with no order — the plain chat path weighs at least the provider it was asked for — so the fix is to the shape, not to that path; not tried with the reporter's WorkBuddy AI account. The 400 itself (WorkBuddy refusing the request's parameters) is a separate question this doesn't touch. 2026-10-04 12:23:43 +08:00
yetone 8c74f935fd gui: Codex's title reconsideration and title_generation calls are tagged Title in Routing and Usage, not shown by their raw names (#742, a first step: what Codex's own calls are should be plain to see) — magpie keeps every x-openai-subagent and thread_source Codex sends as the call's kind (guardian/guardian_review/auto_review, review, compact, memory_consolidation/memgen, thread_title, collab_spawn…), and routing.js's KIND map names each, but thread_title_reconsideration (the turn in which Codex's TUI reconsiders a thread's title) and title_generation were left out, though the gateway's isTitleKind treats both as titles (they inherit the chat's session and follow the Codex titles setting), so their tag read "thread_title_reconsideration" under a "made this call itself" tooltip. Both map to Title (标题) now; Codex's recap turn (thread_source "system") keeps its raw name, being no title, review or memory. routing-kind.test.cjs adds a thread_title_reconsideration route, expected as Title/标题, which fails on the old map; its /api/providers fixture gains presets and excluded (app.js read them and put an error in the footer status) and its viewport is 100px taller, so the seventh row isn't under the footer when it is clicked. BROWSER=webkit node --test internal/gui/tests/routing-kind.test.cjs passes 6 runs in a row; chromium could not launch on this machine (Chrome for Testing: icudtl.dat not found in bundle), before and after. go test -tags nogui ./internal/gui/... passes. 2026-10-04 12:20:03 +08:00
yetone 680ca5508f library: RTK's savings are read from rtk gain's stdout alone, a count rtk couldn't give says why instead of "Nothing saved yet", and the page says whose commands the count is (#741 part 2, on Windows with magpie 0.1.807, RTK 0.51.0 and Codex: the RTK card said 还没有节省记录 while rtk gain in a terminal listed savings) — ReadRTK ran rtk gain --daily --format json with CombinedOutput and json.Unmarshal'd the lot, and took any error, from rtk or from the JSON, for nothing saved. rtk writes warnings to stderr with the JSON on stdout — on Windows the console code page it has no decoder for (warn_unmapped_codepage), a command it couldn't resolve, a history table it found damaged — and any of them in front of the "{" made the output unreadable, so the card said nothing was saved however much was. rtkGain now runs it with stdout and stderr apart (stdin closed, 30s at most), parses stdout from its first "{" to its last "}" (a notice printed ahead of it is passed over), and when rtk fails or prints no summary it can read, gainErr says so with what rtk said ("rtk gain: <last lines>", "rtk gain printed no summary magpie can read: …"), shown on the card as "magpie couldn't read what RTK saved: <why>" and by magpie library as "what rtk saved isn't known". magpie counts no agent itself: the figure is rtk gain's, global, every command any agent or terminal ran through RTK on this computer, and the card now says that after it, so an empty one isn't read as magpie counting only some agents. On Windows, Codex with its Windows sandbox on (config.toml's [windows] sandbox = "elevated"/"unelevated", or the older elevated_windows_sandbox / experimental_windows_sandbox features, and not danger-full-access) runs its commands as its CodexSandbox account, whose own %LOCALAPPDATA%\rtk\history.db keeps what RTK saved there, or with a restricted token that can't write the user's — either way not in the rtk gain the user runs; ReadRTK reports that as codexSandbox, and the card (and magpie library) says why what Codex saves isn't counted. The count is read again each time the RTK tab is opened, so commands run since show without restarting magpie. TestRTKGainRead uses a fake rtk: a code-page warning on stderr with JSON on stdout (read — the old CombinedOutput code says nothing saved), a notice line before the JSON, zero commands, an rtk that exits 1, and one that prints no JSON, with the exact gainErr; TestCodexSandbox covers each config form. library-rtk-gain.test.cjs (webkit) checks the reason and the scope line, the Codex sandbox note, and that reopening the tab re-reads the count, in English and Chinese. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass, as do the Library and click-scroll GUI tests. Not tried on the reporter's machine: which stderr line broke it there isn't known (the card now shows it if it's still there), and the Codex sandbox account's separate history is from Codex's and RTK's source, not seen on a real Windows Codex. 2026-10-04 12:15:40 +08:00