Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/desktop/src/main/__tests__/message-queue-ui-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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[] = [];
Expand Down
16 changes: 14 additions & 2 deletions apps/desktop/src/renderer/app-shell-session-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down