diff --git a/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts b/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts index d9db2f1602..ece5f859fc 100644 --- a/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts +++ b/apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts @@ -109,6 +109,16 @@ test('queue_update events drive the independent desktop queue projection', () => content: { text: 'adjust this run' }, }); assert.deepEqual(removedTransientMessageIds, ['message-steer']); + assert.deepEqual(controller.getState().messageQueueBySession['session-1'], { + queueRevision: 3, + entries: [{ + entryId: 'entry-next', + messageId: 'message-next', + content: { text: 'do this next' }, + placement: 'next_turn', + state: 'queued', + }], + }); handlers.handleEvent('session-1', { type: 'queue_update', @@ -148,6 +158,52 @@ test('queue_update events drive the independent desktop queue projection', () => assert.deepEqual(removedTransientMessageIds, ['message-steer', 'message-next']); }); +test('steering delivery clears a promoted follow-up from the desktop queue', () => { + const controller = createAppShellSessionUiStateController(); + const handlers = createAppShellSessionEventHandlers({ + uiLocale: 'en', + activeIdRef: { current: 'session-1' }, + liveTurnBySessionRef: controller.liveTurnBySessionRef, + refreshMessages: async () => true, + refreshSessions: async () => [], + setLiveTurnBySession: controller.setLiveTurnBySession, + setInteractionBySession: controller.setInteractionBySession, + setMessageQueueBySession: controller.setMessageQueueBySession, + showModelSetupToast() {}, + toastApi: { error() {} }, + }); + + handlers.handleEvent('session-1', { + type: 'queue_update', + id: 'queue-followup', + turnId: 'turn-1', + ts: 1, + queueRevision: 1, + steering: [], + followup: ['adjust this run'], + steeringEntries: [], + followupEntries: [{ + entryId: 'entry-followup', + messageId: 'message-followup', + content: { text: 'adjust this run' }, + placement: 'next_turn', + state: 'queued', + }], + }); + assert.equal(controller.getState().messageQueueBySession['session-1']?.entries.length, 1); + + handlers.handleEvent('session-1', { + type: 'steering_message', + id: 'steering-message-followup', + turnId: 'turn-1', + messageId: 'message-followup', + ts: 2, + content: { text: 'adjust this run' }, + }); + + assert.equal(controller.getState().messageQueueBySession['session-1'], undefined); +}); + test('complete events deliver the durable context compaction outcome to Desktop', () => { const controller = createAppShellSessionUiStateController(); const outcomes: unknown[] = []; diff --git a/apps/desktop/src/renderer/app-shell-session-events.ts b/apps/desktop/src/renderer/app-shell-session-events.ts index fde66da7ca..6d04ef0de5 100644 --- a/apps/desktop/src/renderer/app-shell-session-events.ts +++ b/apps/desktop/src/renderer/app-shell-session-events.ts @@ -367,9 +367,21 @@ export function createAppShellSessionEventHandlers(options: { break; case 'steering_message': // The live Turn projection now renders this same messageId in place. - // Retire only the renderer-owned tail row; a later nack queue_update - // will project it again if the Host returns the message to the queue. + // Retire the renderer-owned tail row and its pending-queue card; a + // later nack queue_update will project both again if the Host returns + // the message to the queue. removeTransientMessage?.(sessionId, event.messageId); + setMessageQueueBySession?.((current) => { + const queue = current[sessionId]; + if (!queue?.entries.some((entry) => entry.messageId === event.messageId)) return current; + const entries = queue.entries.filter((entry) => entry.messageId !== event.messageId); + if (entries.length > 0) { + return { ...current, [sessionId]: { ...queue, entries } }; + } + const next = { ...current }; + delete next[sessionId]; + return next; + }); break; case 'text_complete': void refreshMessages(sessionId, { requiredAssistantMessageId: event.messageId }).catch(() => false);