bd95054997
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
211 lines
9.9 KiB
Markdown
211 lines
9.9 KiB
Markdown
---
|
|
status: building
|
|
created: 2026-08-07
|
|
owner: kandev
|
|
---
|
|
|
|
# Reorder Queued Messages
|
|
|
|
## Why
|
|
|
|
Pending messages run in strict FIFO order, so a later, more urgent prompt
|
|
waits behind everything queued before it. Today the only workarounds are
|
|
remove-and-requeue (destructive) or Send Now (interrupts the active turn).
|
|
Users need to rearrange pending messages so execution order matches priority
|
|
without losing content.
|
|
|
|
## What
|
|
|
|
- Every visible pending message in the queue panel offers a **grab handle**
|
|
on its left edge. The handle is a small dotted grip (2-column dot grid).
|
|
- The handle is a **floating overlay** anchored to the row's left edge,
|
|
vertically centered. It does not participate in layout: no row or panel
|
|
content moves to make room for it. While shown it may cover the row's
|
|
`#N` position label; the label returns when the handle hides.
|
|
- On a fine pointer the handle is hidden by default and appears when the
|
|
pointer hovers the row or keyboard focus reaches it. On a coarse pointer
|
|
(touch) it is always visible, attached flush to the message box's left
|
|
edge, and painted **behind** the row content — no chip surface, so the
|
|
dotted grip reads as part of the box edge. The position label and sender
|
|
icon stay painted above it (and pass touches through), and the interactive
|
|
hit area is at least 44 by 44 CSS pixels.
|
|
- Dragging starts **only** from the handle. Pointer interaction with the row
|
|
body (text selection, expand toggle, edit/merge/remove/send-now actions)
|
|
is unchanged. Dragging requires pointer movement (activation distance),
|
|
so a click on the handle does nothing by itself.
|
|
- Dropping the handle onto another row commits a new order. The queue panel
|
|
updates immediately (optimistic) and reconciles to the backend result.
|
|
- The new order is **persisted server-side** and survives reload, session
|
|
switch-back, and restart. All connected views reconcile to it.
|
|
- Displayed positions remain compact one-based ordinals (`#1` through
|
|
`#N`) derived from the visible order, exactly as after remove/merge.
|
|
- Reordering applies to every visible pending row **regardless of
|
|
provenance** (user, agent, workflow, system), matching the Remove action.
|
|
Server-reserved rows already in flight stay hidden and are never
|
|
reordered.
|
|
- The handle is disabled while a queue mutation or backend cancellation for
|
|
the session is pending (the same `isLoading`/`cancellationPending` gate as
|
|
Send Now), and is hidden while the row is being edited.
|
|
- Keyboard reordering is supported: focusing the handle and using
|
|
Space/Enter plus the arrow keys moves the row, matching the platform's
|
|
sortable-list convention.
|
|
- Reordering changes nothing else: merge, edit, Remove, Send Now, FIFO
|
|
auto-drain, and capacity semantics are unchanged. Send Now **all** and
|
|
FIFO drain dispatch in the current (reordered) order.
|
|
|
|
## API Surface
|
|
|
|
New WebSocket action:
|
|
|
|
```text
|
|
message.queue.reorder
|
|
```
|
|
|
|
Request:
|
|
|
|
```json
|
|
{
|
|
"session_id": "session-id",
|
|
"ordered_ids": ["entry-b", "entry-a", "entry-c"]
|
|
}
|
|
```
|
|
|
|
`session_id` is required. `ordered_ids` is required, non-empty, and must
|
|
contain each visible pending entry id of the session exactly once.
|
|
|
|
Successful response:
|
|
|
|
```json
|
|
{
|
|
"session_id": "session-id",
|
|
"reordered": 3
|
|
}
|
|
```
|
|
|
|
`reordered` is the number of entries in the submitted order — equal to the
|
|
visible pending count on success. A no-op reorder (submitting the current
|
|
order) reports the same count, since positions are rewritten to the compacted
|
|
`1..N` sequence.
|
|
|
|
Errors:
|
|
|
|
- `validation`: `session_id` missing, `ordered_ids` missing/empty, or
|
|
duplicate ids in `ordered_ids`.
|
|
- `queue_changed`: the submitted id set does not match the current visible
|
|
pending set — an entry was drained, removed, merged, or newly queued
|
|
since the client's snapshot. The request is rejected atomically with no
|
|
partial reorder; the client refetches the authoritative queue.
|
|
- Session access is required; the existing non-enumerating session-not-found
|
|
response is used before any queue read or mutation.
|
|
|
|
On success the handler publishes the existing `message.queue.status_changed`
|
|
event so every connected view reconciles.
|
|
|
|
## Data Model
|
|
|
|
No schema change. Order lives in the existing `queued_messages.position`
|
|
integer column (lower = head). A successful reorder rewrites positions to a
|
|
strictly increasing sequence: reserved in-flight rows keep their place in the
|
|
sequence, and the visible rows appear in the submitted order. Positions are
|
|
compacted (`1..N`) by the same operation, so subsequent inserts take
|
|
`MAX(position) + 1` as today.
|
|
|
|
## Permissions
|
|
|
|
- Any caller with access to the session may reorder its visible pending
|
|
entries, regardless of entry provenance (same rule as Remove and Send Now).
|
|
- The action accepts no caller-supplied content, `user_id`, `task_id`, or
|
|
identity. Only `session_id` and the entry ids are taken from the caller,
|
|
and the ids are validated against the persisted visible set.
|
|
|
|
## Failure Modes
|
|
|
|
- **Drain/remove/merge races the drag:** the submitted set no longer matches
|
|
the visible set; the backend rejects the whole request with `queue_changed`
|
|
and the client refetches silently (self-recovering, no error toast).
|
|
- **Backend mutation fails for another reason:** the client refetches to
|
|
restore the authoritative order and shows a localized error toast.
|
|
- **Concurrent clients:** the last successful reorder wins. A client with a
|
|
stale snapshot receives `queue_changed` and reconciles on its next refetch.
|
|
- **Reserved in-flight rows:** they are invisible to the client and excluded
|
|
from validation; they are never reordered and keep their delivery
|
|
reservation.
|
|
- **Rapid repeated drags:** while a reorder request is in flight the handles
|
|
are disabled, so overlapping reorder requests cannot be issued from one
|
|
client.
|
|
|
|
## Persistence Guarantees
|
|
|
|
The reordered sequence persists in `queued_messages.position` in SQLite and
|
|
survives a Kandev restart. It is queue state, not a client preference, and is
|
|
shared by every view of the session. A process crash mid-reorder leaves the
|
|
transaction uncommitted and the previous order intact.
|
|
|
|
## Responsive and Mobile Behavior
|
|
|
|
- The existing inline queue panel remains the desktop and phone composition;
|
|
the queue list stays the single internal scroll owner and the composer
|
|
stays visible.
|
|
- Desktop: the handle is a hover/focus-revealed floating overlay on the
|
|
row's left edge; nothing shifts in layout.
|
|
- Phone/coarse-pointer: the handle is always visible, attached flush to the
|
|
box's left edge, and painted behind the row content (background, no chip
|
|
surface). The row's position label / sender icon render above it and are
|
|
`pointer-events: none`, so touches reach the handle's 44 by 44 CSS-pixel
|
|
target while the label stays readable. `touch-action: none` on the handle
|
|
keeps a touch-drag reordering instead of scrolling; vertical scrolling of
|
|
the queue list is unaffected elsewhere.
|
|
- The queue hook, backend action, optimistic order, and reconciliation are
|
|
shared across viewports; only the handle's visibility and size differ.
|
|
- Mobile Playwright coverage proves the same reorder result with touch input.
|
|
|
|
## Scenarios
|
|
|
|
- **GIVEN** three messages are queued in order A, B, C, **WHEN** the user
|
|
drags C's handle above A, **THEN** the panel shows `#1` C, `#2` A, `#3` B
|
|
and reloading the page keeps that order.
|
|
- **GIVEN** a fine-pointer desktop with a queued message, **WHEN** the
|
|
pointer is not over the row, **THEN** no handle is visible, and the row
|
|
box has the same width and content position as without the feature.
|
|
- **GIVEN** the pointer hovers a queued message row, **WHEN** the pointer
|
|
moves onto the left edge, **THEN** the dotted grab handle appears as a
|
|
floating overlay and the row content does not shift.
|
|
- **GIVEN** a queued message row, **WHEN** the user presses and drags on the
|
|
row body (not the handle), **THEN** no drag starts; text selection and row
|
|
actions behave as before.
|
|
- **GIVEN** a row is being edited, **WHEN** the user looks at it, **THEN**
|
|
no grab handle is shown for that row.
|
|
- **GIVEN** a queue mutation or cancellation is in flight, **WHEN** the user
|
|
looks at any row, **THEN** the handles are visible but disabled.
|
|
- **GIVEN** the user starts dragging entry A and another client drains A
|
|
before the drop, **WHEN** the drop commits, **THEN** the backend returns
|
|
`queue_changed`, the panel silently refetches, and no error toast appears.
|
|
- **GIVEN** a queue whose head is a hidden reserved in-flight lifecycle row,
|
|
**WHEN** the user reorders the visible entries, **THEN** the visible
|
|
entries assume the submitted order around the reserved row and its
|
|
delivery/acknowledgement is unaffected.
|
|
- **GIVEN** the backend reorder fails for a non-race reason, **WHEN** the
|
|
drop commits, **THEN** the panel refetches the authoritative order and
|
|
shows the localized "failed to reorder" toast.
|
|
- **GIVEN** a queue containing user, agent, workflow, and system rows, **WHEN**
|
|
the user drags any of them, **THEN** the row moves to the dropped position.
|
|
- **GIVEN** a reordered queue and the user clicks Send Now **all**, **THEN**
|
|
the bulk prompt concatenates the bodies in the reordered FIFO order.
|
|
- **GIVEN** a phone viewport with queued messages, **WHEN** the user opens
|
|
the queue panel, **THEN** every row shows an always-visible touch-sized
|
|
handle attached flush to the box's left edge and painted behind the row
|
|
content (position labels stay readable), and a touch drag reorders the
|
|
queue with the same persisted result.
|
|
- **GIVEN** the user focuses a row's handle with the keyboard, **WHEN** they
|
|
press Space, Arrow Up, then Space, **THEN** the row moves one position up
|
|
and the order is persisted.
|
|
|
|
## Out of Scope
|
|
|
|
- Bulk selection or multi-row drag.
|
|
- Dedicated up/down move buttons as an alternative to dragging (keyboard
|
|
reordering is covered by the sortable keyboard sensor).
|
|
- Reordering hidden reserved in-flight rows.
|
|
- Cross-session or cross-task reordering.
|
|
- Changing merge, edit, Remove, Send Now, FIFO drain, or capacity semantics.
|