The repo's .npmrc sets engine-strict=true and package.json pins
engines.npm, so an npm outside that range aborts every npm ci /
npm install we run inside the checkout:
npm error code EBADENGINE
npm error notsup Required: {"npm":"<11.10.0 || >=12.0.0"}
npm error notsup Actual: {"npm":"11.10.0"}
Our callers made that worse: _run_npm_install_deterministic sees
`npm ci` fail and falls through to `npm install`, which fails
identically, so the user got a buried EBADENGINE and no remedy.
React to the failure instead of predicting it. npm states the
required range in its own error, so there is no need for a version
probe on the happy path or a semver range matcher — the recovery
reads the constraint out of the output it just produced, upgrades,
and retries once.
Scope is deliberately narrow. Hermes only upgrades an npm inside its
own managed Node tree ($HERMES_HOME/node), installing with --prefix
so bin/npm keeps resolving to the upgraded lib/node_modules/npm; a
managed install writes prefix=~/.local into node/etc/npmrc, so
without the override the "upgrade" would land elsewhere while the
managed npm stayed stale. A system / nvm / brew / Nix npm belongs to
the user, so that case prints the exact command and lets the original
failure stand.
The upgrade runs from a temp cwd with npm_config_min_release_age=0,
otherwise the checkout's own min-release-age gate would refuse the
npm release we need.
_run_npm_install_deterministic's capture_output=False callers (the
desktop install) streamed npm output and returned stderr=None, which
would leave the recovery nothing to read — stderr is now teed, so
live output is unchanged and the text stays inspectable.
Verified end to end against real npm binaries on copies of a managed
tree: managed npm 11.10.0 -> EBADENGINE -> upgraded to 12.0.2 ->
retry exits 0; a foreign npm 11.10.0 hard-fails with the manual
command and is left untouched.
pin brace-expansion to 5.0.8
update concurrently to 10.0.4
update electron-builder to 26.15.3
update eslint to 10.8.0
update eslint-plugin-perfectionist to 5.10.0
update @assistant-ui/react to 0.15.0
update @assistant-ui/react-streamdown to 0.3.8
update radix-ui to 1.6.7
update react-router-dom to react-router@8.3.0 - react-router-dom is no longer a standalone package, it just reexports react-router
remove @radix-ui/react-slot: we import this from `radix-ui`
remove eslint-plugin-react: we imported it, but never actually used it!
The two exact-list assertions in test_tui_gateway_server froze the toolset
list and broke the moment _RECENTLY_SHIPPED_TOOLSETS back-filled bfl onto a
saved platform list — the exact behavior the sibling change ships on purpose.
Assert the invariant instead: the expected base set is present, and anything
extra must be inside _RECENTLY_SHIPPED_TOOLSETS (vacuously exact again once
that set empties between releases).
send_video got the 60s read_timeout but send_voice/send_audio/send_photo/
send_document/send_media_group/send_animation upload through the same PTB
request path and hit the same server-side processing wait before the
response arrives. Same class, all sites: they all pass
_MEDIA_SEND_READ_TIMEOUT now. Also drops an unused test helper.
ChatView migrated tip-keyed composer queue/draft entries onto queueSessionKey
whenever the two ids differed. queueSessionKey is route-driven and can flip to
Session B a frame before the store selection leaves Session A, so migrate
re-homed A queue entries onto B and the idle ChatBar auto-drained them into
the wrong chat.
Gate migrate on same-conversation lineage only (tip to root). Also honor
lineage when background queue drain decides selected/busy, so a root queue key
is not treated as idle/offscreen while the compression tip is still working.
The row fell back to the global $repoStatus whenever repoPath was blank,
painting the main pane's branch and ± onto a tile whose cwd hadn't
resolved yet. The fallback bought nothing — the primary computed is keyed
to $currentCwd, which is empty in exactly that case — and cost a rail
showing a tree the session was never in.
A tile and a branched session each live in their own worktree and render
from their own SessionView slice, so neither is the main pane's session.
Pass foreground: false at both call sites.
applyRuntimeInfo unconditionally mirrored a runtime's cwd, branch, model
and usage into the global composer atoms. Every tile create and session
branch called it, so opening a session in another worktree re-pointed the
MAIN pane's coding rail at that tile's repo — and persisted it, so the
wrong workspace cwd survived a restart.
Collect the patch first, then mirror it once behind a `foreground` gate.
Background callers still get the full patch for their own session state;
they just stop publishing into state they don't own.
main now labels each panel row's kebab with the row's name
(menuLabel={profile.name}), so the hardcoded "Actions" default this test
relied on no longer exists. The name alone is ambiguous — the row-select
button carries it too — so match the menu trigger via `expanded`.
Neither side conflicts textually, so this only surfaced once main merged in.
Addresses review on #73013.
1. Manage Profiles used a hand-rolled delete Dialog next to the shared
DeleteProfileDialog in the same folder. That copy missed the active-
profile re-home fix (f764b0400): deleting the profile the gateway is
on stranded it on a dead backend. Switch to the shared dialog, which
owns the deleteProfile call and re-homes to default. Drops
handleConfirmDelete, the deleting state, and the now-unused Dialog*
imports.
2. The name field regressed to a plain Input during the create-dialog
dedup, losing live slugging. Level both shared dialogs up to
SanitizedInput sanitize={slug} so every entry point gets the behavior
Manage Profiles had — the sanitize primitive means callers never
validate-then-reject.
3. Nothing rendered ProfilesView, which is how the drift got in. Add a
behavior test: create dialog exposes SOUL.md, deleting the active
profile re-homes to default, deleting a non-active one does not.
The Manage Profiles page had its own local CreateProfileDialog/
RenameProfileDialog copies that predated the shared dialogs in
create-profile-dialog.tsx / rename-profile-dialog.tsx. The local
create copy lacked the SOUL.md textarea, so New Profile from the
sidebar rail and New Profile from Manage Profiles rendered different
modals.
Delete both local duplicates and reuse the shared self-contained
dialogs (they own the createProfile/renameProfile/updateProfileSoul
calls), so both entry points show the same modal including SOUL.md.
test_generated_script_contains_umask_else_branch asserted on shell
script text ('else', 'umask', '(0666 & ~0', 'chmod') rather than
behavior — a change-detector test per AGENTS.md. The behavioral
test (test_new_file_gets_umask_default_permissions) already
covers the actual behavior end-to-end via real subprocess.