* 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.
* aside: magpie picks Aside's model, its thinking level and its four task models (X: junjie — Aside Settings → Models has a Default model row magpie could not touch, and Fast/Standard/Deep/Visual rows it could not reach at all)
Aside reads the same provider list Pi does, so magpie hands it the whole
catalogue in a provider block of its own, carrying the magpie-aside token so
its requests are told apart in usage. What differs is the default: Aside keeps
it as one object in ~/.aside/u/0/settings.json naming the provider, the model,
the thinking level and fast mode together, so the model field reads and writes
two keys where Pi's are two strings, and both go through Aside's own settings
API — the running daemon holds its default and writing the file behind it
leaves the daemon on what it already had, read back the same way with the old
model still named after magpie had written the new one. The API is reached
through the repl, the settings command having no set of its own.
The four task roles are the same shape as the default and are wired the same
way, each a Quiet field following the model, so a role the user has not set is
not a line of its own and follows the default. A role is written whole:
thinkingLevel is required by Aside's schema and a role without one is refused,
so a role keeps the level and fast mode it had, a new one takes the default's,
and where neither is set it takes the level Aside gives its own roles. Taking
magpie out puts every role back, the user's own model where there was one and
nothing at all where magpie was the only thing that set it, and a role the user
moved onto a model of their own is left alone.
Each change checks the value that came back: Aside answers a set with the
settings as they now are and drops what it will not take without a word — a
role naming a model it has not resolved yet is the one that happens — so
without the check a change that went nowhere read as one that landed. Caught,
it is written to the file, which is what Aside reads at its next start, and
the Agents page says so.
The account magpie wires is 0: the folder is ~/.aside/u/0 while the CLI names
it u0, and TestMain stands in for the binary so a test can never change the
real Aside's default. A models.json carrying a magpie provider of someone
else's is refused rather than written over, and one that will not parse is the
user's to fix.
With the owner's real Aside under a sandbox HOME: a pick reached the gateway
and was attributed to a client named Aside, the default put the user's own
minimax model back and took the magpie provider out, and a role set through
magpie was read back by the running daemon the moment it had a magpie model to
resolve. Not tried: a change written to the file taking hold at Aside's next
start, which needs a restart. go vet, builds (darwin, GOOS=linux,
GOOS=windows) and go test -tags nogui -count=1 ./... pass (TestBunThroughBridge
fails, reproduced on a pristine worktree of the parent; internal/plugin passes
alone and times out only under the full run's load).
* aside: the row appears for an Aside that is installed and not yet used (the CI preview of #789 could not find .row.agent[data-id="aside"])
Detected() took ~/.aside/u/0, the account folder, and that only exists once
someone has signed in. The binary was the other look and the CLI installer
links it into ~/.local/bin, which a magpie launched from Finder has no PATH
entry for, so an Aside that was installed and never used showed no row at all —
the preview installed one, drove the UI and could not find it in either the
installed list or the row selector. The installer writes ~/.aside/cli and the
daemon writes the rest, so ~/.aside is the folder that is there from the
install, and it is the one Detected() should ask about. Path stays the
account's settings.json, which is the file magpie edits and the only one
worth reading a model out of.
TestAsideIsDetectedBeforeAnyAccount fails on the old Dir: it takes PATH away
so the binary cannot be found either, makes the installer's folder and no
account, and expects the row.
* aside: a wedged repl gives up, and the model comes off through Aside (yetone on #789)
Three things about the way out of an agent and into one, all found on a real
Aside.
The repl had no timeout. proc.Command(...).Output() waits as long as the
command does, so a daemon that has wedged or one waiting on input leaves a pick
in the Agents page or the CLI hanging on it, and switching off asks up to five
times over — the default and four roles. proc.CommandContext with ten seconds:
Aside answers in milliseconds, and a run that does not is written to the file
instead, which is what Aside reads at its next start, with the notice that says
so. TestAsideGivesUpOnAReplThatHangs runs the real command against an aside
that never returns: it passes in the timeout, and without CommandContext it
hangs until the test's own timeout kills it.
Switching off with no model of the user's behind magpie's wrote settings.json
directly. By then the provider block was gone, so the running daemon went on
using magpie/… with no way to reach it, and asideStale was never set, so
nothing said the change needed a restart. It goes through asideApply like
every other change, with the value that came back checked.
The provider block now goes last, after the default and the four roles are put
back. A step that fails on the way out otherwise leaves the settings naming a
provider that is no longer there. TestAsideDefaultTakesTheProviderOutLast
watches the block from inside the settings step and fails on the old order.
The icon is Aside's own brand-embossed-light.svg with the embossing's
transparency taken off, so the mask is solid and takes the row's text colour
like every other agent. As it was, the white fills at 8–20% opacity the dark
and light brand files are built with left the mark almost invisible on a light
row, and it vanished entirely in dark mode. Same paths, same shape, now solid;
checked in the app next to Pi's.
Tested against Aside 1.0.928.1 (daemon 1.26.928, CLI 1.26.916.1741) on
darwin/arm64. go vet, builds (darwin, GOOS=linux, GOOS=windows) and
go test -tags nogui -count=1 ./internal/agent pass (408).
* aside: the model Aside draws with is magpie's to set too (X: Aside Settings → Images had no row for it)
Aside keeps a fifth model setting beside the default and the four task roles:
imageGenerationModel, the provider and the model it draws pictures with and
nothing else — no thinking level, no fast mode, since neither means anything for
a picture — and no key at all on an account that has never picked one.
It is a magpie field like the roles: Quiet, so it is not a line of its own on an
account that has none, and taking the model after while it is unset. A model of
magpie's needs the provider block to reach it through, and what it named before
is remembered the same way a role's is, so `magpie aside model default` puts the
user's own picture model back and takes a picture model of magpie's off rather
than leaving it naming a provider that is no longer there.
Not wired: customModels, which is the user's own model definitions with their own
credentials, and asideModelCatalog, a list of model ids to pin or hide. Neither
is a model magpie picked, so neither is a setting of magpie's to own.
Aside 1.0.928.1, daemon 1.26.928. go vet, the agent/gui/profile tests, the three
CLI cross-builds and the CLI verification under a temp HOME all pass.
* fix(gui): a reply that broke off is told as failed everywhere
733's tryOk rule — a try with a status under 400 and no fail didn't
answer — was put through the Routing page's log and its try tally, but
every place that judges a request as a whole still read the status alone:
a request whose reply had begun and then broke off (its 200 sent, the
vendor's error after it) kept an answerer it never had. Its row was a
green dot reading "provider · model" as if it answered, the replay and
the Agents panel told the same, and the "errors your agent saw" stat,
which counts the request, picked nothing when clicked since it only knew
statuses of 400 and up. The log of the very request said it began
answering and then broke off, so the page disagreed with itself.
outcome() asks tryOk for who answered, and a route with an error is bad
even under 400; the swap and routed marks, on the row, the seat and the
log, only show on a try tryOk answers; the stat's click knows an error;
and the two play animations send a try with a fail back as failed. The
row, the replay, the Agents panel, the stats and the animations now tell
the same story the log does.
routing-broke-off.test.cjs — Chromium and WebKit, English and Chinese —
fails on the old page with the row's class "rt-req ok" and passes: the
row is bad, the errors stat picks the request, and its story says it
began answering and broke off.
* fix(gui): a 200 with a note of its own is still an answer
yetone's review of the first cut: outcome() read a route's error field
for "bad", but a 200 whose error is magpie's own note answered — Codex's
titles off in Settings writes one, as a title reply with no title in it
does, and their tries carry no fail. Every titled thread's first request
turned red. Reads the tries now, as the rest of the rule does: a route
under 400 is bad only when it has tries and none answered.
The Agents panel's row said so too: its class came from the outcome, but
its "where it went" text still branched on the status alone, so a
broken-off request showed red with "→ provider" and no failure. It
branches on the outcome now.
routing-broke-off.test.cjs gains the note case (200, an error, a try with
no fail): its row is "rt-req bad" on the first cut and "ok" with this.
The broke-off case's rows, the errors stat and the story still hold, on
Chromium and WebKit, in English and Chinese.
Validation on Windows: node --test routing-broke-off.test.cjs — 8/8 on
Chromium and WebKit in both languages; both cases fail on the code
before this commit's routing.js (the broke-off row as "rt-req ok", the
noted one as "rt-req bad").
* fix(gui): the errors stat picks a broken-off reply, not a noted 200
yetone's second round: the stat's click looked for a status of 400 or an
error, which a 200 whose error is a note of magpie's own — Codex's titles
off in Settings, a title reply with no title — answers: every titled
thread's first request was a newer "error", and "show the latest request
that failed" jumped to a green row. It asks the same question the rows
and the story do now — outcome(r)[1] === "bad" — so the stat, the rows and
the story name the same request.
routing-broke-off.test.cjs puts the noted 200 above the broken-off one:
clicking the stat picks the broken-off request under it, and the noted row
is not picked. That fails on the previous commit (the noted 200, newer and
with an error, was picked) and passes. Two test fixes go with it: the
unpicked row's aria-pressed is "false", not absent, and the day list is
waited for before it is clicked, which webkit needed mid-run.
Every string the window, the panel and the menus have in Chinese now has a Japanese one too: 2917 entries in I18N.ja, translated to one glossary (プロバイダ, エージェント, ゲートウェイ, ルーティンググループ, 使用量, サインイン…), each keeping its {placeholders}, tags and leading spaces. Language takes "ja" (settings.Langs); System picks ja when navigator.language starts with ja, and <html lang> is ja-JP so CJK text draws in Japanese glyphs. Dates and numbers format through intlLang() (zh-CN or ja-JP) where they said zh-CN only. The tray menu, the quota alerts (7 日枠を 80% 使用、明日 14:30 にリセット) and the restart-to-update item have Japanese of their own (trayLang reads ja from the setting or a ja_JP/ja-JP system). Release notes stay English for ja, as the site has them in en and zh. The 万/亿 units option stays Chinese only.
Looked at in the Omarchy VM (magpie web, tokyo-night, ?locale=ja): Agents, Providers' Add sheet, a link's import, Routing, Usage, Gateway, Settings and the panel read in Japanese with nothing cut or overflowing. gui-ja.test.cjs (every zh key has a ja one with the same placeholders and tags; ja picked, a ja-JP system, en and zh systems, on WebKit and Chromium) and the ja cases in TestTrayMenuLabels and TestAlertText fail on the old code. On WebKit the other GUI tests fail as on the base but mobile-web's desktop comparison with origin/main, whose Settings now has a fourth language. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
The Routing page's group list followed the order the groups were made in, found ones after, so a group used every day couldn't be put first, though /v1/models lists the groups in that order and agents' pickers (Claude Desktop's 3P list among them) show it and some clients probe its first model. A group row's logos are now its handle, as an agent row's logo is (#57): dragged (dragRows, the Agents page's sorter) it moves the row, clicked, or the row right-clicked, it opens Move up and Move down (openRowMenu, with the Agents menu's arrows, now MOVE_UP/MOVE_DOWN), and Alt+↑/↓ moves it from the keyboard, which stays on it. The grip in the row's margin is drawn only while the row is under the pointer, dragged, or its handle is focused or its menu open, as on the Agents page. The list is drawn in its new order at once and put back if the save fails; a later move wins over an earlier one still saving.
The order is kept in providers.json as groupOrder (provider.SetGroupOrder, POST /api/groups/arrange), beside the providers' own order (#499), and applied in groupsIn, which every list of the groups is made from: the Routing page, Catalog and so /v1/models, magpie group ls and the TUI's routing page. A found group, which isn't stored until changed, can be put among the user's this way, which reordering the groups array couldn't do. A group not named (one made or found after) goes last; a renamed one keeps its place. Backup and sync carry the order as they do the providers' (MirrorGroupOrder).
TestGroupOrder (Groups and the catalog follow SetGroupOrder, providers.json has it, a new group last, a rename in place, bad orders refused, a synced order first) fails on the old code. group-arrange.test.cjs (WebKit, en and zh): the grip hidden with the pointer away and shown on the hovered row only, in its margin; right-click Move down with Move up off on the first; Alt+↓ and Alt+↑ with focus kept; the handle's click opens the menu, not the editor; a drag by the grip to the top; each posting every group's id with the removed one last; no click moving the page. It fails on the old code. agent-grip, click-scroll, account-arrange, usage-arrange, provider-order and the group-*/routing-* GUI tests pass in WebKit, but for group-from-model-add's 600x420 cases, which fail on main as well. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
Each saved search API with a key gets Show and Copy next to Remove. Show asks the new POST /api/settings/search-key for the key (served on loopback, as provider/key is) and puts it in the row's line in place of the masked one, selectable; Hide puts the masked one back. Copy copies the key without showing it. The settings still list keys masked. search-api.test.cjs checks Show/Hide/Copy (fails on the old app.js) and TestSearchKeyShown the endpoint. GUI: search-api, searcher-pick, click-scroll pass on WebKit; go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
* fix(provider): mark a window one model's own as capped, as routing holds it
Routing held an account at its usage cap when any window that counts the
model was at or past it, Opus's own window included: CapHeld walks the
allowance with Limit.applies, and a window naming one model counts every
request whose id holds that word. WithCapped, which tells the GUI which
windows a cap counts, left those windows out — it marked a window only
when it named no model. So a Claude account kept at 70% whose Opus week
was at 90% was barred, and then answered 429 with nothing left when it was
the only account, while the Providers page showed the account in use, no
"At its cap" note and no tick on that window's meter. The rows said the
opposite of what routing was doing, and the cap look broken.
WithCapped now marks a window as capping as CapHeld holds it: not Aside,
not Unlimited, one model's own window like the rest. The GUI needs no
change: capHeldOf, capHeldNote and capMark already read Capped, and a
mark on a per-model window says the cap holds that window's model — the
window's row already names its model. The 429-when-every-account-capped
path is untouched: capReached counts only the windows that stop an
account for every model, as its comment says, and is not what WithCapped
marks.
TestWithCappedMarksPerModelWindows fails on the old rule with "the window
one model's own is not marked" and passes: the account-wide window, a
window naming "opus", on-demand spending (Aside) and an unlimited one are
marked, not marked, not marked and not marked, and the windows handed in
are left as they were. TestCapHeld, which spells out that Opus's window
counts for Opus alone, and the rest of the cap tests are unchanged.
Validation on Windows: go test ./internal/provider passes whole;
TestWithCappedMarksPerModelWindows fails without the change. go vet
./internal/... and the darwin, GOOS=linux and GOOS=windows builds pass.
The full go test -tags nogui -count=1 ./... under a temp HOME leaves the
same Unix-only failures as the base commit does on Windows (Keychain,
mode bits, shell-script CLIs, skills' folders); the provider package
passes whole. account-cap.test.cjs's fixtures carry capped on
account-wide windows, so the data change does not touch them; Playwright
is not installed in this worktree, so it was not run.
* gui: an account held at its cap for some models says which, not that it is held
A window that counts some models only (Opus's week, a pool's) is now
marked Capped, so the Providers page said "At its cap" and "sends
requests to the other accounts" for an account routing still used for
every other model. WithCapped also sets CapsSome on such a window;
capHeldOf tells those from the account's own windows, and an account
held by them alone reads "Opus week at its cap · back in 3h", its
tooltip saying other models still use it. When an account-wide window
holds it, that window's renewal is the time it is back.
account-cap.test.cjs: a Codex account whose five hours and week are
under its 70% cap but whose Opus week is at 90% names the Opus week, in
English and Chinese; it fails on the PR's head.
---------
Co-authored-by: yetone <yetoneful@gmail.com>
Search: Code Assist runs googleSearch for Gemini, but Antigravity turns it down beside function tools (400 "Please enable tool_config.include_server_side_tool_invocations to use Built-in tools with Function calling", tried with that flag in toolConfig too), so searchesFor (searchesItself for one request) says a Google sign-in's Gemini searches by itself for a request without tools; buildCodeAssistSent then sends tools [{"googleSearch":{}}], and codeAssistDecoder reads the candidate's groundingMetadata (webSearchQueries, groundingChunks[].web) as a search the client is told of (Anthropic's server_tool_use + web_search_tool_result), once. A request with tools — every agent's — is given magpie's search tool as before, and its searches go to the conversation's own account first (ownSearcher, as a Kimi Code plan's do): searchWith on its Gemini without tools, so googleSearch runs there and no other subscription is spent. A Google sign-in is also a searcher for other models, last of those magpie picks by itself (rank 5; relays named by hand move to 6), and is offered in Settings → 代搜供应商, with its Gemini models only; the row says such sign-ins search for their own models first. smallModel took every Gemini for a small model ("gemini" has "mini" in it), so the first listed, gemini-pro-agent, searched; it now picks a Flash.
Images: Drawers lists the image models a Google sign-in has (gemini-3.1-flash-image), and draw asks them on Code Assist through the account (drawCodeAssist, askTranslated with the conversation's envelope); buildCodeAssistSent asks an image model for responseModalities ["TEXT","IMAGE"], which a chat with one now gets too. AutoDrawer doesn't pick them by itself (no known price), so nobody's automatic drawer changes.
With the owner's real Antigravity account (a copy of the config, a sandbox gateway): a web_search with no other tool on gemini-3.8-flash-low came back with Google's grounding as web_search_tool_result; with a bash tool beside it the model called magpie's search, which went to antigravity/gemini-3-flash (not Codex) and gave pages; /v1/images/generations with antigravity/gemini-3.1-flash-image drew a 1024x1024 JPEG. TestCodeAssistSearchesWithoutTools, TestCodeAssistDecodesGrounding, TestCodeAssistImageModelAsksForImages and TestSmallModelOfGemini are new; searcher-pick.test.cjs checks the note. go vet, builds (darwin, GOOS=linux, GOOS=windows), go test -tags nogui -count=1 ./... under a temp HOME and the searcher GUI tests (webkit) pass. Not tried with a Gemini CLI sign-in, which takes the same path.
* fix(gui): open Help Learn More in the system browser
* gui: set the Help menu on macOS only — on Linux a set application menu becomes a menu bar in every window, the tray panel's too (Wails' linux window takes globalApplication.applicationMenu when it has no menu of its own); Windows shows it only with UseApplicationMenu, which magpie doesn't set
---------
Co-authored-by: yetone <yetoneful@gmail.com>
* agent: AtomCode is wired like its own CodingPlan sign-in — one provider account, one model table per catalog model
AtomCode (atomgit.com/atomgit_atomcode/atomcode, AtomGit's open Rust terminal agent) keeps its settings in ~/.atomcode/config.toml ($ATOMCODE_HOME). Its account schema, what its own CodingPlan sign-in writes, is one [provider_accounts."magpie"] table at the gateway's /v1 (protocol openai, gateway.TokenFor("atomcode") as the key, so a request is its agent's even when the UA is overridden) and one [models."magpie/<provider>/<model>"] table per catalog model: the model string goes to the gateway as it is to resolve, with the model's context window (AtomCode compacts as it nears it; a 128000 one is told when the model's isn't known, and an output above the told window is cut to it), its output limit, vision and reasoning_effort_levels; reasoning_effort rides on the table once the user picks one. Picking a model points default_provider and default_model at it — default_model wins when both are set, as atomcode reads it — and stashes the defaults the user had; stepping out puts them back. The picker offers the user's own model tables and the catalog via magpie; the Library gives AtomCode its ATOMCODE.md, its mcp.json (omp's shape) and skills/; its UA is atomcode/<version>.
Found against a real atomcode 5.2.1: the documented legacy [providers.*] tables work, but its own sign-in writes the account schema, so magpie writes that; reasoning_effort, max_tokens and supports_vision were verified to reach the request from the model table, and default_model to win over default_provider. Verified end to end on a real install: atomcode --provider "magpie/…" answered through the gateway, and the Agents page shows the tile, its picker and the Usage attribution. TestAtomcodeWiring, TestAtomcodeRestoresOwnDefaults and TestAtomcodeSync fail on the old code; TestDriftUnwiredEveryAgent and TestUnsetAgentsReadEmpty cover it from All(). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/agent ./internal/library ./internal/agentenv pass; the plugin-host and timing failures of the full ./... run fail on main without this change too.
* tweak
* agent: AtomCode's config is matched by its own spellings, and Sync keeps the effort — AtomCode rewrites its config through serde on every /model and /cd, and that writes the account header bare ([provider_accounts.magpie]) where magpie wrote it quoted, so after the first rewrite Check read the wiring as gone, Sync and every pick appended a second account table (edited TOML is invalid: table magpie already exists) and clearing left the unquoted table and its api_key behind. The account is now matched by exact name in its three spellings (quoted, bare, literal — magpie-2 stays out) and the model tables by their decoded keys (kimiModelKey, replacing atomcodeModelName), through a new SetTOMLTablesMatching that takes exact names beside prefixes; SetTOMLTables delegates with none, unchanged. Sync carries each magpie model table's reasoning_effort over when rewriting — it ran at every gateway start and catalog change and dropped both an effort picked in magpie and one picked with AtomCode's /effort, and picking another model cleared every table's. The effort Set validates against the levels AtomCode knows (low, medium, high, xhigh, max — its clamp_effort_to_levels drops none, minimal and ultra), a model without levels offers none now, and the low/medium/high fallback is gone with it. supports_vision is written explicitly false, where AtomCode otherwise guesses from the model name. The user's legacy [providers.*] entries join the picker, a stashed default_model naming one comes back on Disconnect, and picking one sets default_provider and takes default_model out, as the legacy shape reads. site docs (en, zh, ja) list AtomCode. TestAtomcodeSync rewrites the config into the bare and single-quoted spellings before Sync and again before clearing and fails on the old code (drift reported, effort dropped, unquoted table left behind); TestAtomcodeLegacyProvidersArePickableAndRestored and TestAtomcodeEffortLevelsMatchAgentSupport are new. Reported by yetone on #736. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/agent ./internal/edit pass.
* agent: TestAtomcodeWiring picks an effort through the field too — the write side of the validation the last commit added (a level AtomCode knows lands as reasoning_effort on the model table, one it does not know is refused) had no coverage left: the test only read back what AtomCode's own /effort had written. provider.SetModelEfforts gives fake/m1 levels to offer.
* agent: Disconnect puts AtomCode's default_provider back only where AtomCode still has it — the restore checked the stash's value was not magpie's but not that the table it names was still there, so a table the user had deleted in between came back as a dangling default (the account schema's default_provider names a model table, the legacy shape a [providers.*] one; own answers for both). The default_model branch checked already. TestAtomcodeRestoresOwnDefaults deletes the stashed provider's model table before clearing and fails on the old code.
* agent: AtomCode joins the CLI updates — npm @atomgit.com/atomcode, the brew cask atomcode, and the curl script's ~/.local/bin/atomcode updating itself with atomcode upgrade, so the Agents page shows its version and a newer one as the other agents' do (#736). TestHowInstalled covers the npm prefix, the cask and the self-updating installer.
* agent: AtomCode's self-updater waits for its install paths to be confirmed — the branch recognized a binary under ~/.local/bin as `atomcode upgrade`, on a docs page, while the README shows another path and the command has not been driven outside the help text; the reviewer asked for it removed until the standalone command and the script's real default are settled. npm and the brew cask stay, with their tests; a comment marks what is pending.
---------
Co-authored-by: googya <118465+googya@users.noreply.github.com>