diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 1c95b763..691edb8b 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -470,14 +470,12 @@ const RevertedMessageDock: React.FC = React.memo(({ se RevertedMessageDock.displayName = 'RevertedMessageDock'; type ComposerAttachmentControlsProps = { - isMobile: boolean; isVSCode: boolean; footerIconButtonClass: string; iconSizeClass: string; fileInputRef: React.RefObject; handleLocalFileSelect: (event: React.ChangeEvent) => void | Promise; handlePickLocalFiles: () => void; - handleOpenCommandMenu: () => void; openIssuePicker: () => void; openPrPicker: () => void; onOpenSettings?: () => void; @@ -486,14 +484,12 @@ type ComposerAttachmentControlsProps = { const ComposerAttachmentControls = React.memo(function ComposerAttachmentControls(props: ComposerAttachmentControlsProps) { const { t } = useI18n(); const { - isMobile, isVSCode, footerIconButtonClass, iconSizeClass, fileInputRef, handleLocalFileSelect, handlePickLocalFiles, - handleOpenCommandMenu, openIssuePicker, openPrPicker, onOpenSettings, @@ -501,27 +497,6 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl return (
- {isMobile ? ( - - ) : null} ); }, (prev, next) => ( - prev.isMobile === next.isMobile - && prev.isVSCode === next.isVSCode + prev.isVSCode === next.isVSCode && prev.footerIconButtonClass === next.footerIconButtonClass && prev.iconSizeClass === next.iconSizeClass && prev.onOpenSettings === next.onOpenSettings @@ -960,7 +934,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const [mentionQuery, setMentionQuery] = React.useState(''); const [showCommandAutocomplete, setShowCommandAutocomplete] = React.useState(false); const [commandQuery, setCommandQuery] = React.useState(''); - const [autocompleteTab, setAutocompleteTab] = React.useState<'commands' | 'agents' | 'files'>('commands'); const [showSkillAutocomplete, setShowSkillAutocomplete] = React.useState(false); const [skillQuery, setSkillQuery] = React.useState(''); const [showSnippetAutocomplete, setShowSnippetAutocomplete] = React.useState(false); @@ -2525,7 +2498,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo if (cursorPosition <= commandEnd && firstSpace === -1) { const commandText = value.substring(1, commandEnd); setCommandQuery(commandText); - setAutocompleteTab('commands'); setShowCommandAutocomplete(true); setShowFileMention(false); setShowSkillAutocomplete(false); @@ -2578,7 +2550,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const isWordBoundary = !charBefore || /\s/.test(charBefore); if (isWordBoundary && !textAfterAt.includes(' ') && !textAfterAt.includes('\n')) { setMentionQuery(textAfterAt); - setAutocompleteTab((current) => current === 'files' ? 'files' : 'agents'); setShowFileMention(true); } else { setShowFileMention(false); @@ -2586,92 +2557,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo } else { setShowFileMention(false); } - }, [inputMode, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete, setSkillQuery]); - - const applyAutocompletePrefix = React.useCallback((prefix: '/' | '@') => { - const nextMessage = message.length === 0 - ? prefix - : (message[0] === '/' || message[0] === '@') - ? `${prefix}${message.slice(1)}` - : `${prefix}${message}`; - setMessage(nextMessage); - requestAnimationFrame(() => { - if (textareaRef.current) { - const nextCursor = Math.min(nextMessage.length, textareaRef.current.value.length); - textareaRef.current.selectionStart = nextCursor; - textareaRef.current.selectionEnd = nextCursor; - } - adjustTextareaHeight(); - updateAutocompleteState(nextMessage, nextMessage.length); - }); - }, [adjustTextareaHeight, message, setMessage, updateAutocompleteState]); - - const handleAutocompleteTabSelect = React.useCallback((tab: 'commands' | 'agents' | 'files') => { - const textarea = textareaRef.current; - if (isMobile && textarea) { - try { - textarea.focus({ preventScroll: true }); - } catch { - textarea.focus(); - } - const len = textarea.value.length; - try { - textarea.setSelectionRange(len, len); - } catch { - // 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(''); - 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, message, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]); - - const handleOpenCommandMenu = React.useCallback(() => { - if (!isMobile) { - return; - } - const textarea = textareaRef.current; - if (textarea) { - try { - textarea.focus({ preventScroll: true }); - } catch { - textarea.focus(); - } - const len = textarea.value.length; - try { - textarea.setSelectionRange(len, len); - } catch { - // ignored - } - } - applyAutocompletePrefix('/'); - setCommandQuery(''); - setAutocompleteTab('commands'); - setShowCommandAutocomplete(true); - setShowFileMention(false); - setShowSkillAutocomplete(false); - }, [applyAutocompletePrefix, isMobile, setAutocompleteTab, setCommandQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]); + }, [inputMode, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete, setSkillQuery]); const insertTextAtSelection = React.useCallback((text: string) => { if (!text) { @@ -4183,9 +4069,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo ref={commandRef} searchQuery={commandQuery} onCommandSelect={handleCommandSelect} - showTabs={isMobile} - activeTab={autocompleteTab} - onTabSelect={handleAutocompleteTabSelect} onClose={() => setShowCommandAutocomplete(false)} style={isDesktopExpanded && autocompleteOverlayPosition ? { @@ -4245,9 +4128,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo searchQuery={mentionQuery} onFileSelect={handleFileSelect} onAgentSelect={handleAgentSelect} - showTabs={isMobile} - activeTab={autocompleteTab} - onTabSelect={handleAutocompleteTabSelect} onClose={() => setShowFileMention(false)} style={isDesktopExpanded && autocompleteOverlayPosition ? { @@ -4368,14 +4248,12 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo
= ({ onOpenSettings, scrollTo <>
void; } -type AutocompleteTab = 'commands' | 'agents' | 'files'; - const BASE_BADGE_CLASS = "text-[10px] leading-none uppercase font-bold tracking-tight px-1.5 py-1 rounded border flex-shrink-0"; const TYPE_BADGE_CLASS = cn( BASE_BADGE_CLASS, @@ -51,9 +49,6 @@ interface CommandAutocompleteProps { searchQuery: string; onCommandSelect: (command: CommandInfo, options?: { dismissKeyboard?: boolean }) => void; onClose: () => void; - showTabs?: boolean; - activeTab?: AutocompleteTab; - onTabSelect?: (tab: AutocompleteTab) => void; style?: React.CSSProperties; } @@ -61,9 +56,6 @@ export const CommandAutocomplete = React.forwardRef { const { t } = useI18n(); @@ -88,7 +80,6 @@ export const CommandAutocomplete = React.forwardRef(null); const pointerMovedRef = React.useRef(false); - const ignoreTabClickRef = React.useRef(false); React.useEffect(() => { const handlePointerDown = (event: MouseEvent | TouchEvent) => { @@ -305,46 +296,6 @@ export const CommandAutocomplete = React.forwardRef - {showTabs ? ( -
-
- {([ - { id: 'commands' as const, label: t('chat.autocomplete.tabs.commands') }, - { id: 'agents' as const, label: t('chat.autocomplete.tabs.agents') }, - { id: 'files' as const, label: t('chat.autocomplete.tabs.files') }, - ]).map((tab) => ( - - ))} -
-
- ) : null} {loading ? (
diff --git a/packages/ui/src/components/chat/FileMentionAutocomplete.tsx b/packages/ui/src/components/chat/FileMentionAutocomplete.tsx index 24fba907..b8afcecf 100644 --- a/packages/ui/src/components/chat/FileMentionAutocomplete.tsx +++ b/packages/ui/src/components/chat/FileMentionAutocomplete.tsx @@ -19,23 +19,16 @@ type AgentInfo = { description?: string; mode?: string | null; }; -const EMPTY_FILES: FileInfo[] = []; -const EMPTY_AGENTS: AgentInfo[] = []; export interface FileMentionHandle { handleKeyDown: (key: string) => void; } -type AutocompleteTab = 'commands' | 'agents' | 'files'; - interface FileMentionAutocompleteProps { searchQuery: string; onFileSelect: (file: FileInfo) => void; onAgentSelect?: (agentName: string) => void; onClose: () => void; - showTabs?: boolean; - activeTab?: AutocompleteTab; - onTabSelect?: (tab: AutocompleteTab) => void; style?: React.CSSProperties; } @@ -44,9 +37,6 @@ export const FileMentionAutocomplete = React.forwardRef { const { t } = useI18n(); @@ -87,9 +77,7 @@ export const FileMentionAutocomplete = React.forwardRef([]); const measureRefs = React.useRef<(HTMLSpanElement | null)[]>([]); const containerRef = React.useRef(null); - const ignoreTabClickRef = React.useRef(false); const normalizedSearchQuery = (searchQuery ?? '').trim(); - const scopeResultsToActiveTab = showTabs === true; const recentFiles = React.useMemo(() => { if (!projectRoot || !projectTabs) { return [] as FileInfo[]; @@ -128,14 +116,12 @@ export const FileMentionAutocomplete = React.forwardRef !scopeResultsToActiveTab || activeTab === 'agents' - ? (normalizedSearchQuery.length > 0 ? agents : agents.slice(0, 2)) - : EMPTY_AGENTS, - [activeTab, agents, normalizedSearchQuery.length, scopeResultsToActiveTab], + () => normalizedSearchQuery.length > 0 ? agents : agents.slice(0, 2), + [agents, normalizedSearchQuery.length], ); - const visibleDirectories = !scopeResultsToActiveTab || activeTab === 'files' ? directories : EMPTY_FILES; - const visibleRecentFiles = !scopeResultsToActiveTab || activeTab === 'files' ? recentFiles : EMPTY_FILES; - const visibleFiles = !scopeResultsToActiveTab || activeTab === 'files' ? files : EMPTY_FILES; + const visibleDirectories = directories; + const visibleRecentFiles = recentFiles; + const visibleFiles = files; React.useEffect(() => { const handlePointerDown = (event: MouseEvent | TouchEvent) => { @@ -452,56 +438,14 @@ export const FileMentionAutocomplete = React.forwardRef ([ - { id: 'commands' as const, label: t('chat.autocomplete.tabs.commands') }, - { id: 'agents' as const, label: t('chat.autocomplete.tabs.agents') }, - { id: 'files' as const, label: t('chat.autocomplete.tabs.files') }, - ]), [t]); - return (
- {showTabs ? ( -
-
- {tabs.map((tab) => ( - - ))} -
-
- ) : null} - {(!scopeResultsToActiveTab || activeTab === 'files') && loading ? ( + {loading ? (
diff --git a/packages/ui/src/components/layout/MainLayout.tsx b/packages/ui/src/components/layout/MainLayout.tsx index 8898dc4a..d6f74cec 100644 --- a/packages/ui/src/components/layout/MainLayout.tsx +++ b/packages/ui/src/components/layout/MainLayout.tsx @@ -20,7 +20,6 @@ import { useUIStore } from '@/stores/useUIStore'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useUpdateStore } from '@/stores/useUpdateStore'; import { useDeviceInfo } from '@/lib/device'; -import { useVisualViewport } from '@/hooks/useVisualViewport'; import { cn } from '@/lib/utils'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; @@ -60,7 +59,6 @@ export const MainLayout: React.FC = () => { const setMultiRunLauncherOpen = useUIStore((state) => state.setMultiRunLauncherOpen); const multiRunLauncherPrefillPrompt = useUIStore((state) => state.multiRunLauncherPrefillPrompt); const { isMobile, isTablet } = useDeviceInfo(); - const visualViewport = useVisualViewport(); const sidebarWidth = useUIStore((state) => state.sidebarWidth); const rightSidebarWidth = useUIStore((state) => state.rightSidebarWidth); const rightSidebarAutoClosedRef = React.useRef(false); @@ -431,10 +429,9 @@ export const MainLayout: React.FC = () => { data-page-scroll-lock="true" className={cn( 'main-content-safe-area', - isMobile ? 'flex flex-col' : 'flex h-[100dvh]', + isMobile ? 'flex h-[100dvh] flex-col' : 'flex h-[100dvh]', 'bg-background' )} - style={isMobile && visualViewport.height > 0 ? { height: visualViewport.height } : undefined} > diff --git a/packages/ui/src/lib/mobileKeyboardMode.ts b/packages/ui/src/lib/mobileKeyboardMode.ts index 683af29e..07283ca2 100644 --- a/packages/ui/src/lib/mobileKeyboardMode.ts +++ b/packages/ui/src/lib/mobileKeyboardMode.ts @@ -5,24 +5,7 @@ export const VIEWPORT_META_SELECTOR = 'meta[name="viewport"]'; export const VIEWPORT_CONTENT_BASE = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'; export const supportsMobileKeyboardResizeContent = (): boolean => { - if (typeof navigator === 'undefined') { - return true; - } - - const userAgent = navigator.userAgent || ''; - const platform = navigator.platform || ''; - const maxTouchPoints = navigator.maxTouchPoints ?? 0; - const isIOS = /iPhone|iPad|iPod/i.test(userAgent) - || ((/Macintosh|MacIntel/i.test(userAgent) || /MacIntel/i.test(platform)) && maxTouchPoints > 1); - - return !isIOS; -}; - -const getSupportedMobileKeyboardMode = (mode: MobileKeyboardMode): MobileKeyboardMode => { - if (mode === 'resize-content' && !supportsMobileKeyboardResizeContent()) { - return 'native'; - } - return mode; + return true; }; export function normalizeMobileKeyboardMode(value: unknown): MobileKeyboardMode; @@ -30,7 +13,7 @@ export function normalizeMobileKeyboardMode(value: unknown, fallback: MobileKeyb export function normalizeMobileKeyboardMode(value: unknown, fallback: undefined): MobileKeyboardMode | undefined; export function normalizeMobileKeyboardMode( value: unknown, - fallback: MobileKeyboardMode | undefined = 'native', + fallback: MobileKeyboardMode | undefined = 'resize-content', ): MobileKeyboardMode | undefined { if (value === 'native' || value === 'resize-content') { return value; @@ -39,7 +22,7 @@ export function normalizeMobileKeyboardMode( } export const getViewportContentForMobileKeyboardMode = (value: unknown): string => { - const mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(value)); + const mode = normalizeMobileKeyboardMode(value); return mode === 'resize-content' ? `${VIEWPORT_CONTENT_BASE}, interactive-widget=resizes-content` : VIEWPORT_CONTENT_BASE; @@ -47,22 +30,22 @@ export const getViewportContentForMobileKeyboardMode = (value: unknown): string export const getStoredMobileKeyboardMode = (): MobileKeyboardMode => { if (typeof window === 'undefined') { - return 'native'; + return 'resize-content'; } try { - return getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(localStorage.getItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY))); + return normalizeMobileKeyboardMode(localStorage.getItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY)); } catch { - return 'native'; + return 'resize-content'; } }; export const setStoredMobileKeyboardMode = (value: unknown): MobileKeyboardMode => { - const mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(value)); + const mode = normalizeMobileKeyboardMode(value); if (typeof window !== 'undefined') { try { - if (mode === 'native') { + if (mode === 'resize-content') { localStorage.removeItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY); } else { localStorage.setItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY, mode); diff --git a/packages/web/index.html b/packages/web/index.html index 07f2b132..2434decd 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -2,7 +2,7 @@ - + @@ -57,46 +57,29 @@ return fallback; }; - const normalizeMobileKeyboardMode = (value, fallback = 'native') => { + const normalizeMobileKeyboardMode = (value, fallback = 'resize-content') => { if (value === 'native' || value === 'resize-content') { return value; } return fallback; }; - const supportsMobileKeyboardResizeContent = () => { - const userAgent = navigator.userAgent || ''; - const platform = navigator.platform || ''; - const maxTouchPoints = navigator.maxTouchPoints || 0; - const isIOS = /iPhone|iPad|iPod/i.test(userAgent) - || ((/Macintosh|MacIntel/i.test(userAgent) || /MacIntel/i.test(platform)) && maxTouchPoints > 1); - - return !isIOS; - }; - - const getSupportedMobileKeyboardMode = (mode) => { - if (mode === 'resize-content' && !supportsMobileKeyboardResizeContent()) { - return 'native'; - } - return mode; - }; - const getViewportContentForMobileKeyboardMode = (value) => { - const mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(value, 'native')); + const mode = normalizeMobileKeyboardMode(value, 'resize-content'); return mode === 'resize-content' ? `${viewportBaseContent}, interactive-widget=resizes-content` : viewportBaseContent; }; const applyStoredMobileKeyboardMode = () => { - let mode = 'native'; + let mode = 'resize-content'; try { - mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(localStorage.getItem(mobileKeyboardModeStorageKey), 'native')); - if (mode === 'native') { + mode = normalizeMobileKeyboardMode(localStorage.getItem(mobileKeyboardModeStorageKey), 'resize-content'); + if (mode === 'resize-content') { localStorage.removeItem(mobileKeyboardModeStorageKey); } } catch { - mode = 'native'; + mode = 'resize-content'; } document.documentElement.setAttribute('data-oc-mobile-keyboard-mode', mode);