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.
This commit is contained in:
Brennan Benson
2026-10-04 15:19:56 -07:00
parent 24c642cd60
commit 6629b3e017
2 changed files with 46 additions and 3 deletions
@@ -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'
@@ -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)