From 6629b3e017b83100b284d311508ea685963770fd Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:19:56 -0700 Subject: [PATCH] fix(mobile): show a message the host recorded and then rejected in place, as on the desktop Design case 1: the phone no longer hands such a message back or banners it (this branch's earlier change), so its row is where it lives; the phone now draws it as not sent, like the desktop, and leaves one a queued card holds to that card. --- ...ile-structured-agent-session-send.test.tsx | 38 +++++++++++++++++++ .../use-mobile-structured-agent-session.ts | 11 ++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/mobile/src/session/use-mobile-structured-agent-session-send.test.tsx b/mobile/src/session/use-mobile-structured-agent-session-send.test.tsx index 85640ff842e..aa2818f5143 100644 --- a/mobile/src/session/use-mobile-structured-agent-session-send.test.tsx +++ b/mobile/src/session/use-mobile-structured-agent-session-send.test.tsx @@ -5,6 +5,7 @@ import type { AgentJournalDispatchState } from '../../../src/shared/agent-sessio import type { AgentSessionSubscribeEvent } from '../../../src/shared/agent-session-wire' import type { RpcClient } from '../transport/rpc-client' import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' +import { agentJournalSubmissionKey } from '../../../src/shared/agent-session-journal-item-key' import { resetMobileStructuredSendOperationJournalForTests } from './mobile-structured-send-operation-journal' import { structuredSendResultFixture } from './structured-agent-send-result.test-fixture' import { useMobileStructuredAgentSession } from './use-mobile-structured-agent-session' @@ -257,6 +258,43 @@ describe('mobile structured send retries', () => { expect(calls().every(([, params]) => !('retryUnknown' in (params as object)))).toBe(true) }) + // The host's row is where a recorded, rejected message lives on the phone, as on the desktop. + it('shows a message the host recorded and then rejected as not sent, in place', async () => { + await mountSession() + const event = snapshotEvent() + const itemId = agentJournalSubmissionKey('rejected-1') + act(() => + listener?.({ + ...event, + page: { + ...event.page, + items: [ + { + itemId, + revision: 1, + sequence: 1, + observedAt: 10, + body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'lost' }] } + } + ], + submissions: [ + { + clientMessageId: 'rejected-1', + fence: 3, + payloadFingerprint: 'fingerprint', + dispatchState: 'rejected', + providerItemId: null, + reason: 'provider_write_failed: broken pipe', + submittedAt: 10, + resolvedAt: 10 + } + ] + } + }) + ) + expect(hook!.session.messages).toEqual([expect.objectContaining({ id: itemId, unsent: true })]) + }) + // The transcript owns a message the host recorded, so the composer never gets it back. it('hands back only a send a Stop withdrew, never one the host recorded and rejected', async () => { let reason = 'provider_write_failed: broken pipe' diff --git a/mobile/src/session/use-mobile-structured-agent-session.ts b/mobile/src/session/use-mobile-structured-agent-session.ts index ae688ab9072..d3dfb826fbb 100644 --- a/mobile/src/session/use-mobile-structured-agent-session.ts +++ b/mobile/src/session/use-mobile-structured-agent-session.ts @@ -157,13 +157,18 @@ export function useMobileStructuredAgentSession(args: { onSendError }) + const queuedMessageIds = useMemo( + () => (queuedMessages ?? []).map((message) => message.messageId), + [queuedMessages] + ) const messages = useMemo( - // Off: the phone hands a rejected message back to its composer, so a row would show it twice. + // As on the desktop: a message the host recorded lives on its row, never back in the composer. () => projectStructuredAgentSessionMessages(state.items, [], state.submissions, { - rejectedInPlace: false + rejectedInPlace: true, + queuedMessageIds }), - [state.items, state.submissions] + [state.items, state.submissions, queuedMessageIds] ) const turnId = activeStructuredAgentSessionTurnId(state.items) const turnTiming = useMobileStructuredAgentTurnTiming(state, turnId)