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,