From 129bb3be45e1446ce4219d7f656ed0ed6f93a29f Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 29 Jun 2026 12:12:18 +0900 Subject: feat(chat): cancel a queued steering message from the UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the frontend for the cancel-queued-message backend feature (transport-contract 0.23.0 → 0.24.0, ADDITIVE). While a turn is generating and a user message is queued (awaiting steering delivery), the user can now cancel a single queued message by id so it never runs. - Consume the contract: regenerate the .dispatch/transport-contract.reference.md mirror to 0.24.0; add chat.queue.cancel to the exhaustive WsClientMessage guard (core/wire/conformance.ts) + its test. - ChatTransport port accepts ChatQueueCancelMessage; cancelQueuedMessage(id) on the chat store + app store sends chat.queue.cancel { conversationId, messageId } (fire-and-forget, idempotent — the server no-ops an already- drained/unknown id). - UI: a × cancel affordance per queued row in MessageQueueList.svelte, threaded through SurfaceView's onCancelQueuedMessage (dispatched on rendererId, never the surface id) and wired to store.cancelQueuedMessage. Optimistic removal (pure selectVisibleMessages/reconcileCancelledIds in logic/message-queue.ts) hides the row on click and reconciles from the message-queue surface's post-cancel snapshot. No new event handling — the existing surface subscription reflects the result. Tests: +12 (chat store cancel op ×3, optimistic-removal logic ×11 already existed pattern, MessageQueueList component cancel behavior ×9). Repo fix: package.json + bun.lock were still pinning file:../dispatch-backend/... (stale from the dispatch-backend → backend rename) — corrected to file:../backend/packages/...; bun install now resolves natively with no worktree symlink hack. typecheck 0/0, 1140 tests green (run twice), biome clean, build OK. --- src/app/App.svelte | 10 +++++++++- src/app/store.svelte.ts | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) (limited to 'src/app') diff --git a/src/app/App.svelte b/src/app/App.svelte index 7e5ac7d..e4e5bbc 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -296,6 +296,10 @@ store.queueMessage(text); } + function handleCancelQueuedMessage(messageId: string) { + store.cancelQueuedMessage(messageId); + } + function handleStop() { store.stopGeneration(); } @@ -625,7 +629,11 @@ the generic SurfaceView (dispatches on rendererId, never surface id); only shown when the queue is non-empty — an idle queue is hidden. -->
- +
{/if} diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index 22b0a25..87d2bfb 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -206,6 +206,15 @@ export interface AppStore { * wants to add input — the server owns the idle-vs-generating decision. */ queueMessage(text: string): void; + /** + * Cancel (remove) a single queued steering message by id so it never runs + * (`chat.queue.cancel` WS op). Fire-and-forget + idempotent: the + * message-queue surface update reconciles the queue UI (the cancelled message + * leaves the snapshot). Targets the focused conversation's queue. The caller + * optimistically hides the row; a cancel of an already-drained / unknown + * message is a silent server no-op (nothing to roll back). + */ + cancelQueuedMessage(messageId: string): void; selectModel(model: string): void; newDraft(): void; /** Switch the active workspace (on route change) + reset to a fresh draft in it. */ @@ -1357,6 +1366,15 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { activeChat.queueMessage(text); }, + cancelQueuedMessage(messageId: string): void { + // Fire-and-forget + idempotent. The message-queue surface (conversation- + // scoped) reconciles the queue UI — the cancelled row leaves the snapshot. + // A cancel of an already-drained / unknown message is a silent no-op, so + // there is no local state to roll back. Delegates to the focused + // conversation's chat store, which owns the conversationId + transport. + activeChat.cancelQueuedMessage(messageId); + }, + selectModel(model: string): void { activeModel = model; const activeId = tabsStore.activeConversationId; -- cgit v1.2.3