Commit Graph
30 Commits
Author SHA1 Message Date
yetone 90b1ac7956 update, gui: Add to PATH on Windows writes a magpie.cmd beside an app not named magpie.exe, so the site's magpie-windows-amd64.exe runs as magpie in a new terminal, and says so (#942, mintonight)
Add to PATH put the portable download's folder first in the user's PATH, but a terminal looks for magpie by that name only, so the command was never found. cliPlace now writes magpie.cmd beside a program not named magpie.exe: it runs %~dp0<the program's name> %*, which an update keeps, since it replaces the program where it is under its name. ReadCLI reads that magpie.cmd as this app (shimRuns), and the GUI says, in Add's tooltip, the row and the status, that magpie.cmd runs magpie-windows-amd64.exe. Going through cmd also makes PowerShell wait for the GUI-linked program and gives it the console. TestCLIShim and cli-path.test.cjs's new case fail without the change.
2026-10-06 02:18:07 +08:00
yetone 9ca64d104f update, gui: a new version Windows won't start is never left in place — tried before it goes in, and a restart into one that doesn't say it is up puts the running one back; that version isn't downloaded again by the clock, and Settings › About › Version says why (#894, hisiling) — on Windows 11 with Smart App Control on, the update put the new unsigned exe in place of the running one and restarted into it; CodeIntegrity (3033/3077) refused it, magpie never started again and the working version sat in .old. Now, on Windows, the downloaded exe is first started suspended with no window and ended at once (Windows checks a program when its process is created), and one refused stays out: the running exe isn't moved. At the restart the new process is given MAGPIE_STARTED, a file it writes first thing (AwaitPredecessor); the old one waits up to 20s for it and, if the start fails (ERROR_SYSTEM_INTEGRITY_POLICY_VIOLATION 4551, 1260 or any other), the new process quits before saying so, or it says nothing in time (then it is ended), puts the running exe back over the new one (the new one renamed .blocked if it can't go) and keeps running instead of quitting; nothing notes it as updated in the app. The version is noted in the config folder (update-blocked.json, this machine's only, never synced; gone once a version at least as new runs); the clock's checks leave it alone and show why, a Check clicked downloads and tries it once more. Settings › About › Version: for an application control policy, "0.1.x didn't start on this computer: Windows' Smart App Control (or another app control policy) blocked the new version, which isn't signed. magpie stays on 0.1.y. Download it and run it yourself, or turn Smart App Control off and check again", else the reason, with Download and Check; the error in full on hover. en/zh/ja/de. A user who double-clicks the refused exe is refused by Windows before magpie runs; only signing helps there. TestRelaunchRefusedPutsBack (4551 at the start), TestRelaunchSilentPutsBack (no word in time, killed; quit at once), TestRelaunchReportedStays, TestReportStarted, TestBlockedNote and TestUpdateThatWontStart (the restart doesn't quit, the note, the clock not downloading it again, a Check that does) are new and fail without the change; on Windows TestInstallBinaryRefusedStaysOut, TestTrialStart and TestStartCheckedPutsBackForReal (real processes: not a program, quits at once, runs on silent) pass on the Windows 11 test box with the whole internal/update package (Smart App Control itself isn't on there, so the refusal is simulated). update-blocked.test.cjs (Chromium and WebKit, en and zh: the reason and both versions, full error on hover, Download opens the release, Check downloads, the other reason, no select, no scroll, no left border) fails without the change; update-check, update-mirror, update-auto, update-wait and update-download-percent pass in WebKit; click-scroll's request-list case fails now and then the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. 2026-10-05 19:43:31 +08:00
yetone dab887db57 update, gui: Settings › About › Download source sets the update mirror, and a download that failed through it offers GitHub (#893, M3chD09) — the mirror updates could come through was only magpie update mirror in a terminal, and the GUI never said where updates came from or that a failed download was the mirror's. About now has a Download source row: GitHub, or the mirror set (the same UpdateMirror setting the CLI writes); Use a mirror… takes a full https:// address (http, a bare host or a malformed one refused, with the reason in the row and the draft kept), Change edits it, Use GitHub takes it away in one click; Enter saves, Esc cancels, no click scrolls. POST /api/settings/update-mirror sets it; the Settings page's own save keeps it. A download through the mirror that doesn't answer, answers other than 200, or sends a file whose SHA-256 isn't usemagpie.ai's feed's is an update.MirrorError (never installed, the file removed, as before); /api/update's error carries the mirror, and the Version row says the download failed through that mirror and offers Download from GitHub, which takes the mirror away and downloads again. magpie names no mirror or CDN of its own. en/zh/ja/de. TestMirrorFailuresAreTheMirrors (tampered, 404 and closed mirrors each the mirror's, no file kept; a direct failure no mirror's; CheckMirrorURL) and TestUpdateMirrorSetting (set trimmed, http/bare/ftp/empty-host refused, Settings save keeps it, off, an updater failing through a 404 mirror reports it with retry, a new check clears it) are new; update-mirror.test.cjs (WebKit, en and zh: GitHub shown, http refused with the reason, https saved with Enter, Use GitHub posts "", a failed download through the mirror offers GitHub, which posts "" then update/install; no select, no left border, no scroll) fails without the change; update-auto, cli-path, cli-behind, settings-sections and click-scroll pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. 2026-10-05 18:49:04 +08:00
yetone d6daa40ce3 update, gui: Settings › About › Command line says what magpie runs in a new terminal and adds it (PAMI on Discord) — the app downloaded from the site, and every Windows one, left terminals without the magpie command, and nothing in the GUI said whether it was there. A Command line row now asks each shell installed (zsh, bash, fish and the login shell; PowerShell and cmd on Windows) for its PATH and says, with a dot each, whether its magpie runs this app, another copy (with that copy's version, read from its .app's Info.plist or Homebrew Cellar folder, never by running it), or none. Add to PATH links this app's program (Homebrew's opt link, the AppImage) into the folder on the default shell's PATH whose magpie runs first, else ~/.local/bin or the like when on PATH, replacing another copy there as install.sh's ln -sf does; on Windows it puts the app's folder first in HKCU\Environment Path and broadcasts WM_SETTINGCHANGE. A shell whose PATH lacks that folder gets its own button naming its profile (~/.zshrc, ~/.bash_profile on the Mac or ~/.bashrc, fish's config.fish), which links into ~/.local/bin and adds that folder with one marked line, once; no profile is written otherwise. A translocated or disk-image app is told to move first and gets no button. It says new terminals take the change (on Windows: new PowerShell/cmd windows, restart Windows Terminal). GET/POST /api/cli; en/zh/ja/de. TestCLIOnPath (temp HOME, stubbed shells: link, fish profile once, a shadowing copy replaced, no folder → zsh's profile kept and appended, an unknown shell refused, plist version, translocated refused) and TestPathFirst are new; cli-path.test.cjs (WebKit, en and zh: per-shell dots, Add posts {} and turns green, fish's button posts fish alone, Windows lists PowerShell and cmd, translocated gets no button, no scroll, no select, no left border, translations) fails without the change; cli-behind and update-auto pass. Not tried on a real Windows machine. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (TestOTelWholeWaterfallFallback and TestDevinSignedInOnlyInMagpie failed once each under load and pass alone). 2026-10-05 17:49:20 +08:00
tobitege 21702d01ff Add German translation; harden paths against unset env vars (#770)
* Add German translation and harden paths against unset env vars

Add a German locale to the GUI, tray menu, usage alerts and website home page, and refuse to start when the home folder is unset or relative so nothing is written under the working folder; the pi plugin no longer adopts the user's PI_CODING_AGENT_DIR, and an inherited APPIMAGE no longer selects portable mode. Scripts refuse to run from the wrong folder or empty a shared out folder, and a new internal/testenv sandboxes every test package from the user's real files, which also fixes the Claude picker and Termux installer tests on Windows.

* Makefile: know magpie's folder by its go.mod, so paths with spaces work

The check compared $(CURDIR) with $(dir $(abspath …)), which make splits on spaces, so a checkout under 'My Projects' or 'John Doe' refused every target. It now reads go.mod relative to the working folder and wants magpie's module there: make -C and a symlinked Makefile work, make -f from another folder still stops before any recipe runs.

* i18n: German for the strings main added since, with a parity test

75 strings main added (group order, patterns, slow-start wait, usage caps, RTK savings …) get their German, and two that main dropped go. gui-de.test.cjs checks, as gui-ja does, that de has every zh string with its placeholders and tags, and that the window reads in German when picked or on a German system.

* appdir: ignore relative folder variables when reading, keep them for children

CheckEnv no longer unsets anything. magpie reads the XDG folders, APPDATA/LOCALAPPDATA and every agent's folder variable through appdir.Getenv/LookupEnv, which pass over a relative value; the environment stays as it is, so the agents magpie starts get the variable as the user set it. That covers what proc.UserPath lends from the login shell too, which is set after main's check and reached the desktop app unchecked. On Windows a path rooted on the current drive (\Users\x, Git Bash's /c/x) counts as usable, and magpie version and help answer without a home, before run makes any folder.

* Sandbox the tests main added since

internal/wslrun gets testenv's TestMain, and four tests main added that set HOME alone set their home with testenv.SetHome, so on Windows they don't read the runner's profile through USERPROFILE.

* agentenv: PI_CONFIG_DIR and OPENCODE_DB take relative names on purpose

omp puts $PI_CONFIG_DIR under the home and OpenCode a relative $OPENCODE_DB in its data folder, so neither depends on the working folder: appdir no longer passes them over.

* i18n: German for the Library's multi-select and System One's decision API

The 19 strings main added since (#791's skill multi-select, the decision API snippets) get their German; the one main replaced (four APIs → five APIs) goes.

* appdir: tidy the import into magpie's group; say what Go makes of /c/x

The appdir import joins the group of magpie's own imports where a file has one. rooted's comment now says that Go takes a Git Bash path like /c/x as \c\x on the current drive, not C:\x.
2026-10-04 23:30:56 +08:00
yetone bc141ad834 update: install.sh and magpie update take --proxy and --mirror, and magpie update mirror keeps one for the app's updates too (akic404 on Discord: 安装和update如果没有梯子也是麻烦事,能否在install.sh和magpie update后加子选项?--proxy等可自定义或默认ghproxy) — install.sh had no options and fetched with curl alone, and magpie update took none; without a way out to GitHub both failed. Now both take --proxy <url> (http, https, socks5, socks5h; install.sh also MAGPIE_PROXY=) for the feed and the download, and --mirror <prefix> (or MAGPIE_MIRROR=), a GitHub download mirror the release file is fetched from as <prefix>https://github.com/…; only github.com / githubusercontent.com URLs are put through it. No mirror is used unless one is given: magpie names none and has none by default. The feed with each file's SHA-256 is always fetched from usemagpie.ai, never through the mirror, so a file the mirror changed is refused and deleted ("… from the mirror X does not match its checksum from https://usemagpie.ai; not installed"); macOS still checks the app's signing team. magpie update mirror <prefix>|off saves Settings' updateMirror (http(s) only), which the app's own background update follows too; magpie update --mirror off skips it once; the Settings page's save keeps it. The app's update already followed Settings → Proxy (#294); magpie update --proxy overrides it for one run. The environment's ALL_PROXY / all_proxy is now honoured when HTTPS_PROXY / HTTP_PROXY are unset (Go's ProxyFromEnvironment ignores it), with NO_PROXY as curl reads it. magpie update takes --flag value or --flag=value, and an unknown word is an error instead of an update. The help, README and the docs' FAQ (en/zh/ja) say how. install.test.mjs's proxy, mirror and tampered-mirror tests, TestAllProxyFromTheEnvironment, TestUpdateThroughAMirror, TestUpdateThroughAProxyGiven and TestUpdateFlags fail on the old code and pass; TestSettingsSaveKeepsWhatItDoesNotSend covers updateMirror. A dry run of install.sh as Linux amd64 into a temp HOME installed the real 0.1.773 with its SHA-256 checked, refused a local mirror serving a wrong file, and with --proxy to a closed port failed reaching the site through it. go vet, builds (darwin, GOOS=linux, GOOS=windows), sh -n, node --test site/install.test.mjs and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeIdentityServedWhileAsked, a timing test that passes alone. Not tried through a real proxy server or a real public mirror; no install.ps1 exists for Windows, and the app has no field for the mirror. 2026-10-04 00:19:47 +08:00
yetone e5f8ff6026 site, gui: release notes survive GitHub limiting usemagpie.ai, and a failure to get them says so, apart from a release without any (#661) — /api/notes answered 503 {"error":"no releases"} with Cache-Control public, max-age=300 whenever GitHub's API limited the worker's shared IP, and /api/latest fell back to the release page's redirect with notes "" (kept 300 s too), so the update window had nothing to show. The worker now: never keeps an error (503 is no-store); takes the notes from GitHub's releases feed (releases.atom, not API-limited) when the API says no, turning its HTML back into the markdown the app draws and putting back the <!-- lang:zh --> marker GitHub strips (before the first Chinese heading after an English one); keeps the last whole answer for 30 days in the edge cache and serves it (merged with the feed's newer releases) when GitHub fails; keeps a degraded answer 60 s instead of 300; and sends a GITHUB_TOKEN secret to the API where one is set (none is yet). In the app, /api/whatsnew says error (and the release page's url) when the notes couldn't be had, and Settings' What's new opens a dialog saying "Couldn't load the release notes" with Try again and "Open the release page" (through the app), or "No release notes provided." with the release page and no retry for a version with none, instead of a passing toast; en/zh. Tests that fail on the old code and pass: site/worker.test.mjs (API limited: notes from the feed in either language, briefly kept; all of GitHub down: 503 no-store, then the last whole answer; the token sent), TestWhatsNewTellsFailureFromNone, and whats-new-failed.test.cjs (WebKit, en/zh: failure with retry and link, retry showing the notes, none without retry, no click moves the page). The feed parser was tried on the real releases.atom (v0.1.730–739: English and Chinese split as the API's bodies are). go vet, builds (darwin, GOOS=linux, GOOS=windows), go test -tags nogui -count=1 ./... under a temp HOME, node --test site/ and whats-new.test.cjs pass. 2026-10-03 17:12:48 +08:00
yetone e2a22596cc update: What's new leaves out the Chinese notes' 安装 section too (#649) — every release's notes end with an Install section that StripInstall takes out, but it matched only the English heading, so with magpie in Chinese each release in What's new repeated 从 usemagpie.ai 下载… StripInstall now knows the heading in each language the notes come in (Install, 安装, 安裝, インストール); a bullet that merely mentions 安装 stays. TestStripInstall covers it and fails on the old code. 2026-10-03 15:00:01 +08:00
erlinerdandClaude Code 94008ce66f update: say so when the magpie command is a copy the app left behind (#623)
* update: say so when the `magpie` command is a copy the app left behind

install.sh links the terminal's magpie into ~/.local/bin to the app's
own binary, so it follows every GUI update for free. A command that
arrived as a copied file — an older installer, a hand cp — keeps
running its own build once magpie.app moves on, and a command behind
the app is a stale build: one that predates the WebDAV short-write
check (#531) truncated a remote backup on `magpie webdav now`, wedging
the sync on every tick after. The gap is macOS-only: on Linux the app
and the command are one file, and its update replaces it in place.

magpie doesn't rewrite the user's PATH itself — that's the installer's
job, and a background update has no business reaching there. What the
command *is* says whether it can follow the app, so update.StaleCLI is
a plain Lstat of the installer's own convention (MAGPIE_BIN_DIR, else
~/.local/bin): a regular file is behind, a link isn't, and nothing
runs the old binary to ask its version. When it's behind, magpie says
so where the user can see it: /api/state carries cliBehind and the
window's status line says it once per run (the panel stays quiet, too
small for advice), and `magpie update` adds the line after updating
the app it found through the same copied command.

TestStaleCLI pins the three file states over a temp bin dir;
cli-behind.test.cjs (WebKit and Chromium, en and zh) fakes a state
with cliBehind and checks the window says it, the panel doesn't, and
a following command is never mentioned. go vet, and go test -tags
nogui ./internal/update ./internal/gui under a temp HOME pass.

* update: only say the command is behind when it is, and how to fix it

Two things the review caught about the notice.

A copy that isn't actually behind the app shouldn't nag. The
standalone magpie-cli build is a legitimate regular file in
~/.local/bin beside the app, and the first cut warned about it on
every window load however current it was. StaleCLI now takes the
app's version and stays quiet unless the copy is a release that
trails it — a copy at or ahead of the app, a build from source, and
one that won't run are all left be. Reading the version runs the old
binary, whose start-up migrates things, but that's the only way to
tell a stale release from a current one, and it's reached only for a
file already known by Lstat to be a copy, once per run.

The advice named a step that didn't fix it. `magpie update` run from
a copied command has no bundle to update (the copy isn't inside a
.app), so it replaces the copy with another copy and the warning
comes back. Both the window's line and `magpie update`'s now point
at what actually works: re-run the installer, or link the command by
hand — `ln -sf <app>/Contents/MacOS/magpie <cli>`, the link
install.sh makes.

TestStaleCLI covers the version decision (behind → say, current,
ahead, source, unreadable → quiet) over a temp bin dir; the browser
test's wording follows. Verified by mutation: dropping the version
guard turns the stale case red, and Stat for Lstat turns the link
case red. go vet, and go test -tags nogui ./internal/update
./internal/gui under a temp HOME pass.

* gui: the window's copied-command advice is an Lstat with a per-version dismiss (yetone's review on #623)

What: the window no longer runs the old binary's `version` to tell a stale
copy from a current one. update.CopiedCLI is a bare Lstat, reached on every
launch; the version read stays in update.StaleCLI, which only `magpie
update` calls. The advice moved from a 9s status line to a small dialog
with "Hide until the next version", kept in settings' cli-behind-quiet
file as the version it was dismissed at, so the next app update asks once
more. POST /api/cli-behind/quiet writes it.

Why: `magpie version` isn't read-only — run() migrates settings before the
command answers, so an old copy rewrote the user's real config with the old
build's migrations (old MoveOffAccountIDs could drop fields), and the read
on the first /api/state cost up to 2s on a cold start. An Lstat can't tell
whether the copy trails, so what the window says is advice, not a warning
that knows — dismissable, and worded as a copy rather than a stale one.

Tests: TestCLIBehindIsLstatOnlyAndDismissible (a non-executable file is
reported — nothing ran; dismiss quiets it and the file holds this version;
the installer's symlink is quiet). cli-behind.test.cjs covers the dialog,
its dismiss POST and the quiet panel, chromium+webkit, en+zh, 4/4.
go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows) and
go test -tags nogui -count=1 ./... under a temp HOME pass (netproxy's
TestBunThroughBridge fails the same way on the branch without this change).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* gui: the dismiss survives a restart, and cliBehindVal is behind a lock (yetone's review on #623)

What: the POST handler wrote cliBehindVal = "" while state() read it on
other goroutines with no lock — a page dismissing while another view
polled /api/state raced. cliBehindVal now sits behind cliBehindMu on
both sides. The test also proves the promise the handler's one-line
clear couldn't: after a simulated restart (fresh Once, empty value)
the dismissed advice stays quiet, and the next version asks once more.

Why: the restart case was untested — short-circuiting cliQuiet() left
the suite green, because the handler's in-memory clear alone satisfied
it. The quiet file is what actually carries the dismiss across runs.

Tests: TestCLIBehindIsLstatOnlyAndDismissible extended with the
restart and next-version legs; `if false && cliQuiet()` now fails it
(mutation-checked). go vet, go test -tags nogui -race ./internal/gui
./internal/update pass; TestGatewayTakenOver fails the same way with
-race on the unmodified branch (pre-existing).

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-03 13:32:42 +08:00
TryWorld 1ca56a9ad8 fix(update): roll back failed Windows binary installs (#632) 2026-10-03 12:51:51 +08:00
YAYOI27 cb82822f3d source: add opt-in China mirror fallback (#566) 2026-10-03 09:50:59 +08:00
yetone 88cccc184e update: on Windows, an exe something holds open for a moment still goes aside for the new version, and one held for good says why in full, with the release page a click away (freecss on Discord: "0.1.628 已下载 · couldn't move magpie-windows-amd64.exe aside to put the new version in: rename C:\Us…") — InstallBinary renamed the running exe to .old once and gave up on the first error, which Go prints as "rename <exe> <exe>.old: <reason>", two full paths ahead of the reason, and the version row cut it off on one line, so what failed couldn't be read. A leftover .old was already handled (RemoveOld, then .old-2…, TestInstallBinaryBesideARunningOld), and Windows renames a running image (it is mapped with FILE_SHARE_DELETE), so the likely cause is another program holding the exe open without delete sharing — an antivirus scan, OneDrive or a similar sync client — or a folder that refuses the change (access denied); which one isn't known until the full message is read. moveAside now tries again for about 3 s (0.1, 0.2, 0.4, 0.8, 1.5 s apart), clearing a read-only attribute after the first failure, with a fresh .old name each try; a missing exe isn't retried. When it still can't, the message is the reason without the paths and what to do: ERROR_SHARING_VIOLATION/ERROR_LOCK_VIOLATION says another program has it open (often an antivirus or OneDrive), access denied names the folder Windows won't let magpie change, anything else says to download the new version; the download is kept for another try, as before. The version row wraps a failed install's or download's reason (.sub.wraps) with the whole text as its tooltip, and a ready-but-failed install offers Download, which opens the release page. The Windows test box wasn't reachable from this session (ssh key refused), so nothing ran on Windows; TestInstallBinaryHeldOpen (windows-only: an exe held open without FILE_SHARE_DELETE for 0.5 s still installs; held for good, the error says another program has it open and the download is kept) was cross-compiled with GOOS=windows go test -c but not run. TestMoveAsideTriesAgain and TestMoveAsideSaysWhy (portable, with a fake rename) and the new update-check.test.cjs case (webkit, en and zh: the reason is not cut off, its tooltip is the full text, Download opens the release page, a later state is one line again) fail without the change; Chromium couldn't launch here (its Playwright cache has no ICU data). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet, go test -tags nogui ./... pass (TestFXInState failed once on a TempDir cleanup race, passes on the rerun; untouched). 2026-10-02 13:26:54 +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 aa42142ea5 update: What's new and the waiting update's notes follow the app's language (freecss on Discord: 更新后的公告的语言是不是应该设置里面的走) — the release notes were English alone. A release's notes are to carry a Simplified Chinese translation below a <!-- lang:zh --> marker (the magpie-releases workflow, changed separately); usemagpie.ai's /api/latest and /api/notes take ?lang=zh for the Chinese where a release has it, and give anything else, an app from before included, the English alone, everything above the marker. The page asks /api/whatsnew, /api/update and /api/update/check in its own language (?lang=); magpie asks the site in it, keeps What's new's notes per language, and asks again for the waiting update's notes when a page's language isn't the one they are held in (at most once a minute); a check by itself asks in the pages' last language, else the app's, as the tray menu has it. Notes that come back whole (a site not yet deployed) are cut in the app too; notes without the marker (every release so far) are English, in either language. The worker must be deployed (npx wrangler deploy in site/) before the workflow change ships, or apps would get both languages. TestInLang, TestFeedsAskInLang, TestAskedLang, TestWhatsNewInPageLang, TestUpdateNotesInPageLang, site/worker.test.mjs (node --test site/worker.test.mjs), whats-new.test.cjs's language check (Chromium and WebKit, en and zh); update-check, update-hide, update-auto, sync-update pass in WebKit. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves flaked once under the full run, passes alone; gateway untouched). 2026-10-02 11:31:11 +08:00
yetone f22218a092 gui: after an update the window shows what changed since the version last run (a Discord user: 点更新后能弹个窗显示更新了啥吗, to see whether their issue was fixed) — magpie updated itself silently, and its release notes were only on GitHub. The version last run is now kept in magpie's own folder (last-version); started on a newer release, the window or magpie web's page asks usemagpie.ai's new /api/notes (beside /api/latest; site/worker.js lists the releases' notes from GitHub, cached as the feed is) for every release since, newest first, without their Install section, and shows them once in a What's new dialog. A fresh install, the same version, a downgrade or a build from source shows nothing; a magpie from before the version was kept, already in use, shows its own version's notes; offline nothing is shown and nothing waits on it; until the site is deployed, the update feed's newest notes stand in when they are the current version's. The tray's panel shows no dialog. The notes are drawn as text, never as HTML: headings, bullets, bold, code, http(s) links, and #NNN links to the issue, opened in the browser as the app's other links are. Settings has a What's new row under Version that opens them again, with a waiting update's notes first, marked Not installed yet; /api/update's notes leave out the Install section too. TestShowNotes, TestStripInstall, TestBetween, TestNotesBetween, TestWhatsNewAfterUpgrade, TestWhatsNewFromBefore, TestUpdateNotesWithoutInstall and whats-new.test.cjs (Chromium, WebKit, en, zh) 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, update-check, update-hide pass. 2026-10-02 03:57:55 +08:00
AronnaxandClaude Opus 5.5 ec4ee5a3a4 fix(proc): end what a CLI probe started, on its timeout and when magpie exits (#414)
* fix(proc): end what a CLI probe started, on its timeout and when magpie exits

The probes that ask an agent's CLI something (claude auth status,
cursor-agent about/models/status, the --version checks) ran through
proc.CommandContext, which on its timeout kills only the command itself.
A CLI that is a script (a version manager's wrapper, a shim) left what it
had started running, adopted by init; and a probe still asking when magpie
exited (a one-shot command, an update, a restart) was left whole, with
nothing to end it. With Omarchy's mise wrappers stuck in their exec loop
(omacom/omarchy#7234) that was five processes holding about half a core
each for up to six hours.

proc.ProbeContext gives such a probe a session of its own on Unix, as
StartTree does, and its context ending kills the whole group. Probes still
running are listed, and proc.EndProbes kills them when magpie exits (after
run in main) or puts a new version in its place (update.Reexec). On
Windows a probe is killed alone, as before, a job being joined only once
the command has started.

TestProbeTimeoutEndsWhatItStarted and TestEndProbes fail without the
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(proc): end probes on a terminal's signals and on a Mac's quit

Review of the first change: a probe leading its own session no longer
gets the Ctrl+C, hang-up or SIGTERM sent to magpie's terminal or service,
and magpie ending on the signal never reached EndProbes, so the probe was
left to init where before it died with magpie. main now catches SIGINT,
SIGTERM and SIGHUP, ends the probes and exits as the signal would have
(128+n). A signal magpie was started ignoring (nohup, a background job)
stays ignored. The account sign-in and plugin commands, which end
themselves on Ctrl+C, go through interruptContext and are left to do so.

On a Mac the desktop app's quit ([NSApp terminate:], also the restart to
update) exits without returning to main, so OnShutdown ends the probes.

TestProbeTimeoutEndsWhatItStarted ended its probe on a 500ms timeout,
which on a Mac could come before the script had written its child's pid
(6 of 10 runs failed in review). It now ends the context once that pid is
read, the same Cancel path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: let the app and the TUI quit on a signal their own way

Review of the signal handling: endProbesOnSignal exited ahead of Wails
and bubbletea, which quit on SIGINT and SIGTERM themselves. `magpie tray`
exited 143 without OnShutdown (no downloaded update installed, and
sessions.Saved skipped in main), and `magpie tui` left the terminal on its
alternate screen with the cursor hidden. The app's and the TUI's entry
points now hand the signals back (ownSignals): the app's OnShutdown ends
the probes, and the TUI comes back to main, which ends them too. A
one-shot command keeps the handler.

TestOwnSignalsHandsThemBack fails without the change (magpie exited 143
before the command could quit its own way); TestSignalExits keeps the
one-shot behavior.

childPid waited 3s for a script just written to start its child, which a
busy Mac can exceed (2 of 3 full runs failed in review). It waits 30s now
and, should the probe end first, says the probe's error rather than "the
script started nothing".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep SIGHUP for the app and the TUI, and die of the signal

ownSignals handed SIGHUP back along with SIGINT and SIGTERM, but neither
Wails nor bubbletea handles it: the terminal closing under the TUI or an
app started from a shell ended magpie on the spot, and its probes, in a
session of their own, out of the hang-up's reach, kept running (before
this PR they were in the terminal's group and ended with it). SIGHUP now
stays with endProbesOnSignal. A tray under a pty whose terminal closes
left 2 processes before, none now.

The handler asked whether a command was ending itself on Ctrl+C only
after EndProbes, which can take up to a second; a sign-in done in the
meantime got magpie ended in the middle of its own cleanup. It is asked
first.

It exited 128+n, which a shell takes for a child that handled the signal:
a loop running magpie went on to the next run on Ctrl+C. It now dies of
the signal (handling reset, the signal sent again); Windows, which can't
send one, still exits with the code.

agentProbe and agentCommand share withProxy; the Windows probeGroup
comment no longer says EndProbes ends what a shim started.

TestOwnSignalsKeepsHangUp and TestInterruptWhileEndingProbes fail without
the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: hand signals to the TUI and the app only once they listen

Review: runTUI handed SIGINT and SIGTERM back before tui.Run, whose
newModel asks agents' CLIs (claude auth status, up to 5s) before
bubbletea listens for signals. Ctrl+C, SIGTERM or SIGINT while it loaded
ended magpie by Go's default and left the probe and its child running.
tui.Run now takes ready, called once the model is made, and runTUI passes
ownSignals.

The app had the same window: runGUI handed them back before gui.Run,
which starts the backend (probes included) before Wails' Run starts
listening. ownSignals is now gui.Started, called on ApplicationStarted,
which Wails emits after its signal handler is set up. A tray sent SIGTERM
0.05s after starting, a probe under way, left 2 processes before and
none now; one sent it once started still quits through OnShutdown
(exit 0).

TestTUIKeepsSignalsWhileLoading fails without the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:42:48 +08:00
yetone bd529a555e netproxy: a host whose IPv6 takes the connection and drops it is reached over IPv4 (tony on Discord: models.dev sync and the update check failed with a bare EOF, direct or auto, while curl got through) — behind a TUN (xray) without IPv6 out, a connection to a site's IPv6 address is accepted, so the dialer's own fallback to IPv4 never starts, and closed once TLS begins. A connection to an IPv6 address that ends before a byte comes back now marks its host:port to be dialed over IPv4 for 30 minutes, and the request that met it is sent once more over IPv4 when nothing of it can have reached the site (HTTPS, or an idempotent method) and its body can be had again; proxied requests and IPv4 connections are left as they are, and TLS, ALPN/HTTP/2, headers and bodies are unchanged. Every transport made with Dispatch (http.DefaultClient, the gateway's) and the update client get it. TestIPv4Fallback fails without the change (Get "https://example.com/": EOF). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFXInState flaked on TempDir cleanup under load, passes alone). 2026-10-01 23:20:58 +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 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 ee8a371cf4 gui, update: saving the Settings page keeps the models hidden from each agent, and update checks go through magpie's proxy and say so (#294: 设置代理之后只有模型代理生效了,代理没有用于检查 magpie 是否有更新) — POST /api/settings carried over the models' names, levels and images but not HiddenModels, which the page never sends, so changing the theme, the language or anything else there showed every model hidden one by one on the Agents page again; it is carried over with the others. The update feed and the release came through http.DefaultTransport, proxied only because main installs netproxy first; the update client now takes netproxy.Func on a transport of its own (Settings' Proxy, else *_PROXY, else the system's). A check that failed at start-up, before a proxy was set, stayed failed for six hours: saving a new proxy now checks again through it. And the Settings row read "requests to vendors go through {proxy}", which says update checks don't: it now names both. TestSettingsSaveKeepsWhatItDoesNotSend (every field prefsKeep doesn't send survives a save of the theme alone; fails on main with HiddenModels emptied), TestUpdateGoesThroughTheProxy (feed and asset at hosts only a local proxy answers; fails without the transport), TestSettingsProxyRechecksAFailedUpdate (fails without the re-check). account-proxy, provider-proxy and 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, known flakes under load, pass alone). 2026-09-30 16:03:52 +08:00
yetone 831df8579e update: a restart to update from the open window (Windows, Linux) opens the window again, on the tab it was asked from, instead of the tray icon alone (#275 Aimer779: 设置页更新重启后主窗口消失,只剩托盘) — RelaunchBinary always started magpie tray, autostart's argument, so the new version came up as a tray icon however the update was asked for; the Mac reopens its .app and was fine. The window's Update pill and Settings' "Restart to update" send the tab they're on, the new process starts as magpie gui <tab>; the tray menu's Restart to Update brings the window back if it was open and keeps to the tray if not; autostart stays tray. magpie gui <tab> / magpie app <tab> open the window on that tab, and hand it to a magpie already running. The MAGPIE_REPLACES handover is unchanged: the new process waits for the old one before its single-instance lock. Tested with TestRelaunchArgs and TestRelaunchBinaryArgs (a script in magpie's place is started with gui settings <pid> / tray <pid>); go vet, go build (GUI and -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried: a real restart to update on Windows or Linux. 2026-09-30 12:33:36 +08:00
yetone 4e83dc6be8 web: magpie web knows its version and updates itself as the app does — the badge's restart puts the new version in and runs it in the old one's place with the same arguments and key, so the open page reloads onto it (#111); routing: a gateway served by magpie serve, which has no window, is told apart from another magpie's window, and the routing view says to stop it so this magpie serves the gateway and shows routing live (#110) 2026-09-27 19:39:45 +08:00
yetone 06dddf7f68 update: magpie update in a Homebrew install (brew install magpie, from the new tap in magpie-releases) says to brew upgrade magpie instead of replacing the binary in brew's Cellar, which would leave brew thinking the old version is there 2026-09-26 20:50:26 +08:00
yetone cb609d2cb4 update: after an update on Windows one magpie is left, not two — a magpie running when another updated the binary (it runs on from magpie.exe.old) no longer downloads the update again to magpie.exe.new and swaps it in when it quits; it says a restart is all that is left. A magpie.exe.new that is magpie.exe over again is removed at start. The tray that found the gateway taken serves it once the magpie that had it is gone, checked every 15 s 2026-09-26 14:30:16 +08:00
yetone 7c3fa7d19e update: on Windows an update goes in while what the last one moved aside still runs — a magpie serve started before an update runs from magpie.exe.old, which then can't be removed, and moving the running exe onto it failed with "Access is denied"; it now goes beside it (magpie.exe.old-2…), the leftovers are removed once free, the download is kept for another try, and the Update button says Update failed (the error in its tooltip) instead of seeming to do nothing. Reproduced on Windows 11 with a running .old: the old swap fails, the new one passes 2026-09-26 01:38:37 +08:00
yetone e8b3be572c windows: the desktop app no longer opens a console window for every command it runs (#54) — it is linked as a GUI program and has no console, so each console CLI it started (devin auth status and models list, devin acp for every Devin turn, claude, codex, cursor-agent and grok probes and runs, grok login) was given a new console by Windows, and a Windows Terminal window with it, some every few seconds while the app sat idle. Every command magpie runs is now made through internal/proc, which starts it with CREATE_NO_WINDOW when magpie has no console of its own: the child gets a console without a window, and its output, which magpie always pipes back, arrives as before. A magpie run in a terminal (magpie serve, the TUI, the desktop app started from one with a command) has one, and its children share it as they did, so Ctrl+C there still reaches them. The relaunch after an update keeps its DETACHED_PROCESS, added to rather than replacing the flags it was made with, and a test fails on any command made with os/exec directly, so a new one can't bring the windows back 2026-09-25 14:04:44 +08:00
yetone 90580ca7e1 update: Update updates magpie in place — a click downloads the new version and restarts into it, asking for the password when magpie can't replace itself, instead of opening the release page; only a magpie running from its disk image or a temporary copy is sent to the download page, with why 2026-09-25 01:35:24 +08:00
yetone 65fdcb1a14 update: the download shows how far along it is — percent and megabytes in Settings — a dropped connection is tried twice more, and a failure says why instead of only that it failed (#21) 2026-09-24 15:07:35 +08:00
yetone 75cb192647 Desktop app for Windows and Linux
The Windows app cross-compiles (WebView2, no cgo) as a GUI program with an
icon, version info and a DPI-aware manifest; run from a terminal with a
command it borrows the console. The Linux app links GTK 3 and WebKitGTK
4.1. Off the Mac the app updates its own binary from its own build:
downloaded in the background, swapped in on restart or quit, and the
relaunched process waits for the old one before taking the gateway. The
window leaves room for traffic lights only on a Mac.
2026-09-24 03:22:02 +08:00
yetone 73ef0e5276 usemagpie.ai, and a magpie that updates itself
site/ is the website: a static page plus a worker that turns the newest
release of yetone/magpie-releases into an update feed (/api/latest, with
every file's SHA-256), stable download links and an install script.

The app asks the feed at start-up and every six hours, downloads a newer
version in the background, and installs it on Restart to Update or on
quit. A download must match its checksum and be signed by the same team
as the app it replaces. `magpie update` does the same from a terminal and
replaces a bare binary in place.
2026-09-24 01:57:50 +08:00