refactor: drop mobile keyboard/viewport hacks, rely on browser
Remove body position:fixed lock, visualViewport listener in MainLayout (keyboard-inset heuristics, scroll-to-zero lock, focusin/focusout RAF), [data-keyboard-avoid-active] translateY rule, and related CSS vars (--oc-keyboard-inset, --oc-keyboard-avoid-offset, --oc-keyboard-home-indicator, --oc-visual-viewport-offset-top). Strip data-keyboard-avoid* attrs and keyboardAvoid props from Dialog/ScrollableOverlay and consumers. Drop isKeyboardOpen from useUIStore. With body unlocked and the layout plain flex-col (h-dvh), the browser shrinks the viewport on keyboard open naturally — composer sits above the keyboard, header stops lagging, input no longer jitters. FilesView.nudgeEditorSelectionAboveKeyboard now derives the occluded bottom locally from visualViewport + documentElement.clientHeight.
This commit is contained in:
@@ -484,7 +484,7 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) =>
|
||||
</div>
|
||||
|
||||
{!isTunnelLocked && (
|
||||
<form onSubmit={handleSubmit} className="w-full space-y-2" data-keyboard-avoid="true">
|
||||
<form onSubmit={handleSubmit} className="w-full space-y-2">
|
||||
{canUsePasskey && (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -640,39 +640,33 @@ export const ChatContainer: React.FC = () => {
|
||||
void load();
|
||||
}, [currentSessionId, hasLoadedSessionMessages, isPinned, loadMessages, resumeToLatestInstant, sessionStatusForCurrent.type]);
|
||||
|
||||
if (!currentSessionId && !draftOpen) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col h-full bg-background"
|
||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
||||
>
|
||||
<ChatEmptyState />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!currentSessionId && !draftOpen) {
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-background">
|
||||
<ChatEmptyState />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentSessionId && draftOpen) {
|
||||
return (
|
||||
<div
|
||||
className="relative flex flex-col h-full bg-background transform-gpu"
|
||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
||||
>
|
||||
{!isDesktopExpandedInput ? (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<ChatEmptyState />
|
||||
</div>
|
||||
) : null}
|
||||
if (!currentSessionId && draftOpen) {
|
||||
return (
|
||||
<div className="relative flex flex-col h-full bg-background transform-gpu">
|
||||
{!isDesktopExpandedInput ? (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<ChatEmptyState />
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10',
|
||||
isDesktopExpandedInput
|
||||
? 'flex-1 min-h-0 bg-background'
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
||||
</div>
|
||||
</div>
|
||||
isDesktopExpandedInput
|
||||
? 'flex-1 min-h-0 bg-background'
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -680,16 +674,13 @@ export const ChatContainer: React.FC = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isSessionHydrating && sessionMessages.length === 0 && !streamingMessageId) {
|
||||
return (
|
||||
<div
|
||||
className="relative flex flex-col h-full bg-background"
|
||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
||||
>
|
||||
{returnToParentButton}
|
||||
<div
|
||||
className={cn(
|
||||
'relative min-h-0',
|
||||
if (isSessionHydrating && sessionMessages.length === 0 && !streamingMessageId) {
|
||||
return (
|
||||
<div className="relative flex flex-col h-full bg-background">
|
||||
{returnToParentButton}
|
||||
<div
|
||||
className={cn(
|
||||
'relative min-h-0',
|
||||
isDesktopExpandedInput
|
||||
? 'absolute inset-0 opacity-0 pointer-events-none'
|
||||
: 'flex-1'
|
||||
@@ -728,26 +719,23 @@ export const ChatContainer: React.FC = () => {
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10',
|
||||
isDesktopExpandedInput
|
||||
? 'flex-1 min-h-0 bg-background'
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
||||
</div>
|
||||
isDesktopExpandedInput
|
||||
? 'flex-1 min-h-0 bg-background'
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sessionMessages.length === 0 && !streamingMessageId) {
|
||||
return (
|
||||
<div
|
||||
className="relative flex flex-col h-full bg-background transform-gpu"
|
||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
||||
>
|
||||
{returnToParentButton}
|
||||
<div
|
||||
className={cn(
|
||||
if (sessionMessages.length === 0 && !streamingMessageId) {
|
||||
return (
|
||||
<div className="relative flex flex-col h-full bg-background transform-gpu">
|
||||
{returnToParentButton}
|
||||
<div
|
||||
className={cn(
|
||||
'relative min-h-0',
|
||||
isDesktopExpandedInput
|
||||
? 'absolute inset-0 opacity-0 pointer-events-none'
|
||||
@@ -764,25 +752,22 @@ export const ChatContainer: React.FC = () => {
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10',
|
||||
isDesktopExpandedInput
|
||||
? 'flex-1 min-h-0 bg-background'
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
||||
</div>
|
||||
isDesktopExpandedInput
|
||||
? 'flex-1 min-h-0 bg-background'
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
<ChatInput scrollToBottom={resumeToLatestInstant} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex flex-col h-full bg-background"
|
||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
||||
>
|
||||
{returnToParentButton}
|
||||
<ChatViewport
|
||||
currentSessionId={currentSessionId}
|
||||
return (
|
||||
<div className="relative flex flex-col h-full bg-background">
|
||||
{returnToParentButton}
|
||||
<ChatViewport
|
||||
currentSessionId={currentSessionId}
|
||||
isDesktopExpandedInput={isDesktopExpandedInput}
|
||||
isMobile={isMobile}
|
||||
stickyUserHeader={stickyUserHeader}
|
||||
@@ -809,13 +794,13 @@ export const ChatContainer: React.FC = () => {
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10',
|
||||
isDesktopExpandedInput
|
||||
? 'flex-1 min-h-0 bg-background'
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
{!isDesktopExpandedInput && sessionMessages.length > 0 && (
|
||||
<ScrollToBottomButton
|
||||
isDesktopExpandedInput
|
||||
? 'flex-1 min-h-0 bg-background'
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
{!isDesktopExpandedInput && sessionMessages.length > 0 && (
|
||||
<ScrollToBottomButton
|
||||
visible={timelineController.showScrollToBottom}
|
||||
onClick={navigation.resumeToLatest}
|
||||
/>
|
||||
|
||||
@@ -787,7 +787,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]);
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const inputBarOffset = useUIStore((state) => state.inputBarOffset);
|
||||
const isKeyboardOpen = useUIStore((state) => state.isKeyboardOpen);
|
||||
const persistChatDraft = useUIStore((state) => state.persistChatDraft);
|
||||
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
|
||||
const isExpandedInput = useUIStore((state) => state.isExpandedInput);
|
||||
@@ -1170,50 +1169,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
// Session activity for queue availability and controls
|
||||
const { phase: sessionPhase } = useCurrentSessionActivity();
|
||||
|
||||
const handleTextareaPointerDownCapture = React.useCallback((event: React.PointerEvent<HTMLTextAreaElement>) => {
|
||||
if (!isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.pointerType !== 'touch') {
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.activeElement === textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent iOS from scrolling the page to reveal the input.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const scroller = document.scrollingElement;
|
||||
if (scroller && scroller.scrollTop !== 0) {
|
||||
scroller.scrollTop = 0;
|
||||
}
|
||||
if (window.scrollY !== 0) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
try {
|
||||
textarea.focus({ preventScroll: true });
|
||||
} catch {
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
const len = textarea.value.length;
|
||||
try {
|
||||
textarea.setSelectionRange(len, len);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
const handleOpenMobileControls = React.useCallback(() => {
|
||||
if (!isMobile) {
|
||||
return;
|
||||
@@ -1226,16 +1181,12 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
|
||||
setMobileControlsPanel(null);
|
||||
|
||||
if (isKeyboardOpen) {
|
||||
textareaRef.current?.blur();
|
||||
requestAnimationFrame(() => {
|
||||
setMobileControlsOpen(true);
|
||||
});
|
||||
return;
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
setMobileControlsOpen(true);
|
||||
}, [isMobile, isKeyboardOpen, mobileControlsOpen]);
|
||||
}, [isMobile, mobileControlsOpen]);
|
||||
|
||||
const handleCloseMobileControls = React.useCallback(() => {
|
||||
setMobileControlsOpen(false);
|
||||
@@ -2136,7 +2087,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const isWordBoundary = !charBefore || /\s/.test(charBefore);
|
||||
if (isWordBoundary && !textAfterAt.includes(' ') && !textAfterAt.includes('\n')) {
|
||||
setMentionQuery(textAfterAt);
|
||||
setAutocompleteTab('agents');
|
||||
setAutocompleteTab((current) => current === 'files' ? 'files' : 'agents');
|
||||
setShowFileMention(true);
|
||||
} else {
|
||||
setShowFileMention(false);
|
||||
@@ -2179,22 +2130,31 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
const cursorPosition = textarea?.selectionStart ?? message.length;
|
||||
const textBeforeCursor = message.substring(0, cursorPosition);
|
||||
const lastAtSymbol = textBeforeCursor.lastIndexOf('@');
|
||||
const nextMentionQuery = lastAtSymbol !== -1
|
||||
? textBeforeCursor.substring(lastAtSymbol + 1).replace(/[\s\n].*$/, '')
|
||||
: '';
|
||||
|
||||
setAutocompleteTab(tab);
|
||||
setCommandQuery('');
|
||||
setMentionQuery('');
|
||||
if (tab === 'commands') {
|
||||
setMentionQuery('');
|
||||
applyAutocompletePrefix('/');
|
||||
}
|
||||
if (tab === 'agents') {
|
||||
setMentionQuery(nextMentionQuery);
|
||||
applyAutocompletePrefix('@');
|
||||
}
|
||||
if (tab === 'files') {
|
||||
setMentionQuery(nextMentionQuery);
|
||||
applyAutocompletePrefix('@');
|
||||
}
|
||||
setShowSkillAutocomplete(false);
|
||||
setShowCommandAutocomplete(tab === 'commands');
|
||||
setShowFileMention(tab === 'agents' || tab === 'files');
|
||||
}, [applyAutocompletePrefix, isMobile, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]);
|
||||
}, [applyAutocompletePrefix, isMobile, message, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]);
|
||||
|
||||
const handleOpenCommandMenu = React.useCallback(() => {
|
||||
if (!isMobile) {
|
||||
@@ -3331,10 +3291,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
className={cn(
|
||||
"relative pt-0 pb-4",
|
||||
isDesktopExpanded && 'flex h-full min-h-0 flex-col pt-4',
|
||||
isMobile && (isKeyboardOpen ? 'ios-keyboard-safe-area' : 'bottom-safe-area')
|
||||
isMobile && 'bottom-safe-area'
|
||||
)}
|
||||
data-keyboard-avoid="true"
|
||||
style={isMobile && inputBarOffset > 0 && !isKeyboardOpen ? { marginBottom: `${inputBarOffset}px` } : undefined}
|
||||
|
||||
style={isMobile && inputBarOffset > 0 ? { marginBottom: `${inputBarOffset}px` } : undefined}
|
||||
>
|
||||
<div className={cn('chat-column relative overflow-visible', isDesktopExpanded && 'flex flex-1 min-h-0 flex-col')}>
|
||||
<AttachedFilesList />
|
||||
@@ -3689,7 +3649,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
onDropCapture={handleDropCapture}
|
||||
onDrop={handleDrop}
|
||||
onDragEnd={handleDragEnd}
|
||||
onPointerDownCapture={handleTextareaPointerDownCapture}
|
||||
onKeyUp={updateAutocompleteOverlayPosition}
|
||||
onClick={updateAutocompleteOverlayPosition}
|
||||
onScroll={(event) => {
|
||||
|
||||
@@ -350,259 +350,6 @@ export const MainLayout: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const root = document.documentElement;
|
||||
|
||||
let stickyKeyboardInset = 0;
|
||||
let ignoreOpenUntilZero = false;
|
||||
let previousHeight = 0;
|
||||
let keyboardAvoidTarget: HTMLElement | null = null;
|
||||
|
||||
const setKeyboardOpen = useUIStore.getState().setKeyboardOpen;
|
||||
|
||||
const clearKeyboardAvoidTarget = () => {
|
||||
if (!keyboardAvoidTarget) {
|
||||
return;
|
||||
}
|
||||
keyboardAvoidTarget.style.setProperty('--oc-keyboard-avoid-offset', '0px');
|
||||
keyboardAvoidTarget.removeAttribute('data-keyboard-avoid-active');
|
||||
keyboardAvoidTarget = null;
|
||||
};
|
||||
|
||||
const resolveKeyboardAvoidTarget = (active: HTMLElement | null) => {
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
const explicitTargetId = active.getAttribute('data-keyboard-avoid-target-id');
|
||||
if (explicitTargetId) {
|
||||
const explicitTarget = document.getElementById(explicitTargetId);
|
||||
if (explicitTarget instanceof HTMLElement) {
|
||||
return explicitTarget;
|
||||
}
|
||||
}
|
||||
const markedTarget = active.closest('[data-keyboard-avoid]') as HTMLElement | null;
|
||||
if (markedTarget) {
|
||||
// data-keyboard-avoid="none" opts out of translateY avoidance entirely.
|
||||
// Used by components with their own scroll (e.g. CodeMirror).
|
||||
if (markedTarget.getAttribute('data-keyboard-avoid') === 'none') {
|
||||
return null;
|
||||
}
|
||||
return markedTarget;
|
||||
}
|
||||
if (active.classList.contains('overlay-scrollbar-container')) {
|
||||
const parent = active.parentElement;
|
||||
if (parent instanceof HTMLElement) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
return active;
|
||||
};
|
||||
|
||||
const forceKeyboardClosed = () => {
|
||||
stickyKeyboardInset = 0;
|
||||
ignoreOpenUntilZero = true;
|
||||
root.style.setProperty('--oc-keyboard-inset', '0px');
|
||||
setKeyboardOpen(false);
|
||||
};
|
||||
|
||||
// Batch visualViewport updates to once per animation frame to avoid
|
||||
// layout thrashing during keyboard open/close animations.
|
||||
let rafId = 0;
|
||||
|
||||
const updateVisualViewport = () => {
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
const height = viewport ? Math.round(viewport.height) : window.innerHeight;
|
||||
const offsetTop = viewport ? Math.max(0, Math.round(viewport.offsetTop)) : 0;
|
||||
|
||||
root.style.setProperty('--oc-visual-viewport-offset-top', `${offsetTop}px`);
|
||||
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
const tagName = active?.tagName;
|
||||
const isInput = tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
|
||||
const isTextTarget = isInput || Boolean(active?.isContentEditable);
|
||||
|
||||
const layoutHeight = Math.round(root.clientHeight || window.innerHeight);
|
||||
const viewportSum = height + offsetTop;
|
||||
const rawInset = Math.max(0, layoutHeight - viewportSum);
|
||||
|
||||
// Keyboard heuristic:
|
||||
// - when an input is focused, smaller deltas can still be keyboard
|
||||
// - when not focused, treat only big deltas as keyboard (ignore toolbars)
|
||||
const openThreshold = isTextTarget ? 120 : 180;
|
||||
const measuredInset = rawInset >= openThreshold ? rawInset : 0;
|
||||
|
||||
// Make the UI stable: treat keyboard inset as a step function.
|
||||
// - When opening: take the first big inset and hold it.
|
||||
// - When closing starts: immediately drop to 0 (even if keyboard animation continues).
|
||||
// Closing start signals:
|
||||
// - focus lost (handled via focusout)
|
||||
// - visual viewport height starts increasing while inset is non-zero
|
||||
if (ignoreOpenUntilZero) {
|
||||
if (measuredInset === 0) {
|
||||
ignoreOpenUntilZero = false;
|
||||
}
|
||||
stickyKeyboardInset = 0;
|
||||
} else if (stickyKeyboardInset === 0) {
|
||||
if (measuredInset > 0 && isTextTarget) {
|
||||
stickyKeyboardInset = measuredInset;
|
||||
}
|
||||
} else {
|
||||
// Only detect closing-by-height when focus is NOT on text input
|
||||
// (prevents false positives during Android keyboard animation)
|
||||
const closingByHeight = !isTextTarget && height > previousHeight + 6;
|
||||
|
||||
if (measuredInset === 0) {
|
||||
stickyKeyboardInset = 0;
|
||||
setKeyboardOpen(false);
|
||||
} else if (closingByHeight) {
|
||||
forceKeyboardClosed();
|
||||
} else if (measuredInset > 0 && isTextTarget) {
|
||||
// When focus is on text input, track actual inset (allows settling
|
||||
// to correct value after Android animation fluctuations)
|
||||
stickyKeyboardInset = measuredInset;
|
||||
setKeyboardOpen(true);
|
||||
} else if (measuredInset > stickyKeyboardInset) {
|
||||
stickyKeyboardInset = measuredInset;
|
||||
setKeyboardOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
root.style.setProperty('--oc-keyboard-inset', `${stickyKeyboardInset}px`);
|
||||
previousHeight = height;
|
||||
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
const keyboardHomeIndicator = isIOS && stickyKeyboardInset > 0 ? 34 : 0;
|
||||
root.style.setProperty('--oc-keyboard-home-indicator', `${keyboardHomeIndicator}px`);
|
||||
|
||||
const avoidTarget = isTextTarget ? resolveKeyboardAvoidTarget(active) : null;
|
||||
|
||||
if (!isMobile || !avoidTarget || !active) {
|
||||
clearKeyboardAvoidTarget();
|
||||
} else {
|
||||
if (avoidTarget !== keyboardAvoidTarget) {
|
||||
clearKeyboardAvoidTarget();
|
||||
keyboardAvoidTarget = avoidTarget;
|
||||
}
|
||||
const viewportBottom = offsetTop + height;
|
||||
const rect = active.getBoundingClientRect();
|
||||
const overlap = rect.bottom - viewportBottom;
|
||||
const clearance = 8;
|
||||
const keyboardInset = Math.max(stickyKeyboardInset, measuredInset);
|
||||
const avoidOffset = overlap > clearance && keyboardInset > 0
|
||||
? Math.min(overlap, keyboardInset)
|
||||
: 0;
|
||||
const target = keyboardAvoidTarget;
|
||||
if (target) {
|
||||
target.style.setProperty('--oc-keyboard-avoid-offset', `${avoidOffset}px`);
|
||||
target.setAttribute('data-keyboard-avoid-active', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
// Only force-scroll lock while an input is focused.
|
||||
if (isMobile && isTextTarget) {
|
||||
const scroller = document.scrollingElement;
|
||||
if (scroller && scroller.scrollTop !== 0) {
|
||||
scroller.scrollTop = 0;
|
||||
}
|
||||
if (window.scrollY !== 0) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleVisualViewportUpdate = () => {
|
||||
if (rafId) return;
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = 0;
|
||||
updateVisualViewport();
|
||||
});
|
||||
};
|
||||
|
||||
updateVisualViewport();
|
||||
|
||||
const viewport = window.visualViewport;
|
||||
viewport?.addEventListener('resize', scheduleVisualViewportUpdate);
|
||||
viewport?.addEventListener('scroll', scheduleVisualViewportUpdate);
|
||||
window.addEventListener('resize', scheduleVisualViewportUpdate);
|
||||
window.addEventListener('orientationchange', scheduleVisualViewportUpdate);
|
||||
const isTextInputTarget = (element: HTMLElement | null) => {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
const tagName = element.tagName;
|
||||
const isInput = tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
|
||||
return isInput || element.isContentEditable;
|
||||
};
|
||||
|
||||
// Reset ignoreOpenUntilZero when focus moves to a text input.
|
||||
// This allows keyboard detection to work when user taps input quickly
|
||||
// while keyboard is still closing (common on Android).
|
||||
const handleFocusIn = (event: FocusEvent) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (isTextInputTarget(target)) {
|
||||
ignoreOpenUntilZero = false;
|
||||
}
|
||||
scheduleVisualViewportUpdate();
|
||||
};
|
||||
document.addEventListener('focusin', handleFocusIn, true);
|
||||
|
||||
const handleFocusOut = (event: FocusEvent) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (!isTextInputTarget(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if focus is moving to another input - if so, don't close keyboard
|
||||
const related = event.relatedTarget as HTMLElement | null;
|
||||
if (isTextInputTarget(related)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On mobile contenteditable editors (CodeMirror), focus can momentarily
|
||||
// leave and return during drag-selection handles. Defer closing until the
|
||||
// next frame and only close when no text target is focused and the
|
||||
// visual viewport inset is actually zero.
|
||||
window.requestAnimationFrame(() => {
|
||||
if (isTextInputTarget(document.activeElement as HTMLElement | null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentViewport = window.visualViewport;
|
||||
const height = currentViewport ? Math.round(currentViewport.height) : window.innerHeight;
|
||||
const offsetTop = currentViewport ? Math.max(0, Math.round(currentViewport.offsetTop)) : 0;
|
||||
const layoutHeight = Math.round(root.clientHeight || window.innerHeight);
|
||||
const viewportSum = height + offsetTop;
|
||||
const rawInset = Math.max(0, layoutHeight - viewportSum);
|
||||
|
||||
if (rawInset > 0) {
|
||||
updateVisualViewport();
|
||||
return;
|
||||
}
|
||||
|
||||
forceKeyboardClosed();
|
||||
updateVisualViewport();
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('focusout', handleFocusOut, true);
|
||||
|
||||
return () => {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
viewport?.removeEventListener('resize', scheduleVisualViewportUpdate);
|
||||
viewport?.removeEventListener('scroll', scheduleVisualViewportUpdate);
|
||||
window.removeEventListener('resize', scheduleVisualViewportUpdate);
|
||||
window.removeEventListener('orientationchange', scheduleVisualViewportUpdate);
|
||||
document.removeEventListener('focusin', handleFocusIn, true);
|
||||
document.removeEventListener('focusout', handleFocusOut, true);
|
||||
clearKeyboardAvoidTarget();
|
||||
};
|
||||
}, [isMobile]);
|
||||
|
||||
const secondaryView = React.useMemo(() => {
|
||||
switch (activeMainTab) {
|
||||
case 'plan':
|
||||
|
||||
@@ -585,7 +585,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
const configuredSetupCount = setupCommands.filter(cmd => cmd.trim()).length;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col h-full bg-background" data-keyboard-avoid="true">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col h-full bg-background">
|
||||
{!isWindowed ? (
|
||||
<header
|
||||
onMouseDown={handleDragStart}
|
||||
|
||||
@@ -593,7 +593,7 @@ export const AgentsPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Header & Actions */}
|
||||
|
||||
@@ -169,7 +169,7 @@ export const CommandsPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Header */}
|
||||
|
||||
@@ -112,7 +112,7 @@ export const GitPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full bg-background">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full bg-background">
|
||||
<div className="mx-auto w-full max-w-3xl space-y-6 p-3 sm:p-6 sm:pt-8">
|
||||
<GitHubSettings />
|
||||
|
||||
|
||||
@@ -1253,7 +1253,7 @@ export const McpPage: React.FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Header */}
|
||||
@@ -1755,7 +1755,7 @@ export const McpPage: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-md" keyboardAvoid>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import JSON Snippet</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -32,7 +32,6 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
||||
if (!section) {
|
||||
return (
|
||||
<ScrollableOverlay
|
||||
keyboardAvoid
|
||||
outerClassName="h-full"
|
||||
className="w-full"
|
||||
>
|
||||
@@ -95,7 +94,6 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
||||
|
||||
return (
|
||||
<ScrollableOverlay
|
||||
keyboardAvoid
|
||||
outerClassName="h-full"
|
||||
className="w-full"
|
||||
>
|
||||
|
||||
@@ -236,7 +236,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
|
||||
if (!selectedProject) {
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-4xl p-3 sm:p-6 sm:pt-8">
|
||||
<p className="typography-meta text-muted-foreground">No projects available.</p>
|
||||
</div>
|
||||
@@ -244,7 +244,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full bg-background">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full bg-background">
|
||||
<div className="mx-auto w-full max-w-4xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Top Header & Actions */}
|
||||
|
||||
@@ -519,7 +519,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
|
||||
if (isAddMode) {
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
<div className="mb-4">
|
||||
<h1 className="typography-ui-header font-semibold text-foreground">Connect Provider</h1>
|
||||
@@ -791,7 +791,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Header */}
|
||||
|
||||
@@ -340,7 +340,7 @@ const SkillsInstalledPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Header */}
|
||||
@@ -558,7 +558,7 @@ const SkillsInstalledPage: React.FC = () => {
|
||||
setIsFileDialogOpen(open);
|
||||
if (!open) setEditingFilePath(null);
|
||||
}}>
|
||||
<DialogContent className="max-w-3xl max-h-[85vh] flex flex-col" keyboardAvoid>
|
||||
<DialogContent className="max-w-3xl max-h-[85vh] flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>{editingFilePath ? 'Edit Supporting File' : 'Add Supporting File'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -230,7 +230,7 @@ export const AddCatalogDialog: React.FC<AddCatalogDialogProps> = ({ open, onOpen
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-xl" keyboardAvoid>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add skills catalog</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -156,7 +156,7 @@ export const InstallSkillDialog: React.FC<InstallSkillDialogProps> = ({ open, on
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md" keyboardAvoid>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Install skill</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -144,7 +144,7 @@ export const SkillsCatalogPage: React.FC<SkillsCatalogPageProps> = ({ mode, onMo
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Header */}
|
||||
|
||||
@@ -148,7 +148,7 @@ export const UsagePage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollableOverlay keyboardAvoid outerClassName="h-full" className="w-full">
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
|
||||
{/* Header */}
|
||||
|
||||
@@ -178,7 +178,7 @@ const SessionFolderItemBase = <TSessionNode,>({
|
||||
{renaming ? (
|
||||
<form
|
||||
className="flex min-w-0 flex-1 items-center gap-1"
|
||||
data-keyboard-avoid="true"
|
||||
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
|
||||
@@ -259,6 +259,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
? 'group-hover:pr-5'
|
||||
: 'group-hover:pr-5 group-focus-within:pr-5');
|
||||
const suppressNextSelectRef = React.useRef(false);
|
||||
const [isTouchPressed, setIsTouchPressed] = React.useState(false);
|
||||
|
||||
const session = node.session;
|
||||
const liveSession = useSession(session.id);
|
||||
@@ -356,7 +357,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0">
|
||||
<form
|
||||
className="flex w-full items-center gap-2"
|
||||
data-keyboard-avoid="true"
|
||||
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
handleSaveEdit();
|
||||
@@ -516,6 +517,16 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
suppressNextSelectRef.current = true;
|
||||
}
|
||||
};
|
||||
const handleRowPointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
if (mobileVariant && event.pointerType === 'touch') {
|
||||
setIsTouchPressed(true);
|
||||
}
|
||||
};
|
||||
const handleRowPointerEnd = (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
if (mobileVariant && event.pointerType === 'touch') {
|
||||
setIsTouchPressed(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sessionMenuContent = (
|
||||
<DropdownMenuContent align="end" className="min-w-[180px]" onCloseAutoFocus={(event) => { if (renamingFolderId) event.preventDefault(); }}>
|
||||
@@ -639,15 +650,19 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isMissingDirectory}
|
||||
onMouseDown={handleRowMouseDown}
|
||||
onClick={(event) => handleRowSelect(event)}
|
||||
disabled={isMissingDirectory}
|
||||
onPointerDown={handleRowPointerDown}
|
||||
onPointerUp={handleRowPointerEnd}
|
||||
onPointerCancel={handleRowPointerEnd}
|
||||
onMouseDown={handleRowMouseDown}
|
||||
onClick={(event) => handleRowSelect(event)}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSessionDoubleClick();
|
||||
}}
|
||||
className={cn(
|
||||
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
||||
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
||||
isTouchPressed && 'bg-interactive-hover/70',
|
||||
mobileVariant
|
||||
? (isVSCode ? revealPaddingClass : 'pr-7')
|
||||
: revealPaddingClass,
|
||||
@@ -700,15 +715,19 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isMissingDirectory}
|
||||
onMouseDown={handleRowMouseDown}
|
||||
onClick={(event) => handleRowSelect(event)}
|
||||
disabled={isMissingDirectory}
|
||||
onPointerDown={handleRowPointerDown}
|
||||
onPointerUp={handleRowPointerEnd}
|
||||
onPointerCancel={handleRowPointerEnd}
|
||||
onMouseDown={handleRowMouseDown}
|
||||
onClick={(event) => handleRowSelect(event)}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSessionDoubleClick();
|
||||
}}
|
||||
className={cn(
|
||||
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
||||
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
||||
isTouchPressed && 'bg-interactive-hover/70',
|
||||
mobileVariant
|
||||
? (isVSCode ? revealPaddingClass : 'pr-7')
|
||||
: revealPaddingClass
|
||||
|
||||
@@ -76,7 +76,6 @@ interface TerminalViewportProps {
|
||||
className?: string;
|
||||
enableTouchScroll?: boolean;
|
||||
autoFocus?: boolean;
|
||||
keyboardAvoidTargetId?: string;
|
||||
}
|
||||
|
||||
const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportProps>(
|
||||
@@ -92,7 +91,6 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
className,
|
||||
enableTouchScroll,
|
||||
autoFocus = true,
|
||||
keyboardAvoidTargetId,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -1514,7 +1512,6 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
tabIndex={-1}
|
||||
enterKeyHint="send"
|
||||
data-terminal-hidden-input="true"
|
||||
data-keyboard-avoid-target-id={keyboardAvoidTargetId}
|
||||
aria-label="Terminal input"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -1542,7 +1539,6 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
tabIndex={-1}
|
||||
enterKeyHint="send"
|
||||
data-terminal-hidden-input="true"
|
||||
data-keyboard-avoid-target-id={keyboardAvoidTargetId}
|
||||
aria-label="Terminal input"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
|
||||
@@ -81,7 +81,6 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
|
||||
'mx-auto max-w-lg',
|
||||
className
|
||||
)}
|
||||
data-keyboard-avoid="true"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{(() => {
|
||||
|
||||
@@ -12,7 +12,6 @@ type ScrollableOverlayProps = React.HTMLAttributes<HTMLElement> & {
|
||||
disableHorizontal?: boolean;
|
||||
observeMutations?: boolean;
|
||||
fillContainer?: boolean;
|
||||
keyboardAvoid?: boolean;
|
||||
/** Prevent scroll from propagating to parent when at boundaries */
|
||||
preventOverscroll?: boolean;
|
||||
useScrollShadow?: boolean;
|
||||
@@ -34,7 +33,6 @@ export const ScrollableOverlay = React.forwardRef<HTMLElement, ScrollableOverlay
|
||||
disableHorizontal = false,
|
||||
observeMutations = true,
|
||||
fillContainer = true,
|
||||
keyboardAvoid = false,
|
||||
preventOverscroll = false,
|
||||
useScrollShadow = false,
|
||||
scrollShadowSize,
|
||||
@@ -51,7 +49,6 @@ export const ScrollableOverlay = React.forwardRef<HTMLElement, ScrollableOverlay
|
||||
preventOverscroll && "overscroll-none",
|
||||
outerClassName
|
||||
)}
|
||||
data-keyboard-avoid={keyboardAvoid ? "true" : undefined}
|
||||
>
|
||||
{useScrollShadow ? (
|
||||
<ScrollShadow
|
||||
|
||||
@@ -80,7 +80,6 @@ DialogOverlay.displayName = "DialogOverlay";
|
||||
|
||||
type DialogContentProps = Omit<React.ComponentProps<typeof BaseDialog.Popup>, "children"> & {
|
||||
showCloseButton?: boolean
|
||||
keyboardAvoid?: boolean
|
||||
children?: React.ReactNode
|
||||
onOpenAutoFocus?: (event: Event) => void
|
||||
onCloseAutoFocus?: (event: Event) => void
|
||||
@@ -90,7 +89,6 @@ function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
keyboardAvoid = false,
|
||||
onOpenAutoFocus,
|
||||
onCloseAutoFocus,
|
||||
...props
|
||||
@@ -104,7 +102,6 @@ function DialogContent({
|
||||
<BaseDialog.Popup
|
||||
data-slot="dialog-content"
|
||||
data-state-slot="dialog"
|
||||
data-keyboard-avoid={keyboardAvoid ? "true" : undefined}
|
||||
className={cn(
|
||||
"bg-background text-foreground fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 shadow-none overflow-hidden pwa-dialog-content",
|
||||
className
|
||||
|
||||
@@ -2150,10 +2150,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
const keyboardInset = Number.parseFloat(rootStyles.getPropertyValue('--oc-keyboard-inset')) || 0;
|
||||
const keyboardHomeIndicator = Number.parseFloat(rootStyles.getPropertyValue('--oc-keyboard-home-indicator')) || 0;
|
||||
const occludedBottom = keyboardInset + keyboardHomeIndicator;
|
||||
const layoutHeight = document.documentElement.clientHeight || window.innerHeight;
|
||||
const occludedBottom = Math.max(0, layoutHeight - (viewport.offsetTop + viewport.height));
|
||||
if (occludedBottom <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -2965,8 +2963,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
<div
|
||||
className={cn('relative h-full', shouldMaskEditorForPendingNavigation && 'overflow-hidden')}
|
||||
ref={editorWrapperRef}
|
||||
data-keyboard-avoid="none"
|
||||
style={isMobile ? { height: 'calc(100% - var(--oc-keyboard-inset, 0px))' } : undefined}
|
||||
>
|
||||
<div className={cn('h-full', shouldMaskEditorForPendingNavigation && 'invisible')}>
|
||||
<CodeMirrorEditor
|
||||
|
||||
@@ -1996,7 +1996,7 @@ export const GitView: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col overflow-hidden', 'bg-sidebar')} data-keyboard-avoid="true">
|
||||
<div className={cn('flex h-full flex-col overflow-hidden', 'bg-sidebar')}>
|
||||
<GitHeader
|
||||
status={status}
|
||||
localBranches={localBranches}
|
||||
|
||||
@@ -604,7 +604,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
// No sidebar available; fall back to direct content.
|
||||
const fallback = renderPageContent(settingsSlug);
|
||||
return (
|
||||
<div className="flex-1 min-h-0 overflow-hidden bg-background" data-keyboard-avoid="true">
|
||||
<div className="flex-1 min-h-0 overflow-hidden bg-background">
|
||||
<ErrorBoundary>{fallback}</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
@@ -622,7 +622,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
const content = renderPageContent(settingsSlug);
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-h-0 overflow-hidden bg-background" data-keyboard-avoid="true">
|
||||
<div className="flex-1 min-h-0 overflow-hidden bg-background">
|
||||
<ErrorBoundary>{content}</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -143,7 +143,6 @@ export const TerminalView: React.FC = () => {
|
||||
const [activeModifier, setActiveModifier] = React.useState<Modifier | null>(null);
|
||||
const [isRestarting, setIsRestarting] = React.useState(false);
|
||||
const [viewportLayoutVersion, setViewportLayoutVersion] = React.useState(0);
|
||||
const keyboardAvoidTargetId = React.useId();
|
||||
|
||||
const streamCleanupRef = React.useRef<(() => void) | null>(null);
|
||||
const activeTerminalIdRef = React.useRef<string | null>(null);
|
||||
@@ -1069,8 +1068,6 @@ export const TerminalView: React.FC = () => {
|
||||
<div
|
||||
className="relative flex-1 overflow-hidden"
|
||||
style={{ backgroundColor: xtermTheme.background }}
|
||||
id={keyboardAvoidTargetId}
|
||||
data-keyboard-avoid="true"
|
||||
>
|
||||
<div className="h-full w-full box-border pl-7 pr-5 pt-3 pb-4">
|
||||
{shouldRenderViewport ? (
|
||||
@@ -1088,7 +1085,6 @@ export const TerminalView: React.FC = () => {
|
||||
fontSize={terminalFontSize}
|
||||
enableTouchScroll={hasTouchInput}
|
||||
autoFocus={isTerminalVisible}
|
||||
keyboardAvoidTargetId={keyboardAvoidTargetId}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -278,7 +278,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
|
||||
</div>
|
||||
|
||||
<Dialog open={Boolean(worktreeDialog)} onOpenChange={(open) => { if (!open) setWorktreeDialog(null); }}>
|
||||
<DialogContent className="max-w-md" keyboardAvoid>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{worktreeDialog?.kind === 'remove' ? 'Remove worktree' : 'Remove other worktrees'}
|
||||
|
||||
@@ -135,7 +135,7 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBu
|
||||
</div>
|
||||
|
||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<DialogContent className="max-w-md" keyboardAvoid>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete agent group</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -53,7 +53,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
||||
const contentClassName = 'flex flex-col gap-3 px-0 pt-1 pb-3';
|
||||
|
||||
return (
|
||||
<section className={containerClassName} data-keyboard-avoid="true">
|
||||
<section className={containerClassName}>
|
||||
<div className={headerClassName}>
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">Commit</h3>
|
||||
</div>
|
||||
|
||||
@@ -223,10 +223,6 @@ textarea[data-chat-input="true"]:focus-visible {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[data-keyboard-avoid-active="true"] {
|
||||
transform: translateY(calc(-1 * var(--oc-keyboard-avoid-offset, 0px)));
|
||||
}
|
||||
|
||||
/* Scroll shadow (HeroUI) fallback styling to ensure visible gradients without the Tailwind plugin */
|
||||
[data-scroll-shadow="true"] {
|
||||
--scroll-shadow-size: var(--scroll-shadow-size, 48px);
|
||||
|
||||
@@ -482,7 +482,6 @@ interface UIStore {
|
||||
pendingFileNavigation: PendingFileNavigation | null;
|
||||
pendingFileFocusPath: string | null;
|
||||
isMobile: boolean;
|
||||
isKeyboardOpen: boolean;
|
||||
isQuickOpenOpen: boolean;
|
||||
isCommandPaletteOpen: boolean;
|
||||
isHelpDialogOpen: boolean;
|
||||
@@ -637,7 +636,6 @@ interface UIStore {
|
||||
setPadding: (size: number) => void;
|
||||
setCornerRadius: (radius: number) => void;
|
||||
setInputBarOffset: (offset: number) => void;
|
||||
setKeyboardOpen: (open: boolean) => void;
|
||||
applyTypography: () => void;
|
||||
applyPadding: () => void;
|
||||
updateProportionalSidebarWidths: () => void;
|
||||
@@ -724,7 +722,6 @@ export const useUIStore = create<UIStore>()(
|
||||
pendingFileNavigation: null,
|
||||
pendingFileFocusPath: null,
|
||||
isMobile: false,
|
||||
isKeyboardOpen: false,
|
||||
isQuickOpenOpen: false,
|
||||
isCommandPaletteOpen: false,
|
||||
isHelpDialogOpen: false,
|
||||
@@ -1470,10 +1467,6 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ inputBarOffset: offset });
|
||||
},
|
||||
|
||||
setKeyboardOpen: (open) => {
|
||||
set({ isKeyboardOpen: open });
|
||||
},
|
||||
|
||||
toggleFavoriteModel: (providerID, modelID) => {
|
||||
set((state) => {
|
||||
const exists = state.favoriteModels.some(
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
--oc-safe-area-bottom-visual: 0px;
|
||||
--oc-safe-area-left: 0px;
|
||||
--oc-header-height: 56px;
|
||||
--oc-visual-viewport-offset-top: 0px;
|
||||
--oc-keyboard-inset: 0px;
|
||||
--oc-keyboard-avoid-offset: 0px;
|
||||
--oc-keyboard-home-indicator: 0px;
|
||||
--oc-wco-left-inset: 0px;
|
||||
--oc-wco-right-inset: 0px;
|
||||
--oc-wco-titlebar-height: 0px;
|
||||
|
||||
@@ -165,16 +165,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Prevent iOS keyboard from scrolling the page */
|
||||
:root.device-mobile:not(.desktop-runtime) body,
|
||||
:root.device-mobile:not(.desktop-runtime) #root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Fix main layout container */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
||||
height: 100vh;
|
||||
@@ -182,16 +172,6 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Fix header positioning */
|
||||
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
||||
position: relative;
|
||||
top: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Fix main content area */
|
||||
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
||||
min-height: 0;
|
||||
@@ -287,22 +267,6 @@
|
||||
padding-bottom: var(--oc-safe-area-bottom-visual);
|
||||
}
|
||||
|
||||
/* iOS keyboard home indicator safe area - used when keyboard is open */
|
||||
:root.device-mobile:not(.desktop-runtime) .ios-keyboard-safe-area {
|
||||
padding-bottom: calc(var(--oc-keyboard-home-indicator, 34px) + var(--oc-safe-area-bottom-visual, 0px)) !important;
|
||||
}
|
||||
|
||||
/* Prevent keyboard-induced page scroll on iOS PWA */
|
||||
:root.device-mobile:not(.desktop-runtime) body {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Fix iOS viewport issues */
|
||||
:root.device-mobile:not(.desktop-runtime) .flex.flex-col.h-screen {
|
||||
min-height: 100vh;
|
||||
|
||||
Reference in New Issue
Block a user