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:
@@ -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()}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user