From 735aba7d0c13ed86884efd6c72f90160bab2ef3f Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Tue, 10 Feb 2026 02:35:56 +0200 Subject: [PATCH] refactor(ui): streamline header, settings layout --- packages/ui/src/components/layout/Header.tsx | 97 +++++++++--- .../sections/openchamber/AboutSettings.tsx | 12 +- .../openchamber/OpenChamberSidebar.tsx | 146 +++++++++++++----- .../ui/src/components/ui/CommandPalette.tsx | 12 +- packages/ui/src/components/ui/HelpDialog.tsx | 17 -- .../ui/src/components/views/SettingsView.tsx | 28 ++-- packages/ui/src/hooks/useKeyboardShortcuts.ts | 9 ++ 7 files changed, 212 insertions(+), 109 deletions(-) diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index ced619a1..f29da598 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -26,7 +26,7 @@ import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { useDeviceInfo } from '@/lib/device'; -import { cn, getModifierLabel, hasModifier } from '@/lib/utils'; +import { cn, hasModifier } from '@/lib/utils'; import { useDiffFileCount } from '@/components/views/DiffView'; import { McpDropdown, McpDropdownContent } from '@/components/mcp/McpDropdown'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; @@ -52,6 +52,7 @@ import type { GitHubAuthStatus } from '@/lib/api/types'; import { DesktopHostSwitcherDialog } from '@/components/desktop/DesktopHostSwitcher'; import { OpenInAppButton } from '@/components/desktop/OpenInAppButton'; import { isDesktopShell } from '@/lib/desktop'; +import { desktopHostsGet } from '@/lib/desktopHosts'; const formatTime = (timestamp: number | null) => { if (!timestamp) return '-'; @@ -112,7 +113,6 @@ export const Header: React.FC = () => { const toggleBottomTerminal = useUIStore((state) => state.toggleBottomTerminal); const toggleRightSidebar = useUIStore((state) => state.toggleRightSidebar); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); - const toggleCommandPalette = useUIStore((state) => state.toggleCommandPalette); const activeMainTab = useUIStore((state) => state.activeMainTab); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); @@ -201,6 +201,7 @@ export const Header: React.FC = () => { const [isMobileRateLimitsOpen, setIsMobileRateLimitsOpen] = React.useState(false); const [isDesktopServicesOpen, setIsDesktopServicesOpen] = React.useState(false); const [isUsageRefreshSpinning, setIsUsageRefreshSpinning] = React.useState(false); + const [currentInstanceLabel, setCurrentInstanceLabel] = React.useState('Local'); const [desktopServicesTab, setDesktopServicesTab] = React.useState<'instance' | 'usage' | 'mcp'>( isDesktopApp ? 'instance' : 'usage' ); @@ -209,6 +210,65 @@ export const Header: React.FC = () => { setDesktopServicesTab('usage'); } }, [desktopServicesTab, isDesktopApp]); + + const refreshCurrentInstanceLabel = React.useCallback(async () => { + if (typeof window === 'undefined' || !isDesktopApp) { + return; + } + + const normalizeHostUrl = (raw: string): string | null => { + const trimmed = raw.trim(); + if (!trimmed) return null; + try { + const url = new URL(trimmed); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return null; + } + return url.origin; + } catch { + try { + const url = new URL(trimmed.endsWith('/') ? trimmed : `${trimmed}/`); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return null; + } + return url.origin; + } catch { + return null; + } + } + }; + + try { + const cfg = await desktopHostsGet(); + const currentOrigin = window.location.origin; + const localOrigin = window.__OPENCHAMBER_LOCAL_ORIGIN__ || currentOrigin; + const normalizedCurrent = normalizeHostUrl(currentOrigin) || currentOrigin; + const normalizedLocal = normalizeHostUrl(localOrigin) || localOrigin; + + if (normalizedCurrent && normalizedLocal && normalizedCurrent === normalizedLocal) { + setCurrentInstanceLabel('Local'); + return; + } + + const match = cfg.hosts.find((host) => { + const normalized = normalizeHostUrl(host.url); + return normalized && normalized === normalizedCurrent; + }); + + if (match?.label?.trim()) { + setCurrentInstanceLabel(match.label.trim()); + return; + } + + setCurrentInstanceLabel('Instance'); + } catch { + setCurrentInstanceLabel('Local'); + } + }, [isDesktopApp]); + + useEffect(() => { + void refreshCurrentInstanceLabel(); + }, [refreshCurrentInstanceLabel]); useQuotaAutoRefresh(); const selectedModels = useQuotaStore((state) => state.selectedModels); const expandedFamilies = useQuotaStore((state) => state.expandedFamilies); @@ -809,8 +869,11 @@ export const Header: React.FC = () => { open={isDesktopServicesOpen} onOpenChange={(open) => { setIsDesktopServicesOpen(open); - if (open && desktopServicesTab === 'usage' && quotaResults.length === 0) { - fetchAllQuotas(); + if (open) { + void refreshCurrentInstanceLabel(); + if (desktopServicesTab === 'usage' && quotaResults.length === 0) { + fetchAllQuotas(); + } } }} > @@ -819,15 +882,19 @@ export const Header: React.FC = () => { -

Instance / Usage / MCP

+

Current instance: {currentInstanceLabel}

{ )} - - - - - -

Command Palette ({getModifierLabel()}+K)

-
-
- - {/* Version row with update status */}
diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberSidebar.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberSidebar.tsx index be63fa95..d35bc49c 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberSidebar.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberSidebar.tsx @@ -1,8 +1,11 @@ import React from 'react'; +import { RiRestartLine } from '@remixicon/react'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useDeviceInfo } from '@/lib/device'; import { isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; import { AboutSettings } from './AboutSettings'; +import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore'; import { cn } from '@/lib/utils'; export type OpenChamberSection = 'visual' | 'chat' | 'sessions' | 'git' | 'github' | 'notifications' | 'voice'; @@ -68,9 +71,20 @@ export const OpenChamberSidebar: React.FC = ({ }) => { const { isMobile } = useDeviceInfo(); const showAbout = isMobile && isWebRuntime(); + const [isReloadingConfig, setIsReloadingConfig] = React.useState(false); const isVSCode = React.useMemo(() => isVSCodeRuntime(), []); const isWeb = React.useMemo(() => isWebRuntime(), []); + const showReload = !isVSCode; + + const handleReloadConfiguration = React.useCallback(async () => { + setIsReloadingConfig(true); + try { + await reloadOpenCodeConfiguration({ message: 'Restarting OpenCode…', mode: 'projects', scopes: ['all'] }); + } finally { + setIsReloadingConfig(false); + } + }, []); const visibleSections = React.useMemo(() => { return OPENCHAMBER_SECTION_GROUPS.filter((group) => { @@ -86,45 +100,105 @@ export const OpenChamberSidebar: React.FC = ({ const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar'; return ( -
- - {visibleSections.map((group) => { - const isSelected = selectedSection === group.id; - return ( -
- -
- ); - })} -
+ {group.badge && ( + + {group.badge} + + )} +
+
+ {group.items.join(' · ')} +
+ +
+ ); + })} + + - {/* Mobile footer: About section */} - {showAbout && ( -
- + {(showReload || showAbout) && ( +
+ {showAbout ? ( + <> + {showReload && ( + + + + + + Restart OpenCode and reload its configuration (agents, commands, skills, providers). + + + )} + + + ) : ( +
+ {showReload && ( + + + + + + Restart OpenCode and reload its configuration (agents, commands, skills, providers). + + + )} +
+
+ )}
)} diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx index 448df09a..620b41ba 100644 --- a/packages/ui/src/components/ui/CommandPalette.tsx +++ b/packages/ui/src/components/ui/CommandPalette.tsx @@ -15,8 +15,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useDeviceInfo } from '@/lib/device'; -import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine, RiTimeLine } from '@remixicon/react'; -import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore'; +import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine, RiTimeLine } from '@remixicon/react'; import { getModifierLabel } from '@/lib/utils'; import { createWorktreeSession } from '@/lib/worktreeSessionCreator'; @@ -106,11 +105,6 @@ export const CommandPalette: React.FC = () => { handleClose(); }; - const handleReloadConfiguration = () => { - reloadOpenCodeConfiguration(); - handleClose(); - }; - const handleOpenTimeline = () => { setTimelineDialogOpen(true); handleClose(); @@ -177,10 +171,6 @@ export const CommandPalette: React.FC = () => { Open Settings {getModifierLabel()} + , - - - Reload OpenCode Configuration - diff --git a/packages/ui/src/components/ui/HelpDialog.tsx b/packages/ui/src/components/ui/HelpDialog.tsx index 4abe8bcc..2066caec 100644 --- a/packages/ui/src/components/ui/HelpDialog.tsx +++ b/packages/ui/src/components/ui/HelpDialog.tsx @@ -17,13 +17,11 @@ import { RiCloseCircleLine, RiCodeLine, RiCommandLine, - RiFolder6Line, RiGitBranchLine, RiLayoutLeftLine, RiPaletteLine, RiQuestionLine, RiSettings3Line, - RiTerminalBoxLine, RiText, RiTimeLine, RiWindowLine, @@ -166,21 +164,6 @@ export const HelpDialog: React.FC = () => { description: "Open Diff Panel", icon: RiCodeLine, }, - { - keys: [`${mod} + 3`], - description: "Open Files", - icon: RiFolder6Line, - }, - { - keys: [`${mod} + 4`], - description: "Open Terminal", - icon: RiTerminalBoxLine, - }, - { - keys: [`${mod} + 5`], - description: "Open Git Panel", - icon: RiGitBranchLine, - }, { keys: [`${mod} + T`], description: "Open Timeline", diff --git a/packages/ui/src/components/views/SettingsView.tsx b/packages/ui/src/components/views/SettingsView.tsx index 68ad56ca..9f2f663e 100644 --- a/packages/ui/src/components/views/SettingsView.tsx +++ b/packages/ui/src/components/views/SettingsView.tsx @@ -290,13 +290,15 @@ export const SettingsView: React.FC = ({ onClose, forceMobile switch (activeTab) { case 'settings': return ( - { - setSelectedOpenChamberSection(section); - handleMobileSidebarClick(); - }} - /> +
+ { + setSelectedOpenChamberSection(section); + handleMobileSidebarClick(); + }} + /> +
); case 'agents': return ; @@ -354,7 +356,7 @@ export const SettingsView: React.FC = ({ onClose, forceMobile return ( -
+
{/* Header with tabs and close button */}
= ({ onClose, forceMobile
) : (
- {renderSidebarContent()} +
+ {renderSidebarContent()} +
) ) : ( @@ -530,7 +534,7 @@ export const SettingsView: React.FC = ({ onClose, forceMobile {hasSidebar && (
= ({ onClose, forceMobile aria-orientation="vertical" aria-label="Resize settings sidebar" /> - {renderSidebarContent()} +
+ {renderSidebarContent()} +
)} diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index af941046..826430ad 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -192,6 +192,15 @@ export const useKeyboardShortcuts = () => { } if (e.key === 'Escape') { + const target = e.target as Element | null; + const isInsideDialog = Boolean(target?.closest('[role="dialog"]')); + const isSettingsMounted = Boolean(document.querySelector('[data-settings-view="true"]')); + + if (isInsideDialog || isSettingsMounted) { + resetAbortPriming(); + return; + } + const { isSettingsDialogOpen, isCommandPaletteOpen,