fix(mobile): stop labeling accepted messages queued (#13658)

This commit is contained in:
Brennan Benson
2026-08-10 16:09:17 -07:00
committed by GitHub
parent 03e740fa50
commit de1b119141
7 changed files with 33 additions and 33 deletions
+10 -15
View File
@@ -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 ? (
<AgentControls
onCopy={handleCopy}
onScrollToTop={
onScrollToMessage && messageIndex !== undefined
? () => onScrollToMessage(messageIndex)
: undefined
}
/>
) : null
const controls = isAgent ? (
<AgentControls
onCopy={handleCopy}
onScrollToTop={
onScrollToMessage && messageIndex !== undefined
? () => onScrollToMessage(messageIndex)
: undefined
}
/>
) : null
return (
<View style={[styles.row, isUser && styles.rowUser]}>
{isUser && queued ? <Text style={styles.queuedTag}>Queued</Text> : null}
<View
style={[
styles.content,
isUser && styles.userBubble,
isReasoning && styles.reasoning,
queued && styles.queued,
copied && styles.copied
]}
>
@@ -71,6 +71,7 @@ type Overrides = {
onClearSendError?: () => void
inputLockReason?: 'disconnected' | 'waiting' | null
onSend?: (text: string) => Promise<boolean>
pending?: Parameters<typeof MobileNativeChatView>[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<typeof createElement> {
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 {
+3 -6
View File
@@ -58,8 +58,7 @@ type Props = {
loadingEarlier?: boolean
onLoadEarlier?: () => void
onSend: (text: string) => Promise<boolean>
/** 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 }) => (
<MobileNativeChatMessage
message={item}
queued={pendingIds.has(item.id)}
toolsExpanded={toolsExpanded}
fontScale={fontScale}
messageIndex={index}
@@ -256,7 +253,7 @@ export function MobileNativeChatView({
onOpenFile={onOpenFile}
/>
),
[pendingIds, toolsExpanded, fontScale, onScrollToMessage, onOpenFile]
[toolsExpanded, fontScale, onScrollToMessage, onOpenFile]
)
const emptyState = mobileNativeChatEmptyState(status, agent ?? null, error)
@@ -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
},
@@ -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,
@@ -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,
@@ -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') {