diff options
| author | Adam Malczewski <[email protected]> | 2026-06-22 03:06:52 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-22 03:06:52 +0900 |
| commit | a4f0a0a7424fefcc19e52b871c531ce54cb06964 (patch) | |
| tree | 39a649fb7ef4e8f8468c7bc72efda3ba384270b4 | |
| parent | 2772e0723cfc7898443320515e165a625de1db46 (diff) | |
| download | dispatch-web-a4f0a0a7424fefcc19e52b871c531ce54cb06964.tar.gz dispatch-web-a4f0a0a7424fefcc19e52b871c531ce54cb06964.zip | |
feat(chat): stop generation button — abort without closing
Consume the stop-generation handoff (no version bumps, no new types).
- App store: stopGeneration() → POST /conversations/:id/stop (fire-and-forget)
- Composer: stop button (square, error color) visible only while generating,
next to the send/queue button
- Existing event flow handles the rest: done with reason 'aborted' clears
generating; conversation.statusChanged: idle updates the tab spinner
686 tests green.
| -rw-r--r-- | src/app/App.svelte | 5 | ||||
| -rw-r--r-- | src/app/store.svelte.ts | 17 | ||||
| -rw-r--r-- | src/features/chat/ui/Composer.svelte | 20 |
3 files changed, 42 insertions, 0 deletions
diff --git a/src/app/App.svelte b/src/app/App.svelte index 350db49..3024a44 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -195,6 +195,10 @@ store.queueMessage(text); } + function handleStop() { + store.stopGeneration(); + } + function handleSelectModel(model: string) { store.selectModel(model); } @@ -372,6 +376,7 @@ <Composer onSend={handleSend} onQueue={handleQueue} + onStop={handleStop} contextSize={store.activeChat.currentContextSize} status={store.activeChat.error ? "error" diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index 1359fcd..f212920 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -142,6 +142,13 @@ export interface AppStore { */ compactNow(keepLastN?: number): Promise<CompactResult | null>; /** + * Stop an in-flight generation (`POST /conversations/:id/stop`). Aborts the + * turn without closing the conversation — partial messages are persisted, the + * turn seals with `reason: "aborted"`, and the conversation goes `active → idle`. + * Returns null when no conversation is focused. + */ + stopGeneration(): void; + /** * The workspace conversation's auto-compact threshold (tokens). `0` = disabled * (manual only); a positive number = auto-compact triggers when the last * turn's input tokens exceed it. Seeded from the backend on focus change. @@ -934,6 +941,16 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { } }, + stopGeneration(): void { + const conversationId = tabsStore.activeConversationId; + if (conversationId === null) return; + void fetchImpl(`${httpBase}/conversations/${encodeURIComponent(conversationId)}/stop`, { + method: "POST", + }).catch(() => { + // Non-fatal — the existing event flow handles the turn settle. + }); + }, + async compactNow(keepLastN?: number): Promise<CompactResult | null> { const conversationId = tabsStore.activeConversationId; if (conversationId === null) return null; diff --git a/src/features/chat/ui/Composer.svelte b/src/features/chat/ui/Composer.svelte index d519efc..5952eca 100644 --- a/src/features/chat/ui/Composer.svelte +++ b/src/features/chat/ui/Composer.svelte @@ -9,6 +9,7 @@ let { onSend, onQueue, + onStop, contextSize = undefined, status = "idle", }: { @@ -20,6 +21,8 @@ * used regardless (tests / non-steering contexts). */ onQueue?: (text: string) => void; + /** Stop the in-flight generation (`POST /conversations/:id/stop`). */ + onStop?: () => void; // Current context occupancy (latest turn's contextSize), or `undefined` // when unknown — the status bar then shows "— tokens", never 0%. contextSize?: number | undefined; @@ -110,6 +113,23 @@ <button class="btn btn-primary w-20 shrink-0" type="submit" disabled={!hasText}> {submitLabel} </button> + {#if status === "running" && onStop} + <button + class="btn btn-error btn-square shrink-0" + type="button" + aria-label="Stop generation" + onclick={onStop} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="h-4 w-4" + > + <rect x="6" y="6" width="12" height="12" rx="2"></rect> + </svg> + </button> + {/if} </div> <!-- Bottom status bar: status icon · context-window fill · token count --> |
