Public: anyone can read this; the crew can change it.
Stop a shell, or stop all idle ones, from your account and through MCP #10630
Check · Review · Land · Live done — Landed and live in production.
You can now stop one of your shells, or all your idle ones, from the account page and through the `sessions` MCP tool. The new shell tests, `test/mcp-size.test.ts` (the MCP size ceilings), `tsc`, `scripts/gate.ts` and `scripts/design-lint.ts` pass. I also ran the MCP, settings, palette and terminal-screen suites (1428 pass, 4 skipped, 0 fail). I did not run the full `bun test`; the forge runs that…
shipped production is serving it deploy 4776c66
You can now stop one of your shells, or all your idle ones, from the account page and through the sessions MCP tool. The new shell tests, test/mcp-size.test.ts (the MCP size ceilings), tsc, scripts/gate.ts and scripts/design-lint.ts pass. I also ran the MCP, settings, palette and terminal-screen suites (1428 pass, 4 skipped, 0 fail). I did not run the full bun test; the forge runs that on push. I did not start the app and click through it.
What changed
- Stop:
POST /settings/shells/:id/stopends one of your live shells. A session id that isn't yours or isn't live gets a 404, the same as an unknown one. A cross-origin post is refused with a 403. - Stop all idle:
POST /settings/shells/stop-idleends every shell of yours with nobody attached and past half the idle limit. A shell with a peer attached is left alone. - Receipt: each route redirects back to
/settings/shellswith a one-line receipt ("Shell stopped.", or "Stopped N idle shells." / "No idle shell to stop."). - Account page: each live line has a Stop button, and Stop all idle appears above the list whenever you have a live shell.
- End record: stopping records
end_reason = 'stopped'and hangs up the pty in the cell, as the idle path already does. The logic isstopShellandstopIdleShellsinsrc/machines/shell.ts. - MCP:
sessionswithkind: "shells"takesaction: "end"with anid, andaction: "end_idle". They say the same words as the page. No new tool and no ceiling raised. - Tests:
test/my-shells.test.tscovers stop recordingstopped, a stranger's id getting a 404, a cross-origin post getting a 403, an attached peer surviving stop-all-idle, and the MCP end and end_idle receipts.docs/features/my-shells.mdis updated.
Chosen not to do
- Stop uses a plain inline form, not the
/-/confirmpage, because stopping a shell loses nothing the forge keeps. - No new Settings sidebar row, for the same first-view budget reason the previous issue gave.
Try it
Open a machine shell, go to /settings/shells, and press Stop or Stop all idle. Over MCP, call sessions with {kind: "shells", action: "end", id} or {kind: "shells", action: "end_idle"}.
The first push was rejected by the repository's pre-push hook, which wants a "Try:" line in a commit message. I added an empty commit with one.
🤖 Generated with Claude Code
The page and MCP halves work and are well tested, but two things must change.
- Both doors: the terminal has no way to stop a shell. src/tui/screens/shells.ts lists shells but offers neither stop nor stop-all-idle. Add both there, calling stopShell and stopIdleShells with the same receipts, and cover them in a test. This comes first.
- Tests: the POST /settings/shells/stop-idle route is never exercised. Only the stopIdleShells function is called. Add a route test that posts it, checks the 303 and receipt, and checks that a cross-origin post gets 403. Also add an MCP test where another person cannot end your shell id (not_found), and a check that the page renders the Stop all idle button when a live shell exists.
Everything else (404 for strangers, same-origin via readForm, end_reason stopped, shared receipt words, docs in docs/features and not README, no new dependency) is fine.
You can now stop a shell, or stop all idle ones, from your account page, the terminal and MCP, and each door gives the same receipt. tsc, scripts/gate.ts and test/my-shells.test.ts plus test/tui-my-shells.test.ts pass (9 tests). I did not run the full suite; the forge's check does that on push.
Terminal screen (src/tui/screens/shells.ts):
j/k(or the arrow keys) pick a line.scallsstopShellon the picked live shell.icallsstopIdleShells.- The receipts come from the shared
STOPPED_ONE,NO_SUCH_SHELLandstoppedIdleSaysand show as a toast. son an ended line answers "No such shell."- The key hints list the new keys.
docs/features/my-shells.mdnow says how to reach Stop and Stop all idle in the terminal.
Tests (test/my-shells.test.ts):
POST /settings/shells/stop-idlegives a 303 with the receipt on the page, and a cross-origin post gets 403.- A shell with a peer attached is left alone.
- The page renders the Stop all idle button when a live shell exists.
- Over MCP, another person's token gets
not_foundfor your shell id, and the shell stays live. - In the terminal,
ion a live shell with a peer attached says "No idle shell to stop.";ssays "Shell stopped." and the shell ends. test/tui-my-shells.test.tscoverssandion an ended line.
Not covered: the route test cannot show stop-idle ending a shell. The route reads the admin idle limit, which has a 5-minute floor, so no shell qualifies as idle in a test. The route test checks a shell with a peer attached survives and the zero-stopped receipt. The "stops idle ones and says how many" path is covered only by the existing function-level test, which passes the limit in.
Since b804cb71db45: the terminal door is added (j/k pick, s stop, i stop all idle, toast receipts from the shared constants), docs/features/my-shells.md carries the how-to (README untouched), and tests cover the terminal and the route paths. Earlier asks are met. Both routes are same-origin form posts that check who; a stranger gets a 404; MCP and the terminal share stopShell/stopIdleShells, so the words match. Non-blocking: the route test cannot show stop-idle ending a shell because of the admin idle floor; the function-level test covers it.
You can now stop one of your shells, or all your idle ones, from the account page and through the
sessionsMCP tool. The new shell tests,test/mcp-size.test.ts(the MCP size ceilings),tsc,scripts/gate.tsandscripts/design-lint.tspass. I also ran the MCP, settings, palette and terminal-screen suites (1428 pass, 4 skipped, 0 fail). I did not run the fullbun test; the forge runs that on push. I did not start the app and click through it.What changed
POST /settings/shells/:id/stopends one of your live shells. A session id that isn't yours or isn't live gets a 404, the same as an unknown one. A cross-origin post is refused with a 403.POST /settings/shells/stop-idleends every shell of yours with nobody attached and past half the idle limit. A shell with a peer attached is left alone./settings/shellswith a one-line receipt ("Shell stopped.", or "Stopped N idle shells." / "No idle shell to stop.").end_reason = 'stopped'and hangs up the pty in the cell, as the idle path already does. The logic isstopShellandstopIdleShellsinsrc/machines/shell.ts.sessionswithkind: "shells"takesaction: "end"with anid, andaction: "end_idle". They say the same words as the page. No new tool and no ceiling raised.test/my-shells.test.tscovers stop recordingstopped, a stranger's id getting a 404, a cross-origin post getting a 403, an attached peer surviving stop-all-idle, and the MCP end and end_idle receipts.docs/features/my-shells.mdis updated.Chosen not to do
/-/confirmpage, because stopping a shell loses nothing the forge keeps.Try it Open a machine shell, go to
/settings/shells, and press Stop or Stop all idle. Over MCP, callsessionswith{kind: "shells", action: "end", id}or{kind: "shells", action: "end_idle"}.The first push was rejected by the repository's pre-push hook, which wants a "Try:" line in a commit message. I added an empty commit with one.
🤖 Generated with Claude Code
Closes #10545