From 7521de42f4096322899f1cc28f05596b4a4fface Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 8 Jun 2026 21:10:24 -0500 Subject: [PATCH] refactor(desktop): dock terminal under chat and simplify file rail Keep the right rail focused on file browsing while moving the persistent terminal into the chat column bottom slot, and make terminal colors follow the active light/dark mode instead of a fixed Solarized palette. --- apps/desktop/src/app/chat/index.tsx | 4 + apps/desktop/src/app/hooks/use-keybinds.ts | 10 +- apps/desktop/src/app/right-sidebar/index.tsx | 117 ++++-------------- apps/desktop/src/app/right-sidebar/store.ts | 4 - .../src/app/right-sidebar/terminal/index.tsx | 29 +++-- .../app/right-sidebar/terminal/persistent.tsx | 8 +- .../app/right-sidebar/terminal/selection.ts | 77 +++++++----- .../terminal/use-terminal-session.ts | 16 ++- 8 files changed, 111 insertions(+), 154 deletions(-) diff --git a/apps/desktop/src/app/chat/index.tsx b/apps/desktop/src/app/chat/index.tsx index 4a0d3829c39a..116427796106 100644 --- a/apps/desktop/src/app/chat/index.tsx +++ b/apps/desktop/src/app/chat/index.tsx @@ -10,6 +10,7 @@ import type * as React from 'react' import { Suspense, useCallback, useMemo, useRef } from 'react' import { useLocation } from 'react-router-dom' +import { TerminalSlot } from '@/app/right-sidebar/terminal/persistent' import { Thread } from '@/components/assistant-ui/thread' import { Backdrop } from '@/components/Backdrop' import { PromptOverlays } from '@/components/prompt-overlays' @@ -388,6 +389,9 @@ export function ChatView({ +
+ +
) } diff --git a/apps/desktop/src/app/hooks/use-keybinds.ts b/apps/desktop/src/app/hooks/use-keybinds.ts index a38afa6cea8d..15653d186922 100644 --- a/apps/desktop/src/app/hooks/use-keybinds.ts +++ b/apps/desktop/src/app/hooks/use-keybinds.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' -import { setRightSidebarTab } from '@/app/right-sidebar/store' +import { setTerminalTakeover } from '@/app/right-sidebar/store' import { PANE_TOGGLE_REVEAL_EVENT } from '@/components/pane-shell' import { matchesQuery } from '@/hooks/use-media-query' import { PROFILE_SLOT_COUNT } from '@/lib/keybinds/actions' @@ -84,9 +84,9 @@ export function useKeybinds(deps: KeybindRuntimeDeps): void { } } - const showRightSidebarTab = (tab: 'files' | 'terminal') => { + const showFiles = () => { setFileBrowserOpen(true) - setRightSidebarTab(tab) + setTerminalTakeover(false) } handlersRef.current = { @@ -128,8 +128,8 @@ export function useKeybinds(deps: KeybindRuntimeDeps): void { toggleFileBrowserOpen() } }, - 'view.showFiles': () => showRightSidebarTab('files'), - 'view.showTerminal': () => showRightSidebarTab('terminal'), + 'view.showFiles': showFiles, + 'view.showTerminal': () => setTerminalTakeover(true), 'view.flipPanes': togglePanesFlipped, 'appearance.toggleMode': () => setMode(resolvedMode === 'dark' ? 'light' : 'dark'), diff --git a/apps/desktop/src/app/right-sidebar/index.tsx b/apps/desktop/src/app/right-sidebar/index.tsx index d5707873c3cd..0b8cc2117935 100644 --- a/apps/desktop/src/app/right-sidebar/index.tsx +++ b/apps/desktop/src/app/right-sidebar/index.tsx @@ -4,22 +4,20 @@ import type { ReactNode } from 'react' import { ErrorBoundary } from '@/components/error-boundary' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' -import { useI18n } from '@/i18n' import { Loader } from '@/components/ui/loader' import { Tip } from '@/components/ui/tooltip' +import { useI18n } from '@/i18n' import { normalizeOrLocalPreviewTarget } from '@/lib/local-preview' import { cn } from '@/lib/utils' import { $panesFlipped } from '@/store/layout' import { notifyError } from '@/store/notifications' import { setCurrentSessionPreviewTarget } from '@/store/preview' -import { $currentBranch, $currentCwd } from '@/store/session' +import { $currentCwd } from '@/store/session' import { SidebarPanelLabel } from '../shell/sidebar-label' import { ProjectTree } from './files/tree' import { useProjectTree } from './files/use-project-tree' -import { $rightSidebarTab, $terminalTakeover, type RightSidebarTabId, setRightSidebarTab } from './store' -import { TerminalSlot } from './terminal/persistent' interface RightSidebarPaneProps { onActivateFile: (path: string) => void @@ -27,24 +25,10 @@ interface RightSidebarPaneProps { onChangeCwd: (path: string) => Promise | void } -interface RightSidebarTab { - icon: string - id: RightSidebarTabId - labelKey: 'files' | 'terminal' -} - -const RIGHT_SIDEBAR_TABS: readonly RightSidebarTab[] = [ - { id: 'files', labelKey: 'files', icon: 'list-tree' }, - { id: 'terminal', labelKey: 'terminal', icon: 'terminal' } -] - export function RightSidebarPane({ onActivateFile, onActivateFolder, onChangeCwd }: RightSidebarPaneProps) { const { t } = useI18n() const r = t.rightSidebar - const activeTab = useStore($rightSidebarTab) - const terminalTakeover = useStore($terminalTakeover) const panesFlipped = useStore($panesFlipped) - const currentBranch = useStore($currentBranch).trim() const currentCwd = useStore($currentCwd).trim() const hasCwd = currentCwd.length > 0 @@ -68,7 +52,6 @@ export function RightSidebarPane({ onActivateFile, onActivateFolder, onChangeCwd } = useProjectTree(currentCwd) const canCollapse = Object.values(openState).some(Boolean) - const effectiveTab: RightSidebarTabId = terminalTakeover ? 'files' : activeTab const chooseFolder = async () => { const selected = await window.hermesDesktop?.selectPaths({ @@ -97,8 +80,6 @@ export function RightSidebarPane({ onActivateFile, onActivateFolder, onChangeCwd } } - const tabs = terminalTakeover ? RIGHT_SIDEBAR_TABS.filter(tab => tab.id !== 'terminal') : RIGHT_SIDEBAR_TABS - return ( ) } -function RightSidebarChrome({ - activeTab, - branch, - tabs -}: { - activeTab: RightSidebarTabId - branch: string - tabs: readonly RightSidebarTab[] -}) { - const { t } = useI18n() - const r = t.rightSidebar - - return ( -
-
- - - {branch && ( - - - {branch} - - )} -
-
- ) -} - interface FilesystemTabProps extends FileTreeBodyProps { canCollapse: boolean cwdName: string diff --git a/apps/desktop/src/app/right-sidebar/store.ts b/apps/desktop/src/app/right-sidebar/store.ts index a560bfddafeb..8c07f0824506 100644 --- a/apps/desktop/src/app/right-sidebar/store.ts +++ b/apps/desktop/src/app/right-sidebar/store.ts @@ -2,14 +2,10 @@ import { atom } from 'nanostores' import { persistBoolean, storedBoolean } from '@/lib/storage' -export type RightSidebarTabId = 'files' | 'git' | 'terminal' | 'web' - const TAKEOVER_KEY = 'hermes.desktop.terminalTakeover' -export const $rightSidebarTab = atom('files') export const $terminalTakeover = atom(storedBoolean(TAKEOVER_KEY, false)) $terminalTakeover.subscribe(active => persistBoolean(TAKEOVER_KEY, active)) -export const setRightSidebarTab = (tab: RightSidebarTabId) => $rightSidebarTab.set(tab) export const setTerminalTakeover = (active: boolean) => $terminalTakeover.set(active) diff --git a/apps/desktop/src/app/right-sidebar/terminal/index.tsx b/apps/desktop/src/app/right-sidebar/terminal/index.tsx index f11a705300ae..3ece92354ee5 100644 --- a/apps/desktop/src/app/right-sidebar/terminal/index.tsx +++ b/apps/desktop/src/app/right-sidebar/terminal/index.tsx @@ -1,17 +1,19 @@ import '@xterm/xterm/css/xterm.css' import { useStore } from '@nanostores/react' +import type { CSSProperties } from 'react' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' import { Loader } from '@/components/ui/loader' import { Tip } from '@/components/ui/tooltip' import { useI18n } from '@/i18n' +import { useTheme } from '@/themes/context' import { SidebarPanelLabel } from '../../shell/sidebar-label' -import { $terminalTakeover, setRightSidebarTab, setTerminalTakeover } from '../store' +import { $terminalTakeover, setTerminalTakeover } from '../store' -import { addSelectionShortcutLabel } from './selection' +import { addSelectionShortcutLabel, terminalTheme } from './selection' import { useTerminalSession } from './use-terminal-session' interface TerminalTabProps { @@ -21,6 +23,9 @@ interface TerminalTabProps { export function TerminalTab({ cwd, onAddSelectionToChat }: TerminalTabProps) { const { t } = useI18n() + const { resolvedMode } = useTheme() + const theme = terminalTheme(resolvedMode) + const { addSelectionToChat, hostRef, selection, selectionStyle, shellName, status } = useTerminalSession({ cwd, onAddSelectionToChat @@ -30,22 +35,17 @@ export function TerminalTab({ cwd, onAddSelectionToChat }: TerminalTabProps) { const label = takeover ? t.rightSidebar.terminalSplit : t.rightSidebar.terminalFocus const toggleTakeover = () => { - // Pre-select the Terminal tab so the slot is ready to host us on return. - if (takeover) { - setRightSidebarTab('terminal') - } - setTerminalTakeover(!takeover) } return (
- {shellName} + {shellName}
-
+
{status === 'starting' && (
)} - {/* Outer div paints the dark inset; inner div is the xterm host so the - canvas sizes to the *content* area and p-2 shows as terminal padding. - Forcing screen/viewport bg avoids xterm's default black peeking - through the unused pixels below the last full row. */} + {/* Outer div paints terminal inset; inner div is the xterm host so the + canvas sizes to the content area and p-2 stays as terminal padding. */}
diff --git a/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx b/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx index 5b9b151f5baf..597aebcafb14 100644 --- a/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +++ b/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx @@ -2,7 +2,9 @@ import { useStore } from '@nanostores/react' import { atom } from 'nanostores' import { type CSSProperties, useEffect, useLayoutEffect, useRef, useState } from 'react' -import { TERMINAL_BG } from './selection' +import { useTheme } from '@/themes/context' + +import { terminalTheme } from './selection' import { TerminalTab } from './index' @@ -56,6 +58,8 @@ const sameRect = (a: Rect | null, b: Rect) => export function PersistentTerminal({ cwd, onAddSelectionToChat }: PersistentTerminalProps) { const slot = useStore($slot) + const { resolvedMode } = useTheme() + const theme = terminalTheme(resolvedMode) const [rect, setRect] = useState(null) const [ready, setReady] = useState(false) @@ -107,7 +111,7 @@ export function PersistentTerminal({ cwd, onAddSelectionToChat }: PersistentTerm visibility: visible ? 'visible' : 'hidden', pointerEvents: visible ? 'auto' : 'none', zIndex: 4, - backgroundColor: TERMINAL_BG, + backgroundColor: theme.background, contain: 'layout size paint' } diff --git a/apps/desktop/src/app/right-sidebar/terminal/selection.ts b/apps/desktop/src/app/right-sidebar/terminal/selection.ts index 4f0049be8e34..c88f1ae549f6 100644 --- a/apps/desktop/src/app/right-sidebar/terminal/selection.ts +++ b/apps/desktop/src/app/right-sidebar/terminal/selection.ts @@ -1,38 +1,55 @@ import type { ITheme, Terminal } from '@xterm/xterm' import type { CSSProperties } from 'react' -// Solarized-derived palette, but with bright ANSI 8–15 promoted to real -// accent variants instead of Schoonover's UI grays. Hermes' TUI skins (gold, -// crimson, ...) emit bright SGR codes that would otherwise wash out to gray. -// We always render the dark canvas — the app's light surfaces can't host the -// default skin without dropping below readable contrast. -export const TERMINAL_BG = '#002b36' - -const THEME: ITheme = { - background: TERMINAL_BG, - foreground: '#839496', - cursor: '#93a1a1', - cursorAccent: TERMINAL_BG, - selectionBackground: '#586e7555', - black: '#073642', - red: '#dc322f', - green: '#859900', - yellow: '#b58900', - blue: '#268bd2', - magenta: '#d33682', - cyan: '#2aa198', - white: '#eee8d5', - brightBlack: '#586e75', - brightRed: '#f25c54', - brightGreen: '#b3d437', - brightYellow: '#f7c948', - brightBlue: '#5fb3ff', - brightMagenta: '#ff6ab4', - brightCyan: '#5cd9c8', - brightWhite: '#fdf6e3' +const DARK_THEME: ITheme = { + background: '#0f172a', + foreground: '#dbe4ff', + cursor: '#f8fafc', + cursorAccent: '#0f172a', + selectionBackground: '#93c5fd55', + black: '#1e293b', + red: '#f87171', + green: '#4ade80', + yellow: '#facc15', + blue: '#60a5fa', + magenta: '#c084fc', + cyan: '#22d3ee', + white: '#e2e8f0', + brightBlack: '#64748b', + brightRed: '#fca5a5', + brightGreen: '#86efac', + brightYellow: '#fde047', + brightBlue: '#93c5fd', + brightMagenta: '#d8b4fe', + brightCyan: '#67e8f9', + brightWhite: '#f8fafc' } -export const terminalTheme = (): ITheme => THEME +const LIGHT_THEME: ITheme = { + background: '#f8fafc', + foreground: '#1f2937', + cursor: '#111827', + cursorAccent: '#f8fafc', + selectionBackground: '#60a5fa44', + black: '#1f2937', + red: '#dc2626', + green: '#15803d', + yellow: '#a16207', + blue: '#1d4ed8', + magenta: '#9333ea', + cyan: '#0e7490', + white: '#d1d5db', + brightBlack: '#4b5563', + brightRed: '#ef4444', + brightGreen: '#22c55e', + brightYellow: '#eab308', + brightBlue: '#3b82f6', + brightMagenta: '#a855f7', + brightCyan: '#06b6d4', + brightWhite: '#111827' +} + +export const terminalTheme = (mode: 'light' | 'dark'): ITheme => (mode === 'dark' ? DARK_THEME : LIGHT_THEME) export const isMacPlatform = () => navigator.platform.toLowerCase().includes('mac') diff --git a/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts b/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts index 7442c64ee867..02ab00bde92a 100644 --- a/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +++ b/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts @@ -3,10 +3,11 @@ import { Unicode11Addon } from '@xterm/addon-unicode11' import { WebLinksAddon } from '@xterm/addon-web-links' import { WebglAddon } from '@xterm/addon-webgl' import { Terminal } from '@xterm/xterm' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { CSSProperties } from 'react' import { triggerHaptic } from '@/lib/haptics' +import { useTheme } from '@/themes/context' import { isAddSelectionShortcut, terminalSelectionAnchor, terminalSelectionLabel, terminalTheme } from './selection' @@ -184,6 +185,9 @@ function quotePathForShell(path: string, shellName: string): string { } export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSessionOptions) { + const { resolvedMode } = useTheme() + const activeTheme = useMemo(() => terminalTheme(resolvedMode), [resolvedMode]) + const initialThemeRef = useRef(activeTheme) const hostRef = useRef(null) const termRef = useRef(null) const sessionIdRef = useRef(null) @@ -266,7 +270,7 @@ export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSes lineHeight: 1.12, macOptionIsMeta: true, scrollback: 1000, - theme: terminalTheme() + theme: initialThemeRef.current }) const fit = new FitAddon() @@ -493,6 +497,14 @@ export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSes } }, [addSelectionToChat, cwd]) + useEffect(() => { + const term = termRef.current + + if (term) { + term.options.theme = activeTheme + } + }, [activeTheme]) + return { addSelectionToChat, hostRef,