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)) {