diff --git a/mobile/src/session/mobile-structured-session-timeline.test.ts b/mobile/src/session/mobile-structured-session-timeline.test.ts index 25c0eea9169..ac4fbffd463 100644 --- a/mobile/src/session/mobile-structured-session-timeline.test.ts +++ b/mobile/src/session/mobile-structured-session-timeline.test.ts @@ -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', () => { diff --git a/mobile/src/session/mobile-structured-session-timeline.ts b/mobile/src/session/mobile-structured-session-timeline.ts index 77133294316..3f52fe906a4 100644 --- a/mobile/src/session/mobile-structured-session-timeline.ts +++ b/mobile/src/session/mobile-structured-session-timeline.ts @@ -92,7 +92,7 @@ export function buildMobileStructuredTimeline( .map( (entry): MobileStructuredTimelineRow => ({ kind: 'message', - key: entry.clientMessageId, + key: agentJournalSubmissionKey(entry.clientMessageId), message: outboxMessage(entry), outbox: entry }) diff --git a/src/renderer/src/components/native-chat/native-chat-typing-indicator.test.ts b/src/renderer/src/components/native-chat/native-chat-typing-indicator.test.ts index 1dad39ef89a..65af547b7c0 100644 --- a/src/renderer/src/components/native-chat/native-chat-typing-indicator.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-typing-indicator.test.ts @@ -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) }) diff --git a/src/renderer/src/components/native-chat/native-chat-typing-indicator.ts b/src/renderer/src/components/native-chat/native-chat-typing-indicator.ts index 44883851666..db3f5fb696f 100644 --- a/src/renderer/src/components/native-chat/native-chat-typing-indicator.ts +++ b/src/renderer/src/components/native-chat/native-chat-typing-indicator.ts @@ -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, diff --git a/src/renderer/src/components/native-chat/structured-agent-session-message-projection.test.ts b/src/renderer/src/components/native-chat/structured-agent-session-message-projection.test.ts index 392ae4662e6..745aa5b4314 100644 --- a/src/renderer/src/components/native-chat/structured-agent-session-message-projection.test.ts +++ b/src/renderer/src/components/native-chat/structured-agent-session-message-projection.test.ts @@ -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' } ]) }) }) diff --git a/src/renderer/src/components/native-chat/structured-agent-session-message-projection.ts b/src/renderer/src/components/native-chat/structured-agent-session-message-projection.ts index 68534966689..9da10c6581a 100644 --- a/src/renderer/src/components/native-chat/structured-agent-session-message-projection.ts +++ b/src/renderer/src/components/native-chat/structured-agent-session-message-projection.ts @@ -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,