diff --git a/apps/desktop/src/app/contrib/controller.tsx b/apps/desktop/src/app/contrib/controller.tsx index 9e01cde324..5ab0af5a78 100644 --- a/apps/desktop/src/app/contrib/controller.tsx +++ b/apps/desktop/src/app/contrib/controller.tsx @@ -1,5 +1,5 @@ import { useStore } from '@nanostores/react' -import { computed } from 'nanostores' +import { atom, computed } from 'nanostores' import type { CSSProperties, ReactElement, PointerEvent as ReactPointerEvent } from 'react' import { PREVIEW_RAIL_MAX_WIDTH, PREVIEW_RAIL_MIN_WIDTH } from '@/app/chat/right-rail' @@ -23,6 +23,7 @@ import { registerLayoutResetHandler, registerPaneCloser, registerPaneOpener, + removeTreePane, resetLayoutTree, revealTreePane, setPaneCollapsed, @@ -38,7 +39,6 @@ import { discoverRuntimePlugins } from '@/contrib/runtime-loader' import { sessionTitle as storedSessionTitle } from '@/lib/chat-runtime' import { FileText, LayoutDashboard, PanelBottom, Zap } from '@/lib/icons' import { type KeybindContribution, KEYBINDS_AREA } from '@/lib/keybinds/actions' -import { Codecs, persistentAtom } from '@/lib/persisted' import { setYoloEnabled } from '@/lib/yolo-session' import { pruneComposerPopoutZones } from '@/store/composer-popout' import { @@ -227,17 +227,6 @@ registry.registerMany([ maxWidth: FILE_BROWSER_MAX_WIDTH }, render: () => idle() - }, - { - // Optional chrome — in NO default layout. Adoption stacks it with the - // terminal; $logsOpen (default off, ⌘K "Toggle logs") reveals it. - id: 'logs', - area: 'panes', - title: 'logs', - // revealOnPreset: the Quad layout places logs, so applying it turns the - // logs pane on (like a ⌘K "Toggle logs") instead of leaving it collapsed. - data: { placement: 'bottom', height: '20vh', minHeight: '7.5rem', maxHeight: '80vh', revealOnPreset: true }, - render: () => idle() } ]) @@ -381,7 +370,7 @@ const QUAD_TREE = split( 'column', [ split('row', [group(['sessions', 'files']), group(['workspace'])], [1, 3]), - split('row', [group(['terminal']), group(['preview', 'review', 'logs'])], [1.4, 1]) + split('row', [group(['terminal']), group(['preview', 'review'])], [1.4, 1]) ], [3, 1] ) @@ -588,15 +577,60 @@ const $previewVisible = computed($previewTabs, tabs => tabs.length > 0) bindPaneVisibility('preview', $previewVisible, closeRightRail) -// Logs are optional chrome: off by default, toggled from ⌘K, persisted. -const $logsOpen = persistentAtom('hermes.desktop.logsOpen', false, Codecs.bool) +// Logs are ⌘K-ONLY chrome: the pane contribution EXISTS only while $logsOpen +// is on. Off (the default) keeps logs out of the registry and the tree +// entirely — no secondary tab riding the terminal strip, no preset or +// adoption path that resurrects it. Session-only on purpose (not persisted): +// a fresh boot never re-opens logs automatically. The palette toggle is the +// single door in; tab ✕ / ⌘W / the toggle itself remove it again. +const $logsOpen = atom(false) + +let unregisterLogsPane: (() => void) | null = null + +const syncLogsPane = (open: boolean) => { + if (open) { + unregisterLogsPane ??= registry.register({ + id: 'logs', + area: 'panes', + title: 'logs', + // Same tool-panel sizing rule as the terminal above. dock: its OWN zone + // beside the terminal — never a tab in the terminal's strip. + data: { + placement: 'bottom', + dock: { pane: 'terminal', pos: 'right' }, + height: '20vh', + minHeight: '7.5rem', + maxHeight: '80vh' + }, + render: () => idle() + }) + // Summoning logs is explicit intent — front it (un-dismisses if a ✕ close + // left a dismissal record behind). + revealTreePane('logs') + } else { + unregisterLogsPane?.() + unregisterLogsPane = null + + // No dismissal record — the next toggle-on must re-adopt cleanly. Also + // sweeps 'logs' out of persisted trees from before it was summon-only. + // Guarded: removePane rebuilds the tree even for an absent pane, and a + // no-op boot sweep would commit (and persist) a fresh identical tree. + const tree = $layoutTree.get() + + if (tree && allPaneIds(tree).includes('logs')) { + removeTreePane('logs') + } + } +} + +// Tool-panel tab semantics (✕ / ⌘W route through the store) so the palette +// toggle stays truthful either way. +markCollapsePane('logs') +registerPaneCloser('logs', () => $logsOpen.set(false)) +registerPaneOpener('logs', () => $logsOpen.set(true)) +syncLogsPane($logsOpen.get()) +$logsOpen.listen(syncLogsPane) -bindPaneCollapse( - 'logs', - $logsOpen, - () => $logsOpen.set(false), - () => $logsOpen.set(true) -) registry.register( paletteToggle({ id: 'logs.toggle', diff --git a/apps/desktop/src/app/contrib/panes.tsx b/apps/desktop/src/app/contrib/panes.tsx index 0f4d013151..36782638ab 100644 --- a/apps/desktop/src/app/contrib/panes.tsx +++ b/apps/desktop/src/app/contrib/panes.tsx @@ -31,8 +31,9 @@ import { $previewTarget, openPreview } from '@/store/preview' import { $currentCwd } from '@/store/session' // --------------------------------------------------------------------------- -// Logs — live agent-log tail. OPTIONAL chrome: not in any default layout, -// hidden until the ⌘K "Toggle logs" command opens it ($logsOpen). +// Logs — live agent-log tail. ⌘K-only chrome: the pane contribution exists +// only while the "Toggle logs" palette command has it summoned ($logsOpen in +// the controller) — never in a default layout, never a standing tab. // --------------------------------------------------------------------------- export function LogsPane() { diff --git a/apps/desktop/src/components/pane-shell/tree/store.ts b/apps/desktop/src/components/pane-shell/tree/store.ts index 548ee352fe..a1abe54478 100644 --- a/apps/desktop/src/components/pane-shell/tree/store.ts +++ b/apps/desktop/src/components/pane-shell/tree/store.ts @@ -1203,9 +1203,8 @@ export function applyTree(tree: LayoutNode, presetId: string) { // (the terminal, whose visibility a store owns) would otherwise stay // collapsed after the tree changes — so reveal the ones that opt in through // their owning store, keeping the ⌃`/toggle state truthful. Iterate the - // preset's DECLARED panes (not the adopted result): logs is auto-adopted - // hidden into every tree, so only a preset that explicitly places it (Quad) - // should turn it on. + // preset's DECLARED panes (not the adopted result) so only panes a preset + // explicitly places are turned on. const panes = registry.getArea('panes') for (const paneId of allPaneIds(tree)) {