Code browser preview → @kolu/solid-fileview
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.
| Strategy | Wire kind | Renderer | Extensions |
|---|---|---|---|
| Source text (default) | text | BrowseFileView → Pierre CodeView (Shiki), wrapped in CommentTextSurface for line/range comments. | Everything not listed below. Truncated past 1 MB. |
| Raster image | binary | BrowsePreviewView → plain <img> on a checkerboard. No iframe — image bytes can’t execute. | .png .jpg .jpeg .gif .webp .ico |
| Sandboxed document | binary | BrowsePreviewView → allow-scripts, opaque-origin <iframe>. HTML gets the artifact-sdk comment bridge; SVG/PDF verbatim. | .html .htm .svg .pdf |
| Video player | binary | video 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.
| Format | Has a source? | Has a rendered form? | What the user gets |
|---|---|---|---|
plain code .ts .rs .py | ✓ | ✗ | Source 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.
| Format | Today | Should be | Cost / seam |
|---|---|---|---|
| HTML / SVG source toggle set | Rendered in sandbox only. | Same toggle as Markdown (text-backed). | Low-medium. Wire must carry text alongside the URL. |
More raster .avif .jxl .bmp .apng | Source text (garbage). | Plain <img>. | Trivial — append to RASTER_IMAGE_EXTENSIONS. |
Jupyter .ipynb | Raw JSON. | Rendered cells. | Medium-high. A notebook renderer appliance. Defer. |
| CSV / TSV | Source text. | Optional rendered table (toggle). | Medium — a virtualized table presenter. |
Fonts .woff .ttf .otf | Garbage. | A specimen sheet. | Low-medium; niche, defer. |
Audio .mp3 | Garbage. | <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})
extractQuote/findQuotein@kolu/artifact-sdk/core. Rendered Markdown was the third surface — and the simplest, because:
- Markdown renders inline in the parent document, not in a sandboxed iframe — so there’s no postMessage bridge. A plain DOM
RangefeedsextractQuotedirectly;findQuotere-locates on rehydrate. - The locator is durable across re-renders by construction (text + context, not a DOM path).
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}):
| Feature | Status today | The work |
|---|---|---|
| GFM tables / task lists | marked parses; renderer emits <table> + checkbox nodes. | Styling polish for document; verify checkboxes read as decorative. |
| Strikethrough / autolinks | Parsed by GFM. | Confirm a del case + bare URLs through safeHref; style if missing. |
| Syntax-highlighted code fences | Shiki-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 links | Slug ids on each heading ( #1155 ). | Shipped via marked-gfm-heading-id. |
| Relative file links | Clicking opens the file in the Code tab ( #1190 ). | Shipped — kolu link policy, injected; Obsidian wikilinks followed ( #1212 ). |
| Frontmatter | Leading YAML block stripped ( #1155 ). | Shipped — strip-and-ignore. |
| KaTeX math · Mermaid | Not 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:
- How a document renders (marked → safe Solid nodes) — volatile in its own right (the spec, sanitization).
- The source ⇄ rendered viewer (the toggle, mode-availability, renderer match) — a generic mechanism any file viewer needs, with zero kolu knowledge.
── 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)
| Package | Encapsulates | The outlet | What it obviates in kolu |
|---|---|---|---|
@kolu/solid-markdown new | marked + 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 new | The 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.
| Phase | Ships | Net kolu LOC | Risk |
|---|---|---|---|
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 fsReadFile→FileView 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 defaultMode — FileView 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. | flat | Low — 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. | flat | Medium — 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. | flat | Low. |
| 3c · More Markdown features grid | The 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. | flat | Low; each independent. |
| 4 · HTML / SVG source | Add the kind:"renderable" wire variant (content + url); the iframe renderer gains a source side. Toggle lights for .html .htm .svg with zero new components. | flat | Low-medium. |
| 5 · Polish | Persisted per-session toggle choice (makePersisted). | flat | Low. |
| 6 · Cheap binary wins | Append .avif/.bmp to the image renderer; add audio. As-built start: the video renderer landed early (
#1219 , .mp4 .m4v .webm .mov .ogv, range-served). | flat | Low. |
| Later | Notebooks, CSV/TSV table, font specimens — each a new appliance; none touches the host. | flat | Medium; demand-gated. |
Files this touches
The grid — new reusable packages:
| Package | Contents |
|---|---|
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:
| File | Change |
|---|---|
intent/IntentMarkdown.tsx | −~250 LOC A thin consumer of @kolu/solid-markdown. |
right-panel/BrowsePreviewView.tsx | deleted Its img/iframe mechanics moved into the library + renderers. |
right-panel/BrowseFileView.tsx | kept Not deleted — wrapped as the injected pierre SourceRenderer (carries kolu’s comment surface). |
right-panel/BrowseIframeRenderer.tsx | new kolu’s iframe appliance: the library IframeRenderer + the artifact-sdk comment bridge. Comments are kolu’s volatility. |
right-panel/BrowseFileDispatcher.tsx | slimmed The fsReadFile→FileView 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).