diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 40a6d3ebc67..8d8c3cfad7c 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -3096,7 +3096,15 @@ function terminalShellEnv() { } } - env.COLORTERM = env.COLORTERM || 'truecolor' + // Strip color/theme-detection vars that ride along when Electron is launched + // from a non-tty agent shell (Cursor's runner sets NO_COLOR/FORCE_COLOR=0 + // /TERM=dumb; some terminals set COLORFGBG which would flip Hermes' TUI into + // light-mode). Our PTY is a real xterm-compat terminal — force truecolor. + delete env.NO_COLOR + delete env.FORCE_COLOR + delete env.COLORFGBG + + env.COLORTERM = 'truecolor' env.LC_CTYPE = env.LC_CTYPE || 'UTF-8' env.TERM = 'xterm-256color' env.TERM_PROGRAM = 'Hermes' diff --git a/apps/desktop/package.json b/apps/desktop/package.json index fa7f5acfe23..7601baeaa24 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -63,6 +63,7 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/addon-unicode11": "^0.9.0", "@xterm/addon-web-links": "^0.12.0", + "@xterm/addon-webgl": "^0.19.0", "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 0f0a64dd5fd..5defcb85de3 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -54,6 +54,8 @@ import { useGatewayBoot } from './gateway/hooks/use-gateway-boot' import { useGatewayRequest } from './gateway/hooks/use-gateway-request' import { ModelPickerOverlay } from './model-picker-overlay' import { RightSidebarPane } from './right-sidebar' +import { $terminalTakeover } from './right-sidebar/store' +import { PersistentTerminal, TerminalSlot } from './right-sidebar/terminal/persistent' import { NEW_CHAT_ROUTE, routeSessionId, sessionRoute } from './routes' import { useContextSuggestions } from './session/hooks/use-context-suggestions' import { useCwdActions } from './session/hooks/use-cwd-actions' @@ -99,6 +101,7 @@ export function DesktopController() { const filePreviewTarget = useStore($filePreviewTarget) const previewTarget = useStore($previewTarget) const selectedStoredSessionId = useStore($selectedStoredSessionId) + const terminalTakeover = useStore($terminalTakeover) const routedSessionId = routeSessionId(location.pathname) const routeToken = `${location.pathname}:${location.search}:${location.hash}` @@ -118,6 +121,7 @@ export function DesktopController() { settingsOpen, toggleCommandCenter } = useOverlayRouting() + const terminalTakeoverActive = chatOpen && terminalTakeover const titlebarToolGroups = useGroupRegistry() const statusbarItemGroups = useGroupRegistry() @@ -466,6 +470,9 @@ export function DesktopController() { const overlays = ( <> + {/* One PTY-backed terminal mounted forever; placeholders + decide where it shows. Toggling fullscreen never rebuilds the shell. */} + { @@ -548,6 +555,12 @@ export function DesktopController() { /> ) + const takeoverTerminalView = ( +
+ +
+ ) + return ( - - + + @@ -650,7 +664,6 @@ export function DesktopController() { diff --git a/apps/desktop/src/app/right-sidebar/index.tsx b/apps/desktop/src/app/right-sidebar/index.tsx index c0950c11db8..02c9708ed7f 100644 --- a/apps/desktop/src/app/right-sidebar/index.tsx +++ b/apps/desktop/src/app/right-sidebar/index.tsx @@ -14,13 +14,12 @@ import { SidebarPanelLabel } from '../shell/sidebar-label' import { ProjectTree } from './files/tree' import { useProjectTree } from './files/use-project-tree' -import { $rightSidebarTab, type RightSidebarTabId, setRightSidebarTab } from './store' -import { TerminalTab } from './terminal' +import { $rightSidebarTab, $terminalTakeover, type RightSidebarTabId, setRightSidebarTab } from './store' +import { TerminalSlot } from './terminal/persistent' interface RightSidebarPaneProps { onActivateFile: (path: string) => void onActivateFolder: (path: string) => void - onAddTerminalSelection: (text: string, label?: string) => void onChangeCwd: (path: string) => Promise | void } @@ -38,10 +37,10 @@ const RIGHT_SIDEBAR_TABS: readonly RightSidebarTab[] = [ export function RightSidebarPane({ onActivateFile, onActivateFolder, - onAddTerminalSelection, onChangeCwd }: RightSidebarPaneProps) { const activeTab = useStore($rightSidebarTab) + const terminalTakeover = useStore($terminalTakeover) const currentBranch = useStore($currentBranch).trim() const currentCwd = useStore($currentCwd).trim() const hasCwd = currentCwd.length > 0 @@ -54,6 +53,7 @@ export function RightSidebarPane({ : 'No folder selected' const { data, loadChildren, openState, refreshRoot, rootError, rootLoading, setNodeOpen } = useProjectTree(currentCwd) + const effectiveTab: RightSidebarTabId = terminalTakeover ? 'files' : activeTab const chooseFolder = async () => { const selected = await window.hermesDesktop?.selectPaths({ @@ -82,15 +82,19 @@ export function RightSidebarPane({ } } + const tabs = terminalTakeover + ? RIGHT_SIDEBAR_TABS.filter(tab => tab.id !== 'terminal') + : RIGHT_SIDEBAR_TABS + return (