Files
orca/mobile/src/session/MobileDiffCommentLineRow.tsx
3d341c132f fix(mobile): dismiss the keyboard after sending to an agent (#17059)
* fix(mobile): dismiss the keyboard after sending to an agent

Sending a message left the software keyboard up, covering the reply the
user was waiting on. Drop it once the send is accepted, on all three send
paths: the terminal live input, the buffered command input, and the chat
composer.

Gated on the tab being an agent session. A plain shell keeps the keyboard
so back-to-back commands stay typeable, a rejected send keeps it so the
handed-back draft stays editable, and the accessory shortcut row is
untouched because dismissing would pull away the row being tapped.

* fix(mobile): gate keyboard dismissal on accepted sends

* fix(mobile): fence keyboard dismissal completions

* fix(mobile): fence stale send completions

* test(mobile): update terminal guard expectations

* fix(mobile): restore rejected buffered drafts by origin

* fix(mobile): preserve intentional buffered draft clears

* fix(mobile): harden send dismissal authority

* test(mobile): preserve Strict Mode send dismissal

* fix(mobile): preserve drafts across terminal remints

* fix(mobile): preserve draft ownership through terminal races

* fix(mobile): harden draft recovery and send freshness

* fix(mobile): fence route reuse and native draft clears

* fix(mobile): preserve native draft edits before clear

* test(mobile): pin the terminal-list sweep that bounds buffered drafts

`bufferedTerminalDraftState.pruneDrafts(retainedHandles)` is the only bound on
two structures that live as long as the session screen — the buffered-draft
record and the pending-restoration map — and nothing failed when it was deleted
or when it was pointed at the raw `terminal.list` handles instead of the
retained set. Both mutations reddened 0 of 3,949 mobile tests.

Adds the wiring pin (both mutations now redden it) plus two behavioural tests
showing why the argument matters: `terminal.list` omits a chat-covered handle
while the desktop graph reloads, so the raw list drops a draft the user is
still holding while the retained set keeps it.

---------

Co-authored-by: Merge Sim <merge@sim.local>
Co-authored-by: Merge Sim <sim@local>
2026-08-30 16:59:55 -07:00

148 lines
5.2 KiB
TypeScript

import { Pressable, Text, TextInput, View } from 'react-native'
import { MessageSquare, Plus, X } from 'lucide-react-native'
import { MobileSyntaxSegments } from '../components/MobileSyntaxSegments'
import { colors } from '../theme/mobile-theme'
import { styles } from './mobile-session-styles'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import type { RenderableDiffLine } from './mobile-session-route-types'
export function MobileDiffCommentLineRow({
line,
title,
index,
comments,
activeCommentLine,
commentDraft,
commentsBusy,
onStartComment,
onCancelComment,
onDraftChange,
onSubmitComment,
onDeleteComment
}: {
line: RenderableDiffLine
title: string
index: number
comments: DiffComment[]
activeCommentLine: number | null
commentDraft: string
commentsBusy: boolean
onStartComment: (lineNumber: number) => void
onCancelComment: () => void
onDraftChange: (value: string) => void
onSubmitComment: (lineNumber: number) => void
onDeleteComment: (commentId: string) => void
}) {
const commentLine = line.newLineNumber
const isCommenting = commentLine !== undefined && activeCommentLine === commentLine
const canComment = commentLine !== undefined
// Why: review notes anchor to the modified side, so show that line number in the single mobile gutter.
const gutterLineNumber = line.newLineNumber ?? line.oldLineNumber ?? ''
return (
<View style={styles.diffLineBlock}>
<View
style={[
styles.diffLine,
line.kind === 'add' && styles.diffLineAdded,
line.kind === 'delete' && styles.diffLineDeleted
]}
>
<Text style={styles.diffGutter}>{gutterLineNumber}</Text>
<Text
selectable
style={styles.diffText}
accessibilityLabel={`${title} diff line ${index + 1}`}
>
<Text
style={[
styles.diffPrefix,
line.kind === 'add' && styles.diffPrefixAdded,
line.kind === 'delete' && styles.diffPrefixDeleted
]}
>
{line.kind === 'add' ? '+ ' : line.kind === 'delete' ? '- ' : ' '}
</Text>
<MobileSyntaxSegments segments={line.segments} />
</Text>
{canComment ? (
<Pressable
style={({ pressed }) => [
styles.diffCommentAddButton,
pressed && styles.diffCommentAddButtonPressed,
commentsBusy && styles.diffCommentButtonDisabled
]}
disabled={commentsBusy}
onPress={() => {
if (commentLine !== undefined) {
onStartComment(commentLine)
}
}}
accessibilityLabel={`Add note on line ${commentLine}`}
>
<Plus size={12} color={colors.textSecondary} strokeWidth={2.3} />
</Pressable>
) : null}
</View>
{comments.length > 0 ? (
<View style={styles.diffCommentList}>
{comments.map((comment) => (
<View key={comment.id} style={styles.diffCommentCard}>
<View style={styles.diffCommentHeader}>
<MessageSquare size={12} color={colors.textMuted} strokeWidth={2.2} />
<Text style={styles.diffCommentMeta}>Line {comment.lineNumber}</Text>
<Pressable
style={styles.diffCommentDeleteButton}
disabled={commentsBusy}
onPress={() => onDeleteComment(comment.id)}
accessibilityLabel={`Delete note on line ${comment.lineNumber}`}
>
<X size={12} color={colors.textMuted} strokeWidth={2.2} />
</Pressable>
</View>
<Text style={styles.diffCommentBody}>{comment.body}</Text>
</View>
))}
</View>
) : null}
{isCommenting ? (
<View style={styles.diffCommentComposer}>
<TextInput
style={[styles.textInput, styles.diffCommentInput]}
value={commentDraft}
onChangeText={onDraftChange}
placeholder="Add review note"
placeholderTextColor={colors.textMuted}
editable={!commentsBusy}
multiline
textAlignVertical="top"
autoFocus
/>
<View style={styles.diffCommentComposerActions}>
<Pressable
style={styles.diffCommentSecondaryAction}
disabled={commentsBusy}
onPress={onCancelComment}
>
<Text style={styles.diffCommentSecondaryText}>Cancel</Text>
</Pressable>
<Pressable
style={[
styles.diffCommentPrimaryAction,
(!commentDraft.trim() || commentsBusy) && styles.diffCommentButtonDisabled
]}
disabled={!commentDraft.trim() || commentsBusy}
onPress={() => {
if (commentLine !== undefined) {
onSubmitComment(commentLine)
}
}}
>
<Text style={styles.diffCommentPrimaryText}>Save note</Text>
</Pressable>
</View>
</View>
) : null}
</View>
)
}