diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx
index a34c8643ab0..10b413d0528 100644
--- a/mobile/app/h/[hostId]/session/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx
@@ -5,8 +5,6 @@ import {
Linking,
type AppStateStatus,
BackHandler,
- FlatList,
- Image,
View,
Text,
ScrollView,
@@ -16,8 +14,7 @@ import {
Platform,
ActivityIndicator,
type KeyboardEvent,
- type LayoutChangeEvent,
- type ListRenderItem
+ type LayoutChangeEvent
} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
@@ -37,12 +34,10 @@ import {
GitBranch,
Globe,
Keyboard as KeyboardIcon,
- MessageSquare,
Monitor,
MoreHorizontal,
Plus,
RefreshCw,
- Send,
Smartphone,
SquareTerminal,
X
@@ -82,6 +77,8 @@ import {
} from '../../../../src/session/mobile-bulk-close-sheet-actions'
import { useMobilePrBranchContext } from '../../../../src/session/use-mobile-pr-branch-context'
import { isFloatingWorkspaceWorktreeId } from '../../../../src/session/floating-workspace'
+import { useAgentSendKeyboardDismissal } from '../../../../src/session/use-agent-send-keyboard-dismissal'
+import { useMobileSendCompletionGeneration } from '../../../../src/session/use-mobile-send-completion-generation'
import { SessionDockColumn } from '../../../../src/session/SessionDockColumn'
import { MobileSessionHeaderIconButton } from '../../../../src/session/MobileSessionHeaderIconButton'
import { MobileSessionHeaderMoreActionsSheet } from '../../../../src/session/MobileSessionHeaderMoreActionsSheet'
@@ -130,6 +127,7 @@ import { useTerminalLiveInputFocus } from '../../../../src/terminal/use-terminal
import { dismissTerminalKeyboard } from '../../../../src/terminal/terminal-keyboard-dismiss'
import type { TerminalLiveInputSender } from '../../../../src/terminal/terminal-live-input-sender'
import { isTerminalSendRpcAccepted } from '../../../../src/terminal/terminal-send-rpc-response'
+import { useBufferedTerminalDrafts } from '../../../../src/terminal/use-buffered-terminal-drafts'
import { sendMobileTerminalQueryReply } from '../../../../src/terminal/mobile-terminal-query-reply'
import { TERMINAL_QUERY_REPLY_INPUT_RUNTIME_CAPABILITY } from '../../../../../src/shared/protocol-version'
import { useTerminalLiveInputCommit } from '../../../../src/terminal/use-terminal-live-input-commit'
@@ -160,7 +158,6 @@ import { MobileAgentIcon } from '../../../../src/components/MobileAgentIcon'
import { TextInputModal } from '../../../../src/components/TextInputModal'
import { ConfirmModal } from '../../../../src/components/ConfirmModal'
import { MobileMarkdownReader } from '../../../../src/session/MobileMarkdownReader'
-import { MobileSyntaxSegments } from '../../../../src/components/MobileSyntaxSegments'
import {
CustomKeyModal,
loadCustomKeys,
@@ -174,12 +171,6 @@ import {
removeDeliveredMobileDiffComments,
removeMobileDiffComments
} from '../../../../src/session/mobile-diff-comments'
-import {
- buildPlainMobileDiffSyntaxLines,
- highlightMobileCode,
- highlightMobileDiffLines,
- resolveMobileSyntaxLanguage
-} from '../../../../src/session/mobile-file-syntax'
import {
getTerminalRecordsFromSessionTabs,
hasConnectedTerminalAbsentFromSessionTabs,
@@ -223,7 +214,6 @@ import {
buildMarkdownDiskFallbackDoc,
shouldReadMarkdownFromDiskAfterReadTabFailure
} from '../../../../src/session/mobile-markdown-disk-fallback'
-import { MobileHtmlPreview } from '../../../../src/components/MobileHtmlPreview'
import { MobileDictationSetupSheet } from '../../../../src/components/MobileDictationSetupSheet'
import {
fetchDictationSetup,
@@ -290,21 +280,18 @@ import {
import { colors } from '../../../../src/theme/mobile-theme'
import { QuickCommandsTabButton } from '../../../../src/session/QuickCommandsTabButton'
import { styles } from '../../../../src/session/mobile-session-styles'
+import { MobileSessionFileReader } from '../../../../src/session/MobileSessionFileReader'
import type { DiffComment } from '../../../../../src/shared/diff-comment-types'
import type { TerminalQuickCommand } from '../../../../../src/shared/terminal-quick-command-types'
import type {
- DiffCommentActions,
DiffNotesDelivery,
- DiffSyntaxState,
DirtyMarkdownDraft,
FileDocState,
- FileSyntaxState,
MarkdownDocState,
MobileDisplayMode,
MobileNewTabAgentLoadState,
MobileSessionTab,
MobileSessionTabType,
- RenderableDiffLine,
RuntimeRepoSummary,
SessionTabsResult,
Terminal,
@@ -324,416 +311,6 @@ function getClosedTabTombstoneExpiry(): number {
return Date.now() + CLOSED_TAB_TOMBSTONE_TTL_MS
}
-function DiffLineRow({
- 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 (
-
-
- {gutterLineNumber}
-
-
- {line.kind === 'add' ? '+ ' : line.kind === 'delete' ? '- ' : ' '}
-
-
-
- {canComment ? (
- [
- styles.diffCommentAddButton,
- pressed && styles.diffCommentAddButtonPressed,
- commentsBusy && styles.diffCommentButtonDisabled
- ]}
- disabled={commentsBusy}
- onPress={() => {
- if (commentLine !== undefined) {
- onStartComment(commentLine)
- }
- }}
- accessibilityLabel={`Add note on line ${commentLine}`}
- >
-
-
- ) : null}
-
- {comments.length > 0 ? (
-
- {comments.map((comment) => (
-
-
-
- Line {comment.lineNumber}
- onDeleteComment(comment.id)}
- accessibilityLabel={`Delete note on line ${comment.lineNumber}`}
- >
-
-
-
- {comment.body}
-
- ))}
-
- ) : null}
- {isCommenting ? (
-
-
-
-
- Cancel
-
- {
- if (commentLine !== undefined) {
- onSubmitComment(commentLine)
- }
- }}
- >
- Save note
-
-
-
- ) : null}
-
- )
-}
-
-function FileReader({
- doc,
- title,
- relativePath,
- language,
- diffCommentActions
-}: {
- doc: FileDocState | undefined
- title: string
- relativePath: string
- language?: string
- diffCommentActions?: DiffCommentActions
-}) {
- const syntaxLanguage = useMemo(
- () => resolveMobileSyntaxLanguage(relativePath || title, language),
- [language, relativePath, title]
- )
- const [fileSyntax, setFileSyntax] = useState(null)
- const [diffSyntax, setDiffSyntax] = useState(null)
- const [activeCommentLine, setActiveCommentLine] = useState(null)
- const [commentDraft, setCommentDraft] = useState('')
- const plainDiffLines = useMemo(
- () =>
- doc?.status === 'ready' && doc.kind === 'diff'
- ? buildPlainMobileDiffSyntaxLines(doc.lines)
- : [],
- [doc]
- )
- const diffCommentsForFile = useMemo(
- () =>
- diffCommentActions?.comments.filter(
- (comment) => comment.filePath === relativePath && comment.source !== 'markdown'
- ) ?? [],
- [diffCommentActions?.comments, relativePath]
- )
- const diffCommentsByLine = useMemo(() => {
- const map = new Map()
- for (const comment of diffCommentsForFile) {
- const list = map.get(comment.lineNumber) ?? []
- list.push(comment)
- map.set(comment.lineNumber, list)
- }
- for (const list of map.values()) {
- list.sort((a, b) => a.createdAt - b.createdAt)
- }
- return map
- }, [diffCommentsForFile])
-
- const startComment = useCallback((lineNumber: number) => {
- setActiveCommentLine(lineNumber)
- setCommentDraft('')
- }, [])
-
- const cancelComment = useCallback(() => {
- setActiveCommentLine(null)
- setCommentDraft('')
- }, [])
-
- const submitComment = useCallback(
- (lineNumber: number) => {
- if (!diffCommentActions) {
- return
- }
- void diffCommentActions.onAdd(relativePath, lineNumber, commentDraft).then((added) => {
- if (added) {
- setActiveCommentLine(null)
- setCommentDraft('')
- }
- })
- },
- [commentDraft, diffCommentActions, relativePath]
- )
-
- const renderDiffLine: ListRenderItem = useCallback(
- ({ item, index }) => (
- {
- if (diffCommentActions) {
- void diffCommentActions.onDelete(commentId)
- }
- }}
- />
- ),
- [
- activeCommentLine,
- cancelComment,
- commentDraft,
- diffCommentActions,
- diffCommentsByLine,
- startComment,
- submitComment,
- title
- ]
- )
-
- useEffect(() => {
- if (doc?.status !== 'ready') {
- return undefined
- }
-
- // Why: defer highlighting one tick so large files show as plain text immediately before colors are applied.
- const timer = setTimeout(() => {
- // file + html share the syntax-segment source view (html's "Source" toggle).
- if (doc.kind === 'file' || doc.kind === 'html') {
- setFileSyntax({
- doc,
- language: syntaxLanguage,
- segments: highlightMobileCode(doc.content, syntaxLanguage).segments
- })
- return
- }
- if (doc.kind === 'diff') {
- setDiffSyntax({
- doc,
- language: syntaxLanguage,
- lines: highlightMobileDiffLines(doc.lines, syntaxLanguage)
- })
- }
- // image: no syntax highlighting.
- }, 0)
-
- return () => clearTimeout(timer)
- }, [doc, syntaxLanguage])
-
- if (!doc || doc.status === 'loading') {
- return (
-
-
-
- )
- }
- if (doc.status === 'error') {
- return (
-
- {doc.message}
-
- )
- }
-
- if (doc.kind === 'diff') {
- const activeDiffSyntax =
- diffSyntax?.doc === doc && diffSyntax.language === syntaxLanguage ? diffSyntax.lines : null
- const commentCount = diffCommentActions?.comments.length ?? 0
- const unsentCommentCount =
- diffCommentActions?.comments.filter((comment) => !comment.sentAt).length ?? 0
- const commentsBusy = diffCommentActions?.busy === true
- const canCopyNotes = commentCount > 0 && !commentsBusy
- const canSendNotes = unsentCommentCount > 0 && !commentsBusy
- return (
-
- {diffCommentActions ? (
-
-
-
-
- {commentCount === 0
- ? 'No review notes'
- : `${commentCount} review ${commentCount === 1 ? 'note' : 'notes'}`}
-
-
-
- void diffCommentActions.onCopyAll()}
- accessibilityLabel="Copy review notes"
- >
-
- Copy
-
-
-
- Send
-
-
-
- ) : null}
-
- `${index}:${line.kind}:${line.oldLineNumber ?? ''}:${line.newLineNumber ?? ''}`
- }
- renderItem={renderDiffLine}
- initialNumToRender={32}
- maxToRenderPerBatch={48}
- windowSize={7}
- removeClippedSubviews={Platform.OS !== 'web'}
- keyboardShouldPersistTaps="handled"
- />
-
- )
- }
-
- if (doc.kind === 'image') {
- return (
-
-
-
-
-
- )
- }
-
- const renderSourceText = (content: string) => (
-
-
-
-
-
-
-
- )
-
- if (doc.kind === 'html') {
- return (
-
- renderSourceText(doc.content)} />
-
- )
- }
-
- return renderSourceText(doc.content)
-}
-
export default function SessionScreen() {
const {
hostId,
@@ -815,7 +392,6 @@ export default function SessionScreen() {
// Why: after an optimistic close, suppress the tab (with expiry) until the publisher confirms, so an in-flight snapshot can't flash it back.
const closedTabTombstonesRef = useRef