The Code tab is a browser → @kolu/solid-browser
The real electricity hiding in the Code tab isn't a history stack — it's the browser. Extract the location + history + link-navigation shell that drives @kolu/solid-fileview over a resolver; back/forward falls out for free, and git becomes an injected resolver.
Plan of record · branch md-back-link · phases 1 + 2 shipped in
#1191
The question that started this was small: what does it take to add back/forward navigation to the Code tab’s preview? The answer kept wanting to be “a history stack.” That answer is wrong — not incorrect, but mis-scoped. A history stack is a transformer: a working part inside a concept. The concept is the browser, and it has been hiding in the Code tab the whole time.
The Code tab is already a browser
The reframe is one move — name the concept, not a mechanism inside it. Two observations make that concrete.
The tell: the renderers are already extracted; the shell isn’t
kolu already pulled the appliances out into packages —
#1079
(@kolu/solid-markdown),
#1082 (@kolu/solid-fileview),
@kolu/solid-pierre (source). See solid-fileview — “invent the
grid, slim the house.” What that effort extracted was how a single document
renders. What it left behind, still smeared across the client, is the layer
above a single document: how you move between documents. That layer is the
browser, and today it is complected across five files:
| Smeared across | Owns (a browser organ) |
|---|---|
right-panel/openInCodeTab.ts packages/client/src/right-panel/openInCodeTab.ts:78-85 | The address bar — the single “navigate to this location” front door. |
right-panel/CodeTab.tsx packages/client/src/right-panel/CodeTab.tsx:515-542 | handleSelect — the navigation controller (apply a location to the view). |
right-panel/markdownImageSrc.ts → now solid-browser/src/relativePath.ts packages/solid-browser/src/relativePath.ts | Relative-link resolution (GitHub rules) — agnostic URI math. |
right-panel/iframePreviewNav.ts → now solid-browser/src/previewPath.ts packages/solid-browser/src/previewPath.ts | In-frame link interception — map an iframe pathname back to a location. |
right-panel/BrowseIframeRenderer.tsx packages/client/src/right-panel/BrowseIframeRenderer.tsx:56-64 | In-iframe link observation — a navigation edge out of rendered HTML (the iframe drawing is already @kolu/solid-fileview’s). |
None of these is about git, and none is about how a file draws (that’s
@kolu/solid-fileview). They are about locations, links, and history — the
vocabulary of a browser.
OpenInCodeTabRequest is already a URL
Look at the front-door request shape packages/client/src/right-panel/openInCodeTab.ts:31-54:
{ ref: { path, startLine, endLine }, repoRoot, cwd, targetMode, allowBasenameFallback }
That is a URL. ref is the path + fragment; targetMode is which “site”
(browse/local/branch); repoRoot/cwd are the base href. And
openInCodeTab() is already location.assign() — the single producer-side entry
every navigation routes through, which is why relative Markdown links
(#1161,
#1190 ) and terminal path:N
links and the right-click Open path menu all funnel into it. The browser is
half-built; it is missing only history.back()/forward() and the popstate
semantics over a stack of these locations.
The electricity — @kolu/solid-browser
A standalone package. As shipped (phases 1 + 2) it depends on
@kolu/url-shape + solid-js and nothing kolu — the dependency arrow points
out of the client, never back in. It owns URIs, locations, links, and history
(createBrowser is the reactive history controller). It knows nothing of git,
repos, modes, terminals — nor of how any single format renders (that is
fileview’s). The diagram below draws the full target shape, including the
parts still deferred: a <Browser> component that composes @kolu/solid-fileview
to draw whatever a location resolves to. Today the host (CodeTab) keeps both
rendering paths and consumes only the history controller + the path utilities —
so the package depends on @kolu/solid-fileview only once <Browser> lands,
not now (see the phase-2 as-built callout for why <Browser> is deferred).
Once <Browser> lands, what the host injects is exactly the volatility kolu
owns:
resolve(location) → FileData— the only part that knows “repo-relative path + git mode → file bytes,” producing the{ path, source?, url? }FileDatathat@kolu/solid-fileviewdraws. Swap it and the same browser reads HTTP, ssh, an artifact store.- The renderers
FileViewdraws with —@kolu/solid-markdown/-pierreand the iframe/image appliances, forwarded straight through to<FileView>. The browser owns no renderer and no render dispatch (match(path)is fileview’s); it decides only which location to show. - Chrome via slots — the file tree and
browse/local/branchmode chips.
Does “browser” clear the electricity bar where “history” didn’t?
Against electricity’s own three tests:
| Test | ”history” stack | @kolu/solid-browser |
|---|---|---|
| ① domain-agnostic | ✓ (but T carried no meaning) | ✓ — URIs/locations/links/history; git is injected, rendering is fileview’s |
| ② hides a hard volatility | no back/forward is a bounded algorithm — nonempty-tier leaf | yes resource resolution (transport) + navigation across heterogeneous rendered content (DOM-anchor, in-iframe, tree → one location model) + GitHub-relative path semantics + history/popstate + the production-build effect-elision invariant the front door was built to dodge packages/client/src/right-panel/openInCodeTab.ts:9-19 |
| ③ graduates — proof is real | meaningless standalone | a docs/wiki viewer, a drishti-style host inspector (logs + configs over ssh, links between them), an artifact viewer — each injects its own resolve and reuses the published renderers |
The design
Two mechanisms carry the feature — one for moving (the seam), one for
remembering where you were (history). Both live in the engine, never in
CodeTab.
The seam — split assign from record
Back/forward hinges on one split the front door doesn’t yet make — separating
applyLocation (assign) from navigate (assign and record):
applyLocation(loc) // the existing batch(openCodeAt + reveal + setPending) — "assign"
navigate(loc) = applyLocation(loc) + history.push(loc) // address-bar navigation
back() = applyLocation(history.back()) // traverse — NO push
forward() = applyLocation(history.forward()) // traverse — NO push
This split is mandatory, not cosmetic: if back/forward went through the
recording path you’d get the classic “going back creates new history” bug.
Traversal must apply-without-recording — which is why the split is a phase-2
(history) concern, not what makes the engine extractable. The extraction
(phase 1) leans on a different property of the same applyLocation: it’s the
one organ that knows about modes and the pending-highlight signal, so it stays
kolu-side while the rest of the engine graduates.
The feature that rides out for free — unified back/forward
Back/forward rides the extraction — the engine has history because a browser does, so phase 2 is mostly exposure, not invention. Settled design (from the originating discussion):
- Unified history, one stack. Because
targetModelives inside the location, back/forward crossbrowse/local/branchnaturally — exactly browser behavior. The per-modeselectedFileByModepackages/client/src/right-panel/useRightPanel.ts:326-344 becomes a derived view of “current location filtered to this mode,” not the source of truth. - Tree clicks count as navigation.
handleSelectpackages/client/src/right-panel/CodeTab.tsx:515-542 routes throughnavigate(), so back returns to the previously-viewed file no matter how you reached the current one. - Restore where you were, not just what. A faithful entry is
{ location, scroll }, wherescrollis captured on the way out (history.replaceTopbefore pushing the next) — browsers restore the scroll you left, not the line you arrived at. Cheap v1: entry = inboundrefonly, back re-fires the highlight (theequals:falsepending signal already re-paints packages/client/src/right-panel/openInCodeTab.ts:62-69); faithful v2 reads live scroll from the renderer. - Keybinds —
codeTabBack/codeTabForwardininput/actions.ts(browser-likeCmd/Ctrl+[/]), guarded to Code-tab focus; checkinput/prohibitedKeybinds.tsand add thekeyboard.test.tscollision case.
Building it
Files this touches
The electricity — a new isolated package:
| Package | Contents |
|---|---|
packages/solid-browser phases 1 + 2 | Shipped: relativePath.ts + previewPath.ts (phase 1) and createBrowser.ts — the generic reactive history controller (phase 2) — with 42 unit tests + a standalone example/docsite second host. deps: @kolu/url-shape (the DOM-free hasOwnScheme leaf) + solid-js (createBrowser’s reactive stack). <Browser> composing <FileView> stays deferred (see the phase-2 as-built callout). Zero kolu imports; builds + tests standalone. |
kolu — the consumer, now just git + chrome:
| File | Change |
|---|---|
right-panel/useRightPanel.ts phase 2 | Hosts a per-terminal Browser<BrowserLocation> (in-memory; seeded on restore, dropped on teardown) and exposes recordNavigation/navigateBack/navigateForward/canNavigateBack/canNavigateForward. selectedFileByMode stays the render + restore truth; history is additive over it. |
right-panel/CodeTab.tsx phase 2 | Records every selection (tree click, in-iframe link, resolved front-door open) into history; ◀ ▶ toolbar buttons + a scoped Alt+←/→ listener re-apply earlier locations. Cheap-v1 re-highlight rides the existing front-door pipeline. |
gitResolver.ts · <Browser> · unified navigate deferred | The resolve(location)→FileData injection, the <Browser> component, and folding the two nav paths into one navigate wait on a uniform viewport — diffs don’t fit FileData, so a <Browser> would wrap only browse mode (hollow). See the phase-2 as-built callout. |
right-panel/markdownImageSrc.ts phase 1 | Delegates resolution to the package; keeps only the file-route URL build (real composition, not a pass-through). |
right-panel/iframePreviewNav.ts deleted (phase 1) | The agnostic inversion moved to the package; the kolu codec is bound inline at BrowseIframeRenderer’s call site — no thin wrapper (.agency/code-police.md → no-thin-wrapper-functions). |
settings/tips.ts +nav | A discoverability tip for back/forward. (No input/actions.ts entry: the keybind is a Code-tab-scoped listener, not a global action — a global mod+[ would shadow the terminal’s ESC byte on Linux.) |
Phasing — extract first, then light the feature
Extraction comes before the feature, deliberately. Phase 1 is a pure,
behavior-preserving lift — the solid-fileview pattern (extract with kolu
working throughout,
#1082 ; let features ride the extraction
after). The subtle point: what gets extracted in phase 1 is today’s
navigation, which is already proven code — the safest possible thing to move.
The new behavior (history) is then added to the clean package, never bolted
onto the client that’s about to be gutted. There is no forced dependency either
way; this ordering is chosen for risk isolation and single-purpose diffs (a
pure-move PR, then a pure-addition PR).
| Phase | Ships | Risk |
|---|---|---|
| 1 · Extract the primitives shipped #1191 | @kolu/solid-browser = relativePath + previewPath over the @kolu/url-shape leaf. kolu re-consumes: markdownImageSrc delegates resolution, BrowseIframeRenderer binds the previewPathCodec, BrowseFileDispatcher imports resolveLinkHref. Behavior-preserving — the two nav paths stay distinct. | Low — pure-move + delegation, unit- + e2e-covered. |
| 2 · The browser proper + back/forward shipped #1191 | createBrowser<L>() — the generic reactive history controller (navigate/back/forward/current/canBack/canForward; forward-truncation; idempotent on same entry). kolu wires it into useRightPanel/CodeTab: every selection is recorded, ◀ ▶ buttons + scoped Alt+←/→ re-apply, cheap-v1 re-highlight rides the front door. As-built deltas (callout below): history is additive over selectedFileByMode (still the render truth), mode-chips don’t record, <Browser>/gitResolver/unified navigate deferred. | Medium — live-surface refactor; the audit + the #818 regression suite guard the selection invariants. |
| 3 · Prove ③ in-repo demo landed | example/docsite — a standalone doc browser over its own { slug } location — reuses createBrowser unchanged (built + tested in CI). In-repo proof of reuse, exactly the bar @kolu/surface’s examples clear; a different application injecting its own resolve (drishti, an artifact viewer) is still the closing argument. | Low — pure-logic reuse. |
Bottom line: don’t extract a history stack — that’s a transformer. Extract the
browser the Code tab already is. The renderers are packaged
(@kolu/solid-fileview, which it composes); what’s left smeared across the
client is the location + history + link-nav shell. Pull it into
@kolu/solid-browser, inject git as a resolver, and back/forward arrives for
free. It clears electricity’s hard-volatility bar where “history”
didn’t. Phase 2 shipped the history controller + the back/forward feature, and
example/docsite is a real in-repo second consumer of it — a different
application injecting its own resolve is the closing argument.