fix(desktop): stabilize burst follow-up messages - #3513
Conversation
e1d5a6c to
f29dc2d
Compare
Route consecutive submissions through the Runtime Host queue authority, expose Queue and Steer controls, restore retracted message content, and deduplicate identical active error toasts. Generated-by: Codex
f29dc2d to
bbf8bbb
Compare
There was a problem hiding this comment.
I found one P3 at exact head bbf8bbbcc4fb0de41fc47b4f99b791d8ec7937d4; no P0–P2 findings.
[P3] “Retract all” remains visible when only an in-flight steering entry exists
When a steering entry has moved from queued to in_flight and there are no queued or follow-up entries, the composer still renders the queue area and the “Retract all” control. The desktop action then reaches Runtime Host’s #retractQueued path, which only handles state.steering and state.followup, not state.inFlight. The action therefore returns an empty result: it neither retracts the delivered message nor restores the draft, while the UI presents it as available.
Please either render the control only when at least one entry is queued, or define the control explicitly as applying only to queued entries and hide it for an in-flight-only state.
中文
在精确 head bbf8bbbcc4fb0de41fc47b4f99b791d8ec7937d4 上发现 1 个 P3,没有 P0–P2。
[P3] 仅剩 in-flight steering entry 时仍显示 “Retract all”
当 steering entry 从 queued 变成 in_flight,且没有其他 queued/followup entry 时,composer 仍会渲染队列区域和 “Retract all” 控件。桌面端随后进入 Runtime Host 的 #retractQueued 路径,但该路径只处理 state.steering 和 state.followup,不处理 state.inFlight。因此该动作返回空结果:既不会撤回已投递的消息,也不会恢复草稿,但 UI 仍把它呈现为可用操作。
建议只在至少存在一个 queued entry 时渲染该控件;或者明确该动作只作用于 queued entry,并在仅剩 in-flight 状态时隐藏它。
Astro-Han
left a comment
There was a problem hiding this comment.
Following up on our earlier comment at this head — a second reviewer went over the same diff independently, and found one thing the first pass did not. Still at exact head bbf8bbbcc4fb0de41fc47b4f99b791d8ec7937d4; the test check is green but the aggregate commit status is still pending, so this remains a COMMENT.
[P3, reachable on a concurrent-failure path] Toast de-duplication drops the session, so one session's error can hide another's.
toastContentKey in packages/ui/src/toast.tsx builds its key from [variant, title, description, action?.label] only. push then reuses the existing toast id for a matching key, so a second toast with identical content replaces the first rather than appearing alongside it.
The session identity is available and discarded: ToastInput carries diagnosticTarget, whose variants all include sessionId, and the session error path populates it.
Concretely: two sessions fail concurrently with the same message text — which is precisely the burst condition this PR addresses. The second toast overwrites the first, so one failure never becomes visible to the user, and the action attached to the surviving toast (Retry, for instance) targets the other session. The user sees one error and retries something they did not intend to.
The added toast.test.ts covers same-content de-duplication and non-merging of different errors, but has no cross-session case, so nothing here fails today.
Including sessionId (or a scope discriminator) in the key, or bucketing toasts per session, would close it. Not blocking, and orthogonal to the queue changes that are the substance of this PR.
One thing we withdrew. We had a candidate finding about lastIndexOf re-anchoring during reference remapping and dropped it: for any realistic input the appended text's start is the rightmost occurrence within the window, and the empty-displayText and activeId-switch cases are both guarded. We could not construct a reproducing input, so we are not reporting it.
Also re-verified independently: the Retract all behaviour from our earlier comment holds — #retractQueued takes [...state.steering, ...state.followup] and never touches state.inFlight, and the UI clears the whole queue display afterwards, so the in-flight message is still delivered while its queue indicator disappears. Two reviewers reached that one separately.
We also checked and found nothing wrong with the routing change itself: the legacy sessions:steer path and the new sessions:enqueue(current_turn) both reach the host's submitMessage with placement=current_turn, so there is no semantic regression, and the new path handles the started/queued disposition the old one ignored.
This review was AI-assisted. Findings were verified against the exact head listed above; any mistakes are ours to correct — please push back where we got it wrong.
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks — both points from our previous comments are fixed, and each is fixed at the right level rather than at the symptom.
Re-reviewed at exact head d7c5d4e146ea8e6e3b052cabbf0b787f8fe3dab7. Our earlier comments were against bbf8bbbc, so they no longer bound; this is a fresh pass.
Retract with only an in-flight entry — fixed. composer-message-queue.tsx now derives hasRetractableEntries from entry.state === 'queued' and gates both the render and the handler on it, so the control is absent rather than present-and-inert. Guarding the render alone would have left the callback reachable; guarding both is the thorough version. does not offer retract when only an in-flight steering message remains pins it.
Toast de-duplication across sessions — fixed, and fixed at the key rather than at the call site. toastContentKey now folds in a diagnosticScope derived from diagnosticTarget — ['session', sessionId, …] or ['profile', profileId]. Two sessions failing with identical text no longer collide, which was the case this PR's own burst scenario made reachable. Patching the individual caller would have left the next one exposed; changing the key closes the class. The three added cases cover same-session de-duplication, cross-session non-merging, and different errors within one session.
The delta over the previous head is confined to those two fixes and their tests — four files, nothing else carried along.
No findings — no P0, P1, P2 or P3. test is green on this head. Approving.
This review was AI-assisted. Findings were verified against the exact head listed above; any mistakes are ours to correct — please push back where we got it wrong.
…order (#3544) * feat(desktop): per-entry composer queue with promote, retract, and reorder Replace the Queue/Steer SegmentedControl from #3513 with a Codex-style pending plate above the composer card. Mid-turn Send is always Send and queues a follow-up; Shift+Enter steers once. Queued follow-ups render in send order (first at the top) with a hover grip to reorder, a per-entry promote (立即发送) that hands the message to the active Turn, and a per-entry retract (收回草稿) that restores the message into the draft. Steering entries leave the plate the moment they are handed to the Turn. Runtime Host stays the sole queue authority through three new receipted operations: queue.entry.promote, queue.entry.retract, and queue.entries.reorder. Each is epoch-local like queue.retract, idempotent via the message receipt store, and rejected while the queue drains into the next root Turn so the terminal transition keeps its exact prefix. The Send/Stop slot gains a comment pinning it as the app's global anchor after repeated regressions. Generated-by: Grok Build * feat(desktop): bump protocol epoch for per-entry queue operations Generated-by: Grok Build * refactor(desktop): drop the retract-all queue chain after per-entry actions The pending plate now exposes per-entry retract/promote/reorder, so the sessions:retractQueue IPC handler, preload bridge, client method, and its tests have no remaining renderer caller. resolveFollowUpModeAtSubmit also loses its defaultMode parameter now that the composer no longer carries a Queue/Steer mode state. Generated-by: Grok Build * docs: regenerate the Astryx surface inventory for the composer queue Generated-by: Grok Build * test(desktop): steer via Shift+Enter in the streaming-remount e2e The Queue/Steer mode switch no longer exists; mid-turn steering is the shifted submit. Generated-by: Grok Build * refactor: fold queue retract into the queued-mutation driver and drop dead queue state - queue.retract now runs through the same receipted driver as the per-entry operations, deleting the parallel PendingRetract path. - The driver takes the operation spec and verb instead of four fields that every call site derived from the same operation. - The renderer stores only the follow-up queue: steering entries leave the plate when handed to the Turn, nothing read the stored steering list or queueRevision, and the legacy Entries-less synthesis could only feed fake entry ids to per-entry Host operations. - A rejected reorder rejects through the app shell, so the pending plate snaps back deterministically instead of after a 2s timer. Generated-by: Grok Build * fix(desktop): preserve retracted drafts across session switches Restore a successfully retracted queue entry from the Host-authored projection captured at click time, keyed to the owning Session even when another Session becomes active. Keep focus conditional on the visible Session. Remove the duplicate message content from the per-entry retract result so queue content has one wire path and mutation receipts carry only the queue revision. Generated-by: Codex * refactor(ui): render queue order from host projection Generated-by: Codex * refactor(runtime-host): unify queue mutation results Generated-by: Codex
Summary
streamingvalueRefs #2262
Visual verification
Before: an active turn has no visible follow-up mode or queued-message state.
After: Queue is the default, Steer remains explicit, and queued content is visible and retractable.
Verification
npm --workspace @maka/ui run test:dist(204 passed)npm --workspace @maka/desktop run test:dist(1158 passed)npm --workspace @maka/desktop run build:with-depsnpx playwright test --config e2e/playwright.config.ts streaming-remount.spec.ts(3 passed)npx biome checkon changed TypeScript, TSX, and CSS filesbbf8bbbccScope
This intentionally leaves queue editing, per-entry deletion, reordering, promotion, and pause/resume for separate Runtime Host protocol and durability work.
AI use
Select exactly one:
Tool(s) and scope: Codex implemented the focused Desktop queue integration, tests, documentation, and verification. The affected commit retains the
Generated-by: Codextrailer.Checklist
Does this PR entail a change in behavior?