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 && (
-
- {animatedDots}
-
- )}
-
-
-
-
- {elapsedLabel}
-
-
-
-
-
- {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 */}
-
+