Docs

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 .url identity, 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”).

PieceState
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 immediate onDidChangeActivePaneItemselectActiveFiledeselect() 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:

  1. Persist the linkserialize() (and copy()) now include filePath, so a reloaded tab keeps getPath(). deserialize already spreads state.data into the constructor, so no deserializer change was needed.
  2. Backfill legacy/pre-fix tabs — the selectActiveFile patch, before reading the active path, reverse-maps a pathless browser tab’s URL back to its .url file (matching each file’s URL= line — the same match the browser’s .url opener uses) via a cached project index (buildUrlIndex / resolveUrlFile, 5s TTL), and restores item.filePath. Because revealActiveFile runs right after selectActiveFile and also reads getActivePath(), both select and reveal then follow the bookmark — no extra sync path needed.

Where everything lives

  • tranquil-config/src/index.js — extended setDefaults('tree-view', …) with autoReveal:true, focusOnReveal:false.
  • tranquil-automations/lib/tranquil-automations.js — toggle command; menus/…json View item; patchTreeViewKeepSelection (Bug 1 + the Bug 2 backfill); resolveUrlFile / buildUrlIndex.
  • tranquil-browser/lib/tranquil-browser-model.jsfilePath added to serialize() and copy() (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 .url browser tab backfilled its path (008-16 comments.url) and the tree selected the matching bookmark (selectedInTree matched backfilledPath).
  • 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 — shared closeTabAndTrashUrl(item) helper (reads the active item by default; instanceof TranquilBrowserModel guard; reads getPath() for the .url, then paneForItem().destroyItem()), plus the tranquil-browser:close-tab-and-delete-url command.
  • lib/tranquil-browser-view.js — an early guard in the webview keyHandler, because a focused guest page swallows the keys before Atom’s keymap sees them. Matches on evt.code === 'KeyW' (macOS mangles evt.key to a dead char while Option is held).
  • keymaps/tranquil-browser.jsonalt-cmd-w (darwin) / ctrl-alt-w (win/linux) on atom-workspace; the command’s instanceof guard 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-viewcloseTabAndTrashUrl falls back to atom.workspace.getCenter().getActivePaneItem() (not getActivePaneItem()), so it closes the active browser tab regardless of where focus is.
  • Ctrl-Tab from a focused tree-view — core pane:show-next-item targets the active pane, so ctrl-tab did nothing while a dock was focused. Added tranquil-automations:cycle-next-center-tab / cycle-previous-center-tab (act on getCenter().getActivePane()), and rebound ctrl-tab / ctrl-shift-tab to 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 core pane: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 a notify toast with the new on/off state instead.
  • .url reverse-map is cached (5s TTL) to avoid re-reading the project on every tab switch; a freshly-created .url may take a few seconds (or a miss-triggered rebuild) to be found.
  • The backfill mutates item.filePath on 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.