fix(ui): avoid mobile prompt focus on switch

Stops auto-focusing the prompt on phone session switches and scopes type-to-focus to the active visible prompt, disabling it on coarse pointers.
This commit is contained in:
Shantur Rathore
2026-02-17 18:00:48 +00:00
parent 29557fba6d
commit dc13d9a7d0
6 changed files with 103 additions and 23 deletions

View File

@@ -823,6 +823,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
instanceId={props.instance.id}
instanceFolder={props.instance.folder}
escapeInDebounce={props.escapeInDebounce}
isPhoneLayout={isPhoneLayout()}
showSidebarToggle={showEmbeddedSidebarToggle()}
onSidebarToggle={() => setLeftOpen(true)}
forceCompactStatusLayout={showEmbeddedSidebarToggle()}

View File

@@ -176,15 +176,26 @@ export default function PromptInput(props: PromptInputProps) {
),
)
onMount(() => {
const isCoarsePointer = () => {
if (typeof window === "undefined") return false
return Boolean(window.matchMedia?.("(pointer: coarse)")?.matches)
}
createEffect(() => {
// Scope global "type-to-focus" behavior to the active, visible prompt only.
if (typeof document === "undefined") return
if (isCoarsePointer()) return
if (props.isActive === false) return
if (props.disabled) return
const handleGlobalKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement
const activeElement = document.activeElement as HTMLElement | null
const isInputElement =
activeElement?.tagName === "INPUT" ||
activeElement?.tagName === "TEXTAREA" ||
activeElement?.tagName === "SELECT" ||
activeElement?.isContentEditable
Boolean(activeElement?.isContentEditable)
if (isInputElement) return
@@ -192,16 +203,25 @@ export default function PromptInput(props: PromptInputProps) {
if (isModifierKey) return
const isSpecialKey =
e.key === "Tab" || e.key === "Enter" || e.key.startsWith("Arrow") || e.key === "Backspace" || e.key === "Delete"
e.key === "Tab" ||
e.key === "Enter" ||
e.key.startsWith("Arrow") ||
e.key === "Backspace" ||
e.key === "Delete"
if (isSpecialKey) return
if (e.key.length === 1 && textareaRef && !props.disabled) {
textareaRef.focus()
const textarea = textareaRef
if (!textarea || textarea.disabled) return
// In session cache mode inactive panes are display:none; avoid stealing focus.
if (textarea.offsetParent === null) return
if (e.key.length === 1) {
textarea.focus()
}
}
document.addEventListener("keydown", handleGlobalKeyDown)
onCleanup(() => {
document.removeEventListener("keydown", handleGlobalKeyDown)
})
@@ -435,7 +455,7 @@ export default function PromptInput(props: PromptInputProps) {
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
disabled={props.disabled}
rows={expandState() === "expanded" ? 15 : 4}
rows={expandState() === "expanded" ? 15 : 3}
spellcheck={false}
autocorrect="off"
autoCapitalize="off"

View File

@@ -17,6 +17,9 @@ export interface PromptInputProps {
instanceId: string
instanceFolder: string
sessionId: string
// Used to scope global "type-to-focus" behavior.
isActive?: boolean
onSend: (prompt: string, attachments: Attachment[]) => Promise<void>
onRunShell?: (command: string) => Promise<void>
disabled?: boolean

View File

@@ -28,6 +28,7 @@ interface SessionViewProps {
instanceId: string
instanceFolder: string
escapeInDebounce: boolean
isPhoneLayout?: boolean
showSidebarToggle?: boolean
onSidebarToggle?: () => void
forceCompactStatusLayout?: boolean
@@ -76,6 +77,9 @@ export const SessionView: Component<SessionViewProps> = (props) => {
(isActive) => {
if (!isActive) return
// On phones, focusing the prompt on session switch is disruptive (it raises the OSK).
if (props.isPhoneLayout) return
// Don't steal focus from other inputs (command palette, dialogs, selectors, etc.)
if (typeof document === "undefined") return
const activeEl = document.activeElement as HTMLElement | null
@@ -314,17 +318,18 @@ export const SessionView: Component<SessionViewProps> = (props) => {
</Show>
<PromptInput
instanceId={props.instanceId}
instanceFolder={props.instanceFolder}
sessionId={activeSession.id}
onSend={handleSendMessage}
onRunShell={handleRunShell}
escapeInDebounce={props.escapeInDebounce}
isSessionBusy={sessionBusy()}
disabled={sessionNeedsInput()}
onAbortSession={handleAbortSession}
registerPromptInputApi={registerPromptInputApi}
/>
instanceId={props.instanceId}
instanceFolder={props.instanceFolder}
sessionId={activeSession.id}
isActive={props.isActive}
onSend={handleSendMessage}
onRunShell={handleRunShell}
escapeInDebounce={props.escapeInDebounce}
isSessionBusy={sessionBusy()}
disabled={sessionNeedsInput()}
onAbortSession={handleAbortSession}
registerPromptInputApi={registerPromptInputApi}
/>
</div>
)
}}