Files
gui: the page runs in macOS 12's WebKit — no regex lookbehind, the few newer built-ins filled in, fallbacks for container queries, subgrid, :has and color-mix (#220 uclort: on macOS 12.7.6 the app opened blank, the panel showing its tab bar, a "Profiles + Save current" heading and "Open magpie · Quit" and nothing else, no button working) — the page runs in the system's WebKit, on macOS 12 Safari 15.x's, and app.js's snippet highlighter had a regex lookbehind, (?<==)\S+ (app.js:2167), which WebKit only parses from Safari 16.4: a SyntaxError in a literal fails the whole file, so app.js never ran (and routing.js and library.js, which lean on it, threw), leaving index.html's static markup; library.js had two more, (?<![\w-]) in the SVG greying (library.js:162-163). The highlighter now finds a value after "=" by hand (the character before the match, a sticky \S+) and the greying checks the character before a colour name in its replace callback, each giving the same output as before (compared on shell, curl, Python and Node snippets and an SVG with attribute and style colours, data-fill and x-color left alone). Built-ins newer than Safari 15.0 in use, routing.js:307's findLast and library.js:1042's structuredClone (15.4), are filled in by a new compat.js loaded before the other scripts (with at, findLastIndex and Object.hasOwn), each only where missing; structuredClone there is a JSON copy, enough for the API's plain data. Styles: a selector list with :focus-visible (15.4) drops the whole rule before 15.4, so the agent handle's hover, an account's Remove and protocol buttons showing on hover, the protocol menu's hover and the editor's focus ring are split from their :focus-visible (or it is dropped where :focus covers it); .acc:has(.aq) and the panel's .profiles:has(> .chip-input) hint (15.4) are classes set by app.js (with-aq, naming); the Routing page's container queries and the request list's subgrid (16) get @supports not fallbacks by the window's width (the tray panel is narrower than all of them) and a row's own columns; and the panel's --panel-tint, a color-mix() (16.2) in a custom property that would have gone invalid and painted the panel clear (tintPanel then handing the system white under dark text), falls back to --bg, as the floating status and Library's sticky head do. Other things newer than 15.0 are left as cosmetic: inert (15.5) on the folded agent list, accent-color, the rest of color-mix() on hover and focus shades, .quota's subgrid rows. The build: Go 1.25 and later run on macOS 12 and up only (the linker stamps 12.0 as the oldest), so Info.plist's LSMinimumSystemVersion and the Makefile's -mmacosx-version-min go from 11.0 to 12.0 and the site says macOS 12+ rather than 11+. Tested with a new internal/gui/tests/old-webkit.test.cjs: every assets/*.js parsed with the Babel parser Playwright bundles, failing on a regex lookbehind, v flag or modifiers (in a literal or new RegExp("…")), a class static block, decorators, import attributes or using (checked against the old highlighter's regex and a static block, and not on named groups, lookahead, ??= or private methods); a grep for built-ins newer than 15.0 (at, findLast, structuredClone, hasOwn, toSorted and co., Set methods, groupBy, withResolvers, Iterator helpers, popover …) unless compat.js fills them in (fails on main's routing.js:307 and library.js:1042 without it); compat.js the first of the page's scripts and every script loaded; the CSS with no :has(), no :focus-visible in a mixed list, no nesting or dvh, and @container, subgrid columns and a color-mix() custom property only with an @supports not fallback in the same file; and, in WebKit with those built-ins deleted, the panel's tabs, Save current's name field and every window page drawn with no page error. Documented in internal/gui/tests/README.md. go build -tags nogui (and GOOS=linux, GOOS=windows), go build (darwin GUI), go test -tags nogui ./... and the whole GUI suite (130 tests) pass. Not tried on a real macOS 12 or Safari 15: the old engine's parse is judged by the parser, not run.
gui: the page is first drawn in its saved theme, not the system's (seg-contrast failed in Chromium on the release branch: in dark the view shown read at 3.96:1, the other views at 4.69:1) — the saved theme was put on the page by app.js, at the foot of index.html, so a window kept dark under a light system had its header parsed and styled light first, and its buttons' colour transitions (the switch's views, Sync, Settings) then faded them to dark as the page loaded; the test read them mid-way, the more so as #471 made the page longer to parse. theme.js, run in the head after boot.js and before the styles, sets it from bootPrefs (or ?theme=) for the first paint; app.js still sets it as before. seg-contrast.test.cjs now holds app.js back and checks the page was already drawn in its theme (fails without theme.js, Chromium and WebKit, en and zh, dark), and reads colours once the page's transitions are over. old-webkit lets theme.js (ES5, URLSearchParams) run before compat.js; api-key-theme, from the release branch, expects a text button in --ctl-fg as #477 set. seg-contrast, settings-sections, header-zoom-fit, header-centre, click-scroll, old-webkit, api-key-theme, omarchy, agent-grip, web-page, gateway-fold, sessions-toolbar pass; go vet, go build -tags nogui, go test ./internal/gui pass.
library, plugins: the installed lists are sorted as the reader picks, and remember it (#481: the installed MCP servers, skills and plugins had no order the user could choose; the plugins came in the order they were installed) — the Library's MCP servers and the installed plugins now have A→Z and Z→A beside their list, A→Z until one is picked, so the plugins are no longer in the order they were installed in; the Library's skills have By source (the groups by repository and their parts, as before, and still the default), A→Z and Z→A, the last two one flat list of every skill by name, the filter still finding a skill by its repository. Each list keeps its own pick in the browser's storage (magpie.sort.<list>, read and written in try/catch), and a pick redraws only its list, the control left where it was on the screen. The skills' heading, the fullest, goes on to a second line in a narrow window rather than past the page's side. Sorting by use or by when a thing was installed waits for the library to keep those. list-sort (Chromium, WebKit; en, zh) fails without the change; library-items, library-all, own-skill, every-skill, the plugin tests, click-scroll and old-webkit pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
library, plugins: the installed lists are sorted as the reader picks, and remember it (#481: the installed MCP servers, skills and plugins had no order the user could choose; the plugins came in the order they were installed) — the Library's MCP servers and the installed plugins now have A→Z and Z→A beside their list, A→Z until one is picked, so the plugins are no longer in the order they were installed in; the Library's skills have By source (the groups by repository and their parts, as before, and still the default), A→Z and Z→A, the last two one flat list of every skill by name, the filter still finding a skill by its repository. Each list keeps its own pick in the browser's storage (magpie.sort.<list>, read and written in try/catch), and a pick redraws only its list, the control left where it was on the screen. The skills' heading, the fullest, goes on to a second line in a narrow window rather than past the page's side. Sorting by use or by when a thing was installed waits for the library to keep those. list-sort (Chromium, WebKit; en, zh) fails without the change; library-items, library-all, own-skill, every-skill, the plugin tests, click-scroll and old-webkit pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
library, plugins: the installed lists are sorted as the reader picks, and remember it (#481: the installed MCP servers, skills and plugins had no order the user could choose; the plugins came in the order they were installed) — the Library's MCP servers and the installed plugins now have A→Z and Z→A beside their list, A→Z until one is picked, so the plugins are no longer in the order they were installed in; the Library's skills have By source (the groups by repository and their parts, as before, and still the default), A→Z and Z→A, the last two one flat list of every skill by name, the filter still finding a skill by its repository. Each list keeps its own pick in the browser's storage (magpie.sort.<list>, read and written in try/catch), and a pick redraws only its list, the control left where it was on the screen. The skills' heading, the fullest, goes on to a second line in a narrow window rather than past the page's side. Sorting by use or by when a thing was installed waits for the library to keep those. list-sort (Chromium, WebKit; en, zh) fails without the change; library-items, library-all, own-skill, every-skill, the plugin tests, click-scroll and old-webkit pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
library, plugins: the installed lists are sorted as the reader picks, and remember it (#481: the installed MCP servers, skills and plugins had no order the user could choose; the plugins came in the order they were installed) — the Library's MCP servers and the installed plugins now have A→Z and Z→A beside their list, A→Z until one is picked, so the plugins are no longer in the order they were installed in; the Library's skills have By source (the groups by repository and their parts, as before, and still the default), A→Z and Z→A, the last two one flat list of every skill by name, the filter still finding a skill by its repository. Each list keeps its own pick in the browser's storage (magpie.sort.<list>, read and written in try/catch), and a pick redraws only its list, the control left where it was on the screen. The skills' heading, the fullest, goes on to a second line in a narrow window rather than past the page's side. Sorting by use or by when a thing was installed waits for the library to keep those. list-sort (Chromium, WebKit; en, zh) fails without the change; library-items, library-all, own-skill, every-skill, the plugin tests, click-scroll and old-webkit pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
providers: each account of a subscription, and each key, can be kept for some of the provider's models only (#474: 增加账号级别的模型选项, a low-quota but verified account kept for the cheap models) — every account or key a provider had served every model it did, so one couldn't be spared for some. An account's or key's row in its provider's editor now has a models badge: a click opens the provider's models as chips, Save keeps the account for those picked, All models gives it every one again; one kept for some says how many, always shown. magpie provider account-models <id> [account|key [models…|all]] does the same and lists them. It is kept in providers.json as accountModels, an account by its name in lower case, a key by its id; one not in it serves every model, as before. The gateway never sends a model to an account or key whose list leaves it out, in every routing mode, in drag order, in a group and as a fallback, and never falls back to it all the same; the trace says it was left out as set to serve other models. When every account or key there is leaves the model out, the request is refused with a 403 saying so and how to change it, not "none of X's models is ready". TestKeyModelsNarrowCandidates, TestAccountModelsNeverChosen, TestAccountModelsCmd and account-models.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; account-models, click-scroll, old-webkit, account-arrange, account-proxy, account-return, routing-account-order, panel-routing, unlisted-models pass.
gui: the header's other views and the text buttons read at WCAG AA, and the view shown stands off the switch (#477: in light mode an unselected tab read at 3.25:1, a text button at 3.33:1 on the page; dark passed by 0.08) — both were drawn in --muted (#85858d light, #8b8b94 dark), meant for captions, not controls, and the thumb on the view shown was white on a pill-grey track (1.13:1) or a 10% white on a 4.5% one (1.36:1). A control's quiet text now has its own colour, --ctl-fg (#63636b light, #a1a1aa dark), for the switch's other views, the Accounts and Sessions switches on the same track, and every .text button: 4.79:1 on the light track, 5.4 to 6.0:1 on the page, a card and the inset editor, 6.1:1 and up in dark. The light track is a shade deeper (#e6e6eb) and its thumb's edge a little firmer, 1.24:1 against it; dark's track is 3.5% white and its thumb 15%, 1.61:1. Sizes and weights are unchanged, so the header fits as before (#442/#457). seg-contrast.test.cjs (Chromium, WebKit, en, zh; light, dark, system dark) reads the computed colours, each background laid over the ones under it, and fails without the change. header-centre, header-zoom-fit, text-size, sessions-toolbar, click-scroll, old-webkit pass.
i18n(zh): plainer words where they read as slang, the rest kept (#465) — the provider editor's Forget (dropping the vendor's fetched list) is 清除, not 忘掉; the Providers page's Take a look is 查看, not 看看; an account's forget tooltip says magpie 移除 its sign-in, as its siblings do; and the routing explanations that said {who} 先上 say 排第一, as every other "goes first" there already does. A session's details named its id line 整个会话, the routing affinity option's word for "Session": the line now uses the Session ID key (会话 ID, as the usage ledger's detail has it), and 整个会话 stays where it means the whole session, beside 一轮之内. 知道了 (OK) and 还有 {n} 个 (+{n} more) are kept: standard and clear where they show. plugin-move and provider-picks-stay assert the new words; sessions-manage and sessions-overview fail without the id line's label. plugin-move, provider-picks-stay, sessions-manage, sessions-overview, routing-steady, routing-manual, click-scroll, old-webkit pass (Chromium, WebKit, en, zh); go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
gui: the page is first drawn in its saved theme, not the system's (seg-contrast failed in Chromium on the release branch: in dark the view shown read at 3.96:1, the other views at 4.69:1) — the saved theme was put on the page by app.js, at the foot of index.html, so a window kept dark under a light system had its header parsed and styled light first, and its buttons' colour transitions (the switch's views, Sync, Settings) then faded them to dark as the page loaded; the test read them mid-way, the more so as #471 made the page longer to parse. theme.js, run in the head after boot.js and before the styles, sets it from bootPrefs (or ?theme=) for the first paint; app.js still sets it as before. seg-contrast.test.cjs now holds app.js back and checks the page was already drawn in its theme (fails without theme.js, Chromium and WebKit, en and zh, dark), and reads colours once the page's transitions are over. old-webkit lets theme.js (ES5, URLSearchParams) run before compat.js; api-key-theme, from the release branch, expects a text button in --ctl-fg as #477 set. seg-contrast, settings-sections, header-zoom-fit, header-centre, click-scroll, old-webkit, api-key-theme, omarchy, agent-grip, web-page, gateway-fold, sessions-toolbar pass; go vet, go build -tags nogui, go test ./internal/gui pass.