fix(native-chat): keep send placeholders stable

This commit is contained in:
Brennan Benson
2026-08-16 22:14:59 -07:00
parent 793c323368
commit 6dcbf41394
6 changed files with 22 additions and 5 deletions
@@ -69,7 +69,7 @@ describe('mobile structured session timeline', () => {
expect(rows[0]).toMatchObject({ kind: 'prompt', key: 'orca:approval' })
expect(rows[1]).toMatchObject({
kind: 'message',
key: OUTBOX.clientMessageId,
key: agentJournalSubmissionKey(OUTBOX.clientMessageId),
outbox: { state: 'unconfirmed' },
message: { blocks: [{ type: 'text', text: 'look' }, { url: 'file:///preview.png' }] }
})
@@ -85,10 +85,12 @@ describe('mobile structured session timeline', () => {
}
const rows = buildMobileStructuredTimeline([wal], [OUTBOX])
const optimistic = buildMobileStructuredTimeline([], [OUTBOX])
expect(rows).toHaveLength(1)
// The entry rides the canonical row, so Retry / edit-queued stay reachable.
expect(rows[0]).toMatchObject({ key: wal.itemId, outbox: { state: 'unconfirmed' } })
expect(optimistic[0]?.key).toBe(rows[0]?.key)
})
it('keeps the device-local thumbnail on the adopted row while delivery is unconfirmed', () => {
@@ -92,7 +92,7 @@ export function buildMobileStructuredTimeline(
.map(
(entry): MobileStructuredTimelineRow => ({
kind: 'message',
key: entry.clientMessageId,
key: agentJournalSubmissionKey(entry.clientMessageId),
message: outboxMessage(entry),
outbox: entry
})
@@ -66,6 +66,18 @@ describe('shouldShowNativeChatTypingIndicator', () => {
).toBe(true)
})
it('shows after a slash-command marker even though an earlier turn replied', () => {
expect(
shouldShowNativeChatTypingIndicator({
messages: [
message('a1', 'assistant'),
message('command:compact', 'system', 'Ran /compact')
],
isWorking: true
})
).toBe(true)
})
it('shows on a session whose transcript is still empty', () => {
expect(shouldShowNativeChatTypingIndicator({ messages: [], isWorking: true })).toBe(true)
})
@@ -6,6 +6,7 @@
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
import { NATIVE_CHAT_STREAMING_ID } from '../../../../shared/native-chat-streaming'
import { isCommandMarkerId } from './native-chat-pending'
export function shouldShowNativeChatTypingIndicator(args: {
messages: readonly NativeChatMessage[]
@@ -19,7 +20,7 @@ export function shouldShowNativeChatTypingIndicator(args: {
// must not suppress the indicator for the send the user just made.
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index]
if (!message || message.role === 'user') {
if (!message || message.role === 'user' || isCommandMarkerId(message.id)) {
return true
}
// Status/system rows interleave mid-turn; they neither suppress nor unsuppress,
@@ -80,9 +80,11 @@ describe('structured agent session message projection', () => {
}
const messages = projectStructuredAgentSessionMessages([walItem], outbox, [pending])
const optimistic = projectStructuredAgentSessionMessages([], outbox, [])
expect(messages.filter((message) => message.role === 'user')).toHaveLength(1)
expect(messages.map((message) => message.id)).toEqual([walItem.itemId])
expect(optimistic[0]?.id).toBe(messages[0]?.id)
})
it('keeps an optimistic send until its acceptance arrives', () => {
@@ -97,7 +99,7 @@ describe('structured agent session message projection', () => {
]
expect(projectStructuredAgentSessionMessages([], outbox, [])).toMatchObject([
{ id: 'client-pending', role: 'user' }
{ id: agentJournalSubmissionKey('client-pending'), role: 'user' }
])
})
})
@@ -27,7 +27,7 @@ export function projectStructuredAgentSessionMessages(
.filter((entry) => !journalled.has(agentJournalSubmissionKey(entry.clientMessageId)))
.map(
(entry): NativeChatMessage => ({
id: entry.clientMessageId,
id: agentJournalSubmissionKey(entry.clientMessageId),
role: 'user',
source: 'transcript',
timestamp: entry.queuedAt,