Tree View Sync With Active Tab — Delivery Notes
Superseded (2026-07-21). Everything below was removed and replaced by an opt-in vertical tab rail in the right dock — see ADR-0012 and the Vertical Right-Dock Tabs delivery notes. Retained as history.
What started as “a toggle to keep the tree in sync with the active tab” grew into a coherent tree ↔ tab navigation system: fixing the deselect-wipe on pathless tabs, giving browser tabs a durable
.urlidentity, a close-tab-and-trash-bookmark shortcut, and center-pane targeting under dock focus. The decision record is ADR-0008; this note is the delivery write-up (what shipped and where).
Status
Implemented and verified live via DevTools instrumentation (see “How it was verified”).
| Piece | State |
|---|---|
tree-view.autoReveal defaulted ON, focusOnReveal defaulted OFF | ✅ |
Command + View-menu toggle (tranquil-automations:toggle-tree-view-sync) | ✅ |
| Bug 1 — selection wiped on switch to a pathless tab (“needs two clicks”) | ✅ fixed |
Bug 2 — browser (.url) tabs don’t highlight their bookmark in the tree | ✅ fixed |
The behavior wanted
“Go to a tab → the tree highlights that file.” This already exists upstream as tree-view.autoReveal (default false): on every center-pane tab switch, tree-view expands
collapsed parents and highlights the active file, without stealing focus. tree-view is
third-party Pulsar core (never edited), so the toggle and fixes live in owned packages.
Bug 1 — deselect-wipe on pathless tabs
TreeView#selectActiveFile() clears the selection whenever the active pane item has no file
path:
selectActiveFile() {
let activeFilePath = this.getActivePath(); // null for a browser tab, settings, etc.
if (this.entryForPath(activeFilePath)) return this.selectEntryForPath(activeFilePath);
else return this.deselect(); // ← wipes the highlight
} Because Tranquil uses pathless tabs heavily (browser tabs), every switch to one called deselect(). This produced both reported symptoms:
- “needs two clicks” — a single tree click opens the file pending with
activatePane:false, so the active item stays the pathless browser tab; the immediateonDidChangeActivePaneItem→selectActiveFile→deselect()erased what the click had just selected. A double-click activates the pane, so the file becomes the active item and the selection survives. - “tab switch doesn’t update the tree” — switching to a browser tab deselected instead of leaving the last file highlighted.
Fix (tranquil-automations/lib/tranquil-automations.js, patchTreeViewKeepSelection): patch
the live tree-view instance’s selectActiveFile so a pathless / not-yet-rendered active item leaves the current selection intact instead of deselecting. Patched on activation (retried via onDidActivatePackage if tree-view isn’t up yet) and reverted on deactivate. The instance is
reached at atom.packages.getActivePackage('tree-view').mainModule.treeView.
Bug 2 — browser tabs lose their .url bookmark link
Opening a .url file creates a browser model with filePath set, so getPath() returns the
bookmark path and the tree can follow it. But HTMLEditor#serialize() (tranquil-browser/lib/tranquil-browser-model.js) did not persist filePath, and getURI() returns the http URL — so after a window reload (or cmd-shift-t) every browser tab came back with filePath: null and the tree could no longer map it to its bookmark. This is why instrumentation
showed active: null for restored HN tabs.
Fix, two parts:
- Persist the link —
serialize()(andcopy()) now includefilePath, so a reloaded tab keepsgetPath().deserializealready spreadsstate.datainto the constructor, so no deserializer change was needed. - Backfill legacy/pre-fix tabs — the
selectActiveFilepatch, before reading the active path, reverse-maps a pathless browser tab’s URL back to its.urlfile (matching each file’sURL=line — the same match the browser’s.urlopener uses) via a cached project index (buildUrlIndex/resolveUrlFile, 5s TTL), and restoresitem.filePath. BecauserevealActiveFileruns right afterselectActiveFileand also readsgetActivePath(), both select and reveal then follow the bookmark — no extra sync path needed.
Where everything lives
tranquil-config/src/index.js— extendedsetDefaults('tree-view', …)withautoReveal:true,focusOnReveal:false.tranquil-automations/lib/tranquil-automations.js— toggle command;menus/…jsonView item;patchTreeViewKeepSelection(Bug 1 + the Bug 2 backfill);resolveUrlFile/buildUrlIndex.tranquil-browser/lib/tranquil-browser-model.js—filePathadded toserialize()andcopy()(Bug 2 persistence).
How it was verified
Launching the GUI headlessly from the agent shell isn’t possible (ELECTRON_RUN_AS_NODE=1 makes
Electron reject --no-sandbox; see the per-window session notes),
so this was verified in the running app via DevTools instrumentation that logged, on each active-item
change, the active URL, the (backfilled) path, and the tree’s .selected entry. Confirmed live:
- Single-click selects + paints the accent pill on the first click.
- Switching to a
.urlbrowser tab backfilled its path (008-16 comments.url) and the tree selected the matching bookmark (selectedInTreematchedbackfilledPath). - Selection persists across switches to pathless tabs instead of clearing.
Companion: ⌥⌘W — close tab + trash its .url
A follow-on shortcut that pairs with the sync: opt-cmd-w (⌥⌘W on macOS, Ctrl+Alt+W on
Win/Linux) closes the active browser tab and moves its source .url bookmark to the system
Trash (recoverable via shell.trashItem), with a toast naming the trashed file — no confirmation.
Because the tree follows the active tab, the bookmark’s tree entry disappears as the file is
removed. Lives entirely in tranquil-browser:
lib/tranquil-browser.js— sharedcloseTabAndTrashUrl(item)helper (reads the active item by default;instanceof TranquilBrowserModelguard; readsgetPath()for the.url, thenpaneForItem().destroyItem()), plus thetranquil-browser:close-tab-and-delete-urlcommand.lib/tranquil-browser-view.js— an early guard in the webviewkeyHandler, because a focused guest page swallows the keys before Atom’s keymap sees them. Matches onevt.code === 'KeyW'(macOS manglesevt.keyto a dead char while Option is held).keymaps/tranquil-browser.json—alt-cmd-w(darwin) /ctrl-alt-w(win/linux) onatom-workspace; the command’sinstanceofguard makes the global binding a no-op on non-browser tabs.
A tab with no .url (blank/link tab) just closes; regular Cmd-W is unchanged.
Companion: drag a tab to a folder → numbered .url
Dragging a browser tab onto a tree-view folder now auto-saves it with no prompt as a numbered NNN-<title>.url, continuing from the highest NNN- already in the drop-target folder — the same
scheme opt+cmd+click uses (saveNumberedLink). Implemented by rewriting saveUrlToTree in tranquil-drag-drop (was a save-dialog round-trip): it scans the target folder for the max NNN- prefix, names the file from the matching tab’s page title (falling back to the URL hostname), and
writes the [InternetShortcut] directly. Still undoable via tranquil-drag-drop:undo. (The removed
dialog was the only ipcRenderer user, so that import was dropped.)
Companion: dock-focus fixes (act on the center, not the focused dock)
Two related fixes for the same root cause: when a dock (the tree-view) is focused, atom.workspace.getActivePaneItem() / getActivePane() return the dock’s item/pane, not the
center’s — so tab-oriented actions did nothing.
- ⌥⌘W from a focused tree-view —
closeTabAndTrashUrlfalls back toatom.workspace.getCenter().getActivePaneItem()(notgetActivePaneItem()), so it closes the active browser tab regardless of where focus is. - Ctrl-Tab from a focused tree-view — core
pane:show-next-itemtargets the active pane, so ctrl-tab did nothing while a dock was focused. Addedtranquil-automations:cycle-next-center-tab/cycle-previous-center-tab(act ongetCenter().getActivePane()), and reboundctrl-tab/ctrl-shift-tabto them in the tranquil-client keymaps (keymaps/{darwin,linux,win32}.cson). Focus stays on the tree; the center’s active tab changes and the tree highlight follows via the sync. The webview Ctrl-Tab path (utils.js) already dispatched to the center pane, so it was unchanged. The other cycling keys (cmd-{/cmd-},cmd-alt-←/→,ctrl-pageup/pagedown) still use corepane:show-*and remain center-focus-only.
Gotchas
- No live menu checkmark. Atom package menus are label→command and can’t reflect config state as
a
[x]. The toggle command shows anotifytoast with the new on/off state instead. .urlreverse-map is cached (5s TTL) to avoid re-reading the project on every tab switch; a freshly-created.urlmay take a few seconds (or a miss-triggered rebuild) to be found.- The backfill mutates
item.filePathon the owned browser model — benign (its own field) and, with the serialize fix, persists across the next reload so the scan isn’t repeated.
Deferred
- Tab-strip order following tree order. A non-prefixed file (e.g.
something.txt) sorts to the bottom of the tree but opens mid-strip (next to the active tab), so the tab order doesn’t mirror the tree’s numeric/alphabetical order. Ctrl-Tab cycling itself is correct (positional, pane order). Considered auto-inserting new tabs in sorted position or a “sort tabs to match tree” command; deferred — it fights manual drag-reordering and the browser’s deliberate “open new tab at the end” behavior.