From 471275f3d30a2242bbb8f6c08e9f31b6a5d75d9e Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sat, 4 Jul 2026 22:00:45 +0300 Subject: [PATCH] feat(framework): a dashboard Stop button to interrupt the run (#218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard was a one-way display; you could only interrupt from the terminal. Add a Stop button that aborts the run from the browser: a POST /stop route calls an onStop handler wired to the run's AbortController, which runFramework checks between phases and the driver honours mid-turn (kills the current agent turn). A stop ends cleanly as 'stopped', not 'failed': the end event gains a stopped flag, the CLI prints '■ Stopped' and exits 0, the dashboard shows a stopped status, and the persisted run records status 'stopped' so --resume reflects it. startDashboard gains an onStop option; the page hides the button when stopping is not wired (a read-only --resume view). Part of #110. Closes #218. --- .changeset/dashboard-stop-button.md | 11 +++++ packages/framework/README.md | 5 +- packages/framework/src/cli.ts | 25 +++++++++- packages/framework/src/dashboard/page.ts | 29 +++++++++-- .../framework/src/dashboard/server.test.ts | 48 ++++++++++++++++++- packages/framework/src/dashboard/server.ts | 31 +++++++++++- packages/framework/src/events.test.ts | 6 +++ packages/framework/src/events.ts | 10 ++-- packages/framework/src/run.ts | 5 +- .../framework/src/store/run-store.test.ts | 6 +++ packages/framework/src/store/run-store.ts | 4 +- 11 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 .changeset/dashboard-stop-button.md diff --git a/.changeset/dashboard-stop-button.md b/.changeset/dashboard-stop-button.md new file mode 100644 index 0000000..cc5fecf --- /dev/null +++ b/.changeset/dashboard-stop-button.md @@ -0,0 +1,11 @@ +--- +'@gemstack/framework': minor +--- + +Stop a run from the dashboard + +The dashboard now has a **Stop** button that interrupts the running build from the browser, instead of only from the terminal. It POSTs to a new `/stop` route that aborts the run's `AbortSignal`, which `runFramework` checks between phases and the driver honours mid-turn (it kills the current agent turn). The run ends cleanly as *stopped* (not *failed*): the CLI prints `■ Stopped` and exits 0, the dashboard shows a stopped status, and the persisted run records `status: "stopped"` so `--resume` shows it that way. + +`startDashboard` gains an `onStop` option (wire it to `controller.abort()`); the page hides the button when no stop handler is wired (e.g. a read-only `--resume` view). The `end` event gained an optional `stopped` flag. + +Part of #110 (first interactive slice of #165's web client). Closes #218. diff --git a/packages/framework/README.md b/packages/framework/README.md index 99676d0..c345b77 100644 --- a/packages/framework/README.md +++ b/packages/framework/README.md @@ -34,7 +34,10 @@ pieces: driver for `--fake` and tests. Codex / opencode slot in behind the same three methods. - **The product shell** - the `framework` CLI and the localhost - [dashboard](./src/dashboard/server.ts) over an event stream we own. + [dashboard](./src/dashboard/server.ts) over an event stream we own. The + dashboard has a **Stop** button that interrupts the run from the browser (it + aborts the same signal Ctrl+C does); the run ends cleanly as *stopped*, and a + persisted run reflects that so `--resume` shows it stopped. Everything runs *through* the driver (single execution path): the architect is a small structured JSON decision the agent returns; build and improve are prompts; diff --git a/packages/framework/src/cli.ts b/packages/framework/src/cli.ts index c1705e6..bf0e348 100644 --- a/packages/framework/src/cli.ts +++ b/packages/framework/src/cli.ts @@ -352,10 +352,18 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise controller.abort(), + }) io.out(`◆ dashboard: ${dashboard.url}`) } catch (err) { io.err(`could not start dashboard (${err instanceof Error ? err.message : String(err)}); continuing headless`) @@ -400,6 +408,7 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise @@ -26,6 +26,12 @@ export function dashboardHtml(title: string): string { #session-link { margin-left: auto; font-size: 12px; color: #7b8496; } #session-link code { color: #b7c0d0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } #status { font-size: 12px; color: #7b8496; } + #stop { margin-left: 12px; font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; + color: #f0a35e; background: #241a15; border: 1px solid #4a3320; border-radius: 6px; + padding: 4px 12px; } + #stop:hover { background: #2f2118; border-color: #6a4a2e; } + #stop:disabled { opacity: .5; cursor: default; } + #stop[hidden] { display: none; } #app-banner { display: flex; align-items: center; gap: 8px; padding: 10px 20px; background: #0f2417; border-bottom: 1px solid #1c3a28; font-size: 13px; } #app-banner .dot { color: #67d98f; } @@ -68,6 +74,7 @@ export function dashboardHtml(title: string): string { connecting… +