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… +