diff options
| author | Adam Malczewski <[email protected]> | 2026-06-29 12:12:18 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-29 12:12:18 +0900 |
| commit | 129bb3be45e1446ce4219d7f656ed0ed6f93a29f (patch) | |
| tree | 5fe54f4bc10c90116a8b0dd5f140c5585b55120c /.dispatch | |
| parent | 1cdb866fbb708a1f6c5e802a075789cece85800d (diff) | |
| download | dispatch-web-129bb3be45e1446ce4219d7f656ed0ed6f93a29f.tar.gz dispatch-web-129bb3be45e1446ce4219d7f656ed0ed6f93a29f.zip | |
feat(chat): cancel a queued steering message from the UI
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.
Diffstat (limited to '.dispatch')
| -rw-r--r-- | .dispatch/transport-contract.reference.md | 55 |
1 files changed, 53 insertions, 2 deletions
diff --git a/.dispatch/transport-contract.reference.md b/.dispatch/transport-contract.reference.md index 0582d94..70d64d9 100644 --- a/.dispatch/transport-contract.reference.md +++ b/.dispatch/transport-contract.reference.md @@ -5,9 +5,20 @@ > permission prompt). Your CODE still imports `@dispatch/transport-contract` normally — this file is for > READING only. > -> **Orchestrator:** SNAPSHOT of `[email protected]` (MCP status + computers + provider concurrency). Regenerate whenever +> **Orchestrator:** SNAPSHOT of `[email protected]` (MCP status + computers + provider concurrency + cancel queued message). Regenerate whenever > it changes. > +> **2026-06-29 update (cancel-queued-message — ADDITIVE, 0.23.0 → 0.24.0):** a per-message +> **cancel** for the steering message queue ships. While a turn is GENERATING and a user +> message is queued (awaiting steering delivery), the client can cancel a single queued +> message by id so it never runs. New `WsClientMessage` member `ChatQueueCancelMessage` +> (`{ type: "chat.queue.cancel"; conversationId; messageId }` — fire-and-forget, idempotent; +> success confirmed by the `message-queue` surface updating, failure as `chat.error`). New +> HTTP path `DELETE /conversations/:id/queue/:messageId` → `QueueCancelResponse` +> (`{ conversationId; cancelled: boolean; queue }`). The cancel is scoped per conversation; +> cancelling a drained/already-cancelled/unknown message is a silent no-op. `@dispatch/wire` +> is unchanged (`QueuedMessage.id` is the cancel target). +> > **2026-06-27 update (concurrency-fixes — ADDITIVE, NO version bump):** the provider concurrency surface gains > (a) a configurable + persisted per-provider release COOLDOWN, and (b) adaptive headroom. `ConcurrencyStatusEntry` > gains FOUR new fields: `cooldownMs: number` (REQUIRED — per-slot release cooldown in ms, default 350; a recycled slot is @@ -560,6 +571,27 @@ export interface QueueResponse { readonly queue: readonly QueuedMessage[]; } +/** + * Response body for + * `DELETE /conversations/:id/queue/:messageId` — cancel (remove) a single + * queued steering message by id so it never runs. + * + * `cancelled` is `true` when a message with the given id was found in the + * conversation's queue and removed (it will never be delivered as steering nor + * carried into a new turn). `cancelled` is `false` when the message was not in + * the queue (already drained/delivered, never existed, unknown conversation) + * OR when the message-queue extension isn't loaded (degraded — feature off). + * `queue` is the post-cancel snapshot (empty when no queue extension is + * loaded). Idempotent — cancelling a message that is no longer queued returns + * `cancelled: false` with HTTP 200 (not an error), so a client may optimistically + * fire-and-forget a cancel and reconcile from the surface. + */ +export interface QueueCancelResponse { + readonly conversationId: string; + readonly cancelled: boolean; + readonly queue: readonly QueuedMessage[]; +} + // ─── Per-conversation LSP status ────────────────────────────────────────────── /** The connection state of a single language server for a workspace. */ @@ -780,6 +812,24 @@ export interface ChatQueueMessage { } /** + * Client → server: cancel (remove) a SINGLE queued steering message by id so + * it never runs. The WebSocket counterpart of the HTTP + * `DELETE /conversations/:id/queue/:messageId` (`QueueCancelResponse`). + * Fire-and-forget: success is confirmed by the message-queue SURFACE updating + * (the cancelled message leaves the snapshot); a failure (missing/empty + * `conversationId` or `messageId`) arrives as a `chat.error`. Idempotent — + * cancelling a message that is no longer queued (already drained/delivered) is + * a silent no-op (no surface update, no error). `messageId` is the stable + * client-visible `QueuedMessage.id` (obtained from the queue surface snapshot + * or the enqueue response). + */ +export interface ChatQueueCancelMessage { + readonly type: "chat.queue.cancel"; + readonly conversationId: string; + readonly messageId: string; +} + +/** * Every client → server WS message: surface ops (`@dispatch/ui-contract`) + chat * ops. A server discriminates on `type`. */ @@ -788,7 +838,8 @@ export type WsClientMessage = | ChatSendMessage | ChatSubscribeMessage | ChatUnsubscribeMessage - | ChatQueueMessage; + | ChatQueueMessage + | ChatQueueCancelMessage; /** * Every server → client WS message: surface ops (`@dispatch/ui-contract`) + chat |
