fix(desktop): make the logs pane summon-only via the command palette

The logs pane no longer exists as standing chrome: it isn't registered in
any default layout or preset, never rides the terminal strip as a secondary
tab, and never opens automatically. The contribution is registered only
while the palette's "Toggle logs" command has it summoned, and closing it
(toggle, tab ✕, ⌘W) removes it from the registry and the tree entirely —
including sweeping it out of persisted layouts from before it was
summon-only. The open state is session-only, so a fresh boot always starts
without it.
This commit is contained in:
Brooklyn Nicholson
2026-08-01 00:09:43 -05:00
parent e08870f62c
commit 75fb139977
3 changed files with 61 additions and 27 deletions
+56 -22
View File
@@ -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(<ReviewPaneContent />)
},
{
// 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(<LogsPane />)
}
])
@@ -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(<LogsPane />)
})
// 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',
+3 -2
View File
@@ -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() {
@@ -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)) {