diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx
index d19a5da3..64b40bae 100644
--- a/packages/ui/src/App.tsx
+++ b/packages/ui/src/App.tsx
@@ -143,6 +143,31 @@ const App: Component = () => {
onCleanup(() => document.removeEventListener("fullscreenchange", syncBrowserFullscreenState))
})
+ onMount(() => {
+ if (typeof window === "undefined") return
+ const vv = window.visualViewport
+ if (!vv) return
+
+ const updateKeyboardOffset = () => {
+ // visualViewport shrinks when the OSK is visible. Use the delta as a bottom inset.
+ const inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop)
+ document.documentElement.style.setProperty("--keyboard-offset", `${Math.floor(inset)}px`)
+ }
+
+ const schedule = () => requestAnimationFrame(updateKeyboardOffset)
+ schedule()
+ vv.addEventListener("resize", schedule)
+ vv.addEventListener("scroll", schedule)
+ window.addEventListener("orientationchange", schedule)
+
+ onCleanup(() => {
+ vv.removeEventListener("resize", schedule)
+ vv.removeEventListener("scroll", schedule)
+ window.removeEventListener("orientationchange", schedule)
+ document.documentElement.style.removeProperty("--keyboard-offset")
+ })
+ })
+
// If the user exits browser fullscreen via browser UI, restore chrome.
let lastBrowserFullscreen = false
createEffect(() => {
@@ -471,7 +496,7 @@ const App: Component = () => {
-
+
= (props) => {
instanceId={props.instance.id}
instanceFolder={props.instance.folder}
escapeInDebounce={props.escapeInDebounce}
+ isPhoneLayout={isPhoneLayout()}
showSidebarToggle={showEmbeddedSidebarToggle()}
onSidebarToggle={() => setLeftOpen(true)}
forceCompactStatusLayout={showEmbeddedSidebarToggle()}
diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx
index 3277ac78..f4f834c1 100644
--- a/packages/ui/src/components/prompt-input.tsx
+++ b/packages/ui/src/components/prompt-input.tsx
@@ -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"
diff --git a/packages/ui/src/components/prompt-input/types.ts b/packages/ui/src/components/prompt-input/types.ts
index 54757793..b4dc44c9 100644
--- a/packages/ui/src/components/prompt-input/types.ts
+++ b/packages/ui/src/components/prompt-input/types.ts
@@ -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
onRunShell?: (command: string) => Promise
disabled?: boolean
diff --git a/packages/ui/src/components/session/session-view.tsx b/packages/ui/src/components/session/session-view.tsx
index e0de2457..484546e3 100644
--- a/packages/ui/src/components/session/session-view.tsx
+++ b/packages/ui/src/components/session/session-view.tsx
@@ -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 = (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 = (props) => {
+ 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}
+ />
)
}}
diff --git a/packages/ui/src/styles/messaging/prompt-input.css b/packages/ui/src/styles/messaging/prompt-input.css
index 4797c241..8cbfb796 100644
--- a/packages/ui/src/styles/messaging/prompt-input.css
+++ b/packages/ui/src/styles/messaging/prompt-input.css
@@ -295,7 +295,33 @@
}
.prompt-input {
- padding-bottom: 1.5rem;
+ /* Prevent iOS Safari input zoom + keep input compact. */
+ font-size: 16px;
+ padding-bottom: 0.75rem;
+ }
+}
+
+@media (max-width: 1279px) {
+ :root {
+ --prompt-input-compact-height: 104px;
+ }
+
+ .prompt-input-wrapper {
+ min-height: var(--prompt-input-compact-height);
+ }
+
+ .prompt-input-field-container {
+ min-height: var(--prompt-input-compact-height);
+ height: var(--prompt-input-compact-height);
+ }
+
+ .prompt-input-field {
+ height: var(--prompt-input-compact-height);
+ }
+
+ .prompt-input-field-container.is-expanded,
+ .prompt-input-field.is-expanded {
+ height: auto;
}
}
@@ -307,9 +333,9 @@
@media (max-width: 640px) {
.prompt-input {
- min-height: 64px;
+ min-height: 0;
padding: 0.5rem 0.75rem;
- padding-bottom: 2.25rem;
+ padding-bottom: 0.75rem;
}
.prompt-input-wrapper {