Commit Graph
44 Commits
Author SHA1 Message Date
yetone 987d72c133 providers: Max concurrent requests — over it, an agent's requests wait in line and go out in the order they came (Lemon on Discord: 希望 provider 或者插件能够支持一个参数类似 max_concurrency,如果超过上限,放到无界队列里面依序请求; Memo: ChatGPT/Codex subscriptions' risk control trips at about 5-6 requests at once) — the gateway sent every request to its provider the moment it came, so a few agents (or one agent's subagents) on one subscription could have any number out at the vendor at once, and nothing in magpie could hold them to what an account takes. A provider now has maxConcurrency (providers.json; nil or 0 is no limit, a negative one is saved as 0), and a signed-in agent's accounts keep theirs with their picks. The limit is per key or account, as the gateway already tells them apart (candidate.who(): provider#keyid, provider@user, else the provider), so a subscription pooling several accounts lets each one have that many out. Before a try is forwarded it takes a slot in that lane (internal/gateway/concurrency.go); with none free it waits in a FIFO queue with no bound, and is let in, first come first served, as one before it ends. The slot is held for the whole try (attempt reads the vendor's reply to its end or until the agent hangs up, and its body is closed when it returns) and given back once whatever happens (sync.Once), and a lane with nothing out and no one waiting is dropped. An agent that hangs up while waiting leaves the queue at once, is never sent and takes no slot (a slot granted in the same instant is handed on), and goes the way of any canceled request (499, nothing recorded). Waiting is not a failure: a request in line stays for its provider and is not moved on to a fallback or another group member, the account isn't rested, and the time it waited is told in its route's try (Try.Queued, ms). A plugin can say how many each of its accounts takes, as its own field: the auth hook's maxConcurrency, else package.json's magpie.maxConcurrency (a whole number over 0, at most 1000); the user's setting on the provider goes over it. GET /v1/magpie/concurrency (local or shared callers, as quotas) gives each lane's out, waiting and limit. The provider editor (keys, custom, signed-in accounts) has "Max concurrent requests" / 最大并发请求数 under Proxy, "超出时排队,按顺序发出;0 或留空为不限制" below it (with a plugin's own number as its placeholder), refuses anything but a whole number from 0 to 1000 before anything is posted, and an empty field saves none (the plugin's, if it has one); a save that doesn't send the field keeps what the provider had. No coloured left border, and a click in the field leaves the page where it is. TestMaxConcurrency (limit 2, five streams: never more than 2 at the vendor, 3 waiting, let in a..e in order; one canceled while waiting leaves the queue, takes no slot and never reaches the vendor; slots given back when each stream ends and when an agent hangs up mid-stream; the next one served at once) fails without the change (all five reach the vendor at once, no lane), as do TestLanesLimitLifted, TestProviderConcurrency and the updated TestPluginOwnIconAndKeyHint (the auth hook's 3, package.json's 5 when the hook's is 2.5); provider-concurrency.test.cjs (chromium and webkit, en and zh: typed and saved, 2.5 refused with nothing posted, empty saved as null, an account opening on its 5 and saving 0, a plugin's 4 as the placeholder, every string translated, no stripe, no scroll on click) passes. No vendor was asked. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... (under a temp HOME) and go test -race on the new tests pass. provider-proxy.test.cjs's account test fails the same on HEAD without the change (its editor redrawn under it). 2026-10-02 13:38:23 +08:00
yetone 65f3a6417a settings: portable mode — a data folder beside magpie keeps everything magpie keeps (#508, suixinex) — like VS Code's portable mode, a folder named "data" (or a file named ".portable", the data folder then made beside it) next to magpie makes it keep all its own files there instead of the user's profile. Where magpie keeps its files is now decided in one place, internal/appdir: Config() (~/.config/magpie, $XDG_CONFIG_HOME/magpie; portable: data/), Cache() (~/.cache/magpie, $XDG_CACHE_HOME/magpie; portable: data/cache) and SystemCache() (the OS's cache folder, which the library's market lists and an update staged where magpie may not write used; portable: data/cache). settings.Dir()/Path(), provider.Path(), profile.Path(), the models.dev catalog, the exchange rate and the plugin market cache go through it, and everything built on them follows: providers, logins and saved accounts' folders, usage ledger, routing history and request archive, plugins, library, backups, sync state and lock, caller keys, redact key, stats. "Beside magpie" is the folder holding the executable with links followed; for a Mac app the folder holding Magpie.app (inside the bundle would break its signature), and for an AppImage the folder holding the .AppImage, not where it is mounted. The decision is made once, at start: an update moves the running copy aside (into .magpie-update on a Mac, to magpie.exe.old on Windows) and its files must not move with it; the updated copy replaces the executable or bundle only, so the data folder beside it is left alone. A portable magpie doesn't copy an old dial install's folders in. Not moved, as they aren't magpie's own: the agents' configs (~/.claude, ~/.codex…), the sign-ins agents keep in the system keychain (a saved Claude account's Claude Code keychain item is named after its folder's path on a Mac, so a portable folder moved elsewhere or to another Mac signs that account in again), autostart and link-handler registrations, and temporary folders. The single-instance id already covers the config folder, so a portable and an installed magpie run side by side, but the gateway port is one. Settings shows the config folder as portable. TestResolve (data folder, .portable, a file named data, a Mac bundle), TestResolveSymlinkedExe, TestResolveAppImage (Linux), TestFolders and TestPortableSettings (Path() lands in the data folder, Save writes there, ~/.config/magpie untouched); TestPortableSettings fails without the change (Path() is ~/.config/magpie/settings.json), TestResolveSymlinkedExe without following links. A nogui build run from a folder with data/ under a sandbox HOME wrote profiles.json, cli-identity.json and cache/models.json into data/ and nothing of magpie's into HOME. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./... pass. 2026-10-02 13:00:22 +08:00
yetone cfd67225c8 plugins: the Plugins page is drawn in parts, Installed at once and npm's figures after (#488: the page showed skeletons for seconds, even on Installed) — the page waited on /api/plugins/market, which answered only once the plugin host, the registry fetch and an npm request for every package (two each, 8s apiece, all at once, kept an hour in memory only) had all come back, so the slowest package decided when anything was drawn. The page now asks for each part on its own, as the Library page does: Installed is drawn from GET /api/plugins as soon as it answers; Discover's cards from the new GET /api/plugins/listings (the registry, with its cached and built-in copies), each with what npm said last; and versions, weekly downloads and an installed plugin's "v… out" fill in from the new GET /api/plugins/npm?names=…, never waited on (a card's button waits, disabled, until npm has said whether it has the package). npm is asked at most six packages at a time and for no more than 8s in all; a package it answers late is what it said last, and its answer is kept for the next time. What npm said is kept on disk (~/.cache/magpie/plugin-npm.json) beside the registry's copy, so a magpie just started shows it before asking, and GET /api/plugins gives each plugin's newest version from it. /api/plugins/market still answers the whole market as before. Once loaded the page shows what it did. TestNPMInfoKeptOnDisk, TestNPMInfoDeadline, TestNPMInfoAtOnce, TestPluginMarketInParts and plugin-progressive.test.cjs (Chromium, WebKit; en, zh; Installed while the market is held pending) fail without the change; plugin-local, plugin-updates, plugin-market, plugin-move, plugin-move-overflow, plugin-release, plugin-git, plugin-key-hint, plugin-plan, list-sort, click-scroll, old-webkit pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-10-02 08:20:32 +08:00
yetone 456208945b plugins: a plugin can be installed from a git repository — github:owner/repo, owner/repo, a GitHub URL or a git+https:// one (Lemon on Discord: can plugins be installed directly from git?) — magpie plugin add and the Plugins field took only an npm name or a path, and turned a git spec away as "isn't an npm package name". A git spec now goes to bun add as given and is kept as given in plugins.json; the package it installs is named by its repository's package.json, which bun records as the dependency whose spec it is, so the plugin is known (list, on/off, rm, its version, its README) by that name or by the spec. update and the row's Update run bun update on it, which fetches the repository's commit now where bun add keeps the one bun.lock has, the spec unchanged; npm is never asked for a newer version of it, nor auto-updates it. Adding the same package from another URL, or from npm, takes its entry's place. The Plugins field says it takes a GitHub repo (en+zh); an installed git plugin shows its package's name, an Update button, and a page with its README, no npm link and a Source link to the repository. Nothing that installed before is turned away. TestGitPlugin (a fake bun fetching from local bare repositories, the real one loading the plugin when on PATH) and plugin-git.test.cjs fail without the change; TestIsGit checks what counts as a git spec. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-git, plugin-local, plugin-market, plugin-updates, click-scroll, old-webkit pass. 2026-10-02 02:24:49 +08:00
yetone eeb906e1b7 plugins: the built-in list's ZCode summary says what the registry does (ZCode's free Start Plan isn't offered by the plugin) — the fallback market.json, used until the registry is fetched, still promised the Start Plan; it now reads as registry.json does. go test ./internal/plugin passes. 2026-10-01 23:20:57 +08:00
yetone 0361189f9e test: every package's tests pass on Windows, and none writes outside its sandbox (the user: 把 Windows 上那几个失败的测试修掉) — on a Windows box the suite failed in most packages, and the root package hung for 25 minutes. Tests set HOME, which os.UserHomeDir ignores on Windows (it reads USERPROFILE), so they read and wrote the runner's own home; each now sets USERPROFILE beside HOME, and Devin's, Goose's, Crush's and Claude Desktop's tests set APPDATA/LOCALAPPDATA, where those keep their files there. said() read the command's output only once it returned, and a pipe holds a few KB on Windows, so magpie model --help blocked forever; it reads while the command writes. Unix permission bits (the gateway key store's 0600 too), exec bits and symlinks Windows lacks are checked only off Windows; TOML strings hold Windows paths as literal strings, JSON as marshalled ones; a URL's path is compared as one. The provider and usage packages run in a home of their own (TestMain, as the gateway's already does), since a CLI's identity kept by a refresh still going when its test ended landed in the runner's ~/.config/magpie — and on a Mac in the user's own. Two product fixes came out of it: imagemcp took a file:///C:/… root or image for the path /C:/…, which doesn't exist on Windows, and now makes a Windows path of it (a host for a UNC share); and session discovery could keep a new Cowork session hidden on Linux, where a folder's mtime is a kernel tick and its size doesn't change, so an entry added within the tick of the last listing left both as they were — a listing made within 2s of its folder's last change is read again, as git does for racy files. TestFilePath and TestRacyDirectoryListedAgain fail without the change. On Windows 11 every package passes (32 of 32) and a run of each package in a fresh home leaves nothing in home, APPDATA or LOCALAPPDATA; on Linux (WSL) and macOS go test -tags nogui ./... passes. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass. 2026-10-01 23:15:07 +08:00
yetone ad1c8fd1ec test(provider): TestFetchNewLeavesListedPlugins waits for the refresh its sign-in started, and TestFetchNewRetriedSoon puts back what it set — signing in with a key (PluginAPIKey) shows the account, which lists the providers while the host's auth event has them stale, so Cached refreshes them in the background; that refresh could end after the test had set the list's time back, rewriting plugin-providers.json, which read as FetchNew asking the plugins again (failed 3 in 5 run after TestFetchNewRetriedSoon, and now and then after TestFetchNewAccount). plugin.Refreshed waits for those refreshes with the host left running, and the test waits before it sets the time. TestFetchNewRetriedSoon now resets newFetches, newSoonAt, wbTokens and the account caches, after waiting for a FetchNewSoon under way. The pair passes 10 in 10 (and with TestFetchNewAccount 10 in 10); ./internal/provider 3 in 3; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-10-01 21:49:16 +08:00
yetone a999770531 plugins: a plugin's subscription does what a built-in's does, in seven places it didn't (the user: 要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — an installed plugin's provider id is kept for it signed in or not, as a built-in's is: a provider of the user's own isn't saved on it, renamed onto it or imported as it (freeID gives id-2). A plugin's account whose plan lacks the model is tried after the ones that list it, as a Codex Free account behind a Plus, by the list the plugin told for it. Cursor's plugin has fast mode where the built-in Cursor has it: a model with a -fast one is saved :fast in a group and sent service_tier "priority", one without is refused. Removing a plugin's provider hides it as a built-in's is, its accounts kept and shown again from Hidden or by signing in, rather than signing every account out ("stays signed in" said so already). magpie accounts lists, adds, switches and forgets a plugin's accounts by its provider's id or name. Grok's plugin beside the built-in Grok (grok-plugin) searches by itself as Grok does. A plugin's plan is named as its built-in's ("Cursor Pro", "SuperGrok"), and a plugin's resets are shown but have no Use button or Auto-use, which spent Codex's reset of the same user. TestPluginIDReservedSignedOut, TestPluginAccountLackingTheModelGoesLast, TestCursorPluginGroupFast, TestAccountsOfAPlugin, TestGrokPluginSearchesItself, plugin-plan and codex-auto-reset fail without the change; TestPluginSignIn and TestPluginAccounts now expect a removed plugin hidden. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut failed once under load, passes alone); the gateway, provider and plugin suites and TestAccountsOfAPlugin pass on Windows, the plugin tests on Linux. 2026-10-01 20:26:55 +08:00
yetone 2a6ba1a20f workbuddy: an added account is told from the others by its uid, and the one WorkBuddy is signed in to stays once WorkBuddy moves on (#413: 添加已提前登录的 WorkBuddy 账号会顶掉默认第一个账号) — magpie matched a new sign-in to a listed account by its name, the nickname: another account of the same nickname replaced the one listed, or was taken for WorkBuddy's own and let go with "signed in". And WorkBuddy's own account is only read from WorkBuddy's file, so signing WorkBuddy in to the next account (to pick it on WorkBuddy's page from magpie) renamed that row, the last account gone, and the one picked, being WorkBuddy's own, was let go: one account at a time. A WorkBuddy or WorkBuddy AI sign-in is now matched by its uid (one of the same name whose uid isn't known is still taken to be it, #155); another account of the same name is added as "name (end of uid)"; the same account renews its sign-in where it is listed, and the one WorkBuddy is signed in to becomes magpie's own sign-in of it, first as it was, so it stays when WorkBuddy signs in to another. The sign-in says so: "该账号已在列表中,已更新登录" in the app, "is already listed — its sign-in was renewed" from magpie accounts add. On the plugin, the host no longer takes two sign-ins of one account id for one account when their uids differ. No request header or user agent changes. TestWorkBuddyAddSameNicknameAsOwn, TestWorkBuddyAddSameNicknameTwice, TestWorkBuddyAddWhatWorkBuddyIsSignedInTo, TestPluginSameNameOtherUID and signin-again.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; signin-again, click-scroll, old-webkit pass. 2026-10-01 19:38:36 +08:00
yetone c3197ac217 plugins: magpie keeps the Bun plugins run on up to date by itself (the user: 我们能否自动去管理一个bun的版本?去自动的下载并维护这个bun的版本到最新版) — plugins ran on the one Bun magpie pinned, or on whatever $MAGPIE_BUN pointed at. Every six hours (first two minutes after start), while there are plugins, magpie now asks GitHub for Bun's newest release and takes it once it has been out two days: downloaded, its SHA-256 checked against Bun's SHASUMS, tried (it says its version, and runs a script with AsyncLocalStorage, fetch and fs), then switched to, the host restarted gracefully so a streaming reply finishes on the old one. BunVersion is the floor; the Bun before stays on disk, and a new one whose try fails, or that the host won't start on, is set aside for the one before and never taken again (bun/state.json). $MAGPIE_BUN is left alone. Plugins' page and magpie plugins show the version in use. TestCheckBunTakesASettledRelease, TestCheckBunWaitsAndKeepsTheFloor, TestCheckBunSetsAsideOneThatFailsItsTry, TestTryBunOnRealBun and TestHostFallsBackWhenTheNewBunDies pass on macOS, Linux and Windows; a real run from 1.3.14 to GitHub's 1.4.2 started the host on all three. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFetchNewLeavesListedPlugins failed once under load, passes alone). 2026-10-01 19:33:04 +08:00
yetone 4f9dcb39e5 plugins: a plugin subscription goes through every path a built-in one does (the user: 要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — a SOCKS5 proxy on a plugin provider or account now carries its requests: Bun speaks http(s) proxies only, so magpie stands a loopback http proxy in front of the SOCKS one, and a proxy that refuses says so as Go's transport does. A plugin's model list and its loader are asked through the account's own proxy, as a built-in's list is fetched through it. The Grok plugin draws and makes videos at Grok's Imagine API through the plugin, and keeps Codex's namespaced tools, as the built-in does; an official plugin beside its built-in (kiro-plugin, zcode-plugin) counts tokens by estimate as the built-in and the moved plugin do. FetchNew no longer re-lists plugin providers every ten minutes, their list being told by the plugin host; a plugin account's usage is cached under one name whichever way it is asked for; and a fetch hook that throws on its sign-in (a refused refresh) marks the account lapsed, as a built-in's refused refresh does. TestPluginSOCKSProxy, TestPluginModelListThroughAccountProxy, TestGrokPluginDraws, TestPluginBesideBuiltinCountsByEstimate, TestFetchNewLeavesListedPlugins, TestPluginUsageReadOnceWhicheverName and TestPluginFetchSignInExpiredLapses fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (the known load flakes pass alone); internal/plugin, netproxy and the plugin/proxy tests of provider and gateway pass on Linux and Windows. 2026-10-01 19:07:12 +08:00
volcanic fc248e381f plugins: the folder a plugin lives in is picked, not typed, and its page reads that folder's README (#411)
Settings → Plugins adds a plugin by npm package name or by a path, and the
path had to be typed in full: the market's field was the only way in, with
nothing to pick the folder from. And a plugin added from a folder had no page
of its own — its README comes from plugin.Readme, which answers only for an
npm package name ("%q isn't an npm package name"), so opening one said "npm
didn't answer" and offered an npm link for a folder that npm has never heard
of.

The field now has the system's folder picker beside it, and a folder plugin's
page shows the README that folder carries.

- POST /api/plugins/choose asks Windows.ChooseFolder, which the app answers
  with the system's panel — the Library's "Choose a project" already used it,
  and a page served by another magpie forwards it to the magpie it is served
  by. pluginsState carries "picker" beside "bun", false for `magpie web`: a
  browser has no such panel (webHost.ChooseFolder refuses it), and the page is
  then left with the field to type into, as before.
- plugin.Readme takes a path: folderReadme reads README.md whatever its case,
  else a markdown or a text one named beside it, and says so when the folder
  has none rather than answering with npm's silence. Updated is that file's
  mtime, and the text is capped at 200 KB as npm's answer is.
- A folder plugin's page drops what only npm can say: no npm link, no version,
  downloads or license, and a failure names the folder rather than npm. Its
  row and its title name the folder it was added from (opencode-mine, not
  /Users/me/plugins/opencode-mine).

TestReadmeFromFolder (internal/plugin) fails without the change: with a
README.txt and an unsuffixed README beside it, the README.md is still the one
read; ReadMe.MD is too, and through file:// and a trailing .; a folder with
none is an error, as is one that isn't there.
TestPluginsPickerWhereItCan (internal/gui) fails without the change: the
picker is offered for the app, not for `magpie web` nor for no host at all.
plugin-local.test.cjs (new; Chromium and WebKit, en and zh) fails without the
change: the picker is beside the field and titled as such, a click asks for it
and what it answers is what the field holds and takes the focus, what the
field holds is what Install posts, the Installed row names the folder rather
than its path, its page renders that folder's README with no npm link, and a
market that offers no picker draws no button (english and Chinese).

Not only the faked page: a sandbox `magpie web` in a HOME of its own, with a
plugin added by path, read that folder's README.md — its mtime as Updated —
and listed it by the folder's name.

go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows
go build, go test -tags nogui ./internal/gui/ pass; plugin-market,
plugin-signin and plugin-local pass in Chromium and WebKit, en and zh.
TestPluginProxy (internal/plugin) and the hermes tests (internal/agent,
internal/profile) fail on main on this machine too — its own proxy and a
hermes install it finds — and are unrelated.

Not done: the panel itself hasn't been clicked in the app. It is the system's
own, and the call under it is the one the Library's folder chooser already
uses, but the screenshots here are the page with a faked backend underneath
(the app's own picker is offered, `magpie web` has none), plus the real
`magpie web` for the README.
2026-10-01 18:46:56 +08:00
yetone 8cd520662f plugin: a plugin can say the list it hands back is a fallback, and magpie keeps the one told last, as a built-in does — Command Code's Go accounts and ZCode give a table of their own when their vendor's list can't be had, not the defaults they were given, so magpie couldn't tell it from a list read and a hiccup swapped the models for the table. A models hook's list marked Symbol.for("magpie.fellBack") (OpenCode never sees a symbol's key) now counts as fallen back; commandcode 0.1.7 and zcode 0.1.6 mark theirs. TestFallenBackListKeepsTheLastOne/throws=own fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/plugin ./internal/provider pass; internal/plugin passes on Windows and Linux. 2026-10-01 17:06:17 +08:00
yetone ca29a7badd plugin: a plugin whose models hook throws keeps the list it told last, as a built-in does — 9abf441d kept the last list for a hook that gave its defaults back, but Cursor's, Grok's and Devin's hooks throw when their vendor fails, and an account past the first whose hook threw was taken to have all the provider's models; a vendor's hiccup still swapped the list for the plugin's defaults. A hook that throws now marks its listing fallen back, the provider's and each account's, and magpie keeps the list told last in its place. TestFallenBackListKeepsTheLastOne/throws=1 fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; internal/plugin passes on Windows, internal/plugin and internal/provider on Linux. 2026-10-01 16:56:41 +08:00
yetone b7037c101e plugin: plugins keep up to date as magpie does (the user: 插件的升级是怎么做的?我们有插件的升级提醒吗?) — a plugin updated only when the reader opened Plugins and clicked Update, save the built-ins' moved plugins held at their least version, so most users stayed on the version they first installed, and updating cut any reply streaming through a plugin. Now the magpie serving the gateway asks npm 30s after it starts and every 6 hours: the community's plugins (@magpie-community/*) update by themselves, and their row says Auto-updated (from which version, when) for three days; someone else's, or one pinned to a version, waits for the reader with a dot on Plugins naming it, gone once it's updated. A host being replaced finishes the calls it is serving (up to 10 minutes) while new ones go to the new host. TestCheckUpdates, TestPinned, TestRestartFinishesTheReply (fails without the change: the plugin host quit: signal: killed) and plugin-updates.test.cjs (Chromium, WebKit, en/zh). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; internal/plugin passes on Windows, internal/plugin and internal/provider on Linux; plugin-market, plugin-signin, plugin-move, plugin-release, click-scroll, old-webkit pass. 2026-10-01 16:27:43 +08:00
yetone 9abf441dfa plugin: a plugin whose model list can't be fetched keeps the one it told last, as a built-in does (ARNO on Discord: the Command Code plugin lists no Pixel Canary, the built-in does) — a built-in whose fetch fails keeps catalog.Live, the list it fetched last, while Command Code's, Kiro's, Qoder's, WorkBuddy's and Zed's plugins catch the failure and give their short defaults back, so a vendor's hiccup shrank the models. The host now sees a models hook that asked its vendor, had no answer from its last ask and handed back the list it was given, and marks that listing; magpie keeps the provider's and each account's last told list in its place, in memory and from plugin-providers.json after a restart, and the vendor back, its list is told again. TestFallenBackListKeepsTheLastOne fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; internal/plugin and internal/provider pass on Windows and Linux. 2026-10-01 15:01:11 +08:00
yetone 7dd809002b accounts: on Linux an account put first is read first at once, and on Windows a file being replaced isn't read as none (goal: 插件订阅 provider 跟内置的所有逻辑都要一模一样, run on Windows and Linux) — the provider tests run on the Windows test box and in WSL found two bugs of magpie's own files. Linux stamps a file's time from a clock a tick (a few milliseconds) coarse, and filememo took same time and size for the same file: two accounts swapped (Devin, Grok, WorkBuddy, ZCode, Claude…) read back in the old order, so magpie kept using the account the user had just moved down, and a moved-back plugin's accounts read as before. A file written within the last two seconds is now also compared by its bytes. On Windows a file being renamed over refuses to open for a moment ("Access is denied"), and one held open refuses to be renamed over: plugin-auth.json, plugins.json and the plugin providers read as nothing (a usage fetch right after a move showed no cards), logins.json failed to save, and host.js, reading plugin-auth.json as {}, would have written the other accounts away on the next sign-in. New internal/steady waits such a read or rename out (half a second at most, Windows only); filememo, edit.WriteAtomic, writePrivate, Kiro's writes and the plugin store go through it, host.js retries its read and rename, and the plugin store writes its files by a rename. Tests: USERPROFILE beside HOME in the test homes, the fake devin (a shell script) skipped on Windows, plugin-auth.json's mode bits unchecked there. TestReadSameSizeSameTick fails on Linux and TestRenameOverAFileHeldOpen/TestReadAFileHeldOpen on Windows without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; on Linux (WSL) the provider and plugin packages pass whole (13 failed before), on Windows the plugin package and the move/plugin/Grok/Devin/usage tests pass 10 times over (TestMoveToPlugin failed one run in a few before); 15 built-in tests that assume Unix (mode bits, Keychain, shell-script CLIs) still fail there, as before. 2026-10-01 13:27:12 +08:00
yetone 181eefdc31 plugins: built-in Devin shows its usage as the Devin plugin does, and a Grok account whose sign-in has expired no longer stops Grok's move (goal: 插件订阅 provider 跟内置的所有逻辑都要一模一样) — the plugin showed a Devin account's day and week quotas, its ACUs and extra balance from GetUserStatus while the built-in showed no usage at all, so routing had no allowance to fail over on until the move; the built-in now reads the same and fills the same windows (1 day, 7 days, ACUs aside), plan end and balance, and a refused key says to sign in again. A move checks each account through the plugin and stopped when one failed; Grok's plugin says an expired sign-in only in words ("Grok's sign-in has expired"), so one dead account kept every Grok account on the built-in. Such an account now moves along untried and unmarked, as a built-in Grok account was never marked; any other failure still stops the move. TestDevinUsage and TestMoveExpiredInWords fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-10-01 12:24:45 +08:00
yetone 4ba51bb627 plugin: a plugin names its own icon and the key field's title and hint (Lemon on Discord) — a plugin's provider took only the market's icon, and an api method's key field always read "<name> API key" with no hint. auth.icon (or package.json "magpie": {"icon"}) as an https URL, fetched once through the public-only icon path, or a data:image URI now comes before the market icon; an api method's label titles the key field as in OpenCode, and a new placeholder hints it, in the window and in magpie plugin login. README has a For plugin authors section. TestPluginOwnIcon, TestPluginOwnIconAndKeyHint and plugin-key-hint.test.cjs (Chromium, WebKit, en/zh) fail without the change. 2026-10-01 11:42:45 +08:00
yetone b274ddd28e plugin: a move's check still goes through the account's proxy, now that it reads usage too — TestPluginProxy took the check's last request for its models; the check asks for the models and then the usage, so the test now looks for both among what the account's own proxy saw. renderExcluded's comment goes back over renderExcluded rather than over renderMovable. go vet, go test -tags nogui ./... pass. 2026-10-01 11:15:27 +08:00
yetone f5a2b5d406 gui: a moved provider's editor reads as the built-in's — its model list dated, its agent's own sign-in said, its move a button (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — after a move the list said "magpie's list · Refresh asks the vendor" where the built-in said "vendor list · 9m ago"; it is now dated by when the plugin last listed it, in the editor and in magpie providers. "Command Code itself stays signed in as it is." is kept for Devin, ZCode, WorkBuddy, Kiro and Command Code, whose agents keep their own sign-in through the move; and Move to the plugin / Use the built-in again is drawn as a button, not a line of text. plugin-move.test.cjs fails without the kept line. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-move, click-scroll, old-webkit, plugin-market, plugin-signin, nokey-editor pass. 2026-10-01 11:15:27 +08:00
yetone 77c2ad12fa migrate: a move that fails leaves nothing behind and says why, and a move only goes through on an account the plugin proved works (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样 / 记得把整套迁移逻辑做得丝滑优雅精致优美简单方便易用) — a failed move left the plugin it had installed (a Plugins row saying signed out) and hidden plugin:<id> rows in logins.json, and a move checked only that the plugin loaded, so an account the vendor refused or couldn't be reached through moved as if it worked. The plugin a move installed now goes with a failed move, and with a move back when it serves nothing else; the stray rows go; the check reads each account's usage as well as its models, and an account the vendor refuses is marked and moves untried, as a lapsed one does, while one whose vendor can't be reached at all fails the move with nothing changed. A move with every account lapsed says so before installing anything. Failures carry a code (offline, install, lapsed, unserved, account) for the page to say in its language, with the models named, not their ids. A moved provider shows the built-in's API host (magpie providers showed the plugin's id), and Zed's accounts take their model list with them, so the plugin can serve it until it reads its own. TestMoveNeedsAWorkingAccount, TestMoveTidiesItsPlugin, TestMoveProvesEachAccount, TestMovedShowsBuiltinHost and TestZedMoveCarriesItsModels fail without the change. go vet, go build -tags nogui, go test -tags nogui ./... pass. 2026-10-01 11:15:23 +08:00
yetone 813fdcb2e5 migrate: a moved built-in keeps its models' reasoning levels, its own account's plan and one own row, and the move checks each account through its proxy (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — Command Code's, Grok's and WorkBuddy's models their vendor gives no levels for lost the ones the built-in borrowed from their maker; moved, they borrow them again. Devin's own account moved without the plan the Devin CLI says, which the built-in showed. The agent signing in to another account while moved wrote a new own row for the built-in, which came back beside the one set aside as a second own account; nothing is written while moved, and going back keeps the one. And the move's check of an account asked the vendor directly, not through the proxy its requests take. Found walking Command Code, Grok, Devin, ZCode and WorkBuddy through sandbox moves. TestMovedKeepsEfforts, TestMovedOwnRowNotWritten, TestDevinOwnMovesWithPlan and TestPluginProxy fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-10-01 07:04:40 +08:00
yetone 30236bf035 migrate: moving Kiro back with an API key no longer hangs magpie, a move's lock left by a killed magpie frees itself, and plugins changed in a terminal reach the running app (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — Kiro's back put the plugin's API key onto the Kiro provider while holding logins.json's lock; saving the provider syncs the agents, which read the accounts and waited on that same lock, so the move back never returned and every request after it hung. The key is now put back once the lock is let go (a mover's settle). A move killed while hung left migrations.json.lock, refusing every move for 15 minutes as "another magpie is moving"; the lock now holds its magpie's pid and one whose magpie is gone is taken. And magpie plugin add, remove or move in a terminal left a running app with the plugins it had loaded (a moved Kiro gone until restart, "magpie knows no model kiro/auto"): a plugins.json changed by another magpie now restarts the host and takes the providers that magpie was told meanwhile. Found walking Qoder, Kiro and Cursor through sandbox moves. TestKiroMover (hangs), TestMoveLockOfDeadMagpie and TestPluginsChangedElsewhere fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-10-01 06:53:29 +08:00
yetone e86a34fce1 migrate: moving back keeps an account's expired sign-in marked, and one refused while the move lists its models goes along marked (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — Zed's, Factory's and MiMo's back write the plugin's sign-in as one just made, so an account the vendor had refused through the plugin came back to the built-in showing signed in until its next refusal; the plugin's mark now goes with it. And a models hook saying a sign-in expired failed the move's strict check, so one refused Zed account stopped the whole move where the built-in would mark it and carry on; it is marked and moves untried, as a lapsed one does. Found walking Zed, Factory and MiMo through a sandbox move and back. TestMoveBackKeepsLapse and TestMoveRefusedWhileListing fail without the change. go vet, go build -tags nogui (GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider ./internal/plugin ./internal/gateway pass. 2026-10-01 06:33:27 +08:00
yetone 78c32292e4 plugins: a models hook can say its account's sign-in expired, as a built-in whose model list the vendor refused marked the account (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — a refusal in provider.models was only logged, so an account Zed or Qoder turned away while listing its models stayed unmarked where the built-in marked it. An error thrown with signIn: expired, kept or renewed now tells magpie, which reads it as it does an answer's X-Magpie-Sign-In. TestPluginModelsSayExpired fails without the change. go vet, go build -tags nogui (GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider ./internal/plugin ./internal/gateway pass. 2026-10-01 06:12:11 +08:00
yetone 1fa21850a3 plugins: a plugin can say its sign-in renewed, and what a usage read means for it (要保证插件跟内置的所有逻辑都要一模一样) — a built-in whose sign-in renewed took the lapse mark off whatever the request then met, and a plugin could only clear it with a success: X-Magpie-Sign-In: renewed now clears it on any status. A usage read marked a plugin account whenever its error said to sign in again and cleared it on any clean read, where Qoder's, Kiro's and others' built-ins marked none from usage: auth.usage may now say signIn: expired, kept or renewed, taken as the header is; one that doesn't say keeps the old reading. TestPluginSaysSignIn and TestPluginUsageLapse fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gateway ./internal/provider ./internal/plugin pass. 2026-10-01 04:46:24 +08:00
yetone 47786c456e plugins: a moved built-in's own account stays the agent's, and Qoder's first goes (插件版…保持一模一样) — the plugin's accounts were listed with no own one, so removing the agent CLI's own account (Devin, Grok, Command Code, Cursor, Kiro, ZCode, WorkBuddy) signed it out of the plugin and its name stayed as signed in at the move; it is now told by the mark each mover leaves on it, named as the CLI is signed in now, only hidden when removed (the CLI stays signed in) and shown again once the CLI or magpie signs in anew, as the built-in's was. Qoder's first account showed Remove and was refused; it now goes, the next put first, as the built-in let it. plugin.UseCached lets a test stand a plugin's provider up without Bun. TestMovedOwnAccount and TestMovedQoderFirstGoes fail without the change. 2026-10-01 04:04:22 +08:00
yetone d4817bea56 plugin: a moved provider goes through its own proxy, and each account through its own (#237) — a plugin's fetches and usage went through magpie's global proxy whatever the provider or the account was set to. Go now hands the host the proxy ctx names, and host.js gives every fetch made for it Bun's proxy option; "direct" goes through none. Bun reads *_PROXY once and puts every fetch through them, so the host is started with them as MAGPIE_*_PROXY and applies magpie's itself to a fetch with no proxy of its own. A plugin sign-in's usage looks its proxy up under the provider's id, not "plugin:<id>". TestPluginProxy and TestMovedLoginProxy fail without the change. 2026-10-01 03:42:06 +08:00
yetone 8756ece70c plugins: a plugin's model is priced only when the plugin prices it, and says whether it takes images when the plugin says — OpenCode prices a model it has no price for at 0, as the plugins give their plans' models, and a 0 price made each plugin model the cheapest where magpie picks by price (a small model to search or describe images with, one to draw with) and showed it free. A price of 0 is none now. A model the plugin (or models.dev) says takes images, or doesn't, carries that answer (ImageInput), as a built-in's list did; one it said nothing of stays unknown. TestPluginCatalogPriceAndImages and TestFakePlugin fail without the change. 2026-10-01 03:41:46 +08:00
yetone c1d86e2ed1 gateway: a moved provider counts a prompt's tokens by estimate, as its built-in did — count_tokens estimated for Cursor, Grok, Devin, Kiro, Qoder, Zed, Factory and a Command Code Go key by their agent, which a moved one's is "plugin", so the count went to the vendor through the plugin (Factory's to its API). A plugin's provider moved from one of them estimates; Command Code's whatever its plan, as only the plugin knows a Go key. The fake plugin takes $FAKE_ID to stand as a built-in's. TestMovedCountTokensEstimated fails without the change. 2026-10-01 03:41:45 +08:00
yetone d8f2e17dd7 plugins: a plugin's fetch gets its body as a string, as OpenCode hands it — the host passed a Buffer, and the plugins that reshape a request (Grok, WorkBuddy, MiMo) touch only a string body, so a moved provider's requests went out as the gateway built them. A UTF-8 body now goes as a string; one that isn't stays bytes. TestFakePlugin fails without the change. 2026-10-01 03:41:42 +08:00
yetone e332451169 plugins: the market lists magpie's community's plugins alone (来自 OpenCode 社区的插件可以删掉了,因为质量不行) — the section of the OpenCode community's plugins is gone, from the list built in, the community repo's registry and any copy fetched before; one is still a search away on npm, and one installed keeps its vendor's icon. TestBuiltinMarket, TestMarketSources, TestMarketIcon and plugin-market.test.cjs (Chromium, WebKit, en/zh) fail without the change. 2026-10-01 03:27:48 +08:00
yetone 566ae09db1 plugins: a moved provider's accounts keep what the built-in's had (你确保插件版的订阅 provider 和内置的一模一样了吗) — each account of a plugin provider has its own plan's models, not the first account's; a move carries each account's plan and a lapsed mark; a usage read that finds the sign-in gone marks the account and a clean one clears it, a network failure shows the last reading; a plugin that fails to load keeps its providers rather than taking them away; Kiro's API key is named and an OAuth account beside it stays off as in the built-in; Command Code Plan keeps its name. TestUnloadedPluginKeepsProviders, TestPluginUsageLapse, TestPluginAccountModels, TestMovedProviderName, TestKiroMover and the move test fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider ./internal/plugin ./internal/gateway pass. 2026-10-01 03:27:47 +08:00
yetone 1ce05f0270 plugins: a plugin's models show as the built-in's did (插件版…保持一模一样) — a model a plugin says its plan serves at no cost (WorkBuddy's x0.00 credits) is marked free, as the built-in marked it; Cursor's plugin models not named 1M hold what the catalog knows their base to (GPT-4o 128K, Kimi K2 131K), not Cursor's 200K, as the built-in's list; ZCode's own account moves marked as ZCode's, for the plugin (0.1.2) to read ZCode's own sign-in for each request and follow its switches, and comes back as ZCode's own with nothing saved. TestPluginUsage (free), TestCursorPluginContext and TestZCodeMover fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-10-01 01:18:04 +08:00
yetone ee5b1b7d44 plugins: a plugin's account behaves as its built-in's (the user: 插件版的订阅 provider 和内置的…保持一模一样了吗) — a plugin's failed sign-in now says why ("the sign-in failed: the sign-in page expired") instead of a bare failure; a plugin account the provider refuses with 401 is marked lapsed, "<user>'s <provider> sign-in has expired — sign in again", beside the account and on its usage card as a built-in's is, and the mark goes when it answers again or is signed in anew; a Devin or Command Code account taken from the agent's CLI moves to the plugin marked as the CLI's and back as the agent's own, so the one it follows stays the one the CLI is signed in to. TestPluginProvider (lapsed), TestMoveSideState (cli) and TestPluginSignIn (reason) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-10-01 01:00:07 +08:00
yetone f5f10bda4d plugins: a plugin's accounts show their allowance as the built-ins' do (the user: 所有插件要完整支持显示额度!跟内置一模一样!) — a plugin's auth.usage now tells plan, user, until, renew, balance, resets (count, until, by window, five hours, weekly) and windows (used past 100 when overspent, resets at/in, display, span, the models a window counts or leaves out, aside), and magpie shows them on the usage page, in the menu bar, beside each account and to the gateway, which passes over an account used up for the model asked; a built-in moved onto its plugin keeps its card's name and icon. Also: Command Code's subscription endpoint answering 200 {"success":false} was read as "No plan" by the built-in (and the plugin), it is now unread; a moved Devin was listed twice, the built-in still finding the Devin CLI's own sign-in; and moving back left the plugin's rows in logins.json, listed as empty accounts. Checked in a sandbox HOME with real Command Code and Devin accounts moved onto the community plugins (0.1.1): GOAT, its period, 5 hours / Weekly / Credits for Command Code, Teams, balance and Daily/Weekly for Devin, the same on the built-in after moving back, no plugin rows left. TestPluginUsage, TestCommandCodeSubscriptionUnread and TestMove (the plugin's rows after moving back) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut and TestUserPath timed out once at load 22, pass alone). 2026-09-30 22:58:56 +08:00
yetone 3bec6c0e40 providers: a built-in subscription moves onto its community plugin and back, its accounts with it (the user: 慢慢下掉内置的那些 Subscription Provider…不要让用户一升级完之后,突然发现他的所有服务都不可用了) — eleven built-ins (Zed, Qoder, Factory, MiMo, Devin, Grok, Command Code Plan, Cursor, Kiro, WorkBuddy, ZCode) each get a mover to their @magpie-community package. A move installs the plugin (updated when older than the mover needs), hands it every account in the plugin's shape, checks the plugin lists models for them, and only then lets the plugin serve the same provider id, so agents' model ids don't change; any failure rolls back and the built-in keeps serving. The built-in's accounts are kept as a backup, and "Run built-in again" (magpie plugin move-back <id>) takes the plugin's current sign-ins back, never copying a rotating token: they are taken out of the plugin in one step, and an account the built-in renewed meanwhile keeps its newer token. An account only the plugin can hold (Cursor's browser sign-in) stays with it. The provider editor shows "Runs on" with the button, a failed move's reason, and the CLI has magpie plugin move / move-back. Automatic moves (Retiring) are wired in but the list stays empty until the packages are on npm. Checked with real accounts in a sandbox HOME: Command Code Plan and Devin moved, answered a chat through the gateway on the plugin, moved back and answered again on the built-in. TestMove*, TestPutBackKeepsNewer, the movers' round-trip tests and plugin-move.test.cjs (Chromium, WebKit, en/zh) are new. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit pass. 2026-09-30 21:21:58 +08:00
yetone 52c8e838d2 providers, sign-in, tray, plugins: four of StringKe's reports on Discord — (1) Removing a provider left its editor open and listed, and a second Remove said there was no such provider: the removal had been made, but moving the agents off its models failed on one agent whose config couldn't be written, and Reseat returned that as the change's error; now such an agent is a move with its error ("still on X, not moved: …", a warning in the panel, a line in the CLI and TUI), the others are moved, and the panel, told a delete failed, re-reads the list and closes the editor when the provider is gone. (2) The sign-in waiting sheet shows the sign-in link itself, selected whole on a click, with a copy button, for a browser other than the one that opens. (3) A subscription with several accounts offers "Account in use" among the menu bar's usages, following the account the gateway goes to first. (4) The Antigravity plugin's sign-in never started: magpie called every plugin's authorize with an inputs object, the way OpenCode's CLI does, and opencode-antigravity-auth then asked its Project ID on the terminal — magpie's stdin — until the minute ran out; inputs now go only to a method with prompts, as OpenCode's TUI passes them (checked against the real package under Bun in a sandbox: the Google sign-in URL comes back at once). TestReseatUnmovable, TestTrayInUse, TestInUseOf, TestAuthorizeNoPrompts, provider-remove.test.cjs, signin-link.test.cjs and tray-inuse.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, tray-usages, plugin-signin, signin-callback, factory-sub pass. 2026-09-30 20:45:23 +08:00
yetone b63a0f1fd5 plugins: a plugin's provider takes more than one account, as a built-in subscription does (the user: 插件当然要支持多账号了) — a plugin kept one sign-in per provider, and signing in again replaced it. Now each sign-in goes beside the others: plugin-auth.json keeps the first under the provider's id, as OpenCode does (so what is signed in already stays the first account), the rest under id#slot; logins.json lists them as it lists the built-ins' accounts, so the editor shows them all, one first, the others ticked on behind it or off, any made first, any removed (which signs it out). The host runs every request in its account's scope, so a plugin's getAuth, client.auth.set (a refreshed token), loader and provider.models see that account alone; a sign-in to an account already there (the same id, else the same secret) replaces that one's rather than listing it twice. The gateway spreads and fails over across the accounts on, as for the built-ins. Accounts with no id are told apart by the end of their key ("API key …abcd"), else by their slot. Also on the Devin page: the header shows the plugin's icon, and Cognition's opencode-devin is off the market (its gateway answers 404 model_not_found for models and chat). TestPluginAccounts (plugin: per-account requests and refreshes, dedupe, sign-out; provider: first, on/off, switch, forget, labels), TestPluginProvider (gateway: a 429 on the first account goes on the second) and plugin-signin.test.cjs (add a second account in the editor, make it first; fails without the change) are new. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-signin, plugin-market, old-webkit, click-scroll pass. 2026-09-30 19:58:22 +08:00
yetone 7926dcc226 gui: a plugin's sign-in picker keeps its question readable, and a plugin's provider shows its vendor's icon (the user: 这个 UI 为啥这样 / 这里必须要有真实 icon) — Devin's plugin names its ways long ("Paste an existing Devin session token…"), and the buttons, kept on one line beside the question, pushed it into a column a character wide and ran off the sheet; the ways now wrap under the question, each wrapping its own label. A plugin's provider showed a plain box for any vendor outside OpenCode's few; it now takes the icon the plugin market lists for its package, else for a listed plugin of the same provider (Devin, Zed, Kiro…), read from the list fetched last, the cached copy or the built-in one, never the network. TestMarketIcon and plugin-signin.test.cjs (question squeezed to 12% of the box) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/plugin ./internal/gui ./internal/provider pass; plugin-signin, plugin-market, old-webkit, click-scroll pass. 2026-09-30 19:33:52 +08:00
yetone 9ec237d4e6 gui: a Plugins tab, a market to find and install provider plugins in (在产品里要有插件市场功能,整个插件市场要美观易用), and a way to it from the providers list (供应商列表里有个精致优雅的按钮) — Discover lists the magpie community's plugins, then OpenCode's community's by weekly downloads, each card with its logo, publisher, summary, downloads, version and one button that takes it all the way: Install (Bun fetched first if need be), Sign in, Signed in; a package not on npm yet says Coming soon. Typing searches the list and, after a pause, npm, with the field keeping its focus. A card opens the plugin's page: version, downloads, licence, updated, what it signs in to, npm/source/homepage links, and its README (no images, HTML or scripts; only http(s) links, opened outside). Installed lists each plugin with its providers' sign-ins, a newer version to update to, Update all, switch off and remove; Settings' Plugins section moves here. The list comes from registry.json in magpie-community/plugins, kept for 6 hours, falling back to the copy fetched last and then the one built in (MAGPIE_PLUGIN_MARKET overrides, "off" never asks). Under Add provider a "More subscriptions in Plugins" pill opens it; the add sheet ends in a Plugins tile, and a search that finds nothing offers to look for a plugin. The header, seven tabs now, draws them in at the narrowest window. plugin-market.test.cjs (Chromium, WebKit, en/zh) and market_test.go fail without the change; add-button, plugin-signin, usage-ledger, text-size, old-webkit, click-scroll pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-09-30 17:57:46 +08:00
yetone 5e3c7b8049 plugin: a plugin's provider has its models on a magpie that never fetched models.dev — the host took the models OpenCode would from models.dev's cache, so on a fresh install (magpie plugin add before the app or gateway ever ran) opencode-gemini-auth and opencode-copilot-auth listed 0 models, and plugin-providers.json kept that. Starting the host now fetches the catalog first when there is none (30 s at most). Tried in a sandbox HOME with the real packages: google-plugin 38 models, github-copilot 34; with a Copilot sign-in copied into the sandbox, magpie provider test github-copilot answered on Responses (gpt-5.4) through the plugin's fetch. go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test ./internal/plugin pass. 2026-09-30 16:48:31 +08:00
yetone a31f97b30d plugin: OpenCode's provider plugins sign in to subscriptions in magpie — an npm package OpenCode users install for a plan (its auth hook: loader, methods, oauth authorize/callback, api keys, chat.headers) runs in a Bun host magpie downloads the first time a plugin needs it, speaking JSON-RPC over stdio; its sign-ins are kept in plugin-auth.json (0600). A provider a plugin signs in to is a provider like the others: its models the plugin lists, each spoken to on the API its AI SDK package speaks (anthropic → Messages, openai/azure → Responses, google → Gemini, Copilot's GPT-5+ → Responses, else chat completions), its requests carried by the plugin's fetch to the base URL its loader gives; an id magpie already has is <id>-plugin. CLI: magpie plugin [list|add|rm|update|on|off|login|logout]. App: Settings → Plugins adds, updates, switches off and removes them and says why one didn't load; Add provider → From plugins signs in with the plugin's methods, its questions (picks and checked text), the browser and a pasted code or a key, followed as the built-in sign-ins are; removing the account signs out. The built-in subscriptions stay. TestFakePlugin, TestPluginProvider (chat, Anthropic and Gemini models through the gateway) and TestPluginSignIn (a wrong code fails, the right one signs in, removing signs out, a key) run a fake plugin on Bun; plugin-signin.test.cjs (Chromium, WebKit, en/zh) covers the add sheet and Settings. signin-callback, add-sheet, zed-sub, account-proxy, own-signin-remove, routing-signin, click-scroll GUI tests pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked, known flakes under load, pass alone). Not yet tried with a real plugin and account. 2026-09-30 16:44:32 +08:00