From a8dab0edcf949ae610820bae9500c433781f7c73 Mon Sep 17 00:00:00 2001 From: Haile <118998054+blackmammoth@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:36:06 +0300 Subject: [PATCH] fix(ui): remove mobile bottom nav, unify processing indicator, and improve tooltip behavior on mobile (#632) * fix: update tooltip component * fix: remove the mobile navigation component In addition, - the sidebar is also updated to take full space - the terminal shortcuts in shell are updated to not interfere with the shell content. * fix: remove mobile nav component * fix: remove "Thinking..." indicator In addition, the claude status component has been restyled to be more compact and less obtrusive. - The type and prop arguments for ChatMessagesPane have been updated to remove the isLoading prop, which was only used to control the display of the AssistantThinkingIndicator. * fix: show elapsed time only when loading --------- Co-authored-by: Haileyesus Co-authored-by: Simos Mikelatos --- src/components/app/AppContent.tsx | 12 +- src/components/app/MobileNav.tsx | 179 ---------------- src/components/chat/view/ChatInterface.tsx | 1 - .../AssistantThinkingIndicator.tsx | 36 ---- .../view/subcomponents/ChatMessagesPane.tsx | 5 - .../chat/view/subcomponents/ClaudeStatus.tsx | 196 ++++++------------ .../git-panel/view/branches/BranchesView.tsx | 2 +- .../git-panel/view/changes/ChangesView.tsx | 2 +- .../git-panel/view/history/HistoryView.tsx | 2 +- .../view/QuickSettingsContent.tsx | 4 +- .../view/QuickSettingsPanelView.tsx | 1 - .../subcomponents/TerminalShortcutsPanel.tsx | 2 +- .../view/subcomponents/SidebarFooter.tsx | 2 +- src/shared/view/ui/Tooltip.tsx | 137 ++++++++++-- 14 files changed, 186 insertions(+), 395 deletions(-) delete mode 100644 src/components/app/MobileNav.tsx delete mode 100644 src/components/chat/view/subcomponents/AssistantThinkingIndicator.tsx diff --git a/src/components/app/AppContent.tsx b/src/components/app/AppContent.tsx index ed14fdb8..e9ac674a 100644 --- a/src/components/app/AppContent.tsx +++ b/src/components/app/AppContent.tsx @@ -7,7 +7,6 @@ import { useWebSocket } from '../../contexts/WebSocketContext'; import { useDeviceSettings } from '../../hooks/useDeviceSettings'; import { useSessionProtection } from '../../hooks/useSessionProtection'; import { useProjectsState } from '../../hooks/useProjectsState'; -import MobileNav from './MobileNav'; export default function AppContent() { const navigate = useNavigate(); @@ -33,7 +32,6 @@ export default function AppContent() { activeTab, sidebarOpen, isLoadingProjects, - isInputFocused, externalMessageUpdate, setActiveTab, setSidebarOpen, @@ -159,7 +157,7 @@ export default function AppContent() { )} -
+
- {isMobile && ( - - )} -
); } diff --git a/src/components/app/MobileNav.tsx b/src/components/app/MobileNav.tsx deleted file mode 100644 index 8a672be1..00000000 --- a/src/components/app/MobileNav.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { useState, useRef, useEffect, type Dispatch, type SetStateAction } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - MessageSquare, - Folder, - Terminal, - GitBranch, - ClipboardCheck, - Ellipsis, - Puzzle, - Box, - Database, - Globe, - Wrench, - Zap, - BarChart3, - type LucideIcon, -} from 'lucide-react'; -import { useTasksSettings } from '../../contexts/TasksSettingsContext'; -import { usePlugins } from '../../contexts/PluginsContext'; -import { AppTab } from '../../types/app'; - -const PLUGIN_ICON_MAP: Record = { - Puzzle, Box, Database, Globe, Terminal, Wrench, Zap, BarChart3, Folder, MessageSquare, GitBranch, -}; - -type CoreTabId = Exclude; -type CoreNavItem = { - id: CoreTabId; - icon: LucideIcon; - label: string; -}; - -type MobileNavProps = { - activeTab: AppTab; - setActiveTab: Dispatch>; - isInputFocused: boolean; -}; - -export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: MobileNavProps) { - const { t } = useTranslation(['common', 'settings']); - const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings(); - const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled); - const { plugins } = usePlugins(); - const [moreOpen, setMoreOpen] = useState(false); - const moreRef = useRef(null); - - const enabledPlugins = plugins.filter((p) => p.enabled); - const hasPlugins = enabledPlugins.length > 0; - const isPluginActive = activeTab.startsWith('plugin:'); - - // Close the menu on outside tap - useEffect(() => { - if (!moreOpen) return; - const handleTap = (e: PointerEvent) => { - const target = e.target; - if (moreRef.current && target instanceof Node && !moreRef.current.contains(target)) { - setMoreOpen(false); - } - }; - document.addEventListener('pointerdown', handleTap); - return () => document.removeEventListener('pointerdown', handleTap); - }, [moreOpen]); - - // Close menu when a plugin tab is selected - const selectPlugin = (name: string) => { - const pluginTab = `plugin:${name}` as AppTab; - setActiveTab(pluginTab); - setMoreOpen(false); - }; - - const baseCoreItems: CoreNavItem[] = [ - { id: 'chat', icon: MessageSquare, label: 'Chat' }, - { id: 'shell', icon: Terminal, label: 'Shell' }, - { id: 'files', icon: Folder, label: 'Files' }, - { id: 'git', icon: GitBranch, label: 'Git' }, - ]; - const coreItems: CoreNavItem[] = shouldShowTasksTab - ? [...baseCoreItems, { id: 'tasks', icon: ClipboardCheck, label: 'Tasks' }] - : baseCoreItems; - - return ( -
-
-
- {coreItems.map((item) => { - const Icon = item.icon; - const isActive = activeTab === item.id; - - return ( - - ); - })} - - {/* "More" button — only shown when there are enabled plugins */} - {hasPlugins && ( -
- - - {/* Popover menu */} - {moreOpen && ( -
- {enabledPlugins.map((p) => { - const Icon = PLUGIN_ICON_MAP[p.icon] || Puzzle; - const isActive = activeTab === `plugin:${p.name}`; - - return ( - - ); - })} -
- )} -
- )} -
-
-
- ); -} diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index 9f9d0bc0..cb78222c 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -338,7 +338,6 @@ function ChatInterface({ showRawParameters={showRawParameters} showThinking={showThinking} selectedProject={selectedProject} - isLoading={isLoading} /> -
-
-
- -
-
- {selectedProvider === 'cursor' ? 'Cursor' : selectedProvider === 'codex' ? 'Codex' : selectedProvider === 'gemini' ? 'Gemini' : 'Claude'} -
-
-
-
-
.
-
- . -
-
- . -
- Thinking... -
-
-
- - ); -} diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index e9cb6dda..63ae4841 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -6,7 +6,6 @@ import type { Project, ProjectSession, SessionProvider } from '../../../../types import { getIntrinsicMessageKey } from '../../utils/messageKeys'; import MessageComponent from './MessageComponent'; import ProviderSelectionEmptyState from './ProviderSelectionEmptyState'; -import AssistantThinkingIndicator from './AssistantThinkingIndicator'; interface ChatMessagesPaneProps { scrollContainerRef: RefObject; @@ -51,7 +50,6 @@ interface ChatMessagesPaneProps { showRawParameters?: boolean; showThinking?: boolean; selectedProject: Project; - isLoading: boolean; } export default function ChatMessagesPane({ @@ -97,7 +95,6 @@ export default function ChatMessagesPane({ showRawParameters, showThinking, selectedProject, - isLoading, }: ChatMessagesPaneProps) { const { t } = useTranslation('chat'); const messageKeyMapRef = useRef>(new WeakMap()); @@ -261,8 +258,6 @@ export default function ChatMessagesPane({ })} )} - - {isLoading && } ); } diff --git a/src/components/chat/view/subcomponents/ClaudeStatus.tsx b/src/components/chat/view/subcomponents/ClaudeStatus.tsx index 90c8ffde..f42e29cf 100644 --- a/src/components/chat/view/subcomponents/ClaudeStatus.tsx +++ b/src/components/chat/view/subcomponents/ClaudeStatus.tsx @@ -23,7 +23,6 @@ const ACTION_KEYS = [ 'claudeStatus.actions.reasoning', ]; const DEFAULT_ACTION_WORDS = ['Thinking', 'Processing', 'Analyzing', 'Working', 'Computing', 'Reasoning']; -const ANIMATION_STEPS = 40; const PROVIDER_LABEL_KEYS: Record = { claude: 'messageTypes.claude', @@ -32,19 +31,10 @@ const PROVIDER_LABEL_KEYS: Record = { gemini: 'messageTypes.gemini', }; -function formatElapsedTime(totalSeconds: number, t: (key: string, options?: Record) => string) { - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - - if (minutes < 1) { - return t('claudeStatus.elapsed.seconds', { count: seconds, defaultValue: '{{count}}s' }); - } - - return t('claudeStatus.elapsed.minutesSeconds', { - minutes, - seconds, - defaultValue: '{{minutes}}m {{seconds}}s', - }); +function formatElapsedTime(totalSeconds: number) { + const mins = Math.floor(totalSeconds / 60); + const secs = totalSeconds % 60; + return mins < 1 ? `${secs}s` : `${mins}m ${secs}s`; } export default function ClaudeStatus({ @@ -55,143 +45,85 @@ export default function ClaudeStatus({ }: ClaudeStatusProps) { const { t } = useTranslation('chat'); const [elapsedTime, setElapsedTime] = useState(0); - const [animationPhase, setAnimationPhase] = useState(0); + const [dots, setDots] = useState(''); useEffect(() => { if (!isLoading) { setElapsedTime(0); return; } - const startTime = Date.now(); - - const timer = window.setInterval(() => { - const elapsed = Math.floor((Date.now() - startTime) / 1000); - setElapsedTime(elapsed); + const timer = setInterval(() => { + setElapsedTime(Math.floor((Date.now() - startTime) / 1000)); }, 1000); - - return () => window.clearInterval(timer); - }, [isLoading]); - - useEffect(() => { - if (!isLoading) { - return; - } - - const timer = window.setInterval(() => { - setAnimationPhase((previous) => (previous + 1) % ANIMATION_STEPS); + const dotTimer = setInterval(() => { + setDots((prev) => (prev.length >= 3 ? '' : prev + '.')); }, 500); - return () => window.clearInterval(timer); + return () => { + clearInterval(timer); + clearInterval(dotTimer); + }; }, [isLoading]); - // Note: showThinking only controls the reasoning accordion in messages, not this processing indicator - if (!isLoading && !status) { - return null; - } + if (!isLoading && !status) return null; - const actionWords = ACTION_KEYS.map((key, index) => t(key, { defaultValue: DEFAULT_ACTION_WORDS[index] })); - const actionIndex = Math.floor(elapsedTime / 3) % actionWords.length; - const statusText = status?.text || actionWords[actionIndex]; - const cleanStatusText = statusText.replace(/[.]+$/, ''); - const canInterrupt = isLoading && status?.can_interrupt !== false; - const providerLabelKey = PROVIDER_LABEL_KEYS[provider]; - const providerLabel = providerLabelKey - ? t(providerLabelKey) - : t('claudeStatus.providers.assistant', { defaultValue: 'Assistant' }); - const animatedDots = '.'.repeat((animationPhase % 3) + 1); - const elapsedLabel = - elapsedTime > 0 - ? t('claudeStatus.elapsed.label', { - time: formatElapsedTime(elapsedTime, t), - defaultValue: '{{time}} elapsed', - }) - : t('claudeStatus.elapsed.startingNow', { defaultValue: 'Starting now' }); + const actionWords = ACTION_KEYS.map((key, i) => t(key, { defaultValue: DEFAULT_ACTION_WORDS[i] })); + const statusText = (status?.text || actionWords[Math.floor(elapsedTime / 3) % actionWords.length]).replace(/[.]+$/, ''); + + const providerLabel = t(PROVIDER_LABEL_KEYS[provider] || 'claudeStatus.providers.assistant', { defaultValue: 'Assistant' }); return ( -
-
-
+
+
-
-
-
-
- - - {isLoading && ( - - )} - - -
- -
-
- {providerLabel} - - {isLoading - ? t('claudeStatus.state.live', { defaultValue: 'Live' }) - : t('claudeStatus.state.paused', { defaultValue: 'Paused' })} - -
- -

- {cleanStatusText} - {isLoading && ( - - )} -

- -
- -
-
-
- - {canInterrupt && onAbort && ( -
- - -

- {t('claudeStatus.controls.pressEscToStop', { defaultValue: 'Press Esc anytime to stop' })} -

-
+ {/* Left Side: Identity & Status */} +
+
+ + {isLoading && ( + )}
+ +
+ + {providerLabel} + +
+ +

+ {statusText}{isLoading ? dots : ''} +

+
+
+
+ + {/* Right Side: Metrics & Actions */} +
+ {isLoading && status?.can_interrupt !== false && onAbort && ( + <> +
+ {formatElapsedTime(elapsedTime)} +
+ + + + )}
); -} +} \ No newline at end of file diff --git a/src/components/git-panel/view/branches/BranchesView.tsx b/src/components/git-panel/view/branches/BranchesView.tsx index 6fd06b00..691d2f32 100644 --- a/src/components/git-panel/view/branches/BranchesView.tsx +++ b/src/components/git-panel/view/branches/BranchesView.tsx @@ -167,7 +167,7 @@ export default function BranchesView({ } return ( -
+
{/* Create branch button */}
diff --git a/src/components/git-panel/view/changes/ChangesView.tsx b/src/components/git-panel/view/changes/ChangesView.tsx index cfcb29f7..ddc21e95 100644 --- a/src/components/git-panel/view/changes/ChangesView.tsx +++ b/src/components/git-panel/view/changes/ChangesView.tsx @@ -151,7 +151,7 @@ export default function ChangesView({ {!gitStatus?.error && } -
+
{isLoading ? (
diff --git a/src/components/git-panel/view/history/HistoryView.tsx b/src/components/git-panel/view/history/HistoryView.tsx index 35f20e55..4636af20 100644 --- a/src/components/git-panel/view/history/HistoryView.tsx +++ b/src/components/git-panel/view/history/HistoryView.tsx @@ -47,7 +47,7 @@ export default function HistoryView({ ); return ( -
+
{isLoading ? (
diff --git a/src/components/quick-settings-panel/view/QuickSettingsContent.tsx b/src/components/quick-settings-panel/view/QuickSettingsContent.tsx index 2bd058b4..60d19912 100644 --- a/src/components/quick-settings-panel/view/QuickSettingsContent.tsx +++ b/src/components/quick-settings-panel/view/QuickSettingsContent.tsx @@ -19,14 +19,12 @@ import QuickSettingsWhisperSection from './QuickSettingsWhisperSection'; type QuickSettingsContentProps = { isDarkMode: boolean; - isMobile: boolean; preferences: QuickSettingsPreferences; onPreferenceChange: (key: PreferenceToggleKey, value: boolean) => void; }; export default function QuickSettingsContent({ isDarkMode, - isMobile, preferences, onPreferenceChange, }: QuickSettingsContentProps) { @@ -45,7 +43,7 @@ export default function QuickSettingsContent({ ); return ( -
+
diff --git a/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx b/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx index 9c07ac7d..0de1bbc7 100644 --- a/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx +++ b/src/components/quick-settings-panel/view/QuickSettingsPanelView.tsx @@ -73,7 +73,6 @@ export default function QuickSettingsPanelView() { diff --git a/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx b/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx index 0378f0d9..94d3d491 100644 --- a/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx +++ b/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx @@ -55,7 +55,7 @@ export default function TerminalShortcutsPanel({ wsRef, terminalRef, isConnected, - bottomOffset = 'bottom-14', + bottomOffset = 'bottom-0', }: TerminalShortcutsPanelProps) { const { t } = useTranslation('settings'); const [ctrlActive, setCtrlActive] = useState(false); diff --git a/src/components/sidebar/view/subcomponents/SidebarFooter.tsx b/src/components/sidebar/view/subcomponents/SidebarFooter.tsx index 3d5eb95f..afa0c6a1 100644 --- a/src/components/sidebar/view/subcomponents/SidebarFooter.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarFooter.tsx @@ -122,7 +122,7 @@ export default function SidebarFooter({
{/* Mobile settings */} -
+