summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-22 03:06:52 +0900
committerAdam Malczewski <[email protected]>2026-06-22 03:06:52 +0900
commita4f0a0a7424fefcc19e52b871c531ce54cb06964 (patch)
tree39a649fb7ef4e8f8468c7bc72efda3ba384270b4
parent2772e0723cfc7898443320515e165a625de1db46 (diff)
downloaddispatch-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.svelte5
-rw-r--r--src/app/store.svelte.ts17
-rw-r--r--src/features/chat/ui/Composer.svelte20
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 -->