Commit Graph
721 Commits
Author SHA1 Message Date
yetone 575e694ed7 settings: a Text size preference (文字大小) that makes everything in magpie's windows larger, 100/110/125/150%, with Ctrl/Cmd + = − 0 in the window and the tray panel, and Windows' text drawn in its own UI faces (Discord, Clarence Dan: the font size is hard on the eyes on Windows) — Settings → Preferences gains a Text size row under Language whose pick posts to a new POST /api/settings/text-size (saved as settings.TextSize, validated against 100/110/125/150, 0 read as 100, and kept by the Settings page's general save, which never sends it). The size is each webview's own zoom, as a browser zooms, not CSS zoom: CSS zoom leaves getBoundingClientRect and clientX in zoomed pixels while offsets, scrollTop and 100vh are not, and app.js, routing.js and library.js place menus, popovers and the panel's height by those measures; under the webview's zoom the page is laid out again in fewer, larger CSS pixels and every measure agrees. Windows and Linux use Wails' SetZoom (WebView2's zoom factor, WebKitGTK's zoom level; the Zoom window option at creation, so the pages open at the saved size), the Mac WKWebView.pageZoom through cgo (macOS 11+), since Wails' Mac zoom is magnification, which lays nothing out again. None is under 100% because Wails clamps SetZoom below 1 on Windows and Linux. The windows grow around the pages: the panel's width and the height its page asks for (still in CSS pixels) are multiplied by the zoom, capped at the screen's work area (textsize.go panelFrame), and resized at once when the size changes; the main window's minimum is 560×420 CSS pixels at the size, capped by the screen, so a window smaller than that grows. Runtime changes wait for the app to be ready (whenReady), and at startup the Mac applies the zoom once its webviews exist. The Mac header, drawn under the native traffic lights which do not grow, divides its height, left padding and the modal's top by --zoom (set from boot.js before the first paint and from the settings), so it stays 50 points tall and level with them. The shortcuts are Cmd on the Mac and Ctrl elsewhere (the other modifier does nothing), step through the sizes and 0 goes back to 100%, with a "Text size 125%" toast; in magpie web the row is hidden and the keys are left to the browser's own zoom. On Windows the page names its faces (html.win): Segoe UI Variable Text, Segoe UI, then Microsoft YaHei UI for Chinese rather than whatever WebView2 falls back to; the Mac and Linux stack is unchanged, and there is no automatic size bump. New strings in en and zh. Tested: internal/gui/tests/text-size.test.cjs in Chromium and WebKit, en and zh, light and dark — a pick posts and does not scroll the settings page, stays picked after a reload with --zoom from boot.js, the Settings page's other saves leave it, the shortcuts step and reset in the window and the panel, and at 150% (the smallest window, 840×630 points, as 560×420 CSS pixels at device scale 1.5) no view, the header or the panel runs off to the side, the Mac header is 50 points; screenshots at 100% and 150% looked at. currency.test.cjs's viewport is 480 tall instead of 420, as settings-groups' is: the new row put the Currency control under the footer, where WebKit's hover could not reach it. Go: TestTextSizeFrames (panel and window sizes) and TestTextSizeSetting (the endpoint saves, zooms the windows, rejects 133, survives the general save, boot.js carries it) in internal/gui, and the settings round trip. go build -tags nogui (and GOOS=linux, GOOS=windows, plain darwin), go test -tags nogui ./... (TestUserPath flaked once under load and passes alone) and the whole GUI suite with --test-concurrency=2 pass. Not tried in the real Wails window on any platform: the Mac pageZoom, WebView2's zoom factor with the resized panel and the Windows font are untested there. 2026-09-30 02:51:17 +08:00
yetone 2493bd72c5 pi: a model's thinkingLevelMap in models.json names every one of Pi's levels, the model's own mapped to its effort and the rest null, so Pi's /thinking offers only what the vendor takes (#243) — magpie wrote only xhigh and max (glm-5.3-flash at low/high/max got {"max":"max"}), and Pi offers every level a map leaves out but xhigh and max, so minimal and medium were offered and sent efforts the vendor turned away. Now glm-5.3-flash gets {"off":null,"minimal":null,"low":"low","medium":null,"high":"high","xhigh":null,"max":"max"}. Pi's off (read from pi-ai 0.99's getSupportedThinkingLevels and its openai-completions, openai-responses and anthropic-messages streams) maps to none for a model that takes none, so off really turns reasoning off; a model without none has off null, since Pi's off asks a Responses model for effort none and leaves a Chat model on the vendor's default; on Anthropic's Messages API off is left out, as before, since there Pi's off sends thinking disabled, which Claude takes, and Claude keeps its xhigh and max. A model whose levels magpie doesn't know still gets no map and reasoning false. Tested with TestPiThinkingLevels (full maps with and without none, Anthropic keeping off, none for unknown) and TestOpenCodeNoResellerLevelsForSwitchModel, which now checks the full map written for a relay's low/high/max model and no map for an unknown one (fails on main with map[max:max]). go vet ./internal/agent, go build -tags nogui (and GOOS=linux), GOOS=windows go build and go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked, TestCodexVersion and TestAddSecondOfPreset failed in the full run under load and pass alone). Not tried in a real Pi session. 2026-09-30 02:48:10 +08:00
yetone cb65895ac3 agents: Factory's Droid CLI is an agent magpie sets up (#242 tasselx) — droid keeps its settings in ~/.factory/settings.json ($FACTORY_HOME_OVERRIDE/.factory when set), its BYOK models the customModels array ({model, displayName, baseUrl, apiKey, provider "anthropic" | "openai" (Responses) | "generic-chat-completion-api", maxOutputTokens, noImageSupport …}), picked as "custom:<id>". Picking a catalog model through magpie appends one entry per catalog model after the user's own (so their ids never move), each with an explicit id "custom:magpie/<provider>/<model>", the catalog's label as displayName, the gateway and token magpie, its context (maxContextLimit) and output when known, noImageSupport always written (droid takes a Chat Completions model as blind otherwise), and the API its provider speaks natively so the gateway relays it as it is: Responses → "openai" at /v1, Messages → "anthropic" at the gateway root (Anthropic's SDK adds /v1), anything else "generic-chat-completion-api" at /v1; and sets sessionDefaultSettings.model. The model shows in magpie as magpie/<ref>; the user's sessionDefaultSettings.model is stashed and put back on reset (a top-level "model", the docs' spelling, is left alone and is what droid falls back to), magpie's entries go and an emptied customModels or sessionDefaultSettings with them; everything else in the file, the user's entries included, is kept byte for byte. The picker offers the user's own custom models by the id droid gives them (its "id", else the displayName with whitespace as dashes and a count of the same name before it), including the legacy ~/.factory/config.json custom_models, which magpie never writes. Sync rewrites magpie's entries as providers change; Check reports a missing entry or a changed baseUrl/apiKey; its requests are Droid's in usage (User-Agent factory-cli/<version>). Icon is Factory's own favicon glyph (factory.ai/favicon.svg); lobehub has none. From the docs: docs.factory.ai/cli/byok/overview (customModels fields, the three providers, config.json legacy snake_case merged under settings.json), /cli/configuration/settings (model, sessionDefaultSettings.*), /droid-exec/overview (custom:<Display-Name>-<index>). Verified by reading the shipped droid 0.229.0 bundle (@factory/cli-linux-x64, unpacked, never installed or run): the zod schema of an entry (also id, index, maxContextLimit, enableThinking, reasoningEffort, bedrock-converse), ids as custom:${displayName.trim().replace(/\s+/g,"-")}-${n} with n counting equal names, an entry's own id winning, the lookup by id, top-level model/reasoningEffort/specModeModel moved into sessionDefaultSettings only when unset there, getModel reading sessionDefaultSettings.model, noImageSupport defaulting by provider, FACTORY_HOME_OVERRIDE, and BYOK requests carrying factory-cli/<version>. Tested in a sandbox HOME (internal/agent/droid_test.go): set/sync/reset on a settings.json as droid leaves it with a Groq BYOK model (its entry byte-identical, the file restored exactly), each protocol's provider and baseUrl, a moved baseUrl caught by Check and healed by Sync, picking the user's own model while on magpie's, no file at all, a top-level model, FACTORY_HOME_OVERRIDE and legacy config.json ids (custom:Local-Qwen-0/-1) offered and never written. go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin flaked under load and pass alone). Not tried with a real droid: whether droid's /model lists the entries, that it reaches the gateway on each protocol, and how it sends thinking for a custom model (magpie sets no reasoningEffort/enableThinking) are unverified. 2026-09-30 02:35:59 +08:00
Karenandyetone 2693cd8828 feat(gui): choose macOS session terminal (#241)
* feat(gui): choose macOS session terminal

* session terminal: build the darwin CLI without cgo, drop the dead macOS 11 path, and open Terminal when the .command default is not a terminal

- terminal_apps_darwin.go imports "C" but had no cgo tag, and terminal_apps_other.go was !darwin only, so `CGO_ENABLED=0 GOOS=darwin go build -tags nogui .` (make release-cli's darwin targets) failed with undefined discoverTerminals. The cgo file and its test are now darwin && cgo; the stub is !darwin || !cgo.
- magpie needs macOS 12 (LSMinimumSystemVersion, -mmacosx-version-min=12.0), so the macOS 11 branch (temp .command file, deprecated LSCopyApplicationURLsForURL, CFRelease) never ran; it is gone along with the CoreServices link.
- With no saved choice, a default .command handler that is not a listed terminal, or a failed lookup, now opens Terminal as before the PR instead of failing; Settings labels the system option "System default (Terminal)" then, and the unused "System default (unavailable)" string is dropped.
- session-terminal.test.cjs covers an editor as the default in Chromium and WebKit, en and zh, and is on the README's node --test line.

---------

Co-authored-by: yetone <yetoneful@gmail.com>
2026-09-30 02:36:12 +08:00
yetone 2364f8b5ff providers: the add sheet's rows carry no region tag, the logos stay put when a dialog opens or closes, and "Add provider" sits at the view's foot and reaches the sheet on the first click (Image #27: 国际中国还是有点像狗皮膏药; 每次打开或者关闭弹窗的时候,Provider的logo都会重新刷新一遍; Image #28: 添加供应商要固定在底栏底部…第一次点击的时候没有任何事情发生) — a vendor's merged global/China row lost its "Global · China" tag (design B1): the region is picked in the editor's Region switch and the row's title still lists each region's host. Every redraw of the Providers page made each logo anew, so each one faded in again whenever a dialog opened or closed; renderProviders now hands the logos it drew to icon() to reuse (the editor drawing its own), so they are moved, not remade. "Add provider" is sticky at the view's foot over a long list and steps aside while the sheet's head is in sight; the first click opened the sheet but WebKit clamped the scroll before it grew, and unrollSheet took that for the reader scrolling and gave up — it now starts from where the view really is, and a click with the sheet open eases the view to it. Tested with add-button.test.cjs (new: the bar at the foot at the top and the end, one click reaching the sheet, the bar away and back, a dialog opened and closed keeping every logo; fails on main) and add-sheet.test.cjs (no tag, the hosts in the title), Chromium and WebKit, en and zh; click-scroll passes; the full GUI suite passes but login-import, which fails on main under load. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. 2026-09-30 02:26:28 +08:00
yetone c35471cf5b provider: Azure OpenAI, asked at the user's own resource on its v1 API with the key in api-key, for Codex, Claude Code and chat clients alike, and brought in from other apps instead of left out — Azure OpenAI had no preset, and importing an Azure entry from Alma was skipped with "magpie doesn't support Azure OpenAI yet". A new Azure OpenAI preset (vendor) has no URL of its own: its editor asks for the resource's endpoint in a field above the key, focused first, with https://<resource>.openai.azure.com as the placeholder and a hint on where the portal shows it and that the model ids are the deployments' names (en and zh); adding without one says "Your resource's endpoint is needed" and sends nothing, and Save refuses one without an endpoint too. Whatever is pasted — the bare endpoint, …/openai, …/openai/v1/, a classic deployment URL with its api-version, a *.cognitiveservices.azure.com or *.services.ai.azure.com host, or only the resource's name — is saved as https://<host>/openai/v1 for both chat completions and Responses. magpie uses the v1 GA API rather than the classic /openai/deployments/<name>/…?api-version= paths: the paths match OpenAI's, the body's model is the deployment's name, and no api-version is needed (the reasoning is in internal/provider/azure.go). The key goes in api-key, never as a Bearer, which Azure takes as an Entra ID token; nothing sends Authorization or x-api-key there. Deployments named after GPT/o/codex models go to Responses first, as OpenAI's do, and others to chat completions. Chat bodies ask max_completion_tokens instead of max_tokens and drop thinking/enable_thinking, both in passthrough and in Claude Code's translated requests, and the Test probe asks max_completion_tokens as well. The model list is the resource's deployments (/openai/deployments?api-version=2022-12-01), falling back to /openai/v1/models, with the key in api-key; when neither answers, the error says to type the deployment names in as model ids. Before a list, no catalog models are offered, since a model the resource hasn't deployed would be turned away. An Alma azure entry, and any imported entry at a resource's host, becomes the preset at its v1 base; an Alma azure entry with no endpoint is skipped as "it has no Azure OpenAI endpoint", and one at a non-Azure host (an API Management gateway) keeps its URL and is still signed with api-key. Adds the lobe-icons Azure icon. Tested in Go against fake resources (httptest): TestAzureRoutes checks that Codex /v1/responses, a chat client's /v1/chat/completions and Claude Code's /v1/messages on both a GPT-named and another deployment reach /openai/v1/responses or /openai/v1/chat/completions with api-key set, no Authorization or x-api-key, the deployment name as the model, max_completion_tokens and no thinking; TestAzureBase covers each pasted form and the hosts that aren't resources; TestAzurePresetSaved covers the endpoint required, both URLs on the v1 base, api-key-only headers, Native, the probe's URL and body, and nothing available before a list; TestAzureModels covers deployments first, the v1 fallback and a refused key's message; TestImportAzure covers Alma rows with an endpoint, with a bare resource name and with none, plus a classic URL from another app. azure-endpoint.test.cjs, in Chromium and WebKit, en and zh, checks the field, placeholder, hint and focus, the warning without an endpoint with nothing sent and the page not scrolled, the endpoint sent as chat and responses with the key, and that OpenAI's editor has no such field. add-sheet and old-webkit pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet and go test -tags nogui ./... pass (TestClaudeAccountNamedAsItsLogin failed under load and passes alone). Not tried against a real Azure resource: whether a key still lists deployments with api-version 2022-12-01, whether Azure's Responses API accepts every field Codex sends, Claude models on Azure AI Foundry's Anthropic endpoint (not wired), Entra ID tokens, and API Management custom domains beyond keeping their URL. Not tried in the real Wails window. 2026-09-30 02:17:15 +08:00
yetone 46c56021a4 usage: DimAgent's quota is asked through its own provider's proxy, like every other subscription's (per-provider proxy landed beside #226) 2026-09-30 02:15:51 +08:00
yetone 125da38943 providers: each provider, a signed-in account like Codex's among them, goes through its own proxy — the one in Settings, none, or its own address (#237 xosi: 目前似乎是全局设置代理的,codex和国模供应商无法单独区分设置代理) — magpie had one proxy, Settings' (or the environment's, or the system's), for every request it made, so Codex, which needs a proxy from China, and a vendor at home, which is slower or refused through one, couldn't both be right. A provider now has a proxy, "" (following the global one, as before and the default), "direct" (none, whatever Settings says) or an http://, https:// or socks5:// address (host:port meaning http), kept as proxy in providers.json only when set, and for an account provider in its picks entry beside its models. It is checked on save by the same rule Settings' proxy is (settings.CheckProxy, now shared), so ftp:// and the like are refused. netproxy.With puts a provider's choice on a request's context and netproxy.Func honours it; netproxy.Dispatch sends such a request through a clone of the transport kept for that proxy alone (at most 32, cleared past that), since a transport shares an HTTP/2 connection by host whatever proxy dialled it; loopback is never proxied. The gateway's client and http.DefaultClient go through Dispatch, and every request made on a provider's behalf carries its choice: the gateway's turns (attempt, forward, draw, decide, the Codex backend), and the model lists, balances, plan and subscription usage, tests, sign-in, Codex's warm-up and reset, while the Claude subscription's and warm-up's CLI runs get the account's proxy in their *_PROXY (netproxy.EnvWith). The provider editor, a key provider's and a signed-in account's alike, has a Proxy row after Fallback: Global proxy | Direct | Custom (全局代理 | 直连 | 自定义), the address field beside the options only for Custom, its room kept otherwise so a pick never changes the centred dialog's height and moves nothing, and a line under it saying what the pick does; Save posts proxy ("" , "direct" or the trimmed address), a save without it keeps the old one, and Custom with no address is refused before anything is posted ("Proxy: type its address, like http://127.0.0.1:7890"). No border stripes; en and zh strings together. Tested with a new internal/gateway/proxy_test.go in a sandbox HOME: with a global proxy set, provider A's turns and model list go through its own fake proxy, B's (direct) straight to the vendor, C's (following) through the global one, none through another, and A set back to following goes through the global one; ftp:// is refused (fails with With neutered). netproxy_test.go's TestWith covers Func, Dispatch's per-proxy transports and EnvWith. A new internal/gui/tests/provider-proxy.test.cjs in Chromium and WebKit, en and zh: a key provider opens on Global proxy with no address, Custom unfilled is refused and nothing posted, a padded socks5 address saved trimmed, Direct saved as "direct" and Global as ""; Codex's account opens on Custom with its address and saves Direct with its models; every pick leaves the option within 1px; no left border. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./..., provider-proxy, click-scroll, old-webkit, balance-fix and add-sheet pass (click-scroll's WebKit case failed once alongside the others and passes alone, twice). Not tried against a real proxy or the real Wails window. 2026-09-30 02:15:50 +08:00
yetone 9992678c64 zcode: an account with no GLM Coding Plan uses ZCode's Start Plan (体验套餐) where ZCode does, with ZCode's own token, and shows its allowance (#236 tasselx: ZCode 3.14.4, magpie 0.1.410, a trial-only account, the free quota not read and every request refused with rate_limit_error "ZCode: [1113][Insufficient balance or no resource package. Please recharge.]"; #225 xiaozhu1337: ZCode ships a built-in trial package) — magpie sent every ZCode account to the Coding Plan's endpoint, api.z.ai/api/anthropic, with the account's zcode-api-key, and read its allowance only from /api/monitor/usage/quota/limit. That key is billed to a Coding Plan, or else to the account's API balance, so an account with only the Start Plan got 1113. ZCode itself (ZCode.app 3.14.3's zcode-builtin.json providerRules and host/index.js: resolveCurrent, buildZaiStartPlanBalanceUrl, hasActiveStartPlan, isZaiStartPlanIdentity, normalizeStartPlanExpiry, normalizeZaiStartPlanBalanceLimits, pollPendingOAuth) serves the Start Plan (account:zai-start-plan and account:bigmodel-start-plan) from zcode.z.ai/api/v1/zcode-plan/anthropic, keyed by its own session token (the credential store's zcodejwttoken, the token its sign-in's poll hands back, which magpie dropped), and reads what is left from /api/v1/zcode-plan/billing/balance?app_version= with that token. Now, in a new zcode_start.go: an account keeps ZCode's token beside its key (zcodeKey.JWT; zcodeOwn reads zcodejwttoken, and user_info from oauth:bigmodel:user_info when there is no Z.ai one; an account with the token and no coding plan key is an account). Whether it is on the Start Plan is asked of /api/biz/subscription/list (a Coding Plan: as before; none: the Start Plan; can't tell: the Start Plan if its balance has an active one), kept 10 minutes (1 when unsure); with no token it is the Coding Plan as before, asking nothing, and with no key it is the Start Plan. On the Start Plan a request is moved to zcode-plan/anthropic, signed with the token (x-api-key and Bearer) and ZCode's own headers (User-Agent ZCode/<ver>, X-ZCode-App-Version, X-Title, HTTP-Referer, X-Platform), refused with "sign in to ZCode again" once the token has run out; its models are the Start Plan's (zcodePlanID account:zai-start-plan for ZCode's config, zcodeModels less GLM-5.3 before it is read). Its allowance is the balance as ZCode reads it: a plan still "active" past ends_at (by server_time) is expired and the buckets of expired plans left out; the plan's name (Start Plan) with Until its end and Renew off; a window per bucket, named by its show_name (or its models, or Credits), Used and "used / total" from total/used/remaining units (numbers or strings), ResetsAt its expires_at, Span from its entitlement's period (daily/weekly/monthly) or period_start..end, and matched to the bucket's models. Signing in from magpie no longer refuses an account with no Coding Plan: with an active Start Plan it is added on that (its key kept too, so a Coding Plan bought later is used), and refused only when the Start Plan is over as well ("this Z.ai account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started — subscribe at z.ai/subscribe, then add it again"). Accounts with a Coding Plan are routed, signed and metered as before. The provider keeps the name ZCode (#225 asked for "GLM Coding Plan"; ZCode names these plans "Z.AI Individual Coding Plan" and "Start Plan", and the plan line already shows "GLM Coding <Level>"). Tested with a new internal/provider/zcode_start_test.go against a fake Z.ai/zcode.z.ai in a sandbox HOME, no real tokens: ZCode signed in with only its token (BigModel user_info) is an account, its provider served at zcode-plan/anthropic, a request signed with the token and ZCode's headers, the Start Plan's models and plan id, its allowance (Start Plan, Until, Renew off, GLM-5.1's window 25%, "250000 / 1000000", reset, a day's span, matching GLM-5.1 only); an expired plan and one active past its end give none; with a key and the token, no Coding Plan moves the request (query kept) to the Start Plan and it reaches it with the token, a Coding Plan keeps it on api.z.ai with the key and no ZCode headers, no token asks nothing, an expired token is refused; a sign-in with only the Start Plan is added on it, with a Coding Plan on that plan, with neither refused naming both. TestZCodeAccounts passes unchanged. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet and go test -tags nogui ./... pass (TestUpdateCLI, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin and TestUserPath timed out once each under load beside other builds and pass alone). Not tried with a real Start Plan account: the endpoint's acceptance of the token as x-api-key/Bearer and whether it needs ZCode's headers, the balance's real field values and period strings, what subscription/list returns for a trial-only account, and ZCode 3.14.4 (read from 3.14.3) are from ZCode's code, not seen on the wire. 2026-09-30 02:15:50 +08:00
yetone 12d9195a2c usage: a currency of ¥ CNY chosen before a restart is the one costs show in, not $ (#212 follow-up, L1cardo: after a restart the setting still showed RMB but the costs were back in USD) — the Go side sends the exchange rate the cny choice converts at in two places: inside /api/settings' answer (settingsJSON.fx) and, in /api/state, beside the settings rather than in them (stateJSON.FX, filled only when cny is chosen). At start the page runs applyPrefs(state.settings), which set the currency to cny from the settings but looked for the rate only as s.fx, inside them, where /api/state never puts it; fx.rate stayed 0, and fmtCost converts only when the rate is above 0, so every cost (the Usage page, the requests ledger, sessions) was drawn in $. Picking ¥ CNY in Settings worked, because the save's answer is /api/settings' and carries fx, and the Settings row reads the stored setting, so after a restart the two disagreed. Nothing was wrong with what was stored: settings.json keeps currency, and internal/fx reads its cached rate from disk; the TUI and CLI call fx.Get directly and were never affected. Now load() passes state.fx to applyPrefs, which takes the rate from there or from the settings' own fx, ignores a rate of 0 (what /api/state sends for usd, which never looks one up), and redraws the costs when the rate arrives or changes while cny is chosen, not only when the currency changes. Tested with a new case in internal/gui/tests/currency.test.cjs, in Chromium and WebKit, en and zh: /api/state faked as the Go side sends it after a restart (settings with currency cny, fx beside them), the Usage page opened straight away shows ≈¥88.85 (12.34 at 7.2), Settings shows ¥ CNY picked, and Usage still shows ¥ after it; without the fix it shows ≈$12.34 in all four. currency, old-webkit, usage-ledger, click-scroll and settings-groups tests pass (58); go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass in a sandbox HOME. Not tried in the real Wails window. 2026-09-30 02:15:49 +08:00
yetone 30431e0450 usage: a Grok id named at a reasoning effort is priced as its model (#224 Aimer779: calls made with Grok Build show no cost on the Usage page) — the Usage page, the Requests ledger and the Sessions page price a model by its models.dev id, and xAI lists grok-4.7 and grok-4.6 ($2 / $6 / $0.5 cache read per million in the cached catalog) but not the ids Grok's models are spelled at an effort: grok-4.7-low, -medium, -high, -xhigh (Cursor's ids, and grok-4.7 at the levels Grok's own model list gives it). Such an id is the same model at another setting, so when its own id has no price it is now looked up as the model it is an effort of (provider.pricedNames: grok-<version>[-…]-minimal|low|medium|high|xhigh|extra-high, with or without a path, any case), in Provider.ListPrice and MakerPrice (the gateway's ledger and summary) and in the Sessions page's priceOf (through provider.PricedName). An id priced by its own name keeps that price; what is recorded is unchanged. Only Grok's ids are read this way, and -max is not stripped. Left unpriced, as no catalog in use lists them and no price is made up: fast ids (grok-4.7-fast, grok-4.7-low-fast, … which cursor_models.go treats as models of their own), grok-4.7-build and grok-4.7-build-fast (Grok Build's own models, from its model list and sessions) and grok-4.7-mini; they keep the + as before. Tested with a new TestGrokEffortListPrice (MakerPrice and the Grok account's ListPrice price grok-4.7 at each effort, in upper case and with x-ai/, and grok-4.6-minimal, at grok's price; the fast, build, mini and -max ids, gpt-6-astra-high and cursor-grok-4.7-high stay unpriced) and TestPriceOfGrokEffort in internal/sessions (the same for a session's models, grok-4.7-build, grok-4.7-mini and the fast ones unpriced); TestSubscriptionListPrice unchanged. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion hit their 5s waits in the full run and pass alone). Not tried against the reporter's usage log or in the GUI. 2026-09-30 02:15:49 +08:00
yetone 4455c7f998 gui: the Windows tray panel no longer shows a faint close X over its Settings gear — its caption buttons are hidden there (#238 mintonight: a blurry red X at the panel's top-right corner, just above and right of the gear) — Wails v3 makes a frameless window on Windows WS_OVERLAPPEDWINDOW and, for the shadow and round corners, extends DWM's frame into the whole client area; the panel left its close button at the zero value, ButtonEnabled, which sets WS_SYSMENU, so DWM drew the caption's close X into that extended frame at the window's top right, and with the panel translucent (BackgroundTypeTranslucent, WS_EX_NOREDIRECTIONBITMAP, a see-through webview) it showed through the page, faint, over the gear. It was not the page: #winclose is removed everywhere but Linux's window, and is a grey pill in the header row. The panel's options move into panelOptions(goos, theme), unchanged, and on Windows its close, minimise and maximise buttons are ButtonHidden, which takes WS_SYSMENU and the min/max boxes off, so DWM has no caption buttons to draw; DisableFramelessWindowDecorations stays off, so the shadow and Windows 11's round corners stay. The Mac and Linux get the same options as before (their button states left at the zero value), and the main window is not touched. Tested with a new internal/gui/panel_options_test.go (!nogui): on windows the three buttons are hidden and the panel still frameless, translucent, off the taskbar and decorated; on darwin and linux the buttons are as before and URL, backdrop and corner radius unchanged; run on the Mac and compiled for Windows (GOOS=windows go test -c). go build -tags nogui (and GOOS=linux), GOOS=windows go build and go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin timed out once at a load average near 60 and pass alone). Not tried on a real Windows desktop: that DWM stops drawing the X without WS_SYSMENU is from the Win32 custom-frame behaviour and Wails' code, not seen. 2026-09-30 02:15:48 +08:00
yetone 5353ed54fe gateway: a vendor refusing the client outright (WorkBuddy's 400 "Illegal API invocation from an unapproved channel") hands a group's request to its next member (PR #232 Y-C-Fan: a routing group stopped at a WorkBuddy member refusing Claude Code's chats) — retryable let a 400 fall over only on quota or unserved-model words, so this refusal, which is of the provider and not the request, went back to the agent. A new refusedWords (unapproved channel, illegal api invocation) joins them. The request body is not changed: a chat opening with Claude Code's own system prompt is still sent as it came. Tested with a new case in TestGroupFailsOverAnUnservedModel's retryable table (false before) and the gateway package. 2026-09-30 02:15:47 +08:00
yetone ac1f522154 library: skills in the user-wide ~/.agents/skills are found, one row with the agents that link or junction to them, and brought in by a link that leaves the shared folder as it is (#227 Aimer779: the Library's "In your agents" never looked in ~/.agents/skills, the shared folder many agents read and where the user keeps skills, many as symlinks to D:\aimer-skills; a skill junctioned from there into Pi and ZCode showed as Pi's with "differs in ZCode") — foundSkills walked only each target's own skills folder, and realDir used filepath.EvalSymlinks, which since Go 1.23 leaves a Windows junction (a mount point, reported as irregular, not a symlink) as it is, so two junctions to one folder were two skills, and the Link check (ModeSymlink) took a junction for a folder of its own, one ImportSkill would move. Now foundSkills reads ~/.agents/skills first (hidden entries, ones without a SKILL.md, ones the library has or linked, skipped as for the agents' folders), a skill there carrying shared (its entry) and the agents whose entry resolves to the very same folder; a byte copy in an agent's folder is still another ("differs in"), as copies were. realDir follows links a part at a time itself (symlinks, and on Windows junctions through os.Readlink, relative targets, links to links, a loop or a missing part giving the path back), and a new linked() (symlink, or junction on Windows) replaces the ModeSymlink checks in foundSkills and ImportSkill. ImportSkill links the library to a skill in the shared folder (Source folder), real folder or link alike, and never moves it; the agents' links to it are replaced by the library's as before; an agent's own folder still moves in. No target writes to ~/.agents/skills: targets.go gives no agent a user-wide ~/.agents/skills (only projects.go's project .agents/skills), so it stays a place skills are found in. The page: a found skill in the shared folder says "shared in ~/.agents/skills/<name>" (共享于) above any "linked from", its Bring in title says it stays where it is and can be given to any agent; the shared path is revealable. Tested with TestSharedAgentsSkills (sandbox HOME/USERPROFILE: a symlink in the shared folder to a folder elsewhere, real folders there, Pi and Codex links to one of them, absolute and relative, Gemini's link to the shared link, a Codex copy, a folder with no SKILL.md and a hidden one; one row each with the right agents, shared, link and others; importing all three leaves the shared folder and the folder elsewhere as they were, the library's entries links to them, the agents' links now the library's, Codex's copy untouched, nothing found after), TestRealDir (absolute, relative, a linked parent, .., a loop, a missing path, linked, within) and TestJunction (mklink /J, Windows only, skipped here), and a new internal/gui/tests/shared-skills.test.cjs in Chromium and WebKit, en and zh. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet for windows and go test -tags nogui ./... pass (TestClaudeSignedOut flaked once, passes alone); the GUI suite passes but for login-import (passes alone) and panel-profiles in WebKit, which fails on main the same under this load. Not tried on real Windows, with real junctions, or in the real window. 2026-09-30 02:15:47 +08:00
yetone 6df6de1010 sessions: the indexing show no longer comes back on every visit — the index is written after the read, in the background, and not counted as indexing (Image #15: 这个还是会出现无数次)
Each visit re-reads the few session files agents appended to since the last
read, which takes milliseconds. But refresh() then saved the whole ~20 MB
~/.cache/magpie/sessions.json before clearing the indexing flag, and that
save took over a second, so /api/sessions/progress said "indexing" past the
0.7s threshold and the hero played every time. The flag now drops as soon as
the parse ends; the index is written from a snapshot (parses are never
changed in place, so a shallow clone is safe) by one background writer that
coalesces saves. Reset and the CLI's exit wait for the write.

Tested: TestSaveBehindTheRead holds the save open and requires List to
return and Indexing to be off meanwhile, then the file to be written (fails
with the synchronous save). Against a copy of the real index (196 sessions)
the flag stayed on at most 22ms per visit. go test -race ./internal/sessions,
go test -tags nogui ./... (TestAddSecondOfPreset flaky, passes alone), go build
-tags nogui (and GOOS=linux, GOOS=windows), go build ./..., and the GUI tests
sessions-overview, old-webkit, menu-scroll, click-scroll pass. Not tried in the
real Wails window.
2026-09-30 02:07:50 +08:00
yetone b713d5cd99 gateway: Gemini behind a proxy on this machine or the LAN is asked for its thoughts too, and a proxy that turns the fields away is asked as before (X @saoyan25: 本地反代的是谷歌AI studio的provider … agent客户端没看到思考链的内容,请求响应体也没看到reasoning_content) — v0.1.413 asked for Gemini's thoughts only when the provider's host was generativelanguage.googleapis.com, and Ben's AI Studio goes through a reverse proxy on his machine, so nothing changed for him. Now a request translated to a Chat upstream is taken for Gemini's OpenAI-compatible API (Request.GeminiCompat, set in forwardTranslated) when the host is AI Studio's, or when the model's id names Gemini and the host is localhost, a .local name, or a loopback or private address, a proxy there being most often one in front of Google; a relay elsewhere (OpenRouter, aihubmix …) and other local models are asked as before. As such a proxy may not pass thinking_config on, a 400 naming extra_body, thinking or include_thoughts marks the provider unfit for it (as the cache key is) and the request is sent again at once with reasoning_effort, as before v0.1.413, and so are its next ones — which also guards AI Studio itself, whose taking of the fields hasn't been tried live. Tested with TestGeminiCompat (AI Studio, 127.0.0.1, localhost, 192.168.x, [::1], .local, a local non-Gemini model, OpenRouter, aihubmix) and TestGeminiThoughtsThroughLocalProxy (Claude Desktop's request, thinking adaptive at effort high, to a fake proxy on 127.0.0.1: extra_body.google.thinking_config include_thoughts and thinking_level high with no reasoning_effort, the <thought> block reaching the client as a thinking_delta and no tags; a proxy answering 400 Unknown name "extra_body": asked again with reasoning_effort high, the reply whole, the next turn not sent the fields), which fail on main; TestAIStudioThinking now sets the flag. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. 2026-09-30 01:58:30 +08:00
Allen Caoandheyallencao db4d68894d gateway: retry OpenRouter mandatory reasoning without disabling it (#240)
Co-authored-by: heyallencao <258265534+heyallencao@users.noreply.github.com>
2026-09-30 01:26:57 +08:00
yetone 9d4f420d27 sessions: the search sits at the right of Latest sessions, the list it filters, and the indexing show waits for a real index run instead of replaying on every catch-up read (Image #14: 搜索会话的输入框不应该在这里吗; 会播放两次或甚至多次正在为你建立索引的那个百分比动画) — the search box moves from the filter bar into the Latest sessions heading's right, and it stays visible with an empty-state line when nothing matches, so the query can be cleared. Every reload or range change re-read the few session files agents had written to since the last read; each short read counted as indexing and replayed the percentage show. The show now appears only once indexing has run for 0.7s, and once it has shown it runs to the end. Tested in sessions-overview.test.cjs, Chromium and WebKit, en and zh: the search inside the heading at its right edge and centred on it, filtering, no-match and Escape; a 0.4s run shows no hero and a 2s one shows exactly one (fails with the delay at 0). menu-scroll's outside click now lands on the filter bar. menu-scroll, old-webkit, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. 2026-09-30 01:15:58 +08:00
yetone 317bc70875 sessions: one index run a page shows once, a spinner, a session-shape chart that reads, and every session counted in it (Image #9–#13: 排版不对…土土的; 这个原点的动画可以换成别的吗; 这个图我着实没看懂; 出现之后又消失又出现…跑完之后突然又出现; 为什么只有5个会话)
- The index hero flickered and ran again: the sessions list, the stats and a
  range change each started their own refresh of just the files they wanted,
  and the page showed a fallback hero after 700 ms whether or not anything was
  being read. List and stats now refresh every changed file in one run, so the
  read after it has nothing to do and a wider range finds the files read; the
  hero shows only while the server says it is indexing, and once it is done it
  stays at 100% ("Indexed · putting the page together…") and never starts over.
- The hero: a dot field in the accent alone, a big percentage, a hairline bar;
  a thin spinning ring in place of the pulsing dot, still when done or for
  reduced motion.
- Session shape: bars stand on a baseline with no grey column behind them,
  an empty bin is "0" over a stub, the busiest bin is in full accent (only
  when one is busiest), "Autonomy" is "Tool calls" (工具调用), and a line says
  what each axis is (横轴:… · 柱高:会话数).
- Only 5 sessions in the shape: parses kept by an early cacheVersion 4 had no
  prompts or replies, and were never read again, so the chart skipped them.
  cacheVersion 5 reads them again (on real data 385 of 462 sessions over 30
  days, rather than 5). Sessions of agents that tell no messages (pi, grok…)
  now count by their minutes at work.

Tested: TestShapesMinutesWithoutMessages; sessions-overview.test.cjs in Chromium
and WebKit, en and zh (the bars, zero stub, peak, both footers, the tool-calls
label); go test -tags nogui ./..., builds for darwin, linux, windows. Two GUI
files failing in the parallel run (click-scroll WebKit, login-import chromium
zh) pass alone.
2026-09-30 01:15:58 +08:00
Ethan Xuandyetone 2f00db65e6 feat: add DimAgent subscription support (#226)
* feat: add DimAgent subscription support

Add OAuth PKCE login with automatic and pasted callbacks, account management, model discovery, quota reporting and gateway integration. Safely rotate credentials during concurrent requests and preserve fresh tokens when updating model caches. Cover protocol, provider, GUI and callback behavior with regression tests.

* dimagent: a refresh hiccup inside the day's lead keeps the token that still runs

The fallback checked !dimagentNearExpiry, which is false whenever a refresh
runs at all, so a 5xx/timeout on /oauth/token failed every request for the
last 24h of a still-valid token. A 401/403 still lapses the account.

* dimagent: review tests (sign forwarding, parallel refresh once, hiccup in lead, LoginUsage)

* dimagent: signing in again takes the callback port from the sign-in it replaces

StartSignIn begins the new flow before it cancels the old one, so the new
flow's listen on 54321 failed (ignored) and the old listener then shut
down a second later: the browser's callback was refused and only pasting
could finish. The older DimAgent flow is now ended and its server closed
first, and the listen retried briefly while the port is let go.

* dimagent: the fake-upstream tests get a name of their own

---------

Co-authored-by: yetone <yetoneful@gmail.com>
2026-09-30 01:11:29 +08:00
yetone 27c90c8d88 panel: profiles open from Profiles at the Agents tab's foot, on its left, instead of a tab of their own (Image #65: tray window 中方案不应该有一个单独的 tab,在 Agents tab 左下角有个方案即可) — the tray panel's tabs are Agents and Routing; the footer's left corner, on the Agents tab only, has Profiles (方案) with how many there are, and a click opens them upward over the list as a popover (the .pop look and fade, reduced motion kept): the chips, + Save current and its name field as before, the popover scrolling within itself when there are many, sized between the tabs and the footer. A click outside, Escape (the name field's Escape closes the field alone first), another tab or a profile applied closes it; the list under it is never moved. A remembered Profiles tab opens Agents. fit() no longer measures the profiles. Tested: panel-profiles.test.cjs reworked (no Profiles tab; the popover above the button at the foot's left and under the tabs, the count shown; scrolling to + Save current inside it, the field whole and nothing moving; Escape, outside click, Routing tab and applying a profile close it; Profiles only on Agents) and old-webkit.test.cjs opening it before Save, in Chromium and WebKit, en and zh. Screenshots looked at in light and dark, en and zh. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the GUI suite pass (click-scroll, panel-profiles, panel-routing and routing-kind failed under --test-concurrency=4 and pass alone). Not tried in the real Wails window. 2026-09-30 01:05:43 +08:00
drunkduckdrownandyetone 7103ba1997 provider: a model can be told to accept images (#228)
* provider: a model can be told to accept images

Reasoning levels can be narrowed per model, but whether a model sees
images came only from the vendor's list. A relay or a model the list
hasn't caught yet could not be marked as taking images, or as text
only. The names editor now has Accepts images. The choice is kept
beside the name and the levels, and is what agents and the gateway's
image check are told. The same answer as the vendor's is not stored.
Restore default clears it.

Tested with TestModelImage. go build -tags nogui passes.

* settings: a save of the Settings page keeps the models' image choices, as it keeps their names and levels

POST /api/settings copied Visible, ModelNames and ModelEfforts over from the current settings but not the new ModelImages, and the Settings page never sends it, so switching the theme or language dropped every "Accepts images" override. Tested with TestSettingsSaveKeepsModelChoices (fails without the fix: images map[]).

---------

Co-authored-by: yetone <yetoneful@gmail.com>
2026-09-30 00:52:07 +08:00
yetone 0f33e53454 gateway: AI Studio's Gemini is asked for its thoughts, so Claude Desktop sees it think instead of waiting in silence (X @saoyan25: Claude Desktop through magpie took 20 s for 你好, 首字 19983 ms, where another client showed Gemini's first output in 2 s) — a request translated to Google AI Studio's OpenAI-compatible endpoint carried only reasoning_effort, and Gemini gives its thoughts there only when asked (extra_body.google.thinking_config.include_thoughts), so the whole think was silence: nothing for the client to show and the first token counted only at the answer. Now a client that asked to see the model think (Anthropic thinking enabled/adaptive, and the like) has AI Studio asked for its thoughts at its effort as thinking_config — thinking_level for Gemini 3 and later (xhigh and max as high), thinking_budget 1024/8192/24576 for 2.x, Google's own steps — in place of reasoning_effort, which Google won't take beside it; with no effort named, the model's own level. A request with thinking disabled (Claude Desktop's title requests) is sent reasoning_effort minimal, the least Gemini 3 thinks (it can't stop), where before it thought at its default. Other hosts get what they did. As the endpoint's way of giving thoughts isn't documented, both are read: reasoning_content, as before, and a leading <thought>…</thought> in the text, the tags split anywhere across chunks, which the chat decoder now passes on as thinking (the rest as the answer; text that only starts like the tag, or has one later, stays text). Tested with TestAIStudioThinking (each effort, 2.5's budget, no effort, off, an effort without thinking, another host), TestChatThoughtTags (whole, split, one byte a chunk, a leading newline, <b>, a lone <, a tag later, never closed) and TestChatThoughtTagsToAnthropic (a thinking block ahead of the text, its first delta written as soon as it came, no tags left), which fail on main; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (three internal/provider tests timed out under the parallel run and pass alone). Not tried against the live AI Studio API: no key for it here. 2026-09-30 00:46:39 +08:00
yetone f18d4e886b providers: the add sheet as A3 — a small green dot after an added name and a grey count past one account, short names with the full name, plans and host in the title, bolder section heads with more room, and a vendor's global and China presets one row marked Global · China whose editor picks the region (keeping the key typed); the editor now grows out of the row clicked on a spring and folds back into it on close, iOS-like, with reduced motion respected (A3; 点击这些供应商,弹出表单Modal以及关闭表单 Modal 的时候,要有一个极其优雅丝滑的动画,类似于iOS的动画) — the list's scroll is restored before the modal closes so the fold lands on the row. Tested with add-sheet.test.cjs in Chromium and WebKit, en and zh, recording the open and close keyframes, and the whole GUI suite (login-import flakes under load on main too). 2026-09-30 00:24:57 +08:00
yetone 9272fd1f62 gui: the session indexing view's narrow layout has a fallback where container queries are missing (#220) — the indexing show's @container (max-width: 420px), added beside the old-WebKit work, had no @supports not fallback, so old-webkit.test.cjs failed on it; macOS 12's WebKit (Safari 15) now stacks it by the window's width, as the Routing page's container queries do. old-webkit.test.cjs passes. 2026-09-29 23:52:46 +08:00
yetone 641b01aa95 settings: the warm-ups and WorkBuddy's check-in sit under one row of tabs, Codex | Claude Code | WorkBuddy, the picked one's rows alone in the card (#124 Mrhe525: asked for the service groups as tabs in the heading's place instead of three stacked groups) — since v0.1.406 the Settings page ran on after Preferences with three groups under their own headings, CODEX (Warm up on reset, Daily warm-up), CLAUDE CODE (the same two) and WORKBUDDY (Daily check-in). Now one section follows Preferences whose heading row is the tabs, in the headings' small capitals, the picked one darker on a soft pill (a tablist labelled "Warm-up and check-in" · 预热与签到), and the card under it shows only that service's rows, unchanged: same rows, same settings keys and /api/settings posts, the same status lines (last warm-up, today's check-in) and tooltips. The WorkBuddy tab is there under the condition the group had (an account signed in or the check-in on). The tab is remembered per viewer (magpie.warmTab, localStorage in try/catch, like the panel's tab), Codex to begin with and whenever the remembered WorkBuddy tab is not there. A click on a tab moves nothing: the card is its own height, and a shorter one picked at the page's very end gets room kept at the view's foot by the page's "where the reader is" hold; Left/Right/Home/End move along the tabs (roving tabindex), the tab reached clicked so the key is held the same way. A pill drawn while its card was hidden measured nothing, so its thumb is put under the picked option, still, when the card is shown. No border stripes; one new string, en and zh together, none left unused. Tested with internal/gui/tests/settings-groups.test.cjs rewritten, in Chromium and WebKit, en and zh: the tabs in order where a heading would be, after Preferences and before Local network, Codex's rows alone to begin with and each tab its rows alone, the status on the lines, no left border; with the page scrolled, each control posts codexWarmup, codexWarmAt (On, a new time, Off), claudeWarmup, claudeWarmAt and workbuddyCheckin as before and a tab posts nothing, the page left where it was; the pills' thumbs under their picks on a card shown later (fails without the fix); the arrows, Home and End; at the page's very end, WorkBuddy's shorter card and Codex's taller one by click and key leaving the tabs within 1px; the tab remembered across a reload; no WorkBuddy tab with no account, Codex's shown when WorkBuddy's was remembered. Screenshots looked at in light and dark, en and zh, at 900 and the window's narrowest 560. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the GUI suite (128) pass; click-scroll and usage-ledger failed once while go test ran beside them and pass alone. Not tried in the real Wails window. 2026-09-29 23:52:42 +08:00
yetone b881932ec2 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. 2026-09-29 23:52:38 +08:00
yetone 6546033109 kiro: removing kiro-cli's or the Kiro IDE's own sign-in hides it in magpie, and it shows again when Kiro signs in anew (Discord 碳碳双键: 本机没安装 kiro,但旧的 Kiro 数据还在,magpie 列出一个"Kiro account"(登录已失效,请重新添加此账号),添加新账号并设为首位后,旧的数据无法移除) — magpie reads Kiro's own sign-in from kiro-cli's database or ~/.aws/sso/cache and lists it among the accounts it signed in; Remove on it said "that is kiro-cli's or the Kiro IDE's own sign-in; run kiro-cli logout or sign out in the IDE", which with neither installed left a dead account there for good. The same refusal was shared by every agent whose own sign-in magpie only reads (forgetSideLogin: Kiro, Devin, Grok, Copilot, ZCode, WorkBuddy and WorkBuddy AI, Command Code, Gemini CLI, Antigravity). Now Remove on the agent's own sign-in hides it: logins.json's entry for it keeps a Hidden mark, and it is left out of the account list and so of the gateway's tries (the first and the ones behind it both come from that list); the agent's files are never deleted or written. It shows again when that sign-in changes: for Kiro, by a mark that is a digest of where the sign-in is (kiro-cli's row or the IDE's file) and its refresh token (never the token itself), so a new kiro-cli login or IDE sign-in, to any account, brings it back; for the others, which have no such mark, when it is another account. Removing it while it is first is allowed now, the next account being put first (saved, so it stays first when the own one comes back); removed when it was the only one, the agent has no account left. One of magpie's own accounts is removed with its home as before, and still can't be while it is first. The error and its per-agent sign-out hints are gone. Logins carry own, and the accounts list shows Remove on the agent's own sign-in also while it is first, its title "magpie stops showing and using Kiro's own sign-in; its files are left as they are, and it shows again when Kiro signs in anew" (magpie 不再显示和使用 Kiro 自己的登录;它的文件保持原样,Kiro 重新登录后会再次出现). Tested with a new internal/provider/side_logins_hide_test.go in a sandbox HOME: an expired kiro-cli sign-in ("Kiro account") behind a Google one magpie signed in is removed, stays gone across reads, isn't the account used nor among AlsoOn, kiro-cli's database byte-for-byte and mtime as it was, the refresh token nowhere in magpie's files; a new kiro-cli sign-in brings it back behind the one first; put first and removed, the next is first; one of magpie's removed takes its home and leaves the other's; the only account removed leaves Kiro with none; Grok's CLI sign-in hidden with auth.json untouched, back once it is another account. The agents' tests that expected the refusal (Command Code, Copilot, Devin, Grok, WorkBuddy, ZCode) no longer do. A new internal/gui/tests/own-signin-remove.test.cjs in Chromium and WebKit, en and zh: kiro-cli's sign-in behind another and alone first both have Remove with that title and post login/forget, magpie's first account has none. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (138) pass. Not tried with a real Kiro, kiro-cli or the Kiro IDE. 2026-09-29 23:52:33 +08:00
yetone cf6b82f70d usage: a Codex, Copilot or catalog-less relay call is priced at its maker's list price, as a Claude account's is (#224 Koohoko: on the Usage page only Claude models show a cost; gpt-6-astra and gpt-6-luna through Codex's ChatGPT account and gemini-3.8-flash and gpt-6-luna through Copilot show tokens and no cost, and the Codex row reads ≈$153+) — the Usage page and the Requests list price a call by the models.dev ids of the provider it went to (Provider.Catalog), and only the Claude account had one (anthropic): Codex's and Copilot's accounts, like Grok's, WorkBuddy's and a relay added without a catalog, have none, so every call they served was counted unpriced although models.dev lists all these models (openai: gpt-6-astra $10/$50, gpt-6-luna $0.1/$0.5; google: gemini-3.8-flash $0.75/$3.75, per million, in the cached catalog). A call is now priced by its provider's catalogs first, as before, else by the first maker among the vendor presets that lists the model (the same makerCatalogs the reasoning levels borrow from), and a call whose provider has gone since is priced by its maker too. The model is matched by its own key first, then without a path or case (a relay's openai/gpt-6-astra is gpt-6-astra), a Bedrock geography, a (variant) or :tag, as the reasoning levels already are (new catalog.PricedBy, Provider.ListPrice, provider.MakerPrice); a model no maker lists (a Cursor or Kiro alias, a relay's own name) stays unpriced and keeps the + as before, no price is made up. Both the Usage summary and the Requests ledger go through the one pricer, so they agree. Tested with a new TestSubscriptionListPrice: calls to codex (gpt-6-astra, gpt-6-luna), copilot (gemini-3.8-flash, gpt-6-luna), claude (claude-opus-5-5) and a relay with no catalog (openai/gpt-6-astra, mystery-1) each get their maker's price in the ledger and the summary, mystery-1 alone unpriced (fails on main: all seven unpriced); TestLedger and TestSummarize unchanged. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestAddSecondOfPreset failed once in the full run with a fourth provider and passes on its own and in the package run; it doesn't touch pricing). Not tried against the reporter's usage log or in the GUI; the Sessions page's own pricing (internal/sessions) was not looked at. 2026-09-29 23:52:32 +08:00
yetone 98efd30a2e routing: requests coming in leave the routing groups where they are on the screen, a group being edited too (Jerell.OvO on Discord #general: on the window's Routing page the request log refreshes live and each refresh scrolls the page up, so the routing groups further down can't be read or edited; a switch to pause it or moving the log to another tab was suggested) — every request redraws what sits above the groups (the stage gains or loses agent and account rows, the story a line, the requests and accounts lists theirs), and app.js's backToReader kept the view's scrollTop as a fixed number, putting it back after any scroll the reader didn't make: WebKit has no scroll anchoring to make up for it and in Chromium backToReader undid the browser's own, so the groups slid up under the reader as the parts above shrank and down as they grew. In WebKit the accounts list, emptied and refilled every second for its countdowns, also made the view scroll on its own. Now a view can name a part of itself to keep in place (keepInView): its position on the screen is taken when the reader leaves it (their own scroll, or a click held), and backToReader follows it wherever the parts above take it, from a ResizeObserver so before paint, with the browser's own anchoring off for that view so the two don't fight; a view at its top stays at its top. The Routing page keeps the groups in place while they're in the upper half of the view or a field in them has the focus, else the lists, and with neither (the stage in sight) nothing moves. The accounts list is patched, keeping each row that's unchanged (rows with a button are always replaced, so their handler is the new one), not emptied and refilled. No live-update switch was needed and no new strings; no border stripe. Tested with a new internal/gui/tests/routing-steady.test.cjs in Chromium and WebKit: with the view wheeled down to the groups, one open in its editor and text typed into its name, six requests streamed in from the trace (new agents and accounts, failures and retries): the part above the groups really changed (more than 50 px), the section's head, the editor and the last group stayed within 1.5 px on the screen on every layout and scroll seen (WebKit scrolls by whole pixels while the parts above are laid out in fractions), the view's scrollTop moved by just what grew above them, and the field kept its focus and its text and went on taking typing. On origin/main it fails in both engines (the head moves 233 → 171 px). go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (126 tests) pass. Not tried in the real window with a live gateway. 2026-09-29 23:52:30 +08:00
SadWoodandyetone 09abaeecf0 fix(gui): 版本检查进行中保留检查按钮 (#223)
连续点击版本检查时,后返回的 checking 会把这一行的按钮删掉。检查未结束时按钮保持禁用并忽略再次点击,迟到的 checking 不再覆盖已经返回的结果。

Co-authored-by: yetone <yetoneful@gmail.com>
2026-09-29 23:43:40 +08:00
drunkduckdrown ae9b3ecdcf decide: send a gateway's own Jev name; ambiguous unfetched is 400 (#207)
Vercel and Cloudflare each serve one Jev, as typesafe-ai/jev and
typesafe/jev. jev-latest is that model, rewritten to the name the
channel lists; jev-preview is not on the channel and is 400.

A bare jev-latest with two providers that have not fetched a list
used to 404 (listed empty, then no single provider to alias). Both
would take it, so it is 400 "more than one" as when the lists are in.

usage.Append still fills Requested and Served on systemOne and
serveSystemOne.

Tested with TestRouteDeciderVercelAlias (preview refused, own name
kept), TestRouteDeciderCloudflareAlias, TestRouteDeciderUnfetchedAmbiguous.
2026-09-29 23:26:25 +08:00
Austie 86a2b8cf47 providers: qianfan personal, team and pay-as-you-go endpoints (#211)
* providers: qianfan personal, team and pay-as-you-go endpoints as its preset regions

* providers: qianfan review fixes — pay-as-you-go fetches /v2/models, enterprise naming, the plans' union models, a zh note

* providers: qianfan plans list nothing, pay as you go does — NoList scoped by region

* providers: qianfan review round two — old id maps to the new preset, the union models, IAM keys and a README section

* providers: qianfan key link follows the region — normalize takes it from the endpoints, the button from the plan picked

* providers: ParseImport takes a region key page
2026-09-29 23:25:56 +08:00
yetone 41841997d6 sessions: a fast index with an animated first run, searchable model and folder pickers, and the overview's session shape, tool use, top skills and heatmap by messages or output tokens (v: 感觉我们的Session可观测性页面也要做的这么的丰富、优雅、简洁…; 顶多第一次打开顶多需要个1分钟,然后之后都是靠索引能够极快的打开; 一定一定要特别炫酷的一个动画; Image #6/#7/#8)
The index: session files are read in parallel and each line told from its first bytes (a Claude line's type, message head and usage; a Codex line's type and payload), so tool results, attachments and compactions are never decoded; a cold index of this machine's sessions takes about 32 s, then the page opens from ~/.cache/magpie/sessions.json (version 4, which re-indexes once) and only changed files are read again. The page keeps showing the last answer while a fresh one is computed, and GET /api/sessions/progress says how far a first run is, which the page shows in an animated hero instead of a spinner (reduced motion respected).

All models and All folders are searchable combo boxes that draw only the rows in sight, so 13k folders open at once.

Each day now counts the prompts typed, replies, tool calls by name and skills called (a subagent's calls and tokens count, its messages don't). The overview adds:
- Session shape (会话形态): sessions by messages (1–5 … 121+), minutes at work, or autonomy (tool calls a prompt, <1 … 21+), toggled and remembered;
- Tool use (Tool 使用): calls and sessions, the top tools with their kind's dot, calls, sessions and share, the kinds (Bash, Edit, Read, Write, Grep, Glob, Task, MCP tools, other) as a stacked bar and by week;
- Top skills (常用 Skills): each skill's calls, sessions, last use, agents' share and projects;
- the activity heatmap by Output tokens (输出 Token) and Messages (消息) beside Tokens, Sessions, Cost and Active.
No border stripes; a toggle leaves the page where it was.

Tested with TestStatsToolsAndSkills (Claude reply blocks laid out both ways, a Skill call, an MCP tool, /clear, an interrupt, a subagent; a Codex rollout's function and custom tool calls) and the stats tests, and internal/gui/tests/sessions-overview.test.cjs in Chromium and WebKit, en and zh: the new metrics and their tips, the shape's buckets and toggles (the page not moved), the tools' rows, kinds and weeks, the skills' lines, nothing overflowing at 1100 and 560 px. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the GUI suite pass (panel-fold flaked once in the parallel run, passes alone).
2026-09-29 23:17:43 +08:00
yetone a4a9893969 providers: the add sheet is quiet rows under four headings, an added provider marked in green, and a custom provider one line at the foot (Image #24: 这个页面看起来有点乱糟糟的,能否变得特别简洁优雅? — design A of four) — the sheet was framed tiles of two lines each (name and host or plans), the added ones faded with a check, sections named "Relays · many vendors behind one key" and the like, and a dashed Custom tile among the local ones. Now each section is a bold name and a faint word on what it is (Subscriptions · sign in, no key; Vendors · the makers' own APIs; Relays · one key, many vendors; On this machine; Removed from magpie · still signed in, 订阅 · 登录即可,无需密钥 …), and under it rows three to a line, 36px, with no border: the icon and the name, the host, note or plans in the row's title. A provider already added is no longer faded: "✓ Added" (已添加) in green at the right, a subscription "✓ Signed in", "1 account" or "2 accounts" (2 个账号), its title saying a click switches or adds an account; a removed one "Add it back". The custom provider is "+ Custom provider · any OpenAI or Anthropic compatible URL" (自定义供应商 · 任意 OpenAI / Anthropic 兼容的 URL) under a hairline at the foot, hidden while searching as the "Nothing called…" line takes its place. Clicks do what they did (a preset's editor, an added one's page, a sign-in); the header, search and Import… are unchanged. Unused zh strings removed. No border stripes. Tested with a new internal/gui/tests/add-sheet.test.cjs in Chromium and WebKit, en and zh (fails on main): the headings and words in order, no row with a border, a second line, overflow or fading, three vendors on a line at 900px, Added in green on Anthropic and OpenRouter and none on OpenAI, "2 accounts" on Claude, the host in OpenAI's title, the foot line and its hint, searching "deep" leaving DeepSeek alone and no foot, and a click opening DeepSeek's editor. The whole GUI suite passes (four files flaked under the parallel run and pass serially), go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. The regions (Kimi and Kimi China …) are not merged into one row. 2026-09-29 22:24:05 +08:00
yetone c86179d75a tests: usage-ledger checks the export's own request, not whichever came last — the Requests test waited for the export POST to be the last request asked, paused 150 ms and then read the last request again, so a GET the page sent in between (a refresh) was checked instead, and WebKit's zh run failed "every page is exported" about one run in six (seen reviewing PR #211 on plain main). It now takes the last POST and also checks there was just one. 15 WebKit runs in a row pass (one in six failed before), and the file passes in Chromium and WebKit. 2026-09-29 22:04:57 +08:00
Liken Tan dc4363acb6 fix: a picker's own button closes it on a second click (#218) 2026-09-29 21:39:04 +08:00
yetone 1e9f3a46cb usage: a Requests tab lists every request of the period, one row each, and exports them as CSV, from the window and from magpie usage --csv (Discord 范不着: a per-request ledger as sub2api keeps, to set beside the upstream's own bill) — the Usage page summed calls by agent and model, and the log did not keep the model the agent asked for or the one the reply said answered, so a single request could not be checked against a vendor's bill. usage.Record now keeps Requested (req, the model id the client sent, a magpie alias or group as it came) and Served (served, the model the vendor's reply named, only when it named one); every usage.Append in the gateway fills them (the relayed and translated paths, Codex's backend, image draws, decide), and records written before load as they did, without them. The swap test the Routing page uses (a dated or pinned name is the same model) moved to usage.Swapped so the ledger marks the same rows. usage.Ledger gives the period's records newest first with each one's list-price cost, the agent it is counted under and whether the model was swapped, filtered by agent, failures alone and a search over the models, provider, account, session and effort; usage.WriteCSV writes them (time, agent, requested_model, provider, host, model, served_model, swapped, effort, input, output, cache write, cache read and reasoning tokens, cost_usd, duration_ms, ttft_ms, status, error, session, kind). The Usage page gets a third tab, Requests (请求), between Overview and Sessions, under the same period picker: an agent filter, All/Failed, a search field and Export CSV over a table of time, agent, requested model, provider · account, model sent, served model (amber with the Routing page's sentence when another model answered, plain on a dated name, a dash when the reply named none), effort, in, out, cache write, cache read, cost, duration and status, a failure's row marked by a red dot and bold code; the totals over it and 100 rows a page with Newer and Older. GET /api/usage/requests serves a page, POST /api/usage/requests/export saves all the filtered rows to Downloads (never over an earlier file) and opens the folder, GET /api/usage/requests.csv hands a browser the file in magpie web. The table scrolls sideways in its own box; at the window's narrowest the words after the head's cost give way so the three tabs fit without the page scrolling sideways. No border stripe; clicks move nothing (the pager held where it was). en and zh strings together. Tested with TestLedger (order, cost, swap, a dated name, an old record without the fields, each filter, the CSV exactly), TestServedModelRecorded and TestServedModelUnnamed (a request through a fake upstream answering with another model records Requested fake/sol, Model sol and the served name; one naming none leaves Served empty), TestUsageCSV (magpie usage --csv, a period, a stray argument refused), TestUsageLedgerRoutes (paging, totals, filters, export to Downloads twice) and a new internal/gui/tests/usage-ledger.test.cjs in Chromium and WebKit, en and zh, light and dark: the tabs and columns, the swap tag on the swapped row only, the 429 row, Older and Newer over 130 requests with the pager left where it was, the agent, Failed, search and period filters asking the server from the first page, Export CSV posting the filters with no page and saying where the file went, at 560 wide no tab scrolling the page sideways and the cost uncut, no left border. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (122 tests) pass. Not tried in the real window through Wails or against a live gateway's log. 2026-09-29 21:26:01 +08:00
yetone 5a54338744 panel: the Routing tab gets a small stage, agents → magpie → where their requests went, each new request a dot flying there and back, and its rows line up (the user: 这个页面有点乱,另外这个在Tree的window中能否也能有一个比较迷你版的路由动画,直观、优雅。) — the tab listed eight two-line rows with an agent's colour dot and a status dot (both green for Pi), the times left in their cells so the right column ran jagged. Over the list there is now a stage, 104px, in a card: up to three agents that asked lately on the left (icon and name), magpie's bird in the middle, up to three accounts they went to on the right (provider, then the account or key in grey, and a dot for how its latest try there went: green, grey pulsing while answering, red when it failed), joined by thin curved wires. Each place keeps its spot while it stays; a newcomer takes the place of the one heard of longest ago. A request as it comes plays as a dot in its agent's colour: along its agent's wire into magpie, out on the wire to the account it was routed to (the wires it travels lit in that colour, the hub's ring pulsing), waiting there, pulsing, while that answers; a try that failed turns red and comes back to magpie for the next account, and the answer flies home green, or red when nobody could answer. At most four play at once, from the same trace the list is drawn from (no new endpoint); nothing plays with reduced motion or while the tab isn't shown. The rows: the agent's colour dot is gone (its icon says who), the second line starts with a faint → (accent when moved, red when failed, pulsing while waiting) instead of a second dot, and the time (now 24-hour, 20:50:53) and the tokens · time taken are both right-aligned; six rows instead of eight, the panel about as tall as before. No border stripes, nothing scrolls. Tested in panel-routing.test.cjs, Chromium and WebKit, en and zh: the stage lists Codex and Claude Code, Relay·team and Claude·ann@example.com with their status, four drawn wires, the hub, everything inside it and above the list, no left border; every row's time and totals share one right edge and each row has one mark; and, with motion allowed, a request coming once the tab is shown makes a dot fly, the wire lit and the hub busy, the dot waiting beside the Claude account while it answers, turning green and gone after, the row's tokens updated. The whole GUI suite passes (login-import, a timing test of an import's passing status, failed once under concurrency and passes alone), go build -tags nogui (and GOOS=linux, GOOS=windows) and go test ./internal/gui pass. Checked by eye with a fake live trace in light and dark (a 429 on Relay failing over to DeepSeek, then a Claude request). Not tried in the real tray panel. 2026-09-29 21:16:56 +08:00
yetone 04dc3bca72 panel: a Routing tab shows the gateway's latest requests, and the allowances' tab is named Allowances (v on Discord #feedback: the panel's Usage tab and the window's Usage page mean different things, and the panel had no routing) — the tray panel had Agents, Usage and Profiles, its Usage being each subscription's allowance while the window's Usage counts requests and Routing shows where they went. The panel's tab now reads Allowances (额度; the window keeps Usage/用量, and the tab's id and remembered choice are unchanged), and a new Routing tab (路由) lists the gateway's last 8 requests as they come, from the trace the Routing page already long-polls (/api/gateway/trace, which routing.js polled in the panel too; no new endpoint): each a two-line row with the agent's dot, icon and name and the model it asked for, the time, then where it went (provider · account or key), the model that try sent and, when the reply named another model (the new served/swapped fields), the amber "served gpt-6-luna" (实际 …) tag of the Requests list, beside the tokens and time taken; a failed one says its status and why (429 · rate limited), one under way who is answering. Over them, today's calls and tokens (the Usage page's /api/usage?period=today, read when the tab is shown and after a request ends, at most every 5 s) and Open Routing. A click on a request opens the window's Routing page on it: POST /api/window/main takes req beside view=routing (mainView lets only a positive id through), and the page, loaded so, picks that request as a click in its list would (its story told) and drops req from the address. The panel's height counts the tab as its tallest, as the others; the tab redraws keeping the panel's scroll, the click moves nothing, no border stripe. Tested with a new internal/gui/tests/panel-routing.test.cjs in Chromium and WebKit, en and zh: the Routing and Allowances labels, four fixture requests listed newest first with agent, model, "Relay · team" and "Claude · ann@example.com", the served tag on the swapped one only (not on a dated name), the 429 row, times, no row overflowing, today's 42 calls · 1.2M tokens, a click with the panel scrolled asking for view=routing&req=100 and leaving the scroll as it was, Open Routing, and the window opened on ?view=routing&req=101 with that request picked and its story shown; and TestMainView. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (102 tests) pass. Not tried in the real tray panel or with a live gateway: the window opening on the request is tested in the page, not through Wails. 2026-09-29 20:22:19 +08:00
yetone 1cd7b4d086 settings: the warm-ups and WorkBuddy's check-in sit under Codex, Claude Code and WorkBuddy headings, with short lines (#124 Mrhe525: a tidier Settings page) — after Open at login, Dock and Proxy the Preferences list ran on with Codex warm-up, Codex daily warm-up, Claude Code warm-up, Claude Code daily warm-up and Check in to WorkBuddy daily, each with a long grey sentence, one flat list. They now come after it in three groups under small headings as Local network has: Codex (Warm up on reset · 窗口重置时预热, Daily warm-up · 每日定时预热), Claude Code (the same two) and WorkBuddy (Daily check-in · 每日自动签到), the rows' names no longer saying the service. The lines say the purpose alone ("One tiny request starts the next window at once", "Starts each account's 5-hour window at this time every day", "Claims each signed-in China account's check-in credits once a day"), the last warm-up (上次启动于17:01) and each account's check-in (今日已签到 +1…) still after them; the rest (06:00 gives three by 21:00, sent through Claude Code, 签到 as in WorkBuddy) moved into the lines' tooltips with the fine print already there. The WorkBuddy group, heading and all, shows only while an account is signed in or the check-in is on, as the row did; a daily warm-up's time field was already there only while it is on. Same settings keys and the same /api/settings posts; en and zh strings replaced together, the old ones removed. No border stripes. Tested with a new internal/gui/tests/settings-groups.test.cjs in Chromium and WebKit, en and zh: the headings in order after Preferences and before Local network, each group's rows and none left in Preferences, the status on the lines, no left border; with the page scrolled, each segment (Weekly, Weekly and 5-hour, daily On, a new time, Off, check-in On) posts codexWarmup, claudeWarmup, codexWarmAt, claudeWarmAt and workbuddyCheckin as before with the page left where it was, the time field coming and going with its On/Off; no WorkBuddy group with no account. All internal/gui/tests/*.cjs pass (108), go build -tags nogui ./... and go test -tags nogui ./... pass. 2026-09-29 20:22:09 +08:00
yetone d2112865ef dsh: a profile DeepSeek Harness makes after magpie set it up gets magpie's model too (Discord 莫: 模型为什么WEB版本有,桌面版就只有官方自带的?) — every dsh profile has its own patch list (~/.dsh/profiles/<name>/cordis.patch.yml), and picking a model in magpie writes the ones there then. The desktop app's is made the first time it opens, so opened after the web's was set up it had none of magpie's entries and listed DeepSeek's own models alone, while Check read the web's (the first) and said all was well. Now Check looks at every profile and says which has none of magpie's models (the row's unwired mark, whose fix sets the model again in all of them), and the sync run at start-up and on each catalog change gives a profile with no llm-deepseek entry at all the model the wired one has; a profile with an llm-deepseek of the user's own is left as it is (reported, until the model is set again). Tested with a new TestDshProfileMadeLater (the web profile set, then a desktop template and a profile with the user's own entry: both reported, the sync fills the desktop's and leaves the user's untouched, setting the model again clears it; fails on main). go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried with the dsh desktop app itself. 2026-09-29 19:59:20 +08:00
yetone c816c9f828 routing: a request answered by another model than the one asked for is marked, from the model the vendor's reply names (v on Discord #feedback: 模型路由情况其实有的时候还是想知道的,比如之前 openai 会把 sol 降智到 luna 这样的,现在只有走 s2a 和 newapi 这些网关服务才能明确看到) — magpie read the tokens off each reply but not the model it says answered, so a vendor serving sol's requests with luna looked, on the Routing page, like sol answering. The reply's model is now kept as it goes by, never delayed or changed: on a relayed reply by the usage sniffer, which already reads every event and JSON body (Chat Completions' model in its JSON and each chunk, the Responses API's response.model in response.created/completed and model in its JSON, Anthropic's message.model in message_start and model in its JSON); on a translated one from the decoders' start event, which already carried it (Chat, Responses, Anthropic, and Gemini Code Assist's modelVersion), streamed or collected; on a Claude subscription's from the CLI's message_start; and on Codex's own models relayed to the ChatGPT backend. It goes in Usage.Served (and so the request log), each try's Served and the route's (the last try's, when it answered), with Swapped when it is another model than the one the try sent the vendor. Vendors mostly echo a dated or pinned name, which is the same model: the names are compared without a maker or path prefix (openai/, models/, Bedrock's us.anthropic.), case, and a version at the end — a date (-2025-08-07, -20250929, -06-05), a build number (-0613, -001), Bedrock's -v1:0, -latest, -preview — so gpt-5 answered as gpt-5-2025-08-07 or claude-3-5-sonnet-latest as claude-3-5-sonnet-20241022 is not marked, while gpt-5 as gpt-5-mini, gpt-5 as gpt-5.1, claude-opus-4 as claude-opus-4-1-… and deepseek-v3 as deepseek-v2 are. A reply that names no model marks nothing. The Routing page's Requests list puts an amber "served luna" (实际 luna) tag by the answer, its title saying what it means, and the request's story a line "requested sol · served luna" (请求 sol · 实际 luna) with the same sentence; no border stripe, and picking the request leaves the page where it is. Cursor, Devin, Kiro and Qoder name the model they were asked for themselves, so they are never marked.
Tested with new internal/gateway/served_test.go: TestServedModelRecorded (fake vendors in each protocol, streamed and not, relayed — Anthropic, Chat, Responses — and translated — Chat from Anthropic, Anthropic from Chat, Anthropic from Responses — each answering luna (marked), sol-2026-01-01 and models/SOL (not): the route, its try and the request log keep the name, Swapped as said), TestServedModelUnnamed, TestSwappedModel (the names above), TestSniffServedModel and TestCodexOwnModelServedTraced; and new internal/gui/tests/routing-served.test.cjs in Chromium and WebKit, en and zh (the tag on the swapped request alone, whole and in its row, no left border, its title; the story's line; a click that doesn't move the page; the dated and unnamed ones unmarked). With three requests alone the Routing page above the list jiggles by a few to 70px in WebKit on its own (on main too, with no served model in the fixture), so the fixture lists six. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and all GUI tests (80) pass. Not tested against a real vendor serving another model.
2026-09-29 19:41:29 +08:00
yetone 729f6bdbdf models: a model its maker lists with a thinking switch alone takes no borrowed levels, and the gateway sends it no more than high (#214 Moody-Max: opencode 的 mimo-v2.6 Flash 你这边设置的默认推理强度是 max 导致400报错)
A model not under its provider's own models.dev vendor (a relay or provider of the user's, a live id that doesn't match, or a preview/-exp or typed id Known looks up) took the levels most listed for it across all of models.dev. For mimo-v2.6-flash those are the resellers' (llmgateway, counted once per upstream host it lists, and requesty, up to max), while Xiaomi lists it with a thinking switch alone. OpenCode was offered variants up to max, Pi and Codex the same levels, and the gateway passed max on unchanged (fitEffort keeps an effort when no levels are known), which Xiaomi turned away with a 400. The same held for Zhipu's glm-5/5.1/5v-turbo, Kimi k2.6, MiniMax M2.x, gemma-4 and others whose makers give no levels while resellers give up to max.

Now catalog.ListedBy looks a model up in given catalogs (vendor prefix, case, a ':free' or '(…)' suffix aside) and gives the levels listed there, none for a switch or no options, and skips an entry with a token budget alone (Claude 4.5), which says nothing of efforts. Known looks in the provider's own catalogs first, and every borrow (Known, effortsOf) asks the makers' catalogs (vendor presets, less Groq and Ollama Cloud, marked Hosts as they serve others' models) before the crowd. The crowd vote counts a provider once per list, so an aggregator listing a model under several hosts no longer outvotes the rest. Provider.Levelless says a model is known to have no levels, and the gateway (fitFor, on the direct and fallback paths) sends such a model no more than high; a model whose levels are simply unknown is still sent what was asked.

Tests: TestEffortsOfOneVoteAProvider and TestListedBy (catalog); TestMakerLevelsBeforeResellers (provider: Xiaomi's preset, a preview left out of the list and a relay get no levels, an unlisted model and a budget-only Claude one still borrow, Levelless); TestOpenCodeNoResellerLevelsForSwitchModel (opencode.json has empty variants for Xiaomi's and a relay's mimo, three for a reseller-only model; Pi has no reasoning or thinkingLevelMap for mimo); TestSwitchModelSentNoMoreThanHigh (gateway: mimo asked max/xhigh sent high, low as asked, an unknown model max). The agent and gateway tests fail on main. An audit of every preset against the current models.dev snapshot found the cases above. Not tested with a real Xiaomi key or a running OpenCode. go build -tags nogui (and linux, windows), go test -tags nogui ./... pass.
2026-09-29 19:41:29 +08:00
yetone 6aa99ef1c0 usage: costs show in RMB at a live rate, when Settings picks it (#212 L1cardo: 添加费用用人名币结算的选项,最好是能根据实时汇率来计算) — the Usage page, the tray panel, the TUI and the CLI all price a call in USD, magpie's list prices being dollars; Settings now has a Currency row (after Usage shows) offering $ USD, kept by default, and ¥ CNY, which converts every one of those at display time only — usage.Totals.Cost and the JSONL records it comes from stay in USD always, and a vendor's own balance (already in its own currency, a relay's ¥) is never touched.
The rate is internal/fx: a free, no-key endpoint (open.er-api.com/v6/latest/USD), cached in memory and in a small file beside magpie's other caches (XDG_CACHE_HOME/magpie/fxrate.json) for 12h, refetched only once that's stale, and falling back to the last rate it ever had — or a fixed 7.2 — while offline; the currency row's tooltip names the rate and when it was learned. usage.FormatCost carries the $/¥ formatting (2 decimals above $1, 3 below, whole numbers above 100) that usage_cli.go and internal/tui/pages.go used to each have their own copy of; both now read Settings' currency once per command/redraw rather than per row. internal/gui/api.go exposes the rate on /api/settings always and on /api/state only when cny is actually chosen, so usd users' hot path never touches the network for a rate nothing shows with.

Tested with new internal/fx/fx_test.go (a fake server: fetches and caches, answers from memory then from the cache file with no further calls, refetches once stale, falls back to a stale cache or Fallback when offline, ignores a reply with no CNY rate), TestFXInState in internal/gui/fx_test.go, TestFmtCostShowsChosenCurrency in internal/tui/usage_test.go, and a new internal/gui/tests/currency.test.cjs in Chromium and WebKit, English and Chinese (the Usage page's $ by default and ¥12.34×7.2 once cny is picked, the currency row's labels/state/tooltip, a real wheel scroll deep into Settings left alone by picking a currency, the choice surviving a reload). go build -tags nogui, GOOS=linux and GOOS=windows builds, and go test -tags nogui -count=1 ./... all pass clean.

A fetch that failed is not tried again for 10 minutes (the stale or fixed rate shown at once meanwhile), so offline each state() does not wait up to 4s on the network: TestGetDoesNotRetryAFailedFetchAtOnce.
2026-09-29 19:41:29 +08:00
yetone 55a0c35f30 agents: Antigravity CLI (agy) takes magpie's models (Discord Str: asked to use agy through magpie) — agy is on the Agents page like the others. Picking one of magpie's models writes ~/.gemini/antigravity-cli/settings.json: modelProvider "gemini", an entry per magpie model under customModelsConfig.customModels (magpie/<ref> → {apiProvider: API_PROVIDER_GOOGLE_GEMINI, modelName: <ref>}) beside the user's own, and model. Every other key is kept, and "as installed" gives back the user's model and modelProvider and takes magpie's entries out (the file byte-identical to before). agy reads its endpoint and key only from GEMINI_API_KEY / GOOGLE_GEMINI_BASE_URL, and in agy 1.2.13 a custom model chosen by settings or its picker fails ("model is empty"): only --model resolves it. So the agent has a Launch command, GEMINI_API_KEY=magpie GOOGLE_GEMINI_BASE_URL=<gateway> agy --model '<model>' (a $env: form on Windows), which magpie agy prints as its notice and the row's new square copies in one click (tooltip shows it; no scroll; zh strings). Check says unwired when the entry is gone or modelProvider isn't gemini. Tested with TestAgy (set/switch/drift/restore, a user's custom model kept, a file with nothing of the user's left as it was) and agy-launch.test.cjs in Chromium and WebKit, en and zh (fails without the app.js change); end to end in a sandbox HOME with agy 1.2.13 against a sandbox gateway on 38472 and a fake OpenAI upstream: magpie agy magpie/fakeup/echo-1 wrote the settings, agy (interactive and --print) answered from the upstream through the gateway, magpie agy default restored the file. Not tested: the Windows command, a real Gemini upstream; agy's requests show in usage as google-genai-sdk (its UA), and its title requests for gemini-3.1-flash-lite-preview get a 404. go build (nogui, linux, windows) and go test -tags nogui ./... pass; the GUI suite passes but for click-scroll's WebKit case, failing already 2026-09-29 19:41:29 +08:00
yetone 13309b07fa panel: a profile saved or deleted shows at once and stays in sight (Image #62: 点击保存没有效果,删除方案也没有效果。你为什么UI UX做这么烂?) — both did their work (profiles.json had the profile), but the page showed nothing of it: every profile action answers with the whole state, which read logins.json (166KB, the accounts' credentials) again for each agent's drift and models, dozens of times — 2.5–4s a state on a real setup — and only then redrew the chips. And with no profiles yet the chip saved came in above + Save current, which the click held where it was on the screen: the hold gave the view room and scrolled it 30px, and the chip went up under the tabs, out of sight. Now the chip saved is there at once, dimmed till magpie answers, the name field closed; × takes the chip away at once; a save or delete refused puts the list back and says why. A view at its top when clicked stays at its top instead of being given room to scroll (what came in above the control moves it down). logins.json is parsed once until it changes (filememo, a copy to each caller): a state went from 2.4s to 0.9s here. Tested with a new case in internal/gui/tests/panel-profiles.test.cjs, Chromium and WebKit, en and zh: magpie answering after 1.5s with no profiles, the chip there and dimmed before the answer, the list unscrolled and the chip below the tabs, × removing it before the answer, a refused save taken off with its reason (fails on main); against a sandbox magpie web in WebKit, the view stays at 0 where main scrolled it to 30. click-scroll passes in both engines; go test -tags nogui ./... (TestClaudeSignedOut flaky under the full run, passes alone), go build for linux and windows 2026-09-29 19:23:14 +08:00
yetone 0b7781ba18 providers: Claude Code is signed in to its next account once the one it is on is spent, as Codex is, and an account keeps its own affinity, backoff and usage across a switch (#208 Cyb3rN8: Claude Code 用自己的模型时,登录账号额度用满不会自动切换 · #209: Smart 选中的账号和 Claude Code 登录账号互不同步…切换登录账号还会把按 id 记的状态串到另一个账号上) — Claude Code on its own model asks Anthropic as the account it is signed in to, never through magpie, so with A spent its sessions got "You've hit your session limit" while the gateway had moved on to B. The Codex watcher (KeepCodexOnAnAccountWithRoom, now KeepOnAnAccountWithRoom, same place: started by the magpie serving the gateway, a minute in and every 5 minutes) now looks at Claude Code too: when the account it is signed in to is spent and another ticked account's allowance is known and has room, it is signed in to that one through SwitchLogin (credentials and .claude.json's oauthAccount), as Use on the Accounts page does; not when the Claude account was removed from magpie. The rule for #209: the sign-in follows Smart, not the other way round — it moves at the share Smart counts an account spent (provider.SpentShare = 98, which the gateway's usedShare now is), for Codex too (it moved at 100%). The Routing field of a Codex or Claude subscription with several accounts says so: routing picks the account for what comes through magpie; the agent on its own uses the one it is signed in to, moved on at 98%. Sessions already running keep the account they started with until restarted, as SwitchLogin says; routing Claude Code's own model through the gateway (the codexFailover way) is not done here. #209's second part was a real bug: the account the agent is on is the candidate "claude" and the others "claude@user", so a switch handed "claude" from A to B — a conversation A answered was kept on B and traced kept=true, and the Least used tokens and the backoff count followed the id. who(), routed.used and routed.failures are now keyed by restKey (provider@user, as rests already were); rest itself (group members, the trace) is unchanged. Tested with new TestClaudeSwitchedWhenSpent (sandbox HOME, keychain off, a fake usage endpoint: 97% stays, 98% signs Claude Code in to b — credentials and .claude.json — a stays ticked; on b it stays; both spent it stays), TestCodexSwitchedWhenUsedUp still passing, new TestLoginSwitchKeepsEachAccountItself (fails on main: the conversation went to b, kept) and new internal/gui/tests/routing-signin.test.cjs in Chromium and WebKit, en and zh; login-import and routing-kind GUI tests pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion timed out at 5s under the full run's load and pass on their own). Not tested against real Claude accounts or a running Claude Code. 2026-09-29 18:56:33 +08:00
yetone 3676d2e713 providers: an account signed in while magpie runs is listed with its vendor's models (#204 uclort: 首次添加 WorkBuddy 模型显示不全,点击刷新后模型全部显示) — a signed-in account's own list was fetched only at start-up, for the accounts there then (and once: a fetch that failed was not tried again). WorkBuddy AI signed in afterwards, in magpie or in WorkBuddy itself, showed magpie's built-in list ("magpie's list · Refresh asks the vendor", 14 models) until Refresh fetched /v3/config's 20-odd. Now provider.FetchNew, which start-up uses in place of its loop, also runs before the providers are listed (providersState, 8s each): each ready account with no fetched list is asked for it, so the first listing after a sign-in is the vendor's. One that fails is asked again after 10 minutes, not on each listing; one fetched is not asked again. Tested with a new TestFetchNewAccount (a fake WorkBuddy /v3/config: nothing asked with no account; a WorkBuddy auth file written while running lists the built-in models, then the vendor's three after FetchNew; not asked again; a 502 asked once over two listings, then again after the retry time). go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath and three Claude sign-in tests timed out once under load, pass on their own and on a rerun of the package). Not tried in the GUI with a real WorkBuddy account 2026-09-29 18:56:32 +08:00
yetone 559ce02147 models: a model's own name given by the user keeps its provider after it too (#203 uclort: 如果在供应商模型页自定义模型名称,在 Codex 中就不会显示供应商了) — Entry.Label() showed "name · provider" only for a model's own (vendor's or models.dev's) name; a name the user gave it (provider's model page → Names & levels) dropped the provider entirely, so a renamed model in Codex's picker (and Pi's, ZCode's, OMP's, magpie's own gateway list — everything magpieModels/Label feeds) could no longer be told apart by vendor at a glance. Label() now appends " · provider" (or "routing group") to a custom name too, the same as it does for the vendor's own, and only leaves it off when the name already contains the provider's (case-insensitively), so a name like "Sol on A" isn't doubled to "Sol on A · A". No settings toggle added: the fix makes the provider suffix unconditional (what the issue actually asked for), so a preference to hide/show it would only reintroduce the gap; there's also no existing "model list" preferences surface to hang it on cheaply — Names & levels only holds the name and reasoning-level pickers. Tested with TestModelName (internal/provider/modelprefs_test.go, sandbox HOME): a custom name now reads "My Sol · A" in Label() and in CodexListed(), and "Sol on A" (already containing the provider) is left alone; and TestModelNameAndLevelsReachAgentFiles (internal/agent/modelprefs_test.go, sandbox HOME) checks the generated Codex model_catalog_json and Pi models.json entries carry "GLM · Relay" for a custom name and don't double it to "GLM via Relay · Relay" when the name already says Relay. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui -count=1 ./... pass except TestUserPath and TestClaudeAccountNamedAsItsLogin, which are load-flaky (pass alone, unrelated to this change) same as the pre-existing rtk.go gofmt / TestConfiguredModelsAdvertiseImageInput flakiness 2026-09-29 18:56:32 +08:00