mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
* fix(mobile): show live input caret and preserve selection edits Display the existing input directly so its native caret shows. Accessory Backspace/Delete edit at the field's caret, read from the input when pressed, so edits that keep the caret (IME composition) still delete, and the caret is restored for subsequent typing. Port and adapt community PR #28294 to the current command dock. Co-authored-by: Kyino <kyino1025@gmail.com> * fix(mobile): keep live input caret keys local and the dock height fixed Hardware Left/Right/Home/End moved both the visible field caret and the PTY cursor, so the mirror erased at the wrong place. With field text they now move only the field caret, like Backspace and Delete; Right/End at the field end still reach the PTY so shells can accept an autosuggestion. On Android the key binding lets plain caret keys through to that decision instead of sending them to the PTY. The dictation and upload status covers the field instead of adding a row, so starting or stopping either no longer resizes the terminal. * test(mobile): annotate the DOM field cast in the accessory input test * chore(mobile): allowlist the live input field strings for localization coverage --------- Co-authored-by: Kyino <kyino1025@gmail.com> Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
186 lines
7.1 KiB
TypeScript
186 lines
7.1 KiB
TypeScript
import { View, TextInput, Pressable } from 'react-native'
|
|
import { hostOs } from '../platform/host-os'
|
|
import { ArrowUp } from 'lucide-react-native'
|
|
import {
|
|
getTerminalCommandKeyboardType,
|
|
getTerminalLiveInputKeyboardType
|
|
} from '../terminal/terminal-keyboard-type'
|
|
import { MobileTerminalLiveInputStatus } from './MobileTerminalLiveInputStatus'
|
|
import { MobileTerminalInputActions } from './MobileTerminalInputActions'
|
|
import { MobileSessionAccessoryKeys } from './MobileSessionAccessoryKeys'
|
|
import { colors } from '../theme/mobile-theme'
|
|
import { useMobileColorScheme } from '../theme/use-mobile-color-scheme'
|
|
import { styles } from './mobile-session-styles'
|
|
import type { MobileSessionController } from './use-mobile-session-controller'
|
|
|
|
// Markdown, file, browser and chat tabs bring their own input; only terminals get the dock and its keys.
|
|
export function sessionCommandDockShown(controller: MobileSessionController): boolean {
|
|
return (
|
|
!controller.activeMarkdownTab &&
|
|
!controller.activeFileTab &&
|
|
!controller.activeBrowserTab &&
|
|
!controller.showNativeChat
|
|
)
|
|
}
|
|
|
|
export function MobileSessionCommandDock({
|
|
controller,
|
|
keysInRail
|
|
}: {
|
|
controller: MobileSessionController
|
|
keysInRail: boolean
|
|
}) {
|
|
useMobileColorScheme()
|
|
const {
|
|
insets,
|
|
bufferedTerminalDraftState,
|
|
autocompleteEnabled,
|
|
liveInputCapture,
|
|
dictationMode,
|
|
dictationEnabled,
|
|
bindCommandField,
|
|
handleLiveInputChange,
|
|
handleLiveInputKeyPress,
|
|
bindLiveInputField,
|
|
submitLiveInput,
|
|
canSend,
|
|
canCompose,
|
|
liveInputEnabled,
|
|
dictation,
|
|
cancelDictation,
|
|
handleDictationToggle,
|
|
handleDictationPressIn,
|
|
handleDictationPressOut,
|
|
handleSend,
|
|
isAttaching,
|
|
attachImage,
|
|
keyboardLift
|
|
} = controller
|
|
return (
|
|
sessionCommandDockShown(controller) && (
|
|
<View
|
|
style={[
|
|
styles.commandDock,
|
|
{ paddingBottom: insets.bottom, transform: [{ translateY: -keyboardLift }] }
|
|
]}
|
|
>
|
|
{}
|
|
{keysInRail ? null : <MobileSessionAccessoryKeys controller={controller} />}
|
|
{}
|
|
{liveInputEnabled ? (
|
|
<View style={styles.inputBar}>
|
|
<View style={styles.liveInputField}>
|
|
<TextInput
|
|
ref={bindLiveInputField}
|
|
style={[
|
|
styles.textInput,
|
|
styles.liveInputCapture,
|
|
!canSend && styles.liveInputCaptureDisabled
|
|
]}
|
|
value={liveInputCapture}
|
|
onChange={handleLiveInputChange}
|
|
onKeyPress={handleLiveInputKeyPress}
|
|
onSubmitEditing={submitLiveInput}
|
|
placeholder="Live input"
|
|
placeholderTextColor={colors.textMuted}
|
|
selectionColor={colors.accentBlue}
|
|
accessibilityLabel="Live terminal input"
|
|
accessibilityHint="Typed text is sent directly to the active terminal"
|
|
showSoftInputOnFocus
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
spellCheck={false}
|
|
smartInsertDelete={false}
|
|
// Why: iOS textContentType overrides autoComplete and can narrow the keyboard; keep IME switching available.
|
|
autoComplete="off"
|
|
keyboardType={getTerminalLiveInputKeyboardType(hostOs())}
|
|
returnKeyType="default"
|
|
blurOnSubmit={false}
|
|
editable={canSend}
|
|
importantForAutofill="no"
|
|
/>
|
|
<MobileTerminalLiveInputStatus
|
|
dictation={dictation}
|
|
dictationMode={dictationMode}
|
|
isAttaching={isAttaching}
|
|
/>
|
|
</View>
|
|
<MobileTerminalInputActions
|
|
canSend={canSend}
|
|
isAttaching={isAttaching}
|
|
dictation={dictation}
|
|
dictationMode={dictationMode}
|
|
dictationEnabled={dictationEnabled}
|
|
buttonStyle={styles.dictationButton}
|
|
activeButtonStyle={styles.dictationButtonActive}
|
|
disabledButtonStyle={styles.sendButtonDisabled}
|
|
onAttachImage={() => void attachImage('library')}
|
|
onAttachFile={() => void attachImage('files')}
|
|
onDictationToggle={handleDictationToggle}
|
|
onDictationPressIn={handleDictationPressIn}
|
|
onDictationPressOut={handleDictationPressOut}
|
|
onDictationCancel={cancelDictation}
|
|
/>
|
|
</View>
|
|
) : (
|
|
<View style={styles.inputBar}>
|
|
<TextInput
|
|
ref={bindCommandField}
|
|
// Why: Android caches IME inputType at mount, so toggling autocomplete must remount there; iOS updates in place.
|
|
key={
|
|
hostOs() === 'android'
|
|
? autocompleteEnabled
|
|
? 'cmd-input-ac-on'
|
|
: 'cmd-input-ac-off'
|
|
: 'cmd-input'
|
|
}
|
|
style={styles.textInput}
|
|
value={bufferedTerminalDraftState.input}
|
|
// Why: iOS kills active dictation/IME if JS writes a value differing from native text; store raw, normalize at send.
|
|
onChangeText={bufferedTerminalDraftState.setInput}
|
|
placeholder="Type a command…"
|
|
placeholderTextColor={colors.textMuted}
|
|
autoCapitalize="none"
|
|
autoCorrect={autocompleteEnabled}
|
|
spellCheck={autocompleteEnabled}
|
|
smartInsertDelete={false}
|
|
// Why: not autofill content, but keyboard must stay default so non-Latin IMEs remain selectable.
|
|
autoComplete="off"
|
|
keyboardType={getTerminalCommandKeyboardType(hostOs(), autocompleteEnabled)}
|
|
returnKeyType="send"
|
|
blurOnSubmit={false}
|
|
// Why: composing is local — an outage must not lock the field or discard typed text (#6713).
|
|
editable={canCompose}
|
|
onSubmitEditing={() => void handleSend()}
|
|
/>
|
|
<MobileTerminalInputActions
|
|
canSend={canSend}
|
|
isAttaching={isAttaching}
|
|
dictation={dictation}
|
|
dictationMode={dictationMode}
|
|
dictationEnabled={dictationEnabled}
|
|
buttonStyle={styles.dictationButton}
|
|
activeButtonStyle={styles.dictationButtonActive}
|
|
disabledButtonStyle={styles.sendButtonDisabled}
|
|
onAttachImage={() => void attachImage('library')}
|
|
onAttachFile={() => void attachImage('files')}
|
|
onDictationToggle={handleDictationToggle}
|
|
onDictationPressIn={handleDictationPressIn}
|
|
onDictationPressOut={handleDictationPressOut}
|
|
onDictationCancel={cancelDictation}
|
|
/>
|
|
<Pressable
|
|
style={[styles.sendButton, !canSend && styles.sendButtonDisabled]}
|
|
disabled={!canSend}
|
|
onPress={() => void handleSend()}
|
|
accessibilityLabel="Send command"
|
|
>
|
|
<ArrowUp size={18} color={colors.textSecondary} strokeWidth={2.5} />
|
|
</Pressable>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)
|
|
)
|
|
}
|