Files
orca/mobile/src/session/MobileNativeChatOverlay.tsx
T
Brennan Benson 32e258d7fc feat(mobile): show attached images as removable thumbnails in native chat
Attaching a picture in mobile native chat gave no visible feedback: the
image uploaded to the host and its path was pasted into the hidden PTY, so
nothing on screen ever showed it was attached.

Defer the TUI paste to send time and hold picked images in composer state,
rendered as a thumbnail tray above the input (spinner while uploading, X
to remove, image-only sends allowed). On send, each ready image's path is
pasted bracketed into the agent TUI right before the message text, matching
the desktop composer-attachment model. The terminal live-input surface keeps
the immediate paste, where the TUI's own input line is the feedback.
2026-07-22 12:31:12 -07:00

91 lines
3.2 KiB
TypeScript

import { StyleSheet, View } from 'react-native'
import { MobileNativeChatView, type MobileNativeChatInputLockReason } from './MobileNativeChatView'
import type { MobileNativeChatController } from './use-mobile-native-chat-controller'
import type { MobileNativeChatPendingImage } from './use-mobile-native-chat-pending-images'
type Props = {
controller: MobileNativeChatController
onAttachImage: () => void
isAttaching: boolean
/** Overrides the controller's plain-text send (e.g. to deliver pending image
* attachments first). Falls back to controller.handleNativeChatSend. */
onSend?: (text: string) => Promise<boolean>
pendingImages?: readonly MobileNativeChatPendingImage[]
onRemovePendingImage?: (id: string) => void
onMicPress: () => void
micActive: boolean
dictationMode: 'toggle' | 'hold'
onMicPressIn: () => void
onMicPressOut: () => void
inputLockReason: MobileNativeChatInputLockReason | null
keyboardInset: number
}
/** Keeps the terminal mounted underneath chat so its PTY subscription survives
* view toggles while the native surface owns the visible composer. */
export function MobileNativeChatOverlay({
controller,
onAttachImage,
isAttaching,
onSend,
pendingImages,
onRemovePendingImage,
onMicPress,
micActive,
dictationMode,
onMicPressIn,
onMicPressOut,
inputLockReason,
keyboardInset
}: Props): React.JSX.Element | null {
if (!controller.showNativeChat) {
return null
}
const session = controller.nativeChatSession
return (
<View style={styles.overlay}>
<MobileNativeChatView
messages={session.messages}
status={session.status}
error={session.error}
agent={controller.nativeChatAgent}
agentWorking={controller.nativeChatAgentWorking}
streamingText={controller.nativeChatStreamingText}
onStop={controller.handleNativeChatStop}
ask={controller.nativeChatAsk}
onAnswerAsk={controller.handleNativeChatAnswerAsk}
onCancelAsk={controller.handleNativeChatCancelAsk}
question={controller.nativeChatQuestion}
onAnswerQuestion={controller.handleNativeChatSend}
permission={controller.nativeChatPermission}
onRespondPermission={controller.handleNativeChatRespondPermission}
onOpenFile={controller.handleNativeChatOpenFile}
hasMore={session.hasMore}
loadingEarlier={session.loadingEarlier}
onLoadEarlier={session.loadEarlier}
onSend={onSend ?? controller.handleNativeChatSend}
pending={controller.chatPending}
composerText={controller.chatComposerText}
onComposerTextChange={controller.setChatComposerText}
onAttachImage={onAttachImage}
isAttaching={isAttaching}
pendingImages={pendingImages}
onRemovePendingImage={onRemovePendingImage}
onMicPress={onMicPress}
micActive={micActive}
dictationMode={dictationMode}
onMicPressIn={onMicPressIn}
onMicPressOut={onMicPressOut}
inputLockReason={inputLockReason}
filePaths={controller.nativeChatFilePaths}
onNeedFiles={controller.loadNativeChatFiles}
keyboardInset={keyboardInset}
/>
</View>
)
}
const styles = StyleSheet.create({
overlay: StyleSheet.absoluteFillObject
})