c06a550b73680289d5a94de7a412356d4040297d
1087
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e961836fb0 |
usage, gateway, gui, provider, cli: the tests pass when a run crosses midnight, and a usage answer reads the clock once (#1046)
* usage: the usage tests pass at any time of day, midnight included The usage tests stamped their calls with the machine's time and then asked for today, 7 or 30 days, or a cached answer, which the code worked out from its own time.Now a moment later. A minute after midnight a call made "a minute ago" was yesterday's, and when midnight fell between the test's read and the code's, the call and the period were of two days. magpie usage, the TUI's Usage page, the two Codex cache-write tests and the computers' shared days failed in the first minutes after midnight (the TUI's after midnight UTC, the zone its package's tests run in since |
||
|
|
9db5753ba3 |
providers: a provider's model list reads the settings once, not three times per model (lml on Discord, Windows: 每次至少都得等五六秒…供应商或者额度列表还在加载) — each model's row asked for its image answer twice (ApplyImage, ImageOverride) and its reply limit once (ReplyLimit), and each ask parsed settings.json again, even inside a GET's hold, since filememo keeps its bytes, not the parse. providerInfo now reads the settings once and uses provider.ImageOverrideIn and Provider.ReplyLimitIn. This removes redundant work found while measuring lml's report; it does not explain the whole 5-6s, which needs their setup (see below).
Measured with a copy of the owner's config (23 providers, ~1500 models) under a sandbox HOME and the real server (magpie web, dead proxy): GET /api/providers on the Windows box 497-558ms → 148-214ms, on the Mac 442-525ms → 174-191ms; the JSON answer is byte-for-byte the same apart from the gateway port. Everything else the window waits on at open answered locally in under 0.6s on Windows, so the rest of a 5-6s wait is network (the usage cards wait for every vendor on a cold start, and on Usage's own open), not this. TestProviderListReadsSettingsOnce (300 models, a big settings file, held as a GET is) takes 5.3s without the change and 41ms with it, and checks an image answer and a reply limit the user set are still said. go vet, GOOS=windows go build, GOOS=linux go build -tags nogui and go test -tags nogui ./... under a temp HOME pass. |
||
|
|
698da36f31 |
gateway, providers, gui: a model on this computer or the local network can be set to get requests unmasked by Settings' redaction (lc on Discord: 本地模型可以不脱敏)
The provider editor has a Redaction row, "Send requests unmasked", while
every address of the provider is on this computer or the local network
(localhost, *.local, a loopback or private IP): Ollama, LM Studio, a vLLM
box. Ticked (provider.Unredacted; CLI `magpie provider set <id>
unmasked=yes`), a request whose provider and fallbacks, or whose routing
group's models, all skip redaction (provider.SkipsRedaction) goes to them
as the agent wrote it; anything that may reach another provider stays
masked for all of them. An address edited to a vendor's masks it again; a
signed-in account, a plugin's provider and another magpie never skip. The
log and recent calls keep the masked body. count_tokens and embeddings
follow the same rule.
It is a per-provider choice, not automatic for local addresses, because
a local address isn't a local model: a relay run locally (new-api,
CLIProxyAPI) or Ollama's cloud models pass the request on to a vendor.
Verified: TestLocalUnredactedProviderGetsTheRequestWhole (set, unset,
fallback to a vendor, fallback to another local one, a group of it alone,
a group with a vendor; chat and count_tokens) and TestLocalUnredactedEmbeddings
fail without the gateway change ("sent whole false, want true: … my key is
{{API_KEY_…}}"; count_tokens alone: "counted whole false"); TestSkipsRedaction;
TestProviderSaveKeepsUnredacted fails without the preset save line.
provider-unmasked.test.cjs (Chromium+WebKit, en/zh, 900 and 440 wide) 32/32,
3 of 4 fail on the old app.js; gui-ja, gui-de, i18n-keys, provider-search
and the other provider editor suites pass. go test -race on gateway,
provider, redact, gui; -race -count=10 on the new tests; go vet,
GOOS=linux/windows builds. Real: a sandbox gateway in front of the local
Ollama (llama3 8B) through a logging proxy: unset, Ollama was sent
{{API_KEY_…}}; with unmasked=yes it was sent the key as written, and
answered both times.
|
||
|
|
5d8717239d |
routing: accounts sit beside the requests only where each request still keeps to one line (#860, huoranxuanyuan: 宽窗口下变成两栏,请求行折成三行)
The two columns ( |
||
|
|
fdcdb5af21 |
usage: the requests table fits a narrow window with the reporter's rows, Speed and Status in sight (#860, huoranxuanyuan: 窗口较窄的提示出现了,速度和状态列仍被挤出去)
Hiding the optional columns (tight) wasn't enough for rows whose provider · account cell takes two lines and whose cost carries a price-reference line: on macOS fonts that table needed 858 (zh) / 923 (en) CSS px, and the reporter's 1569px window at 175% is about 897. When the table still overflows once tight, ledFit adds tighter: the price-reference line goes (it stays in the cost's tooltip and the row's details), model and provider names are cut at 110px with their tooltips whole, cells have 4px sides and headers may wrap. The minimum is now 734 (zh) / 760 (en). The ResizeObserver refits until tighter. usage-ledger-fit.test.cjs is built from the reporter's rows (Codex, openai · chatgpt.com, codex-auto-review priced as gpt-5.6-luna): at 820px the table fits with Status in sight, the hint shows, the reference line is hidden and is in the cost's title and the details; at 1100px the line shows. Chromium and WebKit, en and zh. Not run on Windows. |
||
|
|
2fb538c4d7 |
panel: the tray panel's usage has All beside Today, 7 days and 30 days, as the window's Usage does (#860, huoranxuanyuan: 托盘里用量只有今天/7天/30天,没有全部)
PANEL_USE_PERIODS gains ["all","All"] (already a t() string, in ja/de/zh), and a saved period is checked against the list so All survives a restart. panel-usage.test.cjs picks All in en and zh, Chromium and WebKit, at the panel's width: the request reads period=all, the segment is on, and the four segments stay on one line inside the bar without scrolling the panel. |
||
|
|
8ef53f2ec8 |
gui: an Antigravity card keeps its order when a refresh leaves out a pool's 5 hours (#860, huoranxuanyuan: 刷新额度有时会让5h和七天的顺序颠倒)
retrieveUserQuotaSummary sometimes sends a pool without its 5-hour bucket (the reporter's Gemini, its week used up). Since #745 the pool's models then keep their own 5-hour windows, shown as a family row; poolWindows put that row after every pool, so the card read Gemini · 7 days, Claude & GPT · 5 hours, Claude & GPT · 7 days, Gemini, and on a refresh that sent the 5 hours, Gemini · 5 hours, Gemini · 7 days, … — in the card's two columns the 5 hours and the weeks swapped sides. poolWindows now puts a 5-hour-less pool's family rows in its 5-hour place, before its week; a pool read whole is as before. quota-pool-order.test.cjs is the reporter's card (Gemini's week only, Claude & GPT both): on the old code the Usage page lists Gemini last (fails in Chromium and WebKit, en and zh); it passes now, with the panel's rings Gemini, Gemini, Claude, Claude. quota-pools, quota-families, usage-quota-head and routing-quota-left pass in both engines. Not tried with a real Antigravity account answering a partial summary. |
||
|
|
92469ee9f9 |
library, gui: a check for updates lists the skills a repository added beside the library's skills from it, to Add or Ignore (Raven on Discord)
Raven: "check for updates" only updated skills already in the library; when
dontbesilent2025/dbskill or mattpocock/skills adds a skill, magpie never said.
CheckSkills now asks GitHub's git trees API once per repo@ref (ETag-cached, a
304 is free) and only when a skill folder under the installed skills' common
folder (newBase) is neither in the library nor already offered does findNew
fetch the tarball for names and descriptions. library.json's seenSkills keeps,
per repo@ref, the paths the install picker showed and the ones set aside, so
an offered skill is offered once. The Skills page shows them under "Also in
their repositories" with Add (to the agents that have the repo's other skills)
and Ignore, plus Add all / Ignore all (/api/library/skills/{add-new,ignore-new}).
Verified: TestCheckSkillsFindsNewOnes (fake repo: nothing offered after
install, no tarball on a second check; two added skills offered, a nested one
and a .wip one not; Add installs with the repo's agents; Ignore keeps it out
on the next check via 304; remove clears) fails without the change (no
NewSkills / findNew) and passes -race -count=10; TestNewBase. A real run in a
sandbox HOME against GitHub: tdd from mattpocock/skills/skills/engineering
offered its 19 siblings; dbs from dbskill offered nothing (the picker showed
all), 33 with seenSkills wiped, and Add installed dbs-action. GUI:
skill-new-in-repo.test.cjs (Chromium+WebKit, 860/440px, en+zh) fails on
main's library.js and passes; gui-ja, gui-de, click-scroll, library-items,
every-skill, pick-skills, import/remove-all-skills, skill-groups,
skill-repo-groups, github-token, library-all pass. -race on library, gui,
tui; gofmt, vet, darwin/linux/windows builds.
|
||
|
|
764bbfdad5 |
sessions, gui, tui, cli: the Sessions list is as long as the count beside it, and drawn in pages (#1019)
The Sessions page's list and its session count came from two different
lengths: the count is over every session (statsFor's Overview), while
GET /api/sessions fell back to sessions.Limit (200) when the page asked for
no number — and the page never asks for one. So the page said "238 sessions"
over a list of 200, and each agent whose sessions were oldest lost them
whole: Codex had 121 sessions here and the list showed 7, OpenCode 33 showed
2, ZCode 20 showed 3.
* sessions.All (5000) is what a listing that means every session asks for,
and the endpoint reads it when no number is asked of it; a caller that
asks for one still gets it. It lives in internal/sessions so the page, the
TUI and the CLI ask for the same number.
* sessions.Limit stays 200 and is not raised: traceRecentFiles sizes the
window gateway attribution is judged over by it (TraceSessionVisible
decides whether a session has a readable local transcript), so raising it
would change which calls count as magpie's.
* The page draws that list in pages: SESS_PAGE (200) rows, and "Show N
more" appends the next. A filter click or a keystroke in Search draws the
first page and no more, so the cost does not grow with the history. The
data stays complete, so the count and the list still agree. A list served
4000 sessions long rebuilt every row on every keystroke — 275–320 ms a
keystroke on a fast Mac with very small rows, worse on Windows — and now
draws 200.
* The note under the list counts the list the filters kept, not every
session on the computer: it says "showing {drawn} of {n}" while pages are
left and "{n} session"/"{n} sessions" once the whole list is drawn. Before
it counted the whole history beside the drawn rows, so a search matching
one session read "the list holds all 2600, 1 drawn" — as if 2599 were
behind a "Show more" that isn't there. It also no longer says "drawn",
which is how the page is built, not what a reader sees.
* The CLI read sessions.List(0) — 200 — and then cut to its own --limit, so
--limit 500 could never show more than 200 and the "%d more with --limit"
foot was counted off the truncated list. It reads the whole set now; only
o.limit rows are printed, so that costs a parse, not output.
* The TUI read List(0) beside a head that says how many sessions there are,
and then List(sessions.Limit+1) — 201, still not the whole set. It reads
sessions.All now.
What the wider list costs is unchanged: every session file is read either way
to sort by last activity. Measured here with 384 sessions and 84.5k calls:
cold List(200) 10.3 s, List(5000) 10.5 s. What it adds is per-row work while
List holds the lock — a Codex session's titles stat, a pi session's omp
look-up, 25× the rows of JSON — hundreds of milliseconds, not seconds.
Tests, each failing without its change:
* TestSessionsListIsAsLongAsTheCount (internal/gui): 205 Claude Code
sessions as Claude Code keeps them; all 205 with no number asked, 5 with
?limit=5, 201 with ?limit=201. Fails with the list holding 200.
* TestSessionsListIsNotCutAtTheLimit (internal/tui): 210 sessions, of which
the ten past sessions.Limit are all in the list. Fails at 201.
* TestSessionsMoreCountsTheWholeSet (root): the default --limit 20 over 212
sessions says "192 more with --limit", and --limit 205 prints 205 rows.
Fails saying 180 (counted off the truncated list).
* sessions-paging.test.cjs (Chromium and WebKit, en and zh): 2600 sessions;
the first page is 200 rows with "Show 200 more", the button appends to
600 with the rows above left in place, a keystroke in Search draws a page
in under 250 ms and starts the pages again, a search matching one session
reads "1 session" and never the 2600 on the computer, and the note follows
the list the filters kept (200 of 1111 after that search). Fails on
origin/main (200 rows are not a page there) and with the pages taken out;
counting every session again, or writing "1 sessions", fails it too.
Verified: go vet; go build (windows); go test ./internal/sessions
./internal/tui ./internal/gui . — only what origin/main fails (TestCursor,
TestGatewayFixQuitsOnlyAMagpie, TestPurposeAssetMatchesUsage,
TestUpdateThatWontStart, TestPluginListSaysMiddleware,
TestPluginOnOffRmShortName); node --check app.js and i18n.js; the browser
suites above plus sessions-overview, sessions-today, sessions-usage-open,
sessions-toolbar, sessions-calendar-fill, usage-sessions-agents-fit,
i18n-keys, usage-ledger and click-scroll.
Co-authored-by: wizzy-yang <wizzy-yang@users.noreply.github.com>
|
||
|
|
d7b1d9df41 |
gui: with Settings' Bar icon on, Omarchy's bar shows one magpie, not the widget and the tray icon side by side (Alex on Discord)
On Omarchy the tray's StatusNotifierItem is always registered, so a user who had magpie in the bar's tray and turned on Bar icon got the widget beside it: two magpies. Wails has no Hide on Linux (a no-op), so the item now says Status "Passive" while the widget is in (set through Wails' own properties, as the tooltip is, with NewStatus sent), and "Active" again once it is taken out. Omarchy's Tray.qml leaves Passive items out of both its pinned row and drawer (bucket()), as waybar's tray does; Omarchy itself hides Dropbox's tray item when its Dropbox widget is in the bar. The status is set when the app starts (from whether the widget is in) and whenever Settings' Bar icon adds or removes it (onBarIcon), from what is in the bar afterwards even if the omarchy command failed. TestBarIconTellsTheTray (POST /api/omarchy/widget on, then off, with stand-in omarchy commands) fails with the call taken out: "Bar icon on: the tray was told [], want [true]"; passes with it, -race -count=10. On the omarchy VM (Omarchy 4.0.4 shell): a test SNI item pinned in the bar showed when Active, was gone within a second of Passive + NewStatus and came back on Active. This build (go build -tags gtk3 on the VM, vet clean), run in a sandbox HOME with MAGPIE_OMARCHY=1: with the widget's files in its config the tray item's Status read "Passive" over busctl, without them "Active". Not run on the VM: turning the switch at runtime (the route is covered by the test). go test -race ./internal/gui ./internal/omarchy, gofmt, vet, darwin/linux nogui/windows builds pass. |
||
|
|
14af0f100b |
provider, plugins: a plugin's browser sign-in finishes from the address the browser ended on, for magpie on a server or in Docker (Chicring)
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.
|
||
|
|
60f87223c3 |
gui tests: skill-how clicks a By agent choice once its sheet has finished growing in (skill-how.test.cjs red on WebKit about 2 runs in 5, Codex's way posted "" where "link" was asked)
The test's press() reads a button's box and clicks the middle of it with the mouse. The By agent sheet opens with the dialog's entrance, which grows it from about a third of its size, and on WebKit the box was read some 25 ms in (transform matrix(0.34…), Links 16 px wide at x 346); by the time the click landed the sheet had grown, and that point was over Library's way, left of Links. So Codex was given the library's way, and posts[2] said how "" in both en and zh. Chromium was mostly through the entrance by then. library.js has no race here: a click lands where the sheet is drawn when it lands. press() now waits until #modal has no running animations before reading the box, as other GUI tests do with getAnimations. Before: 3 of 6 WebKit runs failed with how '' at line 168 (a probe logged the box mid-entrance each time). After: 15 of 15 WebKit and 15 of 15 Chromium runs of skill-how.test.cjs pass. No change outside the test. |
||
|
|
5b9b9c9b05 |
gui: a call's JSON body in the Gateway page's recent calls folds, is coloured and copies a key or a value (owner: 这里应该支持 json 对象折叠以及代码高亮以及方便复制某个key/value)
Request and response bodies that are JSON, a stream's events and the reply put together are drawn as a tree: keys, strings, numbers and keywords in the code colours, an arrow before each object or array folds it to {…} with its count (lazily drawn on first unfold, folds kept across the list being redrawn), a copy button at the end of a line (on hover; faded rather than hidden so the pointer crossing to it keeps the line hovered) copies a string's text or an object's JSON, and a click on a key copies its name. The body's innerText stays the pretty-printed JSON (buttons are SVG only, 0 wide, nothing out of flow, which Chromium would break lines around). The {…} summary sits on the head's line, so a click on it holds that line, not the next one the unfolding pushes down. A cut or non-JSON body is as before.
json-tree.test.cjs passes in Chromium and WebKit, en/zh/ja/de, 1000px and 440px; sse-body, recent-calls-more, request-archive, request-archive-usage, gateway-caller-keys, routing-side-calls, gui-ja, gui-de, i18n-keys and click-scroll pass in both engines.
|
||
|
|
dcbae2ef98 |
library, gui: the MCP servers page has the skills page's Select, Turn all on, Turn all off and Remove all, under one "In the library" heading (#1027, emo172)
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.
|
||
|
|
6e53ee1f1d |
library, gui: skills installed from GitHub are above and this computer's below, and one author's repositories are one group (#1031, mintonight: 同一个作者的skill没有自动分组,原本在local的一些技能自动跑到上面来了 … local和online上下分开,给local中的skill分组后也只显示在下方的local里面,恢复上方online的自动分组功能)
Two changes put local skills among the online ones. |
||
|
|
f311a61859 | gui tests: usage-speed.test.cjs's comment gives the numbers the fixture makes (75.0 tok/s for glm-fast against the hour's 69.7, not 80.8 against 75.2) — comment only | ||
|
|
70e7e36675 | gui tests: a press is made only on a target wholly in sight, so Playwright's click doesn't scroll it the last fraction of a pixel itself (request-archive-usage.test.cjs: "a click moved the page: 860 to 861" in Chromium once the Usage page's archive switch shows, which #1017 makes it do) — reader.inView took a target cut off by up to a pixel at the view's foot as in sight; Fetch from archive spanned 539.80–562.59 in a 562px view (the switch makes .led-head 4px taller, so the 20px wheel steps land the rows elsewhere), and Playwright's click scrollIntoViewIfNeeded'd it 1px before dispatching, before the app's capture-phase click listener took the positions hold() keeps, so the app saw nothing to put back. A raw page.mouse.click at the same point moves nothing; no page code scrolls or focuses there, and overflow-anchor: none changes nothing. inView now wants the target wholly in the view (or, taller than it, its top in sight), and the press test's web ±1 allowance ("settles a row's first opening up to a pixel off") is gone: a click moves the page by nothing. With #1017's diff applied, request-archive-usage.test.cjs failed in Chromium (both app modes) and passed in WebKit; with this it passes 5/5 in both, and 3/3 with the allowance removed. The 32 suites using reader.cjs, request-archive*, usage-*, click-scroll and ledger-* pass in Chromium and WebKit (275/275). No app change. | ||
|
|
aad799eae6 |
library: an RTK release winget or Homebrew doesn't have yet says "v0.51.0 is out · waiting for winget" with no Upgrade, until it does (#1025, Fermin214)
On Windows, RTK installed by winget was 0.50.0 and GitHub had 0.51.0, which winget didn't yet. Library → RTK showed "v0.51.0 可升级" with Upgrade; Upgrade ran winget upgrade, which rightly said "winget's RTK is 0.50.0 so far…", and the card went back to "可升级" with Upgrade. The card compared rtk's version only with GitHub's latest release, never with what the package manager that installed it has, and the upgrade's answer was forgotten as soon as the card was read again.
RTKView.CheckLatest now also asks, when the latest release is newer than this rtk and it was installed by winget or Homebrew, what that package manager has: winget show --id rtk-ai.rtk --exact (the version is the first one after the line naming the package, as winget's labels are in the system's language), and brew info --json=v2 rtk without auto-update, as the Agents page's CLI updates read Homebrew. An upgrade through either records the version it left as the newest that package manager has. The answer is kept six hours (a failure fifteen minutes) and is asked again after that. When it has nothing newer than this rtk, the view says waiting (winget or Homebrew) and waitingHas. The card then shows "v{v} is out · waiting for {via}" (zh "v{v} 已发布 · 等待 {via}", ja, de), with a tooltip saying what it has, and no Upgrade button. Once it has the release, Upgrade is back. A package manager that can't be asked, or doesn't say, leaves Upgrade offered as before. GET /api/library/rtk runs the whole check within its 3s budget. magpie library rtk prints the same. Cargo (built from rtk's repository) and rtk's own script (its GitHub releases) can't lag, so they are unchanged. The Agents page's CLI updates already compare with the installing channel's own latest (Homebrew's or npm's), so they don't have this bug. The Japanese "v{v} is out" is shortened to "v{v} 公開済み", because with Upgrade beside it the card head overflowed at 440px.
TestRTKWaitsForWinget (a fake winget whose show prints Chinese labels: unaskable leaves Upgrade; asked, it waits; the reporter's upgrade waits with the "so far" note, and so does the card read again; winget given 0.51.0 and asked again, Upgrade is back and works) and the extended TestRTKUpgradeBrew (Homebrew's 0.49.0 after brew upgrade waits, on the card read again too) fail without the change (waiting <nil>) and pass. TestRTKChannelVersion covers winget show in English, German and Chinese with spinner frames, no package, and brew info's JSON. -race -count=10 on them. library-rtk-upgrade.test.cjs's new test (Upgrade finds winget behind: the waiting tag, no button, the note; a reload still waits; winget has it: Upgrade back; card head and page fit at 440px) fails without the library.js change and passes on Chromium and WebKit in en, zh, ja and de. i18n-keys, gui-ja, gui-de and the library-rtk-* tests pass on both engines. go test -tags nogui ./internal/..., go vet, gofmt -l, darwin, linux (nogui) and windows builds pass. Not tried with a real winget or on Windows: winget show's output is taken from its documented format, and the version line is assumed to follow the package line in every language.
|
||
|
|
555f570660 | gateway, provider: the Routing page says what a lone key or account has left, and a key bought as a plan (Kimi Code, GLM Coding Plan, MiniMax, OpenCode Go, Command Code) is weighed by its plan's windows (#1016, gphysign: Kimi Code (China) with one key on, Smart routing; the card said only "Kimi Code (China) 只启用了一个 Key,无需选择。" while /v1/magpie/quotas had its 5 hours 49 used, 51 left and its 7 days 45 used) — two gaps. weighRouted returned before reading anyone's allowance when there was one candidate, so a lone key or a lone subscription account was never known in the trace, and the Routing page, which shows a bar and "{n} used · renews in {d}" for a known row, had nothing to show; every single-account subscription said "what's left not known yet" the same way. And KeyAllowance read only a sub2api key's limits, so a key whose plan the Usage page reads (planQuotaSourceOf: Kimi Code, GLM Coding Plan and Z.ai, MiniMax Coding Plan, OpenCode Go, Command Code) was never known to routing, with one key or several. weighRouted now reads what each candidate has left before returning a lone one (one alone is still weighed against nobody). readsKeyWindows takes a plan key too; keyWindows reads it through planKeyWindows (planWindows, with Zhipu's team quota behind a key that has none of its own, as PlanQuotas does), and PlanQuotas' own read of a card is noted for routing at once (noteKeyAllowance), so the Usage page and routing never ask twice; the card kept on disk seeds it after a restart (keyCardTag keys a plan card as plan:, a sub2api one as balance:). A key at such a vendor that turns out to be pay-as-you-go, with no windows, is asked again every 30 minutes (keyNoWindowsAge), not every minute it is used. So, beyond the issue: several plan keys on one provider are now weighed by their windows under Smart, usage and pace, and a plan key the vendor refuses as out of quota rests until its window renews and comes back once read short of full, as a sub2api key does. affine no longer says a lone key or account all but used up "moved" its conversation: there is nobody to move to (at > 0). The GUI needed no change. TestLoneOneTellsWhatItHasLeft (a lone Kimi key known at 49% with both renews, the week first; a lone Codex account known; a plain DeepSeek key not), TestLoneSpentKeyKeepsItsConversation (fails with the at > 0 guard taken out), TestPlanKeyAllowance (the windows read behind the request, asked once; DeepSeek and Moonshot keys never asked), TestPlanKeyAllowanceFromItsCard (PlanQuotas' read known at once without a second ask; after a restart with the vendor down, the card on disk stands in and a failed read keeps it) and TestPayAsYouGoKeyAskedSeldom (a GLM pay-as-you-go key asked once, not two minutes on, again 31 minutes on) all fail without the change and pass -race -count=20 (provider) / -count=10 (gateway). routing-key-windows.test.cjs gains a lone Kimi Code key at 1100px and 440px (the one-key story, the row's bar at 49%, its provider row "49% used · renews in …" fitting, no sideways scroll) and passes in full on Chromium and WebKit in en, zh, ja and de; routing-quota-left and panel-routing pass. go test -tags nogui ./internal/..., go test -race on ./internal/provider and ./internal/gateway in full, go vet, gofmt -l, darwin, linux and windows (nogui) builds pass. Not tried with a real Kimi Code key. StepFun's Step Plan is read through a sign-in, not the key, so it isn't one of these. docs/subsystems/gateway-routing.md, providers-accounts.md and docs/reference.md say it. | ||
|
|
3bcf71b003 |
gui: the speed chart's marks that leave their column get a leader line (#1018)
The Speed trend's column for a day is that day's own speed — every reply's tokens over every reply's decode time — while each model's mark is drawn at its own speed. A model faster than the day it is in leaves its mark above the column, in nothing, reading as a stray dash (huoranxuanyuan, #860): with five models the fastest is nearly always above the day's own, so this is the chart's usual look, not an edge case. Each mark that leaves its column now gets a dotted leader line down to the column's top, so it reads as that column's; the line stops at the top when the mark is within a pixel of it, or it would be drawn back down into the column. The column itself is drawn in the track's colour rather than the grey the stacked charts use for "Other", so it reads as the track the marks sit on. The colour goes through style, not the presentation attribute: var() substitutes in a style and not in an attribute. box.emphasize() strokes a line and fills a rect: the marks' leader lines are lines, so dimming a day or emphasizing a model sets their stroke where it sets the marks' fill, and the two read the same. usage-speed.test.cjs's hour(0) fixture now has the day's own speed under glm-fast's, so the case asserts the leader line — that it is drawn, that it runs down the mark's middle from the column's top to the mark's foot, that it is dotted, that the column is the track's colour, and that the colour resolves (it goes through style). It fails on origin/main, where the mark is drawn with no line at all. Verified: node --check; go vet; go build (windows); the new assertions failing on origin/main's app.js and passing on this one; usage-speed 6/6 and usage-ledger-chart 18/18 in Chromium and WebKit, en and zh; 560px with nothing past the view and no sideways scroll. Co-authored-by: wizzy-yang <wizzy-yang@users.noreply.github.com> |
||
|
|
655cdd0abd | provider: the hook a landed usage refresh tells is held atomically and told before the refresh is done (#1023) — OnSubscriptionUsage was a plain package variable that the background refresh SubscriptionUsage starts read after closing its pending channel, unsynchronised, while the tray sets it as it starts (watchTrayUsage) and a test swapped it: go test -race ./internal/provider failed in TestSubscriptionUsageServesStale when TestSubscriptionUsageReadingWhileStale's refresh, waited for by its pending, was still reading the hook after it (subscription_usage.go:185 against subscription_usage_test.go:84). OnSubscriptionUsage is now a setter over an atomic.Pointer, so setting it while a refresh is under way is safe in the app too, and the refresh tells it before closing pending, so whoever waits for a refresh has it finished, hook and all, and may change what it reads. The tray's hook only signals a buffered channel and never waits, so telling it first holds no one; the test's hook no longer blocks either. With -race -count=20 the two tests failed on main with the DATA RACE and pass now; go test -race -tags nogui ./internal/provider passes in full; go vet, gofmt -l, darwin, linux and windows (nogui) and windows builds pass. | ||
|
|
e6dd5d20bb | usage, gateway, gui: a reply that reasoned is timed by its answer, from its first text, not by all its output from its first token (tony on Discord #general: Usage › Requests read "gpt-6.1-sol 163 token/秒 · 8,036 个请求 · 首字 14 秒" and asked whether subagents were added up) — speed was every output token over the time after the first content (text, reasoning or a tool call). Output counts the reasoning, and a model whose reasoning is hidden (OpenAI's encrypted reasoning, a summary sent when it is done, Claude's omitted thinking) writes it before the stream shows anything, inside the TTFT; so a 2,000-token think and a 200-token answer over the 1.5 s after the first token read 2,200 in 1.5 s, 1,467 tok/s. Concurrency was not the cause: the aggregate is Σ tokens over Σ per-request windows, so subagents running at once never add up. In the owner's own usage.jsonl codex/gpt-6-luna turns read up to 600 tok/s (out 496, reasoning 224, 11,055 ms, ttft 10,228) and its aggregate 133; it is 80 now. usage.DecodeOf is the one rule: a reply with reasoning counts out − reasoning over ms − firstText, one that reasoned and wrote only tool calls tells no speed, one without reasoning is timed from its TTFT as before; DecodeWindow's burst cut (#731) still applies. Record.Decode feeds summarize, the ledger's Totals and its timeline Parts; a route keeps its reply's reasoning (Route.Reasoning, from gateway.go and codex_backend.go), and routing.js's decodeOf/speedOf times its rows, the story and a session's average by it (an older route takes its served usage's reasoning); app.js's ledDecode does the Usage page's rows, whose tooltip says the reasoning left out, and the Output speed help says the rule, in en, zh, ja and de. History is read the same way, as it keeps the reasoning and the first text. Routing never ranks by speed. TestReasoningReplyTimedByItsAnswer (the 2200/2000 turn, a summary 10 ms before the text, a concurrent subagent, tool calls only, reasoning over output, no reasoning, not streamed: 1,200 tokens in 11 s through summarize, addRow, LedgerSeries and Decode) fails without the change and passes -race -count=10; TestUsageKeepsTimeToFirstToken now has its paced replies report reasoning and checks the route keeps it. reasoning-speed.test.cjs (Routing rows 133, 100, none and 100, the story never 1,467, the session 105; the Usage rows, tooltip and KPI the same; no sideways scroll) passes on Chromium and WebKit in en, zh, ja and de, and fails against the old assets (1467 tok/s); routing-request-metrics, routing-burst-speed, usage-speed, usage-ledger-detail, i18n-keys, gui-ja, gui-de, ledger-hscroll and click-scroll pass on both. go test -race -tags nogui ./internal/... under a temp HOME passes but internal/proc's TestUserPath ("the login shell's PATH never came"), which failed under the full run's load, passes 3/3 alone and is not touched here; go vet, gofmt -l, linux and windows (nogui) builds pass. Not tried with tony's data. docs/reference.md says how output speed is counted. | ||
|
|
bd33e575f1 |
gui: let users choose installed interface and code fonts (#1007)
* 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. |
||
|
|
490d2ea362 | gui: the tray panel's Allowances tab hides subscriptions and moves them, from an Arrange at its foot (H20 on Discord: "菜单栏的详情页能不能选择某些不显示,有点额度并不想看到或者可以调整顺序,目前的还不行") — the panel's tab already followed the Usage page's order (usageOrder, byUsageOrder), but it could be set only by dragging a card's logo on the window's Usage page, which nothing in the panel said, and there was no way to leave a subscription out of the panel at all. The tab's foot now has Arrange, and "{n} hidden" beside it when some are: Arrange lists a row a subscription (balances too) in the shared order, its logo the handle (a drag, or Alt+arrows; the grip by it on hover only, as on the Usage page) and a Hide/Show pill, under a note that hiding is the panel's only and the order is the Usage page's too; Done draws the cards again. What's hidden is kept in magpie's settings (settings.PanelUsageHidden, panelUsageHidden), not the browser: routing, caps, the Usage page and the menu bar's cells still have it, and a menu bar cell for a hidden subscription still opens its card (panelPeek) until the panel is put away or the tab left. POST /api/usage/arrange takes order and panelHidden and changes only the one it is sent (both were whole-order before; an order alone still works), and the Settings save keeps panelUsageHidden as it does usageOrder. Saves draw at once and are put back with the error if they fail; no click moves the page (the view's scroll is kept across the redraw); no native select, no left-border stripe. Which allowances the menu bar shows beside its icon stays Settings' pick (trayUsages). New t() strings have zh, ja and de. TestUsageArrangePanelHidden (each field set and cleared on its own, a Settings save between) passes -race -count=20, TestSettingsSaveKeepsWhatItDoesNotSend's fixture names the field. panel-arrange.test.cjs (440px: the hidden card left out and counted, Arrange keeping the reader's scroll, the rows and note, the hover-only grip, no stripe, no sideways overflow, Show/Hide posting panelHidden without a scroll and a failed save put back, Alt+arrow and a drag posting the order, Done's cards in it, a menu bar cell opening a hidden card until the panel hides, the Usage page keeping the hidden card) passes on Chromium and WebKit in en, zh, ja and de; click-scroll, gui-ja, gui-de, i18n-keys, panel-fold, panel-usage, quota-*, routing-quota-left, tray-cell-click, tray-inuse, tray-logos, tray-usages, usage-accounts(-fold), usage-arrange, usage-fold, usage-quota-head, usage-card-refresh, usage-stale-answer, usage-remote-cards, api-key-usage, claude-usage-asked and usage-checkin pass on both. go test -tags nogui ./internal/... under a temp HOME, go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried in the real tray panel on a Mac. docs/reference.md and docs/subsystems/gui-shell.md say it. | ||
|
|
588e51ea4c | agent, gui: an agent connected by its config to an address where nothing answers is told, not shown connected (#1013, cjyrainbow: Codex Desktop's WSL environment reads Windows' ~/.codex/config.toml, whose openai_base_url and [model_providers.magpie].base_url the reporter pointed at Windows as WSL sees it, forwarded to 127.0.0.1:3425 by a portproxy of their own; after a reboot the portproxy listened no more and the Agents page still said Codex was connected) — Drift looked only at the config, which was right. Now an agent pointed at an address off loopback (this machine's Codex at an address the user kept, #816, through Agent.reach; every WSL twin under NAT, at Windows as the distro sees it, through Agent.Gateway) has that address tried from here (reach.go: GET /v1/models, any HTTP status an answer, 2 s timeout, in the background, at most every 20 s per address, never waited for), and one that doesn't answer while the gateway answers on loopback is a new drift, "unreachable", with Addr and the advice in Detail: under WSL (or on Windows) magpie with Settings › Share on local network on, or the user's netsh interface portproxy, which may need setting up again after a restart; Windows' firewall allowing the port for the WSL network; on Windows 11, networkingMode=mirrored; elsewhere, the forward to start again or gateway.URL() to point at. An answer not had yet, and a gateway down on loopback too, are no drift. The config is never changed for it, except where the address is one a WSL distro reached Windows at before (distro.Was, the last 4 kept in wsl.json as a re-probe finds Gateway changed) and the distro reaches Windows at another now: Drift's Move names that one, and Reapply moves the config there (Agent.move; Codex's codexMoveGateway rewrites openai_base_url and magpie's own provider's base_url, only where they begin with the old address). An address no distro was seen at is the user's own and is never offered a move. In the GUI the row's line says "{agent} is set up, but nothing answers at the address it reaches magpie by" in red with the advice as its tooltip, and its pill is "How to fix" (an i, not the reapply arrow), which opens the advice in a dialog (too long for the status line; it wraps and can be copied) and sets nothing — or "Use <address>" (the existing "Use {url}" string) when there is a Move, which reapplies; two new t() strings with zh, ja and de. The probe runs from Windows, not from inside WSL, so a firewall keeping WSL out isn't seen; the advice names it. TestKeptAddressNotAnswering (the reporter's case: Codex at 172.28.96.1, unreachable with the address and loopback in the detail; no drift once it answers, none with the gateway down too), TestKeptAddressProbeNeverWaitedFor, TestKeptAddressMovedWithWSL (Move from a Was address, Reapply rewrites both URLs, Check empty and no drift after; a 10.0.0.7 address is told but never moved) and TestWSLAgentAddressNotAnswering (Codex in a NAT distro told, with Share on local network, portproxy and firewall; a mirrored one never probed) fail without the Drift change and pass; TestLoopbackNotProbed, TestReachProbe (httptest: a 401 is an answer, a closed port none) and TestWSLWas pass. TestMain stubs reachProbe so no test dials out. -race -count=10 on them; go test -tags nogui ./internal/... under a temp HOME, go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. agent-unreachable.test.cjs (the window at 560px and the tray panel at 440px: the red line and tooltip, How to fix's dialog posting nothing, Use <address> posting the reapply, no sideways scroll) fails on main and passes on Chromium and WebKit in en, zh, ja and de; agent-connect, agent-layout, agent-field-names, aside-native, gui-de, gui-ja and i18n-keys pass on both. Not tried on a real WSL NAT setup (the Windows test box didn't answer ssh). docs/subsystems/agent-wiring.md says it. | ||
|
|
b0a96cbbd7 |
gateway, provider, gui: a Sub2API key given 5h/1d/7d limits is weighed and rested by its own windows, like an account (#1004)
* 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.
|
||
|
|
87487c5ef0 | plugin, provider: a plugin can press its vendor's daily check-in itself (auth.checkin), and magpie runs it as its own check-ins (Lemon on Discord #feedback: provider 每日签到的方法能放到插件里面去做吗; ARNO earlier asked for Qoder's) — the check-ins magpie had (WorkBuddy, Trae CN, MiniMax Code, Qoder) are each written in magpie against the vendor's endpoint, even for a subscription that moved to its plugin, so a plugin author had no way to give their provider one. A plugin's auth hook may now have checkin(getAuth, provider) beside usage: the host lists the provider with checkin: true (plugin.Provider.Checkin) and answers checkin({provider, account}) (plugin.AccountCheckin), refreshing the account as usage does, with {outcome, credit, streak, message}; outcome is claimed, done, ineligible, inactive, captcha or failed, and a throw or an outcome outside these is failed with the reason. internal/provider/plugin_checkin.go runs these through checkin.go's checkiner, as the built-ins are: each signed-in account in use once a Beijing day (first look 2 minutes after start, then every 30), a failure asked again after wbCheckinRetry, every other outcome settled for the day, results kept in plugin-checkin.json by provider|account; a failure is recorded as failed and resets nothing. captcha is new (CheckinCaptcha): the vendor wants a captcha, which magpie never solves, so it is said as "check in in its own app", not as a failure, and not asked again that day. The results mark the account's Usage card (checkinBy "plugin:<provider>", WithCheckins), so the card's check-in row, its Auto check-in and Check in now work for it (POST /api/settings/plugin-checkin {provider, on}, POST /api/usage/plugin-checkin {provider}); Settings → Usage gains a Plugins tab, shown while such a plugin is signed in, a Daily check-in row per provider with Off/On and how each account went; magpie accounts checkin and c on the TUI's Usage page include them (the TUI's "no account" line names check-in plugins). The switch (settings.PluginCheckins, by provider) is off by default; for WorkBuddy, Trae CN, MiniMax Code and Qoder it follows that vendor's existing switch until set on its own, and magpie's own check-in through the plugin's fetch leaves a provider whose plugin checks in to it (pluginChecksIn), so no account is checked in twice. The Settings page's save carries PluginCheckins over (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). No plugin declares the hook yet; Qoder's check-in stays magpie's own here. TestPluginCheckinRunsAsTheBuiltinsDo (the fake plugin with FAKE_CHECKIN, five accounts answering claimed, done, captcha, a throw and an unknown outcome: off presses nothing; on, each outcome kept with its vendor; a minute on nothing pressed again; past the retry only the two failures; the next day all five; PluginCheckins and the cards marked; another provider's press asks none) and TestPluginCheckinTakesOverTheVendors (a qoder plugin without the hook leaves magpie's Qoder check-in its account, with it none; PluginCheckinOn's fallbacks) pass -race -count=10, and the latter fails with pluginChecksIn stubbed false; TestTUIChecksPluginsIn passes -race -count=20. plugin-checkin.test.cjs (the card's row with the captcha said, Auto check-in posting {provider, on}, no page move; the Settings Plugins tab at 440px with the row fitting, no sideways scroll, On posting) passes on Chromium and WebKit in en, zh, ja and de; usage-checkin, i18n-keys, gui-ja, gui-de and settings-* pass. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with a real plugin or account: no published plugin has the hook yet. docs/subsystems/provider-plugins.md (the hook's contract) and docs/reference.md say it. | ||
|
|
38cc30b09a |
tray: with the menu bar's magpie icon off, a click opens the panel on the tab left last, not Allowances (the owner: 每次都会自动跳转到 allowances,我记得之前是可以的)
#662 ( |
||
|
|
5361a2178e | agent, gateway: Cursor Private Inference's row has an effort, which magpie's gateway asks its requests for (#1003, gjf7) — other agents' rows pick a reasoning effort that magpie writes into their config, but Cursor Private Inference reads only its base URL and key from the environment, and its app offers no Reasoning control for most of magpie's models, so there was no way to set one short of a routing group with a member fixed at a level, which the reporter used as a workaround. Its row now has an effort field like the others': the default (as Cursor asks) first, then every level the models it lists have (cursorLocalEfforts: their levels in MemberEfforts order, low/medium/high where a model reasons with levels unknown, none at all when nothing it lists reasons or it isn't connected). The pick is kept in magpie, settings' agentEfforts by agent (provider.AgentEffort / SetAgentEffort, a level of MemberEfforts or "" for none), and the gateway's candidate loop asks a cursor-local request for it with withFixedEffort, fitted to the model's levels and bounded by a group's, on a model that reasons (the new Provider.Thinks: levels, its list's Reasoning, or models.dev's). Precedence: a group member's fixed effort, then a :effort suffix, then a routing rule's turn pick, then the agent's pick, then the request's own; the pick wins over a level Cursor sent, as Cursor sends its own default as often as a choice, and Default hands the choice back to Cursor. A model that doesn't reason and every other agent's requests go as they came. The Settings page's save carries agentEfforts over as it does fastPicks (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). In the window the field sits under "Settings" rather than "New sessions" for this agent alone, as it applies to every request from then on, not to what a session starts on; no new t() strings (Settings, effort and default are translated in zh, ja and de already). TestAgentEffortOnCursorLocal (Chat and Responses with and without a level of their own, Claude on Messages made to think, OpenCode's request untouched, a non-reasoning model untouched, a member fixed at low kept, the pick taken off) fails without the gateway change and passes; TestCursorLocalEffort covers the options (none with no reasoning model, the union of levels, the low/medium/high fallback, none while disconnected), Apply and a bad level refused; both pass -race (-count=20 gateway, -count=10 agent). cursor-local-effort.test.cjs (the window at 560px and the tray panel at 440px: the field under Settings, the slider's five stops with Default first, a pick posting the field, no sideways scroll) passes on Chromium and WebKit in en, zh, ja and de; claude-effort, subagent-effort, agent-field-names, agent-connect, agent-layout, agent-tweak-fold, agent-models, cursor-local-launch and i18n pass on both. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with the real Cursor Private Inference: whether it sends a reasoning level of its own on models it shows a Reasoning control for is unverified, which the precedence makes moot. docs/subsystems/gateway-routing.md and agent-wiring.md say it. | ||
|
|
28c58adcb3 | feat(gui): discover local provider configurations with persistent dismissal (#879) | ||
|
|
51bec20c09 | gui: a gateway key's menu puts its accounts beside their provider's models, not one block at the end (a follow-up to #932, July, LCorleone) — the menu the key's badge opens listed every provider's models and then all the accounts and keys together after them, so "which models" and "which plan" of one provider sat in two far-apart places, hard to give someone one vendor's plan alone. Each provider's accounts and keys now follow its own models, inside its block — the order the /api/caller-keys/models comment already described — an account by who is signed in with the plan in its note, a key by its fingerprint; a provider with accounts and no model in the list (one kept unlisted, or serving none now) keeps its accounts after the models; what the CLI kept that no provider has now stays behind every live entry, a model gone before an account signed out. Nothing of the picking changes: the same proto menu, the same opts and literalName, the same accountsSet that splits what comes back into "models-key" and "accounts-key", and no new strings to translate. gateway-key-accounts reads the order off the real menu now — relay's two accounts between its models and OpenAI's, the key behind GPT-5 — and pins the dead tail on a key held to a gone model and a signed-out account; its last badge read waits for the row to be drawn with the answers (the wait gateway-key-models already makes): without it a slow engine read the hold one action behind, a flake this sandbox shows on main's own test 5/6 runs on WebKit. gateway-key-accounts passes Chromium and WebKit, en and zh; gateway-key-models, gateway-key-groups, gateway-caller-keys, gateway-connect-keys, gateway-key-limit, i18n-keys and usage-keys-fold pass on Chromium — gateway-key-models' scroll assertion doesn't survive this machine's frozen WebKit build on main either. No Go change (the assets are embedded; no Go test reads their content). docs/reference.md's sentence on the menu follows the new order. (#988) | ||
|
|
efd501ec74 |
agent: add Reasonix Studio with separate Executor and Plan models (#427)
* 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
|
||
|
|
f109b270c0 |
gui: the main window opens as it was last left — maximised again if it was, at its last size otherwise (#995)
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. |
||
|
|
8754221203 |
gateway, usage: when the credits switch holds every account, the record and 429 say so accurately; the switch's text says it follows the latest reading (#999)
* 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. |
||
|
|
6f391bc8f2 |
provider, agent, cli: a routing group's id carries Claude Code's [1m] mark (#1000)
wherever a ref is read back from an agent's settings An agent on a routing group is shown, on the Providers page, on each of that group's providers — the row's middle holds one button per agent, and `uses` is what the Routing page's "n agents routed through it" counts. A group whose model has a 1M window is written into Claude Code's settings with the mark Claude Code itself puts on it (`group/auto-deepseek-v4-1-flash[1m]`), and GroupFor takes the mark off for the gateway; the rest did not. So the group was not found, and an agent Claude Code had on it was listed on none of the group's providers, while every request it made went through them. Four places read such a ref back: - provider.GroupFinder, which the Providers page asks (agentUses) - agent.sameModel, which moves an agent whose group is gone to the same model from a provider still serving it — the mark left on made the comparison against AutoGroupOf find nothing, and the agent stayed on a group nobody serves - agent.groupNamed, which says whether two values of a field reach the same group, one of them read back from the agent's own settings - the CLI's groupUses, which lists "← agent" beside a group: the marked id was filed under one no group has, and Claude Code was on no row Each takes the mark off, as GroupFor does. A group's own members are not touched: those are the group's structure, never a ref from an agent's settings, and a [1m] there is a model's, not the group's. group_uses_test covers the page (a group found with and without the mark, and the row naming the agent on the group's provider); applied_test covers groupNamed; reseat_test covers the move (the marked group ref finds the same model elsewhere, and a group that is nobody else's still gives nothing); groups_cli_test covers the CLI, with Claude Code's settings.json as the reporter's reads. Each fails without its fix. go build, go vet pass; go test ./internal/agent/ passes, and the failures that remain are the ones a clean origin/main has too (Aside's symlinks, ~/.codex dependence, VSCode's launchers, a flaky Zed case). Co-authored-by: wizzy-yang <wizzy-yang@users.noreply.github.com> |
||
|
|
5610f1e68a |
gui: Save shows a spinner and "Saving…" while the provider's model list is fetched (zola_xynb on X: "做个转圈或者提示,不然让人觉得没反应")
Save on a provider (and Add, Replace on import, and a subscription's Save) asks the vendor for its model list, which can take seconds. The button was only faded by .busy, which looked as if the click had done nothing. saving() now puts a spinning icon and what is under way ("Saving…", "Adding…", "Replacing…") in place of its label, at full opacity, aria-busy set and still not clickable twice; doneSaving() puts the label back when the save fails, so the error shows beside a Save that can be clicked again. Reduced motion stops the spin. zh/ja/de added.
provider-save-busy.test.cjs (en/zh/ja/de, Chromium and WebKit, 440px and 900px; the fake save held until the test answers) fails on the old code (no button.saving) and passes, with a failed save restoring Save and a second save closing the editor; the 30 related GUI test files pass (175/175). Tried in a sandbox magpie (-tags dev, MAGPIE_DEV_UI) against a custom provider whose /v1/models answers after 6s: Save showed the spinner and "Saving…" in Chromium and WebKit until the save came back.
|
||
|
|
56b770ec99 |
settings: keep awake can keep the display on too (#975, Hu9956: magpie's keep-awake held the system up, but the screen still went black and locked, and their agents record the screen to check their work, which a locked screen stops) — Settings › General's "Keep awake while agents work" row now has three segments, Off / On / Screen on too, saved as keepAwake and the new keepAwakeDisplay (default off, so On and every existing settings.json behave as before). awake.Keep takes a Level (Off, System, Display) in place of a bool; a level changed while held releases and takes the hold anew. On a Mac the display level runs caffeinate -i -d -w <pid> (pmset -g assertions on a caffeinate of the same flags shows PreventUserIdleDisplaySleep beside PreventUserIdleSystemSleep); on Windows SetThreadExecutionState gets ES_DISPLAY_REQUIRED too; on Linux logind's idle inhibitor (systemd-inhibit --what=idle:sleep, unchanged) is all there is, and whether the screen stays on is up to the desktop. keepAwakeDisplay is this computer's own (KeepOwn) and goes in prefsKeep, so another setting's save keeps it. The row's line says the display stays on when it's chosen; it keeps its English in data-en, so a language change re-translates it. zh/ja/de strings added. No TUI or CLI has keep-awake, so none changed. TestKeepDisplay (taken once, taken anew with the display, not again at the same level, back to System, released at Off), TestCaffeinateArgs, TestExecutionState (GOOS=windows test build) and TestKeepAwakeKeptAndOwn cover it; go test ./internal/awake ./internal/settings ./internal/gui ./internal/gateway pass, -race -count=5 on awake. keep-awake.test.cjs now runs en/zh/ja/de at 900px and 440px in Chromium and WebKit (display posts keepAwake+keepAwakeDisplay true, the line changes, another setting's save keeps it, segments fit the row, On and Off clear it, no click moves the page): 32/32 twice; gui-ja, gui-de, i18n-keys and click-scroll pass. go vet (darwin, windows, linux), darwin, linux (nogui) and windows builds pass. Not tried with an agent recording the screen; the reporter is asked to confirm.
|
||
|
|
a7c4e5ddde |
provider: a Codex account the Codex app still sends for keeps Codex signed in — spend cap or overage with credits, limit_reached alone; a hold survives a failed read (#996)
* 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. |
||
|
|
2cd25e2e88 |
codex: each ChatGPT account can have its own daily warm-up time (#957) — two Plus accounts both warmed at the one codexWarmAt (07:00) started and ran out their 5-hour windows together. settings.codexWarmAtOf holds an account's own time ("HH:MM" or "off"), by its name in lower case; an account not in it follows codexWarmAt as before. warmNow picks each account's time, and the loop wakes for an account's own time even with the global one off. Settings › Usage › Warm-up, Codex: with two accounts or more, each has a row under Daily warm-up — Same (the time above), Own (a time field) or Off — saved through POST /api/settings/codex-warm-at, and the full Settings save keeps the map. zh/ja/de added.
TestWarmAtEachAccountsOwn, TestWarmLookAtAnAccountsOwnTime and TestSetCodexWarmAt pass with -race -count=20, and the first two fail when an account's own time is ignored. TestSettingsSaveKeepsWhatItDoesNotSend covers the new field and fails without the keep in api.go. warm-at-own (two accounts: rows, Own posts 06:00 then 09:00, Off, back to Same, the page doesn't move, nothing cut, no <select>; one account: no rows) passes in Chromium and WebKit, en/zh/ja/de, 900px and 440px, and fails 8 of 9 with main's app.js. i18n-keys, gui-ja, gui-de, settings-groups, settings-sections, click-scroll, reset-reminder, usage-alert and codex-auto-review pass in both engines; go test ./internal/provider ./internal/settings ./internal/gui and vet pass. |
||
|
|
b2b72ea026 |
menu-scroll test: its fixture answers /api/upstream — the test failed 24/24 on main (Chromium and WebKit, every case "Target page, context or browser has been closed") since 2a990175 (#971), which has the Usage and Providers pages ask /api/upstream for the vendors' status pages. menu-scroll's route handler asserts on any API it doesn't serve; that assertion ("Unexpected API: /api/upstream") was an unhandled rejection, which ended the test, and its after hook closed the browser under the first gesture, so every later case failed on page.goto. The fixture now answers it with no vendor readings, as a page that couldn't be read marks nothing. No other GUI test throws on an unknown API without serving /api/upstream. No app change. menu-scroll passes 24/24 three runs in a row (it fails 12/12 Chromium at 2a990175 and passes 12/12 at its parent 62f9612b); click-scroll passes 18/18.
|
||
|
|
a4553cec17 | gui: with reduced motion asked for, a page fades in without sliding up — sessions-talk's WebKit en and zh cases failed with "opening Continue in moved the page" (179 vs 184). No click scrolled: a probe saw no scroll event and no scroll call. The Sessions page itself was still coming in: every .view plays view-in, a fade with a 5px rise over .24s, and in WebKit the test's first row was drawn and measured inside it (main#view-sessions at 55, then 50; every ancestor moved by the same 5px and no sibling changed height), so the row reached its place after the click. view-in took no notice of prefers-reduced-motion, which the test asks for and which the app's other motions honour; there the page now only fades (fade-in), so nothing moves under the reader's pointer. Without reduced motion it is unchanged. sessions-talk passes 40/40 (10 runs, Chromium and WebKit, en and zh) with Playwright 1.62.1 and 1.63, where it failed 1–2 of 2 WebKit cases a run before; click-scroll, view-scrollbar, every sessions-*/session-*/sess-* test and the tests that look at animations (add-sheet, by-agent, library-all, claude-usage-asked, removed-*, ux-safety …) pass. menu-scroll fails 24/24 ("Target page, context or browser has been closed") with and without this change; not this commit. | ||
|
|
b37ac53bef |
codex: a switch for whether an account spends its credits (#974)
* 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. |
||
|
|
14b6b2b2ad |
settings: the model Codex writes its memories with is picked in Settings › General too, under "Codex auto-review model" (#985, Hu9956: it was only the notebook square on Codex's card, unlabeled beside the subagent squares, and wasn't found; on a Free account two rounds of memory consolidation took the 30-day window from 100 to 34) — a "Codex memories model" row with a line that says what happens: unset, Codex's own gpt-5.6-luna and gpt-5.6-terra whatever model the conversation is on; set, this model. Its button opens the same app model picker as the square, on Codex's own memories field, and a pick goes through the same setPick and /api/set, so the row and the square are one setting: renderAgents draws the row again after every pick, from either place. No Codex with the field to pick (or gateway mode): no row. The square stays; its tooltip and aria-label now read "Codex memories model: …" in place of the bare "memories: …". zh/ja/de strings added. codex-memories-settings (Chromium and WebKit, 440px and 1100px, en/zh/ja/de: the row beside auto-review, no native select, fits the row, a pick posts {codex, memories} and lights the square, Default on the square puts the row back to Codex's own, no click scrolls Settings; no Codex, no row) fails on origin/main 97c2fac1 (no row) and passes 18/18; codex-memories (its square label updated), codex-auto-review, codex-titles, i18n-keys, gui-ja, gui-de and click-scroll pass. No Go change.
|
||
|
|
97c2fac127 |
usage: the Sessions calendar refits on the next frame, not inside its ResizeObserver's call — sessions-overview's WebKit en case failed about 2 runs in 3 with the page error "ResizeObserver loop completed with undelivered notifications". sessCalWidth called the chart's fitCal in the observer's own call; when the room for the weeks changed, the calendar was put again with more or fewer leading cells, the card's height moved (1028x187 → 1028x190, read from a probe that logged each observer's entries), and the observer was owed that change in the same frame, which WebKit reports as the loop error. The refit now waits for the next animation frame, once per chart however many sizes arrive; a calendar drawn or redrawn still fits at once (renderSessChart calls fitCal itself). sessions-overview passes 10/10 in WebKit and 10/10 in Chromium, en and zh (it failed 3 of 4 WebKit runs before); sessions-calendar-fill, sessions-today, sessions-overview and the other sessions-* tests and click-scroll pass in both, except sessions-talk's WebKit "opening Continue in moved the page" (179 vs 184), which fails the same on origin/main b45fe28c and is not this change.
|
||
|
|
b45fe28c9d |
usage-sessions-agents-fit test: in WebKit the last agent is clicked as an element — the test failed ~70% of runs in WebKit, en and zh (a timeout waiting for Zed to be picked), and had since c6e318e1 added it: no commit turned it red. Playwright's WebKit stops hit-testing a scroller's children once it has been scrolled: on a bare page with a display:flex, overflow-x:auto strip, after scrollLeft is set every click on its buttons lands on the strip (57/57 lost; Chromium 0/58), elementFromPoint gives the strip from the next frame on, and in this test a wheel scroll does it too. The system's WKWebView, which the Mac app uses, takes the click: the same page in a WKWebView in an AppKit window, scrolled the same way, hit-tests and clicks the button. So it isn't the app. Chromium still clicks with the mouse; WebKit clicks the button through the DOM, which runs the same onclick and redraw. What the test guards is kept: with the button reuse and the bring-into-view both taken out of app.js, the WebKit cases still fail ("the picked agent stays in view"). No app change.
Verified: usage-sessions-agents-fit 8/8 runs green in Chromium and WebKit, en and zh; also at 440px (a copy with 860/440); sess-agent-strip, sessions-toolbar and sessions-usage-open pass. sessions-overview's WebKit en case fails 2 runs in 3 on its own, with "ResizeObserver loop completed with undelivered notifications" caught as a page error, a different cause, not in this commit. |
||
|
|
07326a3c26 |
usage test: usage-ledger-detail expects each gateway row's protocol badge and the details' Protocol (red on main since c5b57b90) — c5b57b90 (蓝猫 on Discord) tagged every gateway row of Usage › Requests with the protocol it was sent upstream in (a "src proto" badge: "Responses", "Anthropic → Chat") and added Protocol to a request's details after Endpoint. The test's "only the session file's rows are marked" read any .src, so it found the protocol badges (['Responses', 'Anthropic → Chat', …] against ['', '', …]) and failed 6/6 in Chromium and WebKit; that hid two more stale checks behind it: the details' labels and value positions without Protocol, and "the click moved the page" (50 vs 30): a row with badges is two lines tall (51px, was 32), so the first row was no longer wholly in sight at the wheel's 30px and reader.click's own wheel brought it there before clicking. The test now reads the local-session badge (.src.local) and the protocol badges (.src.proto) apart, expects Protocol (协议) in both details with its value (Responses, Anthropic → Chat), and takes the page's scroll after reader.inView of the first row, so a scroll by the app is still caught. No app change: c5b57b90~1 passes the old test 6/6, and with the row badge turned off the page doesn't move. usage-ledger-detail passes in Chromium and WebKit, en and zh; ledger-*, usage-* and click-scroll pass (257 cases), except usage-sessions-agents-fit's WebKit cases, which time out alone too, on origin/main's app (not in this commit).
|
||
|
|
7f45f45cb5 |
codex: a sign-in kept on ChatGPT when its account is used up (#981)
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. |
||
|
|
391c45e367 | cleanup: gofmt internal/gui/caller_keys.go and internal/provider/logins.go — whitespace only (git diff -w is empty). | ||
|
|
cc89448e69 | gui tests: group-arrange reads the grips in one go, after the move's answer is drawn — moveGroup draws the new order at once and again from groups/arrange's answer, and the test measured the grips (a locator's evaluateAll) between the two: in Chromium the rows were detached by then, read as no style and no place (x NaN), and mouse.move failed with "Input.dispatchMouseEvent: Invalid parameters" (5 of 6 Chromium runs on origin/main; WebKit's timing let it through). grips() now reads the rows in one page.evaluate, and the drag waits for the answer and a frame. No app change. group-arrange passes 20/20 (5 runs × Chromium, WebKit, en, zh). | ||
|
|
8c71cd27f9 |
quotas: a computer using another's magpie (remote-magpie) shows that magpie's quota cards (莫 on Discord: the second computer had the models and groups but /v1/magpie/quotas was empty, so Usage, the menu bar and magpie quota showed nothing) — each named with the remote ("Codex · Office", id office/codex, from: Office) and kept in its kind (subscription, plan, balance), on the Usage page, the tray, the TUI and magpie quota (which matches by the remote's id or name); their history comes with the local one. Only the computer holding the sign-ins asks vendors: it answers the new GET /v1/magpie/quotas/cards from what it has kept (CachedCards, never a read, whatever the age), asked here at most once in 30s; a card's refresh here POSTs /v1/magpie/quotas/refresh (that card, or every card from the remote's own card), which the remote does at most once in 30s per card. A remote keeps showing its last cards when it can't be reached; with none its one card says why (nothing read there yet, or a magpie too old to share — "update magpie there", zh/ja/de added). Cards from a third magpie aren't passed on, and the refresh path refuses them (400); a Codex reset or check-in isn't offered on a remote card. Same auth as /v1/magpie/quotas (loopback, or LAN with the key).
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. |