← the Atlas

Code browser preview → @kolu/solid-fileview

feature · budding ·

Invent the grid, slim the house — a Source ⇄ Rendered file-view toggle built on reusable leaf packages. Markdown is the first lit toggle; HTML/SVG next.

The Code tab can open any file, but “open” means two things: show its source (syntax-highlighted text) or its rendered form (the image, the page, the document). Markdown sat in the gap — a README.md opened as source, never as a rendered document. The organizing idea is a Source ⇄ Rendered toggle: for any file with both forms, the user picks. Markdown is the first lit toggle ( #1093 , shipped); HTML and SVG join next.

What can be previewed today

Four render strategies, chosen below the fsReadFile wire boundary by file extension. The classifier is deliberately node-free so server and client import the same lists.

StrategyWire kindRendererExtensions
Source text (default)textBrowseFileView → Pierre CodeView (Shiki), wrapped in CommentTextSurface for line/range comments.Everything not listed below. Truncated past 1 MB.
Raster imagebinaryBrowsePreviewView → plain <img> on a checkerboard. No iframe — image bytes can’t execute..png .jpg .jpeg .gif .webp .ico
Sandboxed documentbinaryBrowsePreviewViewallow-scripts, opaque-origin <iframe>. HTML gets the artifact-sdk comment bridge; SVG/PDF verbatim..html .htm .svg .pdf
Video playerbinaryvideo renderer → <video controls>, range-served ( #1219 )..mp4 .m4v .webm .mov .ogv

The three binary sets are disjoint; their union is BINARY_PREVIEWABLE_EXTENSIONS — the partition is structural, so a new previewable format lands in exactly one category.

The hidden insight: kind conflates two independent questions

The text/binary discriminator answers “how do I fetch and render this by default.” The real structure is two orthogonal yes/no axes — and the toggle is meaningful exactly at their intersection.

FormatHas a source?Has a rendered form?What the user gets
plain code .ts .rs .pySource only (today’s default — correct)
Markdown .md✓ (client md render)Toggle — default Rendered
HTML .html .htm✓ (iframe)Toggle — default Rendered
SVG .svg✓ (iframe)Toggle — default Rendered
images .png .jpg✓ (<img>)Rendered only — no source exists
PDF .pdf✓ (iframe)Rendered only — no source exists

The toggle is offered iff both columns are ✓. This is a property of the format, not a per-presenter convention.

  fsReadFile (server)                     wire kind          BrowseFileDispatcher (client)
  ┌───────────────────────┐   text   ┌───────────────┐   →  BrowseFileView   (Pierre CodeView)
  │ isBinaryPreviewable(p) │ ───────▶ │ {kind:"text"} │
  │   in previewable.ts    │   binary │ {kind:"binary"}│  →  BrowsePreviewView (<img> | sandboxed iframe)
  └───────────────────────┘ ───────▶ └───────────────┘
                                       ▲ the single switch point — add a kind here

What cannot yet be previewed — but should be

Ranked by value ÷ cost.

FormatTodayShould beCost / seam
HTML / SVG source toggle setRendered in sandbox only.Same toggle as Markdown (text-backed).Low-medium. Wire must carry text alongside the URL.
More raster .avif .jxl .bmp .apngSource text (garbage).Plain <img>.Trivial — append to RASTER_IMAGE_EXTENSIONS.
Jupyter .ipynbRaw JSON.Rendered cells.Medium-high. A notebook renderer appliance. Defer.
CSV / TSVSource text.Optional rendered table (toggle).Medium — a virtualized table presenter.
Fonts .woff .ttf .otfGarbage.A specimen sheet.Low-medium; niche, defer.
Audio .mp3Garbage.<audio> at the URL.Low — an audio renderer appliance. Defer. (Video shipped — #1219 , .mp4 .m4v .webm .mov .ogv via the video renderer.)

Non-goal: Office formats, archives — they belong to a “download / open externally” affordance, not in-pane preview.

The feature — a Source ⇄ Rendered toggle, Markdown first

Markdown needs no new wire kind confirmed by P3

Markdown is text — it arrives as {kind: "text", content, truncated}. The render decision is purely client-side and reversible: default rendered, one flip to source. So Markdown is a renderer appliance plugged into the grid, above the wire. P3 confirmed it: no new kind, no defaultMode — the dispatcher passed a one-entry rendered={[markdownRenderer]} matched by isMarkdown; FileView’s “both forms → default Rendered, show the toggle” rule did the rest.

3a · Comments on rendered Markdown — shipped

v1 shipped comments only in source view (CommentTextSurface wraps Pierre’s CodeView); #1162 closed the seam — the rendered document now takes selection-anchored comments. kolu already had one anchoring model in two surfaces: the W3C TextQuoteSelector ({quote, prefix, suffix})

Volatility split (Lowy), as built: the generic mechanism (subtree-scoped selection → locator, locator → highlight) landed in @kolu/artifact-sdk/core, consumed by kolu’s CommentTextSurface/useTextSelection wrapping the rendered view — @kolu/solid-fileview gained only a controlled mode prop (for comment-tray jumps), not the planned rendered-annotation hook. The comment feature (tray, threads, persistence) stays kolu’s. One anchoring model across all three surfaces, not a fourth invented for Markdown.

3c · More Markdown features — appliances on the grid

Each lives inside the appliance (@kolu/solid-markdown); the host never changes. Grounded against what marked ships ({gfm: true, breaks: true}):

FeatureStatus todayThe work
GFM tables / task listsmarked parses; renderer emits <table> + checkbox nodes.Styling polish for document; verify checkboxes read as decorative.
Strikethrough / autolinksParsed by GFM.Confirm a del case + bare URLs through safeHref; style if missing.
Syntax-highlighted code fencesShiki-highlighted ( #1155 ).3b — shipped as a demand-loaded dynamic import("shiki") in @kolu/solid-markdown’s highlight.ts (a direct dep, not via @kolu/solid-pierre).
Heading anchor linksSlug ids on each heading ( #1155 ).Shipped via marked-gfm-heading-id.
Relative file linksClicking opens the file in the Code tab ( #1190 ).Shipped — kolu link policy, injected; Obsidian wikilinks followed ( #1212 ).
FrontmatterLeading YAML block stripped ( #1155 ).Shipped — strip-and-ignore.
KaTeX math · MermaidNot handled.Optional appliances plugged into the same outlet later — no host changes. Demand-gated.

Invent the grid — extract three packages, slim kolu

The naïve build wires each format into kolu’s right-panel/ as a Switch arm — the pre-electricity house. Two things vary independently and must be encapsulated apart (Lowy), and neither is about kolu:

  ── kolu app (the house) ───────────┐    ── the grid: reusable packages ──────────────────────
   fsReadFile wire {kind, …}         │
        │ thin adapter (wire→props)  │     @kolu/solid-fileview  ── the outlet ──
        ▼                            │       <FileView source={…} rendered={[…]} />
   <FileView                         │         owns: the Source ⇄ Rendered toggle,
      source={pierreRenderer}  ──────┼──▶       which modes exist, registry.pick(path)
      rendered={[md, img, iframe]} ──┼──▶     RenderedRenderer = { match, render }  ← appliances
   />                                │
        ▲ kolu injects its renderers │     @kolu/solid-markdown  ── an appliance ──
          (theme, comment bridge)    │       <Markdown variant="document" />  (marked + sanitize)
  ───────────────────────────────────┘     @kolu/solid-pierre   ── already a package (source) ──

The three packages (Lowy: each encapsulates one volatility)

PackageEncapsulatesThe outletWhat it obviates in kolu
@kolu/solid-markdown newmarked + GFM + safeHref + md→Solid styling. Volatile: the spec, the sanitizer.<Markdown markdown variant="inline"|"document" />The ~250 LOC token-walk core inside intent/IntentMarkdown.tsx.
@kolu/solid-fileview newThe toggle, mode-availability, renderer-registry pick. Knows nothing of oRPC, git, comments.<FileView path source? renderedUrl? source={Renderer} rendered={Renderer[]} />The render mechanics of BrowseFileView + BrowsePreviewView + BrowseFileDispatcher.
@kolu/solid-fileview/renderers/{markdown,image,iframe}Each strategy as an independent appliance. Generic, kolu-free.Each is a RenderedRenderer value.The strategy code hand-written in BrowsePreviewView.

Decoupling the source renderer too. FileView does not hard-depend on a highlighter — the source view is an injected renderer like any other. kolu passes one backed by @kolu/solid-pierre carrying kolu’s theme. So @kolu/solid-fileview has no rendering deps at all: pure mechanism, every concrete renderer an appliance.

HTML / SVG need the wire to carry both source and URL

HTML/SVG are text on disk but render in an allow-scripts opaque-origin iframe at a server-built URL (and HTML splices the comment bridge at that route) — the render path genuinely needs the URL. To also show source, the client needs the text the iframe path never carried. A third fsReadFile variant:

{ kind: "renderable", content, truncated, url }
//        ^source view ──────────┘          └── iframe render

Markdown stays kind:"text" (no URL); images/PDF stay kind:"binary" (no content). The discriminator now reads off the same two axes: text = source-only, binary = rendered-only, renderable = both.

Phasing — invent the grid, then plug in

The first two phases are pure extraction: no new feature, no wire change, kolu working throughout — and kolu’s LOC drops at each.

PhaseShipsNet kolu LOCRisk
1 · Extract @kolu/solid-markdown shipped #1079 Lift the token-walk core out of IntentMarkdown.tsx (257→30 LOC) into a package with inline/compact/document variants; migrate the intent surface. Behavior-preserving.Low — mechanical, one call-site.
2 · Extract @kolu/solid-fileview shipped #1082 The toggle host + mode logic + registry + image/iframe renderers. Rewrote right-panel/ preview as a thin fsReadFileFileView adapter. Renders exactly today’s formats. As-built: BrowseFileView.tsx kept (injected as SourceRenderer); only BrowsePreviewView.tsx went.↓↓ (3 presenters → 1 adapter)Medium — live-surface refactor, e2e-covered. (One defect caught only by the Nix build: the new package was missing from default.nix’s fileset.)
3 · Light the Markdown toggle shipped #1093 The first user-visible win. The markdown renderer landed in the library as @kolu/solid-fileview/renderers/markdown (wrapping the document variant). No new wire kind, no defaultModeFileView defaults to Rendered + shows the toggle whenever a file has both renderers. Server classification relocated out of kolu-git/previewable.ts into node-free kolu-common/preview.ts; kolu-common gained a test:unit runner. A tip + README landed.flatLow — the grid did the work. Hickey: 0; Lowy: 2 No-op.
3a · Comments on rendered Markdown shipped #1162 Closed the v1 seam via the shared TextQuoteSelector model over a plain DOM Range (no iframe/bridge since Markdown renders inline). As-built: subtree-scoped anchoring in @kolu/artifact-sdk/core + kolu’s CommentTextSurface/useTextSelection wrapping the rendered view; @kolu/solid-fileview gained only a controlled mode prop for comment-tray jumps — the planned generic rendered-annotation hook wasn’t needed.flatMedium — anchoring to a char range in rendered output was the open problem; the selector model was the way through.
3b · Syntax-highlighted code fences shipped #1155 Fenced code in rendered Markdown via Shiki, inside @kolu/solid-markdown (highlight.ts). As-built: a demand-loaded dynamic import("shiki") — a direct dep, not routed through @kolu/solid-pierre.flatLow.
3c · More Markdown features gridThe appliance evolves; the host doesn’t. Shipped: GFM polish, inline HTML, light/dark theming, heading ids, frontmatter strip ( #1155 ), relative file links opening in the Code tab ( #1190 ), and beyond-plan Obsidian wikilinks ( #1212 ). Remaining: optional KaTeX/Mermaid, demand-gated.flatLow; each independent.
4 · HTML / SVG sourceAdd the kind:"renderable" wire variant (content + url); the iframe renderer gains a source side. Toggle lights for .html .htm .svg with zero new components.flatLow-medium.
5 · PolishPersisted per-session toggle choice (makePersisted).flatLow.
6 · Cheap binary winsAppend .avif/.bmp to the image renderer; add audio. As-built start: the video renderer landed early ( #1219 , .mp4 .m4v .webm .mov .ogv, range-served).flatLow.
LaterNotebooks, CSV/TSV table, font specimens — each a new appliance; none touches the host.flatMedium; demand-gated.

Files this touches

The grid — new reusable packages:

PackageContents
packages/solid-markdown shipped #1079 The token-walk core (marked + safeHref) + inline/compact/document variants. Its document variant now also backs the Code-tab markdown appliance.
packages/solid-fileview shipped #1082 #1093 FileView host (toggle, mode-availability, registry) + the SourceRenderer/RenderedRenderer contracts. Core entry has no rendering deps. Sub-path renderers /renderers/{image,iframe,markdown}.

kolu — the net shrink:

FileChange
intent/IntentMarkdown.tsx−~250 LOC A thin consumer of @kolu/solid-markdown.
right-panel/BrowsePreviewView.tsxdeleted Its img/iframe mechanics moved into the library + renderers.
right-panel/BrowseFileView.tsxkept Not deleted — wrapped as the injected pierre SourceRenderer (carries kolu’s comment surface).
right-panel/BrowseIframeRenderer.tsxnew kolu’s iframe appliance: the library IframeRenderer + the artifact-sdk comment bridge. Comments are kolu’s volatility.
right-panel/BrowseFileDispatcher.tsxslimmed The fsReadFileFileView adapter; #1093 added a one-entry rendered={[markdownRenderer]} on the text path. A projection, not logic.
kolu-common/preview.ts (was kolu-git/previewable.ts)relocated Server-side wire classification moved to node-free kolu-common/preview — out of kolu-git. Gained isMarkdown/MARKDOWN_EXTENSIONS. P4 adds the renderable set here.
settings/tips.ts+1 New amb-markdown-preview tip surfacing the toggle.
default.nix+1 As-built: each new package must be added to the Nix build fileset or the Vite build fails to resolve it.

The accounting: two packages and a feature added, yet the kolu app tree ends up with fewer lines and one fewer responsibility — the electricity payoff. The complexity didn’t vanish; it moved behind an outlet and stopped being kolu’s to maintain.


Bottom line: the feature is a Source ⇄ Rendered toggle, offered wherever a file has both forms. Markdown is the first lit toggle ( #1093 ) — it cost a single library appliance plus a one-entry renderer list, because the grid was already built. As-built footnote (P3): Hickey 0 findings; Lowy 2, both No-op (keeping FsReadFileOutputSchema in kolu-git is volatility-correct; the host-overridable testId was rejected as configuration-as-complexity).