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.
* 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.
* 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>
* 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>
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.
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.