The page already sends one request for a group (agents-some), but its one
change ran a sync that, for every skill given as a copy (every WSL agent,
Windows without the right to link, agents set to copies), hashed the copy
and the library's skill twice, in every agent, on every change, and the
page's read hashed the copies once more. Over a WSL distro's share that is
every file of every skill read through 9P each click.
A copy's mark now keeps the hash of what was copied (markCopy). A copy
whose mark says the library skill's current hash, with nothing in it newer
than the mark, is current without its files being read (copyIsFresh);
takeEdits' walk of a copy is not done a second time in the same sync, and
each library skill is hashed once per change (libHash). Copies with an old
mark, or edited since, are hashed as before. Claude Desktop's copies and
project copies use the same check.
Measured in a sandbox (55 skills x 31 files, copies for 10 agents, a
5-skill group switched on for an 11th): the change went from 1.54s to
0.31s and the page's read from 0.66s to 0.25s on a local SSD; the copies
not touched are no longer read at all, which is the cost over WSL.
TestSyncDoesNotReadUnchangedCopies fails without the change ("codex's
copy of pdf was read and made again"): an unreadable file in an unchanged
copy is left alone now. TestCopyFreshByMark checks a changed library skill
is still copied again, an old mark without a hash still works, and an edit
in a copy is still taken. go test -race ./internal/library ./internal/gui
pass, the new tests pass -race -count=10; vet, gofmt, darwin/linux/windows
builds pass. Not run on Windows or WSL.
The built-in sign-ins that come back to a port on this machine (Codex,
ChatGPT API, Devin, Kiro, Zed, Gemini, Antigravity) already took the
pasted address. Their plugins didn't: a subscription moved onto its plugin
(Devin, Kiro, Zed) or a plugin's own browser sign-in (Trae) showed only
"the account shows up here as soon as you're done", and a browser on
another computer than magpie's ended on a page that won't load with no way
to finish.
Now a plugin's "auto" sign-in whose page names a port on this machine to
come back to (redirect_uri, auth_callback_url, callback on localhost, or
Zed's native_app_port: loopbackPorts) sets pasteCallback, so the GUI shows
the same Callback URL field as the built-ins, and `magpie plugin login` /
`magpie accounts add` ask for it too. SubmitSignInCallback opens the pasted
address on 127.0.0.1 at that port, never through a proxy, so the plugin's
own listener finishes the sign-in as it would for the browser; an address
on another port or host is refused, a page that sends the browser on to a
page that comes back to the same port (Kiro's AWS sign-in) becomes the next
page to open. Command Code's page posts its key from script, which leaves
no address to paste, so it keeps offering the plugin's API key way.
Verified: TestPluginSignInTakesAPastedCallback (fake plugin with a loopback
sign-in, $FAKE_LOOPBACK) fails without the wiring in pluginBegin ("a
sign-in that comes back to a port here takes no pasted address:
{... PasteCallback:false ...}") and passes with it, -race -count=10;
TestLoopbackPorts covers the community plugins' page addresses.
go test -race ./internal/provider ./internal/plugin and the root package's
Plugin|Login|SignIn tests pass; vet, gofmt, darwin/linux nogui/windows
builds pass. With the real community Devin, Zed and Kiro plugins in a
sandbox HOME, each sign-in offered the field and a pasted address reached
the plugin's listener and the vendor (bogus code refused by Devin 401, Zed
"couldn't be decrypted", Kiro 400). signin-paste-plugin.test.cjs (new),
signin-callback and signin-paste-codex pass in Chromium and WebKit, en and
zh, at 360px; no new strings. Not tried: a real successful sign-in from a
browser on another computer.
Follow up #965: keepUnreadLogins still ignored read and backup failures,
so a new sign-in could replace credentials that had not been recovered.
Return those errors and stop writeLogins before replacing the original.
A missing logins.json can still be created, and successful backups keep
the existing behavior. Document the save failure contract.
TestUnreadLoginsWriteErrorsKeepCredentials fails on the latest base for
both faults with "original credentials overwritten" and passes with the
fix. The unread-logins tests also pass under -race -count=20.
On 588e51ea, the full nogui suite, go vet, and macOS/Linux(nogui)/Windows
builds pass under a temporary HOME with pinned Go caches. The full suite
uses a temporary, checksum-verified Bun for the CLI's plugin test.
The servers page had only By agent and the order, each with its own "In the library" heading, so with both the heading showed twice. Turning every server on or off, or removing them all, took one chip or one row at a time. The page now has one heading (lib-serverhead, which wraps when narrow) holding the order, Select, Turn all on, Turn all off, By agent and Remove all, as the skills page does. The skills page's helpers now serve both pages rather than being copied. picking names the page ("skills" or "servers"). pickingFor, pickRow, pickBar and groupChips(kind) are shared, and so are everyButtons, confirmEveryOff and everyOf, which replace everySkillButtons and everySkill. removeEveryButton, confirmRemoveEvery and removeEvery replace removeEverySkillButton. The skills page says and does what it did before.
- Turn all on and Turn all off post every agent shown to servers/agents-all (EveryServerAgents, now serversAgents(nil, …)). A hidden agent is left alone. Turn all on is greyed once every server is on for every agent that can reach it. Turn all off asks first, in the page.
- Select puts a box on each row. A row's click ticks it rather than opening the server. The bar's chips post the servers picked to the new servers/agents-some (SomeServersAgents), which gives an agent only the ones it can reach (mcpFile.supports) and leaves unnamed servers and agents as they are. An agent that can reach none of the picks has its chip greyed, with the reason.
- Remove all asks first. It says which agents lose the servers, the projects they are taken from, the sign-ins forgotten, and that the agents' own servers stay. It then posts every name to the new servers/remove-all (RemoveServers). This is one change that takes each server out as RemoveServer does: out of the library, its projects and its icon (removeServer, now shared by both), then out of every agent through syncMCP, which takes only what Applied says magpie wrote, and then forgets its sign-in. A name the library hasn't is listed in Unremoved, and the rest go all the same.
- The pick bar wraps, so its chips and Done fit at 440px in ja and de.
TestRemoveServers checks a server the user put in Claude Code's file and Codex's own "docs" of a library server's name: both stay, while fs, docs and neon leave the library, both agents and the project's .mcp.json, and the project's own "team" stays. neon's sign-in is gone, "nope" is reported, and an empty list or one with only "nope" errors. TestSomeServersAgents checks that Codex gets fs but not the SSE web, that git stays untouched, that a call naming no servers or no agents errors, and that a call naming an unknown server writes nothing. Both pass with -race -count=10, alongside TestEveryServerAgents and TestRemoveServer*.
library-servers-bulk.test.cjs passes on Chromium and WebKit in en, zh, ja and de at 440px. It checks one heading with every button, Turn all on's post without the hidden Copilot CLI, Turn all off's Cancel posting nothing and Confirm keeping Copilot CLI's, the pick bar's codex chip posting agents-some for the two picked and its greyed state for web alone, the All box, Done, and Remove all's lines, Cancel and post. It also checks for no sideways scroll and that each control clicked stays where it was on the screen. The fake check answers every server, since an unchecked one is checked again at each drawing. It fails without the change and passes 6 runs beside pick-skills, every-skill and gui-ja at --test-concurrency=4. by-agent, pick-skills, every-skill, remove-all-skills, group-*, library-*, mcp-signin, project-servers, skill-*, i18n-keys, gui-ja and gui-de pass. skill-how's WebKit case and click-scroll's "Live and a day" fail the same on the unchanged origin/main when run beside each other, and pass alone. go test -tags nogui ./internal/..., go vet, gofmt -l, and the linux (nogui) and windows builds pass. Not tried in the real app. docs/subsystems/library.md says it.
docs/reference.md gets a section "How magpie picks an account" after Routing
groups; docs/subsystems/gateway-routing.md links it and notes the two
points users most often misread. Docs only; no behavior changes.
Every claim in the issue was checked against origin/main (38a0f470):
internal/gateway/routing.go (weighRouted, restAfterMarked, rateRest),
affinity.go (affine), fallback.go (perKeyBarred, plan, restLast),
gateway.go (serve), internal/provider/routing.go (Allowance.For, Renewal,
Pace, Allowances), codex_switch.go (NextLogin, KeepOnAnAccountWithRoom),
subscription_usage.go (claudeWindows), claude_limits.go, login_usage.go,
and app.js's Make first.
Claims in the issue that were wrong or incomplete:
- The order has more than four steps: Sink (when on) after the routing,
the provider's fallback models after its accounts, then resting ones
last over all of them, then affinity.
- A Claude account that has to be signed in again is not filtered out: it
stays a candidate and is passed over at its turn, without resting.
- "Used to about 90% and it switches" holds only for new conversations:
affinity keeps a conversation on an account until 98% (100% In order).
- Smart: Make first is not "useless once resets differ by an hour". Resets
are compared window by window, biggest first, each truncated to the clock
hour, so 13:55 and 14:05 differ while 14:05 and 14:55 tie; the order
counts only when every window ties (or, at 90%+, shares are equal).
- Weekly pace: Make first counts only when the recent tokens are equal,
not "similar" (a decayed float: in practice both zero).
- Saved Claude accounts' usage heard from rate_limit_event has no 1-hour
validity: it is kept until replaced (a window past its reset reads as
empty). claudeHeard's hour only lets such a reading stand over a failed
/usage of the signed-in account.
- Claude's /usage also runs once after start and on `magpie accounts`, the
TUI's Usage page and `magpie quota wait`, not only the GUI refresh.
- The direct-use switch also moves on at a usage cap, uses 100% under In
order, goes back once the account it moved off has every window under
90%, and never moves to a saved Claude account whose allowance isn't
known yet.
- "Response header" in the rest order is magpie's own carrier of the
refusal's reset time; Retry-After is held to an hour.
No UI hint: the natural spot (Make first's tooltip) would reword existing
strings in four languages; left for a separate change.
Follow up on the Menlo style ordering noted in PR #1007. Compare normal, italic and oblique explicitly after weight, retaining the existing width and name tie-breakers.
The Menlo regression fails before the change with Italic, Regular, Bold Italic, Bold, and passes with Regular, Italic, Bold, Bold Italic. Group/tie-break tests, fresh Windows native discovery, focused nogui GUI/font tests, go vet and Windows/Linux nogui builds pass.
A member whose reply had begun and whose vendor's safety filter then
refused the turn — Chat's code content_filter, Anthropic's stop_reason
refusal, Responses' bio_policy — was booked as a vendor failure
(Fail other) and rested with a backoff, so later requests skipped a
healthy account. The same refusal, said before any of the reply was, is
booked failRefused and rests nobody, and the request is handed to the
next member (#248). Only the timing differed: a refusal is about the
request's content, not about the account (5353ed54 rested every
WorkBuddy AI account over one system prompt).
The refusal was read only while the stream was still held for its first
content (`scan`): once that content has been said the reply goes through
as it comes, and an event after it is never classified. The holdWriter
now reads the events of a reply that has begun for a refusal in one
(`refusalAfter`), with a word filter before the JSON is read, so an
ordinary stream pays one substring test a write, and the mid-reply
branch books such a refusal as failRefused without resting it.
What the agent gets does not change: the reply and the vendor's own
error event go to it as they came, nobody else is asked (its 200 has
been sent), and the conversation is still booked as unanswered. An
error of another kind — an unreachable vendor, an overloaded one —
still rests as it did (#733), as does a reply too long.
TestRefusalMidReplyRestsNobody is new. On main, with a group of two
Chat members whose first answers half and is then refused:
after content: try0: status=200 fail=other rest=other
before content: try0: status=400 fail=refused rest=<nil>
With this change the after-content case reads fail=refused,
rest=<nil>, as the before-content one does, and the test fails on main
and passes. TestRefusalAfterContentPassesThrough passes unchanged.
go test -race -count=5 -run TestRefusal, go test ./internal/middleware,
go vet (windows, darwin), the darwin, linux and windows builds, and
gofmt -l on the three files pass. One test in internal/gateway fails on
this machine either way — TestSweepBridgeProjectsTakesOnlyThe-
BridgesFolders needs SeCreateSymbolicLink — so it is skipped by name
rather than taken for a baseline. Not tried with a real vendor's
safety filter.
* gui: choose installed interface and code fonts and styles
Discover installed styles through DirectWrite, CoreText and Fontconfig. Keep choices per computer, apply them before paint and notify both desktop webviews after saving; defaults retain platform and Omarchy fallbacks.
Verified seven focused Go tests, including real Windows discovery (90 families, 276 styles), 14 font browser cases in Chromium/WebKit, 68 settings/zoom/Omarchy/web cases, go vet, Windows amd64/arm64 and Linux nogui builds. Against the original assets, the new UI test fails with Settings must offer an interface font picker (0 != 1). Full Windows suites have separately reproduced upstream failures; macOS/Linux native discovery is covered by the added CI step.
* gui: retain bundled Mac fonts and variable style precision
Only filter an undownloaded CoreText face when it is explicitly downloadable; bundled fonts are installed without being downloaded. Use resolved font names when descriptors omit them. Preserve fractional weights and nonnegative CSS widths for named variable styles.
* gui: restore the authored font cascade when returning to defaults
Apply family-only trait overrides only while a custom face is active. The browser regression catches code under a bold parent becoming 700 instead of its original 400. Keep the compatibility shim first, and apply boot fonts before visible content.
* gui: keep font picker review separate from CI integration
Leave the native test sources and manual verification command with the feature. Move the added workflow step to a follow-up so the font picker can take the normal review path requested on PR #1007.
* gateway: a Sub2API key out of its 5h/1d/7d limit ("…限额已用完") rests as out of quota, not a 1-minute rate limit
Sub2API refuses a key that has spent the 5-hour, day or 7-day limit its
owner gave it with 429 "api key 7天限额已用完" (or 日限额, 5小时限额),
typed rate_limit_exceeded. It matched rateWords and not plannedWords
("限额" is not "额度"), so failure() said rate: the key rested a minute,
was tried again, refused again, and sank, for the days its 7-day window
had left. plannedWords now has 限额已用完, so it is failQuota.
The relay's other refusals keep their classes, fixed by the test: the
pool out of accounts (v0.1.149's 503 "No available accounts": other;
later versions' 429 "All available accounts are currently
rate-limited": rate), its upstream's rate limit (rate), the key's own
total quota API_KEY_QUOTA_EXHAUSTED (quota) and the wallet's 403
INSUFFICIENT_BALANCE (credit).
Verified: TestSub2APIKeyLimitIsQuota, with Sub2API v0.1.149's message
bodies. Without the change it fails: "429 {…api key 7天限额已用完…}:
rate, want quota", and the same for 日限额 and 5小时限额.
* provider: a Sub2API key's /v1/usage shows its 5 hours / 1 day / 7 days windows instead of nothing at "remaining"
A Sub2API key its owner gave a 5-hour, day or 7-day spending limit
answers /v1/usage in quota_limited mode with rate_limits — each
window's limit, used and reset_at in dollars — and, without a total
quota of its own, no "remaining". With the Balance URL set to it, the
key's card said `nothing at "remaining" in the reply`.
readSub2APIKeyLimits reads rate_limits into the windows the cards
already name 5 hours, 1 day and 7 days, in USD. A window not started
(used 0, no reset_at) is taken to renew a span from now; a window with
a limit of 0 is none. The card shows them, beside a balance field the
user wrote; with none, Check balance and `magpie provider show` give
what the fullest window has left. A wallet key's reply is unchanged.
KeyAllowance gives routing the same windows, per provider and key: read
behind the request, never waited for, at most once a minute (once for
any number of asks at a time); a failed read keeps the last; after a
restart it is seeded from the card's last reading in quotas.json;
StaleKeyAllowance has it read again at the next ask. ForgetBalances (a
key or Balance URL changed) has every key read again, a read begun
before dropped, the last reading standing until then. Only keys whose
Balance URL is a Sub2API /v1/usage are asked: any other key, and every
account, costs nothing here.
Verified with real bytes of a limited key's reply, its key removed
(testdata/sub2api_usage_key_limits*.json): TestReadSub2APIKeyLimits,
TestSub2APIKeyLimitsOnItsCard, TestKeyAllowance*. Each fails without
its part, e.g. the card without the windows: `Error:nothing at
"remaining" in the reply`; KeyAllowance waiting for the read: "waited
30.0s"; asking every time: "asked 11 times"; a failed read asked again
at once: "asked 22 times after failing"; not seeded: "seeded = [],
false"; any key asked: "asked /api/usage/token"; ForgetBalances
dropping what was read: "after ForgetBalances, and a read begun before
it: []"; leaving a read under way loading: "the key's windows were
never read"; a stale-generation read landing: it replaced 95%; a limit
of 0 taken: "a day's limit of 0 = [{… Used:100 …}]"; Balance from the
first window only: "$300.00" for $80 left.
* gateway, gui: keys whose windows magpie reads are weighed and rested by them, like accounts
Smart, least-used and pace routing weighed only subscriptions by their
windows: weighRouted skipped every key (c.p.Account == nil), so a
Sub2API key beside Codex accounts in a group went by its place in the
order, and, refused for its 7-day limit, rested a quarter of an hour at
a time while the window had days to run.
A key whose own windows magpie reads (KeyAllowance: a Sub2API key given
a 5h/1d/7d limit, its Balance URL /v1/usage) is now weighed as an
account is: fine, low (90%) or spent (98%), the one renewing soonest
first, in a provider and in a group alike, and its trace row carries
them. Out of quota it rests till the window it filled renews, by
candidate.full; when its windows as last read aren't full it rests
quotaRest, and either way they are read again (staleKeyAllowance).
Every other failure of a key backs off as before, never to the window:
the relay out of accounts for everyone (503/429) says nothing of the
key's own spend. capHeld and pooled stay accounts' only. Keys magpie
reads no windows of are ordered exactly as before.
The Routing page tells such a key as it does an account: its row's bar,
used share and renewal, why it went first by its allowance, "Every key
and account here is at 90% or more", and the Smart header. A key not
read yet beside one read is told as weighed with it, not "keys go in
their order" or by its tokens. New strings in en, zh, ja and de.
Other users of the relay draining its accounts don't show in a key's
windows, which count only its spend: they show as the pool's 503/429,
which back off and fall over to the next key, account or fallback.
Verified: TestKeysWeighedByTheirWindows, TestGroupWeighsKeyWindows,
TestKeyOutOfItsWindowRestsUntilReset, TestKeyPoolErrorDoesNotRestForWindow;
and routing-key-windows.test.cjs in Chromium and WebKit (en/zh/ja/de).
Without the change they fail: "smart: kw#a kw#b kw#c"; "the key renewing
first: codex sub2api"; "full: rests 14m59.99s by quota"; "read again:
[]"; backing off to the window: "pool empty: rests 71h59m59.99s by
window (other)"; and on the page "relay-a goes first: keys go in their
order, those that suit the request first." where all are at 90% or
more, or a key not read yet beside one read.
* gateway, provider: a Sub2API key out of its limit is back once its windows read short of full, not only when the window renews
A key refused for its 5h/1d/7d limit rests till the window it filled
renews: days, for the week. Its windows are read again behind the next
requests (a minute apart) and by its card, but nothing lifted the rest
when they came back short of full, so the owner raising the key's limit
in the relay's panel, or resetting its usage, left the key out till the
week turned, though the relay would take it at once. Accounts have this
already: a Codex reset spent tells OnRenewed, and the gateway lifts the
account's rests (renewed).
A key's reading, routing's or its card's, now tells OnRenewed the same
way, as agent "" and the key's KeyAllowanceID, when a window it was full
in (Allowance.Full at SpentShareOf its routing, as candidate.full counts
it) is full no more, or full till sooner. Its first reading, a failed
read, or one that finds it fuller tells nothing. The gateway notes the
key's KeyAllowanceID on its rest out of quota only, so renewed lifts
that rest and no other: the relay out of accounts for everyone (503)
still backs off, and another key still out of its week stays out.
Where nothing weighs the key by its windows, nothing read them again
either: an ordered group weighs each member's keys alone, so a member
with one key, like a provider with one key, never asks for them. After
the one read its 429 starts, the key sat out its week on that reading.
restingOf, which plans a resting key behind the rest, now asks for the
windows of a key resting out of them (agent "" and a user on its rest),
so the next request planned past a minute reads them, behind it, and a
reading short of full brings the key back there too. No card is read
for it, and a key resting for any other reason asks nothing.
Verified: TestKeyAllowanceRenewed (provider) and
TestKeyBackOnceItsWindowIsNotFull (gateway, end to end through the key
cards' reads and restAfter), each failing with a piece of the change
undone: never telling, "its week's limit raised to $1,000: told []" and
"a, its limit raised: still rests by window till …"; telling on any
change, "its first reading, out of its week: told [/sub2api#…]"; a
failed read telling, "a read that failed: told [… …]"; the card not
telling, "its usage reset, read on its card: told [/sub2api#…]"; the
rest not noted, "a, its limit raised: still rests by window"; every
rest of a key noted, "c, the relay out of accounts: …, false"; renewed
not matching the key, "b, still out of its week: …, false". And
TestRestingKeyReadAgainInAnOrderedGroup (gateway: an ordered group, its
first member one key on a fake /v1/usage, no card read after the 429),
failing without restingOf's ask: "its limit raised: still rests by
window till …, its windows read 1 times".
Tried on a real Sub2API v0.1.149 relay, in a sandbox HOME, its key's 7d
limit lowered under its spend for the test and then restored, with
magpie serve on a group ordered: the key, then a local stand-in. The
key answered 429 "api key 7天限额已用完"; it rested by window till its
7d reset, 2026-10-13 00:00 +08:00, and the stand-in served. Planned
again a minute later, still over, it still rested. With the limit back
at $800, the next request was planned with it resting, the stand-in
serving, and read its windows; the one after went to the key, 200.
* feat(agent): add Reasonix Studio 2.x integration
* agent: select Reasonix planner independently from executor
* agent: restore Reasonix when its credential file was initially absent
* agent: preserve Reasonix planner choices and model controls
Keep configured and saved native planner references selectable, and retain full model picker behavior and role labels. Exercise native planner restoration and both browser layouts. Separate temporary CLI first-launch work from version and intentional timeout assertions.
* test(gui): follow the current Reasonix connection flow
* test(gui): open connected Reasonix details before checking roles
* agentenv: isolate Reasonix configuration in test sandboxes
* gui: restore Reasonix roles without disconnecting the other
* gui: Reasonix's role labels read in every language, and its confirm names them
The review found four things on the Reasonix roles' strings.
"executor" and "planner" are field labels like any other, so t() translates
them; they had no i18n entry in any language, so the zh, ja and de windows
showed the English key. They now read 执行模型/规划模型, 実行モデル/計画モデル
and Executor-Modell/Planner-Modell, beside model/effort/thinking in each block.
The confirm said "neither Executor nor Plan" — names that appear nowhere in
the UI — and its zh text said "Executor 和 Plan", while the fields are labelled
executor and planner. It now names the labels, and each language names its own
translation of them (zh 执行模型和规划模型, ja 実行モデルも計画モデル, de vom
Executor- bzw. Planner-Modell).
Both new strings had a zh entry and no ja or de entry, which is what turned
gui-ja and gui-de red: every zh key needs its ja and de with the same
placeholders. "restore the previous {field} selection" also started at column
0 in i18n.js; it is indented with the lines around it now.
reasonix-roles.test.cjs asserts the rendered label, so its zh expectations name
执行模型/规划模型; the en ones stay as they were, the en label being the key.
Its en assertions on the confirm's first sentence are unchanged, and the
reworded second sentence is not asserted.
Verified on this head: gui-ja passes 3/3 and gui-de 3/3 (every zh string has
its ja and de, same placeholders and tags); reasonix-roles.test.cjs passes 8/8
in Chromium and WebKit, en and zh, window (1000px) and panel (440px); go vet
./... and the four builds pass. The rest of internal/gui/tests was run and
compared against a clean main worktree at e0bf3e61: the same assertions fail on
both, and none of them is about i18n or Reasonix (see the PR comment).
* edit: a TOML value is written the way TOML reads it, preserving or not
setTOMLTop spelled every value with strconv.Quote. Go's escapes are not TOML's:
a control byte came out as \x01, and TOML defines no \x escape, so a preserving
edit wrote a config.toml that no parser reads — an agent's own value could not
be written at all. Worse, it only failed at writeTOML, after the edit was
already decided.
The preserving path and the path that adds a key now both use tomlLiteral, as
SetTOMLKey already did, so both write the same encoding. tomlString spells a
basic string from the TOML spec (\b \t \n \f \r \" \\, \uXXXX for the other
control bytes and DEL, everything else as it is, non-ASCII included). A byte
that is not valid UTF-8 has no TOML escape, so it is left as it is and writeTOML
refuses the file, rather than the value being quietly replaced by U+FFFD.
TestTOMLPreservingKeepsAValueTOMLCanParse parses the file back with the TOML
parser and compares the value, and TestTOMLPreservingAndPlainAgreeOnEncoding
puts the same value through both paths and compares the bytes. TestSetTOMLKeepsCRLF's
"top = \"9\"" is "top = 9" now: an int is a TOML integer, the same value
SetTOMLKey writes for it, and no caller but that test passes anything but a
string here.
Verified on this head: go test ./internal/edit/ passes whole; the two new tests
and TestSetTOMLKeepsCRLF pass under -race -count=10; go vet ./... and the four
builds pass. TestTOMLBigFileParsesInLinearTime fails under -race -count=10 on
this head and on a clean main at e0bf3e61 alike (a 1s budget against ~1.2s under
the detector); it passes without -race.
---------
Co-authored-by: h4rK8s <63350517+h4rk8s@users.noreply.github.com>
On a MacBook the window had last been zoomed. magpie skipped a maximised
window when it saved the size, so it reopened at an older, nearly full
size: 1712×1083 at (8,34) on a 1728×1084 work area, neither full nor the
size it was given nor the default.
settings.WindowMaximised (this computer's own, KeepOwn) now records
whether the window was maximised when it settled; Window stays the size
it restores to (settle). A window not shown yet, let go, full screen or
minimised, or on Linux hidden, records nothing.
Reopened maximised: on the Mac and Windows placeMain maximises the made
window just before its first Show (on Windows once its page has come);
Wails' StartState is undone by the Mac's centring and shows a Windows
window before its page. The least size Maximise drops is put back. On
Linux makeMain sets StartState, since GTK can't be asked about a window
not mapped yet.
A size kept on a larger screen: Windows fits the window to the work area
and centres it, title bar in reach, and keeps the larger size for the
larger screen (fitted); the Mac fits and zooms it itself, and magpie
doesn't zoom it again, so it can still be restored.
TestSettle fails with upstream's rule (5 of 9 cases); TestOpenSize,
TestFitRoom, TestWindowMaximisedKeptAndOwn; the Mac's real build under a
sandbox HOME, 11 cases.
* gateway: a request refused as every account is held records why, caps or credits
When every account was held, the request's record always read "every
account at its usage cap", though the client's 429 (cappedError) already
told a usage cap, credits set not to be spent, and the two together
apart. The record now comes from the same classification (heldBy): the
cap-only wording is kept, credits-only reads "every account held: set not
to spend its credits", and a mix names both.
TestCodexNoCreditsEveryAccount checks the record for both requests; with
the old fixed string it fails with: recorded 429 "every account at its
usage cap" (twice).
Follow-up to #974.
* gateway: the 429 for accounts held off their credits says to spend them only if one holds some
When every account was held as set not to spend its credits, the 429
told the user to let an account spend them to go on now. An account that
holds no credits is refused by the vendor at 100% all the same, so
following that got one more 429. The hint now reads "if an account holds
credits, let it spend them", in the credits-only and the mixed message.
The gateway's allowance readings carry only the windows, and a Codex
reading's balance is "" for none, unlimited and unreadable alike, so a
known-empty balance isn't available to drop the hint by.
TestCodexNoCreditsEveryAccount checks the new wording in both messages;
with the old one it fails with: no "if an account holds credits, let it
spend them" in the body (both requests).
Follow-up to #974.
* usage, docs: a Codex account set not to spend its credits is held by magpie's latest reading
The Use credits hint, magpie quota credits' help and the docs said that,
turned off, the account's credits aren't spent. The hold goes by magpie's
latest reading of the account's windows, which is read again about once a
minute, so for a minute or two after a window fills, or while nothing is
read yet, a request can still reach the account and spend some credits.
Each now says the hold follows that reading, refreshed about every minute
(Usage card in en/zh/ja/de, quota help, reference.md, gateway-routing.md).
codex-credits (Chromium and WebKit, en/zh/ja/de: 8/8), i18n-keys (1/1),
gui-ja (3/3) and gui-de (3/3) pass; codex-credits checks the reworded
string has its zh, ja and de.
Follow-up to #974.
* provider: a Codex account at a spend cap or past its overage with credits left keeps Codex signed in, as the Codex app's composer still sends for it
#966 held such an account (and one whose limit_reached alone was true),
following the readiness check Tht in ChatGPT.app 26.930. Tht has one
caller, the work-credit-restriction-recovery poller; it isn't the
composer's gate. The composer's submit is disabled only by hP (search
"n.rate_limit?.allowed!==!1||Jpe(n)||SIe(") and hardBlocked (search
"hardBlocked:r.rate_limit?.allowed===!1"): both read rate_limit.allowed
alone and let credits (has_credits or unlimited) through before any spend
cap or overage. So the app sends for that account, and holding it gains
nothing: CodexUsedUp only moves Codex to magpie as provider while Codex is
on a magpie model, whose turns already reach magpie through
openai_base_url. It only takes the app out of ChatGPT (durable threads,
remote control), the loss #966 set out to stop.
codexHeld is back to that gate: held when allowed is false, no credits,
and not a workspace within its overage (no spend cap, an ordinary
reason). Its comment names the build, both gates and why Tht isn't one,
each with a string to search for. The spend-cap and overage subcases now
expect not held; new subcases pin allowed as the only trigger. Five
subcases fail on the merged codexHeld. agent-wiring.md and reference.md
say the same.
Follow-up to #966.
* provider: a Sci, Law, FinServ or Enterprise trial/view-only workspace within its overage keeps Codex signed in
codexHeld lets a workspace with no credits through while it is within
its overage, as the Codex app's composer does, but the workspace list
came from codex-rs's PlanType alone. The app's own plan_type switch for
its usage banner (ChatGPT.app 26.930.61225, app-initial) also names sci,
law, finserv, enterprise_cbp_trial and enterprise_cbp_view_only, so such
an account read held and magpie took Codex off its ChatGPT sign-in while
the app still sent. They are added; the comment cites that switch. guest
there is a personal plan and stays out, and a plan not listed still
counts as no workspace (held).
Verification: "Law without credits, within its overage" in
TestCodexUsedUpIsWhatTheAppHolds fails without the list change
(CodexUsedUp = true, want false) and passes with it.
* provider: a held Codex reading with no window at 100% stays held through a failed read
A reading kept for when /wham/usage fails dropped Held whenever none of
its windows read 100%. That is right for a hold that came with a used-up
window which has since started again, but a hold with no window used up
(the backend says not allowed, no credits, every window under 100%) has
no reset to wait for: the first 503 after it read not held and put Codex
back on its ChatGPT sign-in for an account its app sends nothing for.
Held is now cleared only when the saved reading had a window used up and
none is after the passed resets are applied; any other hold stands till
a fresh read.
Verification: TestCodexHeldWithNoWindowUpKeptTillAFreshRead (the real Pro
reply, credits spent, its window at 40%: a good read, then a 503) fails
without the change ("CodexUsedUp = false on the kept held reading, no
window used up") and passes with it. TestCodexHeldKeptOnlyTillTheReset
still passes; both share their setup now. -race -count=10 passes.
* agent, gui, docs: Codex's sign-in notes say magpie steps in while the Codex app blocks the account, not whenever it is used up
#981's sign-in notes said magpie becomes Codex's provider "while its
account is used up". Since codexHeld, a ChatGPT account at 100% with
credits left keeps Codex signed in; magpie steps in only while the Codex
app blocks the account (out of allowance, no credits left), and the notes
now say that, Always ChatGPT's to match, in English, Chinese, Japanese
and German. agent-wiring.md's sign-in line and reference.md's Codex
paragraph say when the app holds an account as codexHeld reads it: OpenAI
no longer allows it and it has no credits left, a workspace still within
its overage excepted; at 100% with credits Codex stays signed in.
Verification: codex-signin-always (Chromium and WebKit, en/zh/ja/de, 1100
and 440px) 8/8, i18n-keys 1/1, gui-ja 3/3, gui-de 3/3. git grep finds the
old English notes nowhere under internal/gui/tests or internal/gui/assets.
* provider, codex: a ChatGPT account at 100% with credits left keeps Codex signed in — magpie becomes its provider only while the Codex app itself holds the account
magpie made itself Codex's provider (model_provider = "magpie", #540)
whenever any of the active ChatGPT account's windows read 100%. A Pro
account at 100% on its week with credits left keeps working in the Codex
app: the backend answers every turn (about 490 200s in one day's usage log
at 100%), yet magpie moved Codex off it, and as a provider without
requires_openai_auth the app lost its ChatGPT state ("Sign in to ChatGPT
to start a durable thread", no remote control).
CodexUsedUp now reads codexHeld, which mirrors the Codex app's own
composer block (ChatGPT.app webview bundle, hP and its helpers): the
backend says the account isn't allowed (rate_limit.allowed = false), it
has no credits (has_credits / unlimited), and it isn't a workspace still
within its overage (overage_limit_reached = false, no spend cap, an
ordinary rate_limit_reached). Window percent never decides it. Where the
app goes by what /wham/usage can't show (a statsig gate, the gpt-reserve
fallback, Team's reserve experiment that ignores overage), it counts as
held: a wrong move costs ChatGPT extras, a missed one every turn. A
reading kept through a failed read (keepLast) stops saying held once its
used-up window has started again, as its usage does. NextLogin's account
rotation and its thresholds are unchanged.
A user stuck on the old move (codex.out = 1) is put back beside the
sign-in by the next Sync, as #540 already does once the account has room.
Verification:
- TestCodexUsedUpIsWhatTheAppHolds (real /wham/usage replies, ids
replaced) fails on the old trigger: 7 subcases, "CodexUsedUp = true,
want false". Each workspace guard (spend cap, reason, overage, plan
list, Team reserve) was mutated and fails its own subcase.
- TestCodexHeldKeptOnlyTillTheReset fails without the quota_last change.
- The real Pro account (7-day window 100%, ~28k credits, allowed false):
CodexUsedUp true before, false after.
- go vet ./...; go build (darwin), GOOS=linux -tags nogui, GOOS=windows;
go test -tags nogui ./... under a sandbox HOME; -race -count=20 on the
new and related Codex tests.
- Not run: a live Codex app turn on an account truly held (none at hand),
Windows/WSL runtime, GUI suites (no GUI change).
* provider: a Codex account at a spend cap or past its overage is held, credits or not; limit_reached counts as not allowed
codexHeld let credits through before the spend cap, so a Team seat at its
cap (spend_control.reached, allowed false) with has_credits read not held
and magpie left Codex on an account the backend refuses. The app's
readiness check (26.930.61225, app-primary) returns limited on
spend_control.reached or credits.overage_limit_reached before it looks at
credits, and treats rate_limit.limit_reached like allowed false; held now
reads the same. A spend cap with the rate limit still allowed (a
workspace's included usage) stays not held.
* tui: show a ChatGPT account's credits beside its windows
The Usage page printed only the balance for an account that had one, so a
ChatGPT account holding credits lost its 5-hour and weekly windows there.
The balance now follows the windows, as on the app's cards and in
magpie quota.
* accounts: tell a ChatGPT account's credits
magpie accounts left out the credits an account holds, which magpie quota
and the Usage page show. Its rows now carry them, in the line and as
balance in --json.
* codex: a switch for whether an account spends its credits
A ChatGPT account holding credits keeps answering once a usage window is
used up: the vendor spends the credits, and a task goes on. That stays the
default. Each Codex account now has a say in it (Usage card and menu bar
panel: Use credits; magpie quota credits <account> on|off; settings'
codexNoCredits). Turned off, the gateway holds the account at 100% of a
window, as a usage cap holds it at its share, so the other accounts,
groups and fallbacks take the request and no credits are spent. With none
left the request gets a 429 naming what holds each account and how to go
on, unless the account spends its resets by itself and its week is used
up: then a reset is spent first, whether every candidate was held or the
last one tried was refused. A cap set on the account takes precedence.
A reset made now forgets the account's cached allowance, so neither a cap
nor this hold outlives the windows it started again. The switch is
routing's only: which account Codex is signed in to is unchanged.
The Routing page tells why such an account was left out, and magpie quota,
magpie accounts and the TUI mark credits not spent.
* usage: an auto-reset switch keeps by its first line
The auto-reset row centred its switch on its words, so the switch moved
when they rewrapped on a click, and it sat lower than the Use credits
switch beneath it. Both now keep the switch by the first line.
* gui tests: quota-curve expects the Use credits row to close a Codex account's section
The credits switch adds a row after the Auto-use one, so the account after
a Codex account follows the curve, the Auto-use row and then the Use
credits row.
Codex's sign-in, ChatGPT, keeps Codex signed in to ChatGPT with magpie's
models beside its own, but once the ChatGPT account is used up magpie
makes itself Codex's provider (#540): the Codex app sends nothing at all
on an account it knows is out, a magpie model's turn included. That move
went unsaid, and a user who chose ChatGPT expected Codex to stay there.
ChatGPT now says it, and a third way, Always ChatGPT (login "chatgpt"),
never makes the move, by a pick or by Sync; choosing it puts a Codex moved
before back beside the sign-in. What it costs is said beside it: the Codex
app may send nothing till the account has room, while Codex CLI's turns
still reach magpie by the base URL, its routing and other accounts serving
them.
Verified: provider tests (fake remote: names/kinds, buttons stripped, third-magpie cards dropped, one ask per 30s, refresh paths, unreachable keeps last cards, 404 → update card, cache-only CachedCards, histories) and the gateway test (an in-process gateway with a fake balance vendor counting hits, asked through a remote-magpie provider: nothing read until a refresh, 5 card asks read nothing, two refreshes of a card read once, a remote card's refresh 400, another machine without the key refused) pass with -race -count=20; go test provider/gateway/gui/tui/root, vet, linux and windows builds pass. Two sandbox gateways (3481 with a fake balance vendor, 3482 with it as remote-magpie): magpie quota on the second showed the "nothing read" card with 0 vendor hits, then office/bal "Bal · Office" $12.00 after one refresh (1 hit), 10 more asks 0 hits, two card refreshes 1 hit. usage-remote-cards (names, translated remote errors, refresh ids) passes in Chromium and WebKit, en/zh/ja/de and fails without the app.js change; i18n-keys, gui-ja, gui-de, usage-card-refresh, usage-refresh, quota-reading-again pass. Not tried with a real second computer.
A cold-start account add could replace logins.json without keeping its
credentials when the JSON was syntactically valid but could not decode as
saved accounts. Match readLogins' typed decoding in the backup check, so
invalid field types and a wrong root type are kept aside before a write.
Readable account files still need no backup.
TestUnreadLoginsFieldTypesKeptBeforeWrite fails without the change because
no original-file backup is made for a string boolean, invalid time or
object root. The fixed cases and the readable-file control pass, including
five race-enabled runs. Update the providers/accounts failure contract.
go vet, the full nogui Go suite, the native macOS build and Linux/Windows
cross-builds pass under a temporary HOME with pinned Go caches.
Resolve deliberately accepts arbitrary model names under an existing provider.
The auto-review setting consequently accepted fake/missing and fake/, replacing
a valid reviewer with one that fails when Codex requests approval.
Validate against the served catalog at the settings endpoint. Keep model and
group choices, unlisted providers, whitespace trimming and clearing. Rejected
values leave the saved reviewer and model-list tag unchanged.
TestCodexAutoReviewSetting fails against the upstream API with fake/missing
and fake/ returning 200, and passes with the fix. Focused auto-review tests,
go vet ./..., the Windows build and Linux nogui build pass. The full Windows
suite has failures also reproduced with upstream production code; details
are recorded in the PR.
* gui: edited forms ask before they are discarded, removals ask before they take effect, and the header keeps its first layout — Cancel, Escape, the backdrop and a page change could throw away a provider's draft, and several account, key, plugin and library removals took effect at once. Modified provider, settings, library and routing forms now ask before discarding; unmodified forms still close without asking, and refresh or closing the browser tab uses its leave confirmation. Removal and discard confirmations are in-app dialogs over the intact editor, centered, with Cancel focused first, Tab kept inside, Escape canceling and focus returned afterwards; destructive buttons have readable foregrounds in light and dark themes. Text and connection settings have Save/Cancel rather than saving on blur; simple switches still save at once. Editors associate labels and help with fields, make the background inert while open and keep focus through redraws; navigation keeps browser back/forward history and its current-page state, errors remain until dismissed or replaced, and focus and validation can scroll into view. Mobile settings tabs have larger targets, fields use 16px text and page zoom stays available. layout.js applies mode and platform before the header paints, theme.js applies the saved text size before styles, and mode-specific window controls stay hidden while app.js downloads. zh/ja/de strings added. routing-shape's global .rt-day selector selected the hidden By session button on WebKit; its history click now stays in .rt-days. ux-safety covers unchanged and modified closes, canceled and confirmed removals, Escape, backdrop, modal focus, light/dark centering and button contrast, explicit saves, browser history and sync-form discard. Rebased onto upstream/main 2f183c4c. Chromium and WebKit each pass 31 related browser tests in en and zh; gui-ja and gui-de pass all 6 browser and translation-parity tests. go vet -tags nogui ./..., go build -tags nogui, go test -tags nogui ./internal/gui, JavaScript syntax checks and git diff --check pass.
Co-authored-by: XYenon <register@xyenon.bid>
* gui: edited providers, routing groups and library forms ask before their drafts are discarded, and only immediate removals ask before taking effect — Settings keep saving on Enter and blur, switches save at once, and the reader's scroll position is restored after an unasked-for scroll. Header rows, fallback models, group members, patterns and rules stay ordinary reversible draft edits. The confirmation dialog keeps its own readable destructive colors; the window's text selection, muted and faint colors, timed status messages, replaceState navigation and iOS viewport policy stay as before. The keyboard scroll assertion is restored, and click-scroll's date buttons are scoped to .rt-days .rt-day so WebKit cannot pick a hidden request-grouping button. The safety tests cover dirty editor closes and page changes, immediate provider deletion, labels, modal focus, Escape and backdrop cancellation, with the withdrawn settings, history and persistent-error assertions removed. First-paint layout and the saved theme/text size remain early.
Co-authored-by: XYenon <register@xyenon.bid>
* gui: old WebKit gets its layout before the header paints, and browser tests answer the new discard and removal questions — compat.js runs before layout.js, whose viewport setup uses ES5; the confirmation uses vh rather than Safari 15.4 dynamic viewport units. Opening a provider from its address keeps the editor pending until its page loads, and fields keep their own nested labels when Names & levels is reopened. An empty model preference map is compared as Save sends it, so opening model settings without changing anything closes without a question. Removal tests assert nothing is sent before confirmation; draft tests answer Discard and retain their Save and Cancel assertions. Plugin migration uses the reader’s wheel rather than a scripted scroll, keeping every scroll assertion. Targeted WebKit regressions pass 40/40, Chromium regressions pass 41/41, and the model-setting and editor-safety checks pass in both engines. go vet -tags nogui ./..., go build -tags nogui, JavaScript syntax checks and git diff --check pass.
Amp-Thread-ID: https://ampcode.com/threads/T-01a108b0-c6a0-768d-ad6a-9a571a10a847
Co-authored-by: XYenon <register@xyenon.bid>
* gui tests: the upstream model list keeps folding with the new discard question — models-fold answers Discard only after picking another model; the short list and an unchanged folded list still close without asking. ux-safety waits for its Relay row rather than matching loading placeholders. Rebased onto upstream/main c353f7bd, including the new model-list expansion. models-fold passes in WebKit and Chromium, as do the English and Chinese editor-safety checks; the related model drafts and first-paint phone tests pass on WebKit.
Amp-Thread-ID: https://ampcode.com/threads/T-01a108b0-c6a0-768d-ad6a-9a571a10a847
Co-authored-by: XYenon <register@xyenon.bid>
* gui: an editor redraw keeps its removal confirmation and menus interactive — Only a fresh modal makes the existing background inert, and closing restores exactly the elements it changed. A redraw no longer disables a confirmation or popup appended over the editor; elements already inert and overlays added later retain their own state. The original background is retained if a new editor interrupts the closing animation. ux-safety redraws Relay twice under its Remove confirmation, clicks Cancel, and verifies no deletion was sent; it also picks a menu item after an editor redraw and checks background restoration. The regression reproduced on WebKit before the fix. All 8 English and Chinese safety checks pass on WebKit and Chromium. The related WebKit removal, draft, routing and phone checks pass, with account-arrange passing separately after a detached-element race in the concurrent run. go vet -tags nogui ./..., go test -tags nogui ./internal/gui, go build -tags nogui, JavaScript syntax and git diff --check pass.
Amp-Thread-ID: https://ampcode.com/threads/T-01a108b0-c6a0-768d-ad6a-9a571a10a847
Co-authored-by: XYenon <register@xyenon.bid>
* gui tests: editor safety uses the page's boot preferences, not its settings — gateway in boot.js is now the gateway-mode flag, whereas settings.gateway is the gateway's URL. Spreading settings into the fixture boot data mistook the URL for gateway mode and redirected a dirty editor's attempted page change back to Providers. The fixture now emits the same language, theme, text size and web fields as the server. The 8 English and Chinese editor-safety tests pass on Chromium and WebKit; gateway-mode, plugin middleware and plugin-market pass on WebKit, and phone-web passes separately after a scroll race under concurrent load. Rebased onto upstream/main 2f135ed4 with gateway-mode navigation and initialization retained. go vet -tags nogui ./..., go test -tags nogui ./internal/gui, JavaScript syntax and git diff --check pass.
Amp-Thread-ID: https://ampcode.com/threads/T-01a108b0-c6a0-768d-ad6a-9a571a10a847
Co-authored-by: XYenon <register@xyenon.bid>
* gui: upstream's new removals and fields follow the editor-safety rules too — Audited GUI changes from the original PR base 2f183c4c through upstream/main 9debc583, including earlier rebases. Bulk group removal uses the in-app consequence dialog rather than a second click, and clearing middleware options asks before deleting saved options. Cancel sends nothing and keeps the selection or editor. Custom group context, Settings' compact threshold, the inline key-weight input and middleware JSON have accessible names; the JSON note/error and group context hint are linked, and invalid input is exposed. The copy-out-of-date skill action is a keyboard button supporting Enter and Space. A skill's immediately saved per-agent transfer choices update the modal baseline, so Done, Escape and navigation no longer ask to discard already saved changes. Settings save behavior, draft-only removals, scroll guard, viewport and themes stay intact. Chinese, Japanese and German consequence strings added; obsolete bulk-removal armed styles and strings removed. The six affected browser files pass across Chromium and WebKit in English and Chinese (54 tests after scoping compact-at to its provider rather than skeletons). Additional provider price, key import/weight, group member/first-token, caller-key, purpose and gateway-mode regressions pass on WebKit; key-weight also passes in Chromium, with a concurrent status-message race passing separately. go vet -tags nogui ./..., go test -tags nogui ./internal/gui, go build -tags nogui, JavaScript syntax and git diff --check pass. The Chinese bulk confirmation was rendered and inspected on WebKit.
Amp-Thread-ID: https://ampcode.com/threads/T-01a108b0-c6a0-768d-ad6a-9a571a10a847
Co-authored-by: XYenon <register@xyenon.bid>
* gui: new upstream key removals and nested group jumps keep the editor-safety rules — Bulk deletion of failing, off and matching keys uses the app's consequence dialog, with Cancel and Escape sending nothing and a redraw leaving its buttons interactive. The shared group-editor entry asks before replacing a dirty draft, and nested navigation waits for acceptance before upstream's explicit scroll. Codex's Sign out uses its existing confirmation once. Group and key filters have accessible names. A focused model-price input is included in leave protection before its change/blur parser runs, and reverting the raw value is clean. Settings saving, scroll protection, draft-only removal, viewport and themes retain their upstream behavior. The affected flows pass in Chromium and WebKit, English and Chinese; Names & levels also passes in Japanese and German. The price fixture focuses before filling because the previous blur normalizes sibling fields, a race reproduced on upstream. go vet -tags nogui ./..., go test -tags nogui ./internal/gui, go build -tags nogui, JavaScript syntax and git diff --check pass. The Chinese bulk-key dialog was rendered and inspected on WebKit.
Co-authored-by: XYenon <register@xyenon.bid>
Amp-Thread-ID: https://ampcode.com/threads/T-01a108b0-c6a0-768d-ad6a-9a571a10a847
* gui tests: Codex's newly removable active account waits for the consequence question (#820) — The latest upstream multiaccount Remove path uses accountAction's immediate-removal dialog. Its test now verifies that the first click sends nothing, Cancel keeps the account, and accepting sends exactly one login/forget request. The original account, status and translation assertions remain. Rebased onto upstream/main 8d3cf5f8; the final Codex, nested-group, bulk-key, focused-price and editor-safety tests pass 48/48 across Chromium and WebKit in English and Chinese. Upstream's new quota, skill-group, hit-rate and translation checks pass; search-first's detached-element failure reproduces on isolated upstream. The complete WebKit suite ran 1413 tests before this final upstream delta, with 1289 passes and 124 failures; failing files were compared with upstream or rerun separately. Syntax and diff checks, go vet and the nogui build pass.
Co-authored-by: XYenon <register@xyenon.bid>
Amp-Thread-ID: https://ampcode.com/threads/T-01a108b0-c6a0-768d-ad6a-9a571a10a847
* gui: the Sessions agent strip reports which reused button is selected (#820, #929) — Rebased onto upstream/main 1330b966, retaining the strip's own scrolling, visible selected agent and preserved buttons. Each redraw updates aria-pressed for the selected agent and clears it for the previous selection. The regression checks All initially, the unselected alternatives, then Zed selected with All cleared; on unmodified upstream it fails with null instead of true. Both strip files pass 8/8 across Chromium and WebKit in English and Chinese, including narrow and wide layout and scroll assertions. The latest ChatGPT API, Codex and editor-safety browser checks pass 32/32. Final GUI, Agent, Provider and Gateway Go checks, go vet -tags nogui ./..., Linux nogui and Windows builds, syntax and diff checks pass. Linux native GUI build is blocked by the orb's GTK 4.8 lacking APIs used by Wails beta.24. The complete WebKit run finished with 1289 passes and 124 failures; all failing files were checked against upstream or rerun separately, with details retained in the review artifacts. The full Go suite's sole sessions rewrite failure also reproduces on upstream.
Co-authored-by: XYenon <register@xyenon.bid>
Amp-Thread-ID: https://ampcode.com/threads/T-01a108b0-c6a0-768d-ad6a-9a571a10a847
* gui: closing editors immediately give clicks back to the page (#820, #944) — Restore the background when the close animation starts and protect it again when an editor reopens. Both New group buttons and the model entry ask before replacing a dirty routing draft; creating a group from a provider waits for navigation to be accepted. The GUI shell reference describes these transitions.
The real mouse click during closing fails without the change in all four engine/language combinations, with null instead of received. The unguarded New group and model entries fail waiting for the discard question; the old model navigation creates a routing draft even after Cancel. ux-safety, provider-remove, click-scroll and group-new-head pass 50/50 in Chromium and WebKit, English and Chinese. Closing/reopening also passes five additional rounds, 20/20. After the final upstream translation and TUI additions, i18n-keys, Japanese/German, ux-safety and group-new-head pass 23/23. Ten Chromium update-wait runs have no no-click-scroll failure; two fail on the Restarting status assertion, also reproduced on upstream.
The complete WebKit run finished with 1,307 passes and 141 failures across 1,448 tests, with no skips or cancellations. It is not green: failures in 63 of its 70 failing files also occur on upstream; the other seven files pass separate PR reruns. The complete suite preceded the final upstream translation deduplication, whose resolved keys and values are unchanged, and the TUI/docs additions. go vet and the Linux GUI build with the Makefile's gtk3 tag, Linux nogui and Windows builds, GUI/TUI Go tests, syntax and diff checks pass. Full nogui Go tests are not green: the sessions same-size rewrite check and the final run's WebDAV slow-transfer stall both reproduce on upstream with -race -count=20 under an isolated HOME with pinned caches.
Co-authored-by: XYenon <register@xyenon.bid>
Amp-Thread-ID: https://ampcode.com/threads/T-01a108b0-c6a0-768d-ad6a-9a571a10a847
---------
Co-authored-by: Amp <amp@ampcode.com>
Since #870, magpie's provider in Codex's config carries x-openai-actor-authorization, so Codex offers its image_gen namespace tool on every request. A vendor's Responses API that can't take it, a relay's included, answered "bad response status code 400" to "只回复 OK". After a bare 400 (one that names no field or message magpie already knows to drop), a provider other than a ChatGPT sign-in or openai.com is asked once more without the tool. This works both passed through (the namespace) and translated (image_gen__imagegen flat). If the retry answers, the provider is marked unfit for tool:image_gen. If it doesn't, the tool goes back and the vendor's 400 stands, and the cache-key guess runs on the request as it was. The header stays as #870 wrote it.
image_tool_test.go is new. Its fixture is the image_gen tool Codex 0.160.0 sent to a capture server with magpie's provider table and catalog. TestImageToolLeftOutWhereRefused (translated, passed through) fails with the reporter's 400 when gateway.go is reverted, and so does TestImageToolKeptOverAnotherRefusal. TestImageToolKeptForChatGPT fails when the ChatGPT exemption is dropped. docs/subsystems/gateway-routing.md has the rule. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
The first review covers 2026-10-05's 220 commits (~170 releases): about 40 fixed something released that day or the day before. The lessons are grouped by kind (misread report, fix one sample of a class, user files, thin verification, red tests and releases, merging and closing), each with its evidence. Three that recurred dozens of times move into docs/code-standards.md: a fix reaches every sibling of the bug, a test red on main is fixed rather than used as a baseline, and a release needs a finished green CI run, not a cancelled one. AGENTS.md points at LESSONS.md and CLAUDE.md loads it. Docs only; no code or test changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>