From de1b119141bb801917d3a33247fe5934cba6777e Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:09:17 -0700 Subject: [PATCH] fix(mobile): stop labeling accepted messages queued (#13658) --- .../src/session/MobileNativeChatMessage.tsx | 25 ++++++++----------- .../src/session/MobileNativeChatView.test.ts | 17 +++++++++++++ mobile/src/session/MobileNativeChatView.tsx | 9 +++---- .../mobile-native-chat-message-styles.ts | 9 ------- .../session/mobile-native-chat-render-data.ts | 2 +- .../mobile-native-chat-send-classification.ts | 2 +- .../use-mobile-native-chat-message-send.ts | 2 +- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/mobile/src/session/MobileNativeChatMessage.tsx b/mobile/src/session/MobileNativeChatMessage.tsx index 97e92decc4d..0a676061e5b 100644 --- a/mobile/src/session/MobileNativeChatMessage.tsx +++ b/mobile/src/session/MobileNativeChatMessage.tsx @@ -276,7 +276,6 @@ function AgentControls({ function MobileNativeChatMessageImpl({ message, - queued, toolsExpanded = false, fontScale = 1, messageIndex, @@ -284,7 +283,6 @@ function MobileNativeChatMessageImpl({ onOpenFile }: { message: NativeChatMessage - queued?: boolean toolsExpanded?: boolean /** Multiplies all chat text sizes for pinch-to-zoom (1 = no change). */ fontScale?: number @@ -328,27 +326,24 @@ function MobileNativeChatMessageImpl({ // Copy + scroll-to-top, shown inline with the first tool call (or after the // prose when there are no tools). - const controls = - isAgent && !queued ? ( - onScrollToMessage(messageIndex) - : undefined - } - /> - ) : null + const controls = isAgent ? ( + onScrollToMessage(messageIndex) + : undefined + } + /> + ) : null return ( - {isUser && queued ? Queued : null} diff --git a/mobile/src/session/MobileNativeChatView.test.ts b/mobile/src/session/MobileNativeChatView.test.ts index fb4072c4f2d..1021671b837 100644 --- a/mobile/src/session/MobileNativeChatView.test.ts +++ b/mobile/src/session/MobileNativeChatView.test.ts @@ -71,6 +71,7 @@ type Overrides = { onClearSendError?: () => void inputLockReason?: 'disconnected' | 'waiting' | null onSend?: (text: string) => Promise + pending?: Parameters[0]['pending'] } function assistantTurn(id: string, text: string): NativeChatMessage { @@ -117,6 +118,13 @@ describe('MobileNativeChatView', () => { return (list.props.data as { id: string }[]).map((row) => row.id) } + function renderedRow(id: string): ReturnType { + const list = renderer!.root.find((node) => node.type === 'FlatList') + const data = list.props.data as NativeChatMessage[] + const index = data.findIndex((row) => row.id === id) + return list.props.renderItem({ item: data[index], index }) + } + function banners(): ReactTestInstance[] { return renderer!.root.findAll((node) => node.props.accessibilityRole === 'alert') } @@ -187,6 +195,15 @@ describe('MobileNativeChatView', () => { expect(listIds()).toEqual(['a1', 'streaming']) }) + it('renders an accepted optimistic image send without a queued state', async () => { + await render({ + pending: [{ id: 'pending-1', text: 'look', images: ['file:///phone-photo.jpg'] }] + }) + + expect(listIds()).toEqual(['pending-1']) + expect(renderedRow('pending-1').props).not.toHaveProperty('queued') + }) + it('keeps a visible lock through a subscribed-end lease blip', async () => { vi.useFakeTimers() try { diff --git a/mobile/src/session/MobileNativeChatView.tsx b/mobile/src/session/MobileNativeChatView.tsx index 697887bfe75..190001417a3 100644 --- a/mobile/src/session/MobileNativeChatView.tsx +++ b/mobile/src/session/MobileNativeChatView.tsx @@ -58,8 +58,7 @@ type Props = { loadingEarlier?: boolean onLoadEarlier?: () => void onSend: (text: string) => Promise - /** Optimistic queued sends (owned by the route so they survive view switches). */ - /** Optimistic user echoes, including any ridden-along image preview URIs. */ + /** Accepted user echoes awaiting transcript replacement, including image previews. */ pending: MobileNativeChatPendingItem[] /** Local photo URIs retained when the authoritative transcript replaces an * optimistic image bubble. */ @@ -176,9 +175,8 @@ export function MobileNativeChatView({ [] ) - const pendingIds = useMemo(() => new Set(pending.map((p) => p.id)), [pending]) // `data` is the list source: folded transcript + synthetic streaming bubble + - // route-owned optimistic queued messages. Memoize on the same deps so the + // route-owned accepted echoes. Memoize on the same deps so the // downstream autoscroll effects/`renderItem` keep referential stability. const { data } = useMemo( () => @@ -248,7 +246,6 @@ export function MobileNativeChatView({ ({ item, index }: { item: NativeChatMessage; index: number }) => ( ), - [pendingIds, toolsExpanded, fontScale, onScrollToMessage, onOpenFile] + [toolsExpanded, fontScale, onScrollToMessage, onOpenFile] ) const emptyState = mobileNativeChatEmptyState(status, agent ?? null, error) diff --git a/mobile/src/session/mobile-native-chat-message-styles.ts b/mobile/src/session/mobile-native-chat-message-styles.ts index df1bad92815..ad7cf4b4009 100644 --- a/mobile/src/session/mobile-native-chat-message-styles.ts +++ b/mobile/src/session/mobile-native-chat-message-styles.ts @@ -49,15 +49,6 @@ export const styles = StyleSheet.create({ reasoning: { opacity: 0.7 }, - queued: { - opacity: 0.55 - }, - queuedTag: { - color: colors.textMuted, - fontSize: 11, - fontWeight: '600', - marginBottom: 2 - }, toolRun: { marginTop: spacing.xs }, diff --git a/mobile/src/session/mobile-native-chat-render-data.ts b/mobile/src/session/mobile-native-chat-render-data.ts index 1ab2c0b03c3..73a45db6962 100644 --- a/mobile/src/session/mobile-native-chat-render-data.ts +++ b/mobile/src/session/mobile-native-chat-render-data.ts @@ -51,7 +51,7 @@ export function foldMobileNativeChatMessages(messages: NativeChatMessage[]): Nat /** Assemble the list data the chat renders: the folded transcript, then a * synthetic bubble for the streaming text the gate let through, then the - * route-owned optimistic "queued" messages at the tail. */ + * route-owned accepted optimistic messages at the tail. */ export function buildMobileNativeChatTransientData({ folded, streaming, diff --git a/mobile/src/session/mobile-native-chat-send-classification.ts b/mobile/src/session/mobile-native-chat-send-classification.ts index 18568c05789..2a34efabdd2 100644 --- a/mobile/src/session/mobile-native-chat-send-classification.ts +++ b/mobile/src/session/mobile-native-chat-send-classification.ts @@ -2,7 +2,7 @@ // (src/renderer/src/components/native-chat/NativeChatComposer.tsx): slash/skill // sends are TUI control actions, not chat turns — they never echo as a user // bubble, because the transcript will never contain a matching user turn and -// the optimistic echo would sit at "Queued" forever. +// the optimistic echo would never reconcile. import { getNativeChatAgentProfile, diff --git a/mobile/src/session/use-mobile-native-chat-message-send.ts b/mobile/src/session/use-mobile-native-chat-message-send.ts index 1d59e25431c..338c800cecd 100644 --- a/mobile/src/session/use-mobile-native-chat-message-send.ts +++ b/mobile/src/session/use-mobile-native-chat-message-send.ts @@ -181,7 +181,7 @@ export function useMobileNativeChatMessageSend(args: { }) // Why (desktop parity): a slash/skill send dispatches into the agent's own // TUI, not the conversation — the transcript never echoes it as a user - // turn, so an optimistic bubble would sit at "Queued" forever and the + // turn, so an optimistic bubble would never reconcile and the // unconfirmed hold could never observe a landing. const classification = classifyMobileNativeChatSend(agent, text) if (outcome === 'unknown') {