Skip to content

Commit d7a85a3

Browse files
committed
Debug window: closed-tabs section with L/R split
Mirrors the existing `Navigation history` panel. Lists each pane's closed-tab stack with most-recent on top (`↑` marker, bolded), older entries marked `·`. Paths use the `useShortenMiddle` action with `preferBreakAt: '/'` for pixel-accurate mid-truncation; hovering shows a multi-line tooltip with path, volume, sort, view mode, pin state, cursor filename, original index, and tab id. - New `DebugClosedTabsPanel.svelte` mounted in `routes/debug/+page.svelte` directly under `DebugHistoryPanel`. - `DualPaneExplorer.svelte` emits a `debug-closed-tabs` event whenever either pane's `closedStack` changes (dev mode only, skipped in tests, mirrors the `debug-history` emit pattern). Uses `$state.snapshot` so the payload is plain JSON for Tauri's event channel. - Styles colocated in `routes/debug/+page.svelte`, mirroring the existing `history-panes` rules.
1 parent 65417fb commit d7a85a3

3 files changed

Lines changed: 183 additions & 0 deletions

File tree

apps/desktop/src/lib/file-explorer/pane/DualPaneExplorer.svelte

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,22 @@
382382
})
383383
})
384384
385+
// Emit closed-tab stacks to debug window (dev mode only, skip in tests)
386+
$effect(() => {
387+
if (!import.meta.env.DEV || import.meta.env.MODE === 'test') return
388+
// Snapshot reads every property, setting up reactivity on push/pop/mutate.
389+
// It also produces plain JSON so Tauri's event channel can serialize it —
390+
// raw `$state` proxies + nested NavigationHistory throw on structured-clone.
391+
const leftSnap = $state.snapshot(leftTabMgr.closedStack)
392+
const rightSnap = $state.snapshot(rightTabMgr.closedStack)
393+
const focused = focusedPane
394+
untrack(() => {
395+
void import('@tauri-apps/api/event').then(({ emit }) => {
396+
void emit('debug-closed-tabs', { left: leftSnap, right: rightSnap, focusedPane: focused })
397+
})
398+
})
399+
})
400+
385401
// Derived volume paths - handle 'network' virtual volume specially
386402
const leftVolumePath = $derived(
387403
leftVolumeId === 'network' ? 'smb://' : (volumes.find((v) => v.id === leftVolumeId)?.path ?? '/'),

apps/desktop/src/routes/debug/+page.svelte

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import DebugDriveIndexPanel from './DebugDriveIndexPanel.svelte'
55
import DebugToastPanel from './DebugToastPanel.svelte'
66
import DebugHistoryPanel from './DebugHistoryPanel.svelte'
7+
import DebugClosedTabsPanel from './DebugClosedTabsPanel.svelte'
78
import DebugErrorPreviewPanel from './DebugErrorPreviewPanel.svelte'
89
910
let pageElement: HTMLDivElement | undefined = $state()
@@ -91,6 +92,7 @@
9192
<DebugDriveIndexPanel />
9293
<DebugToastPanel />
9394
<DebugHistoryPanel />
95+
<DebugClosedTabsPanel />
9496
<DebugErrorPreviewPanel />
9597
</div>
9698
</div>
@@ -256,6 +258,75 @@
256258
font-style: italic;
257259
}
258260
261+
/* Closed-tabs styles (mirror history-panes layout) */
262+
:global(.closed-tabs-panes) {
263+
display: flex;
264+
gap: 12px;
265+
}
266+
267+
:global(.closed-tabs-pane) {
268+
flex: 1;
269+
background: var(--color-bg-secondary);
270+
border-radius: var(--radius-md);
271+
padding: 8px;
272+
min-width: 0;
273+
}
274+
275+
:global(.closed-tabs-pane.focused) {
276+
outline: 2px solid var(--color-accent);
277+
}
278+
279+
:global(.closed-tabs-pane h3) {
280+
margin: 0 0 var(--spacing-sm);
281+
font-size: var(--font-size-sm);
282+
font-weight: 600;
283+
color: var(--color-text-secondary);
284+
text-transform: uppercase;
285+
}
286+
287+
:global(.closed-tabs-list) {
288+
list-style: none;
289+
margin: 0;
290+
padding: 0;
291+
font-size: var(--font-size-sm);
292+
font-family: var(--font-mono);
293+
}
294+
295+
:global(.closed-tabs-list li) {
296+
display: flex;
297+
align-items: center;
298+
gap: var(--spacing-xs);
299+
padding: 3px 4px;
300+
border-radius: var(--radius-sm);
301+
color: var(--color-text-secondary);
302+
min-width: 0;
303+
}
304+
305+
:global(.closed-tabs-list li.top) {
306+
background: var(--color-bg-tertiary);
307+
color: var(--color-text-primary);
308+
font-weight: 600;
309+
}
310+
311+
:global(.closed-tab-marker) {
312+
flex-shrink: 0;
313+
width: 12px;
314+
text-align: center;
315+
}
316+
317+
:global(.closed-tab-path) {
318+
flex: 1;
319+
min-width: 0;
320+
overflow: hidden;
321+
}
322+
323+
:global(.no-closed-tabs) {
324+
margin: 0;
325+
font-size: var(--font-size-sm);
326+
color: var(--color-text-tertiary);
327+
font-style: italic;
328+
}
329+
259330
/* Drive index styles */
260331
:global(.index-panel) {
261332
background: var(--color-bg-secondary);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<script lang="ts">
2+
import { onMount, onDestroy } from 'svelte'
3+
import { tooltip } from '$lib/tooltip/tooltip'
4+
import { useShortenMiddle } from '$lib/utils/shorten-middle-action'
5+
import type { ClosedTab } from '$lib/file-explorer/tabs/tab-state-manager.svelte'
6+
7+
interface ClosedTabsPayload {
8+
left: ClosedTab[]
9+
right: ClosedTab[]
10+
focusedPane: 'left' | 'right'
11+
}
12+
13+
let leftStack = $state<ClosedTab[]>([])
14+
let rightStack = $state<ClosedTab[]>([])
15+
let focusedPane = $state<'left' | 'right'>('left')
16+
let unlisten: (() => void) | undefined
17+
18+
onMount(async () => {
19+
try {
20+
const { listen } = await import('@tauri-apps/api/event')
21+
unlisten = await listen<ClosedTabsPayload>('debug-closed-tabs', (event) => {
22+
leftStack = event.payload.left
23+
rightStack = event.payload.right
24+
focusedPane = event.payload.focusedPane
25+
})
26+
} catch {
27+
// Not in Tauri environment
28+
}
29+
})
30+
31+
onDestroy(() => {
32+
unlisten?.()
33+
})
34+
35+
/** Multi-line tooltip text for one closed tab (tooltip CSS is `white-space: pre-line`). */
36+
function tabDetails(entry: ClosedTab): string {
37+
const t = entry.tab
38+
const sort = `${t.sortBy} ${t.sortOrder}`
39+
const cursor = t.cursorFilename ?? '(none)'
40+
const lines = [
41+
`Path: ${t.path}`,
42+
`Volume: ${t.volumeId}`,
43+
`Sort: ${sort}`,
44+
`View: ${t.viewMode}`,
45+
`Pinned: ${t.pinned ? 'yes' : 'no'}`,
46+
`Cursor: ${cursor}`,
47+
`Original index: ${entry.originalIndex}`,
48+
`Tab id: ${t.id}`,
49+
]
50+
return lines.join('\n')
51+
}
52+
</script>
53+
54+
<section class="debug-section">
55+
<h2>Closed tabs</h2>
56+
<div class="closed-tabs-panes">
57+
<div class="closed-tabs-pane" class:focused={focusedPane === 'left'}>
58+
<h3>Left pane</h3>
59+
{#if leftStack.length > 0}
60+
<ul class="closed-tabs-list">
61+
{#each leftStack as entry, i (`${entry.tab.id}-${i}`)}
62+
{@const isTop = i === leftStack.length - 1}
63+
<li class:top={isTop} use:tooltip={tabDetails(entry)}>
64+
<span class="closed-tab-marker">{isTop ? '' : '·'}</span>
65+
<span
66+
class="closed-tab-path"
67+
use:useShortenMiddle={{ text: entry.tab.path, preferBreakAt: '/' }}
68+
></span>
69+
</li>
70+
{/each}
71+
</ul>
72+
{:else}
73+
<p class="no-closed-tabs">No recently closed tabs</p>
74+
{/if}
75+
</div>
76+
<div class="closed-tabs-pane" class:focused={focusedPane === 'right'}>
77+
<h3>Right pane</h3>
78+
{#if rightStack.length > 0}
79+
<ul class="closed-tabs-list">
80+
{#each rightStack as entry, i (`${entry.tab.id}-${i}`)}
81+
{@const isTop = i === rightStack.length - 1}
82+
<li class:top={isTop} use:tooltip={tabDetails(entry)}>
83+
<span class="closed-tab-marker">{isTop ? '' : '·'}</span>
84+
<span
85+
class="closed-tab-path"
86+
use:useShortenMiddle={{ text: entry.tab.path, preferBreakAt: '/' }}
87+
></span>
88+
</li>
89+
{/each}
90+
</ul>
91+
{:else}
92+
<p class="no-closed-tabs">No recently closed tabs</p>
93+
{/if}
94+
</div>
95+
</div>
96+
</section>

0 commit comments

Comments
 (0)