← the Atlas

Welcome, revamped — pin it, reach it, run agents, watch it

feature · budding ·implemented ·

One story across three surfaces — a bird's-eye in-app welcome, the kolu.dev home page that now carries the whole guide, and a hero demo clip filmed by the e2e harness so it can't drift from the real app. The spine — one `tailscale serve` gives kolu both the HTTPS secure context for one-click PWA install and remote reach. Plan of record + build journal.

This note is the plan of record and build journal for kolu’s whole first-run story, which spans three surfaces that turned out to be one:

  1. a bird’s-eye in-app welcome — a revamp of the empty state, re-openable on demand ( #1199 );
  2. the kolu.dev home page, which started as a separate /welcome guide and was later folded in whole so a visitor understands kolu in one scroll ( #1213 );
  3. a hero demo clip at the top of that page — short, looping, and filmed by the e2e harness so it regenerates from source and can’t drift from the real app ( #1213 ).

The first two thread around one verified insight (the spine). The third is its own subsystem with its own hard-won pipeline — both are below.

kolu · welcome
Terminals on an infinite canvas. Any agent.
Three things worth doing first.
📌
Pin it
Install kolu as an app — its own window, dock icon, and live agent badge.
PWA
🌐
Reach it anywhere
One Tailscale command and kolu follows you to your phone, over real HTTPS.
Tailscale
🤖
Run agents
Open a repo, drop a tile, launch Claude / Codex / OpenCode. Agent-agnostic.
Canvas
New terminal ⌘⏎  ·  Palette ⌘K
Full guide → kolu.dev
Closed it? ⌘K → "Tutorial" brings it back anytime.

The spine: one command unlocks two features

kolu’s two onboarding goals — pin it as an app and reach it from anywhere — look separate. They are the same action.

A self-hosted kolu is almost always served over plain HTTP on a LAN (192.168.x.x) or Tailscale (100.x) address. You can still pin it from there — Chrome’s Create shortcut → Open as window and iOS Safari’s Add to Home Screen both work over http:// — but the frictionless one-click install (the beforeinstallprompt prompt / omnibox install icon) and the OS app badge need a secure context (HTTPS, or the loopback/localhost set). The very same tailscale serve that makes kolu reachable from your phone gives it a real https://…ts.net origin — so it unlocks the one-click install and remote access in one command. Tell it as one story.

Plain HTTP
http://100.x:PORT — manual pin only, off-LAN unreachable
tailscale serve
tailscale serve --bg PORT
✓ Real HTTPS
https://box.tailnet.ts.net
↳ secure context → 1-click install + app badge
↳ on your tailnet → Reachable anywhere

Verified facts (the plan rests on these)

Four load-bearing claims, each adversarially fact-checked against current (2025–26) primary sources.

The in-app welcome

Revamp the empty state in place rather than add a modal — shipped as packages/client/src/WelcomeMoments.tsx rendered above packages/client/src/EmptyState.tsx‘s restore card and shortcut list. The patterns worth stealing: VS Code Walkthroughs (a short, re-openable checklist — not a forced tour; it explicitly warns against excessive steps), Warp (one welcome surface, success measured as habit not completion), Zed (empty-state-as-welcome, re-openable from the palette), and Raycast’s EmptyView (the empty state carries the onboarding with actions).

The moments (cap ~3–5, hard). The three cards above — Pin it, Reach it anywhere, Run agents — sit above the existing restore card and shortcut list. Each is one verb + one line + one chip; depth lives behind the “Full guide → kolu.dev” link, not inline. Lean on kolu’s auto-detection (recent repos, recent agents in packages/client/src/wire.ts) so the welcome shows the user’s own data, reinforcing zero-setup.

Re-openable via the palette. The empty state only auto-shows at zero terminals, so the welcome would vanish forever once you’re working. A stable palette command — labelled “Tutorial” (alias “Welcome”) — re-summons it anytime, mirroring VS Code’s openWalkthrough. Wired as an action in packages/client/src/input/actions.ts and surfaced through packages/client/src/commands.tsx.

No dismiss state — by design. Zero terminals always shows the welcome. The way you “dismiss” it is to open a terminal; open zero again and it comes right back. There is no welcomeSeen flag, nothing to persist, nothing to go stale — the empty canvas is the trigger. The only addition is the “Tutorial” palette command, with a feature-discoverability tip pointing at it (per .claude/rules/conventions.md) so the re-open is discoverable.

The PWA card is context-aware

This is where the spine pays off. The card reads the runtime and renders one of three states — it never shows a button that can’t work:

http:// (insecure)
Pin it manually
You're on http://box:7777. Use the browser menu (Create shortcut → Open as window) or Add to Home Screen. HTTPS via Tailscale adds one-click + a badge.
How → Reach it anywhere
https + Chromium
Pin kolu to your dock
Its own window, app icon, and a live badge for finished agents.
Install
iOS Safari
Add to Home Screen
Tap Share ⬆ , then “Add to Home Screen”. (Instructions — iOS has no install button.)
Show me

Gate order (apply top-down — the first match wins):

1. already installed            → render nothing
     matchMedia('(display-mode: standalone)').matches
     || navigator.standalone === true
2. secure + beforeinstallprompt → real one-click Install button (Chromium)
3. secure, no event             → per-platform manual instructions (table below)
4. !window.isSecureContext      → manual-install instructions (browser menu /
     Add to Home Screen) + recommend HTTPS via Tailscale for one-click + badge

Per-platform instruction set (render the literal glyph as inline SVG next to numbered steps — visual beats prose):

Platform / browserWhat kolu shows
Chromium desktop / Android, event capturedReal Install button → prompt() in the click handler
Android Chrome (no event)Menu ⋮ → Install app
Android FirefoxMenu → Install (no event, but install works)
iOS — Safari / Chrome / Edge / Firefox (16.4+)Illustrated Share ⬆ → Add to Home Screen (instructions, not a button)
Safari desktop (macOS Sonoma / Safari 17+)File → Add to Dock (works on any page; no event)
Firefox desktopNative install not shipped yet (experimental Taskbar Tabs) — suggest Chrome/Edge
Any non-secure origin (http://)Manual-pin steps — Chrome Create shortcut → Open as window, iOS Add to Home Screen — plus “HTTPS via Tailscale adds one-click + a badge.” Never a dead one-click button

The kolu.dev home page

The full guide began as a separate website/src/pages/welcome.astro — a Raycast-style skill ladder, each section copy-pasteable and ending at a verifiable success state. It worked, but a second page split the story: the home hero pointed at it, the visitor had to click through. So once the hero demo (next section) could carry the explaining, the guide was folded into the home page whole and /welcome deleted ( #1213 ). The goal, stated plainly: the kolu.dev home page contains everything — you don’t need to navigate elsewhere to understand kolu.

The home page (website/src/pages/index.astro) now runs: hero → the demo clip → features → the seven guide sections → latest post, in one scroll. The sections keep their skill-ladder shape and stable anchors:

  1. Quickstart #quickstart — Nix install → running kolu; ends at “you should see kolu open with an empty canvas.”
  2. First 5 minutes #first — open a repo → launch an agent in a tile (mirrors in-app moment #3).
  3. Core concepts #concepts — canvas, tiles, dock, worktrees, command/sub-palette, auto-detection.
  4. Pin it as an app #install — the per-OS steps above, HTTP/secure-context caveat up front, Tailscale as the fix.
  5. Reach it anywhere #remote — the minimal Tailscale sequence (below), the Serve-vs-Funnel safety note.
  6. Power features #power — multi-agent at scale, keybindings, sessions.
  7. FAQ / Troubleshooting #faq — leads with “I see no Install button” → you’re on HTTP, use the ts.net URL.

Every in-app card and empty-state hint deep-links to the matching anchor on the home page — packages/client/src/WelcomeMoments.tsx points GUIDE_URL at https://kolu.dev and links #remote etc. directly. This page stays the single source of truth, kept in sync with in-app copy and the README.

The demo that films itself

The guide tells you what kolu does in prose. The thing prose can’t carry is kolu actually doing it — so the top of the home page is a short, looping clip of a real kolu, driven by the e2e harness and recorded off the screen. A clip recorded by hand goes stale silently and can’t be refreshed when the UI moves; this one is a build artifact, regenerated by just record hero-demo, so it can’t drift from the app.

One clip ships today — hero-demo, the home hero — but the subsystem is built for N. It’s one real workflow that exercises the whole surface at once: click ”+” to open claude on a cloned repo, ”+” again for codex on a second repo in a light theme (Catppuccin Latte vs T1’s Vaughn) that buries the first tile — so the dock groups two repos with live agent status — then click claude’s dock row to raise its tile, open a file, select + comment on it, copy the comment with the real button, and hand it to claude, which edits the file with the change landing live in the open source view. The comment-on-any-file → agent → result loop, end to end.

Crisp by construction. The load-bearing constraint was “no low-quality nonsense,” and a screen clip has two quality ceilings. Capture above display size: a headful Chrome at --force-device-scale-factor=2 under Xvfb, grabbed with ffmpeg -f x11grab — ffmpeg samples the framebuffer on its own fixed clock in physical pixels, so it’s structurally smooth and crisp at once (the thing no in-Chrome capture API could do). Then transcode audio-free, never GIF: an H.264 mp4 (crf 18 ≈ visually lossless for screen content) plus a VP9 webm (crf 32, served first because it’s smaller) plus an exact-frame WebP poster.

A poster-first looping <video>. The asset lives at website/public/demo/hero-demo.{mp4,webm,webp} (committed — Astro serves public/ directly, single-MB each) and embeds with the pattern every serious dev-tool site converges on:

<video autoplay muted loop playsinline preload="metadata" poster="/demo/hero-demo.webp">
  <source src="/demo/hero-demo.webm" type="video/webm" />  <!-- VP9 first: smaller -->
  <source src="/demo/hero-demo.mp4"  type="video/mp4"  />  <!-- H.264 fallback -->
</video>

muted (autoplay exemption), playsinline (or iOS forces fullscreen), webm <source> first (the browser takes the first decodable one), poster = frame 1 (instant first paint, seamless swap). Gate autoplay on prefers-reduced-motion: reduce — render the poster only, with a play control.

Reproducible, not hand-recorded. The capture rides a seeded recording module: deterministic clone names, pinned viewport / DPR / fonts, and — because the finale edits a real checkoutensureClone reverts tracked files (git checkout -- .) each run so the edit is always a fresh, visible change.

Architecture — through the electricity lens

Read against electricity.mdx, infrastructure that ① is domain-agnostic, ② hides a hard volatility, and ③ graduates to a second consumer earns its own @kolu/* package; everything else stays kolu domain. This work splits cleanly into electricity and domain on both the welcome side and the screencast side.

@kolu/surface-app — manifest + installability signals (graduated)@kolu/solid-pwa-install — install card + cross-browser volatility (publish?)web-screencast engine — AGNOSTIC capture: Xvfb + app-mode Chrome + x11grab -> transcode (publish?)kolu app — DOMAINwebsite home — index.astro (#quickstart .. #faq) + /demo/hero-demo.{mp4,webm,webp}WelcomeMoments / EmptyState — 3 moments + install card + Tutorial cmdrecordings/hero-demo.recording.ts — name, theme, viewport, drive(world)step library / testids / shortcuts reads canInstallPwa / isInstalledrenders the cardinstallability signalscapture({chrome,size}, drive)drive() usesdemo assetsdeep-links #anchors
The welcome surface across the domain line. Electricity (own packages, arrows pointing out): @kolu/surface-app owns the manifest + installability signals; @kolu/solid-pwa-install owns the install-card volatility, reading installability from surface-app; the agnostic web-screencast engine knows nothing of kolu. kolu DOMAIN (the in-app welcome + each recording) depends on all three. Outputs land on the home page.

@kolu/surface-app #1154 — the app-shell electricity. Owns the manifest (installPwaManifest, whose ...extra passthrough absorbs the enrichment above) and the headless “relationship-to-server” model (useSurfaceApp()). Installabilitywindow.isSecureContext + standalone display-mode — is a sibling environment fact, so the canInstallPwa / isInstalled signals belong in that same headless model. Graduates today (drishti is the second consumer).

@kolu/solid-pwa-install — a focused SolidJS adapter wrapping @khmyznikov/pwa-install. Behind one socket (“install this web app”) it owns the cross-browser install volatility — beforeinstallprompt capture, the per-platform instruction screens, appinstalled — reading installability from ①. @kolu/surface-app may itself depend on it and compose the card, so a consumer wires install exactly once. Passes all three tests; graduates to any surface app.

③ The web-screencast engine — agnostic capture, a graduation candidate not yet electricity. “Drive a headful browser at 2× under Xvfb, x11grab the framebuffer, transcode to web assets” names no terminal/canvas/git; it hides a hard volatility (Xvfb lifecycle, app-mode launch, the fps-vs-resolution wall, x11grab/ffmpeg flags, SIGINT finalize, the ffmpeg-full nix gotcha). It lives in its own folder with the dependency arrow pointing out (packages/tests/screencast/engine.ts, nix deps in screencast/shell.nix), flagged publish? — mint @kolu/web-screencast only when a second consumer is real. The recordings (packages/tests/screencast/recordings) are kolu domain: one file per clip declaring { name, chrome, theme, display, viewport, drive(world) }, applying kolu display knobs inside drive via kolu shortcuts. The engine exposes capture({ chrome, size }, drive) where drive(world) is a kolu-domain closure — so the engine stays agnostic while each recording carries its own declarative display properties.

④ kolu app — domain. The welcome itself is kolu’s story:

AreaFileChange
In-app welcomepackages/client/src/WelcomeMoments.tsxThe 3 moments + the @kolu/solid-pwa-install card; GUIDE_URLkolu.dev deep-links
Re-open commandpackages/client/src/input/actions.ts, packages/client/src/commands.tsx“Tutorial” action + palette entry — re-summons the welcome as an overlay
Discoverability tippackages/client/src/settings/tips.ts, packages/client/src/settings/useTips.tsTip pointing at “Tutorial”; drop the PWA tip on an insecure origin. No welcomeSeen
Manifest call-sitepackages/server/src/index.ts:254Pass description + maskable icon into installPwaManifest
Home pagewebsite/src/pages/index.astroThe folded-in skill-ladder guide + the hero demo embed
Recordingpackages/tests/screencast/recordingshero-demo.recording.ts + shared helpers.ts

No new server contract; no service worker (kolu has none by design).

Decisions

Pitfalls

Log — build journal

A running record of this work’s research, dead-ends, and decisions — raw material for a future blog post. The in-app welcome + the kolu.dev guide shipped in #1199 ; the guide later folded into the home page alongside the hero demo in #1213 . The screencast arc, newest at the bottom:

1 · Plan + prior-art sweep. workflow #1 A fan-out research workflow (terminal recorders vs browser capture · embed patterns · CI determinism), adversarially verified, concluded: PTY recorders (vhs, asciinema) can’t capture kolu’s xterm-on-canvas pixels — it’s a raster GUI, not a replayable ANSI stream — so pixel capture via the existing Cucumber + Playwright evidence harness is the path. Embed pattern: poster-first autoplay/muted/loop/playsinline, mp4 + webm, reduced-motion.

2 · The crispness constraint. “Video needs to be crisp — no low-quality nonsense.” This became the spine. The evidence path records Playwright recordVideo = VP8 @ ~1 Mbit/s, 720p — fine for PR proof, too soft for a marketing page.

3 · Spike A (VP8). Reused the harness (KOLU_EVIDENCE) on a real worktree-agent scenario → 1280×720 VP8 → ffmpeg H.264. Smooth, but the text shimmered. Verdict: “could be crispier.”

4 · The fps-vs-resolution wall. Chasing 2×, every in-Chrome capture API hit a structural limit: CDP Page.startScreencast ignores deviceScaleFactor → caps at CSS pixels; Page.captureScreenshot can do 2× but each 2560×1440 grab is compositor-bound at ~260 ms → ~3 fps slideshow; page.screenshot() serialises behind the driver → ~10 frames over a 4 s scenario; a capture loop that started before load made frame 0 a blank white flash that strobed on every loop. The lesson: real-time frame capture is either smooth-but-low-res or high-res-but-too-slow. You can’t win it from inside Chrome.

5 · Reset + second research pass. workflow #2 A second fan-out (virtual-time beginFrame · OS-level capture · purpose-built recorders · native Playwright options), adversarially verified. Verdict: move the capture boundary outside ChromeXvfb + ffmpeg -f x11grab of a headful 2× browser samples the framebuffer on its own fixed clock in physical pixels, so it’s structurally smooth and crisp at once. Also surfaced: no top dev tool ships an auto-captured hero clip — they’re “designed, not recorded” (Screen Studio etc.). We deliberately take the reproducible-over-cinematic path.

6 · x11grab, working. Implemented KOLU_X11CAP: Xvfb, headful Chrome at --force-device-scale-factor=2, ffmpeg -f x11grab at 30 fps, SIGINT to flush the moov atom. Gotcha that cost a cycle: nixpkgs ffmpeg is built --disable-xlib (no x11grab device — “Unrecognized option ‘draw_mouse’”) → switched the e2e devShell to ffmpeg-full. Result: smooth + crisp. “B looks good.”

7 · The browser-chrome realisation. x11grab records the whole window — the clip showed Chrome’s tab strip + localhost:38425 address bar. Reads as “a browser tab,” not “the app.” Fix: Chrome app-mode (--app=<url>) = the chromeless window an installed PWA uses.

8 · Scope crystallised into a subsystem. A lowy/hickey-clean Recording abstraction — data + script per clip, separate from the capture pipeline. The engine stays agnostic; recordings carry the kolu-specific display knobs.

9 · Subsystem shipped. #1213 The agnostic engine (packages/tests/screencast/engine.ts, nix deps in screencast/shell.nix), the Recording modules, a When I record "<name>" dispatcher, and a just record recipe. Captured locally, not on pu: the demo’s climax launches a real, authenticated agent, so it only renders on a machine already running it — a deliberate, documented tradeoff (quality > box-purity; reproducibility lives in the recipe + modules, which do run on pu).

10 · Refined to one hero clip. Dropped a planned pwa-install recording — an in-clip browser→app transition isn’t reachable (bare Xvfb has no window manager, so the Fullscreen API can’t drop Chrome’s chrome). Two harness fixes made the dock track the live agent: under KOLU_X11CAP, omit the KOLU_CLAUDE_*_DIR / KOLU_CODEX_DIR overrides so kolu watches the real agent dirs; and a 240s timeout on the I record step (a real agent query exceeds the default budget). The dock’s high-level state is data-bucket (“working”/“awaiting”), not the raw data-agent-state — polling the wrong one made clips run 70–150s.

11 · Polish → the comprehensive hero. Privacy: Claude Code’s banner prints name/email/plan, so the identity-neutral half runs codex (--ask-for-approval never --sandbox read-only; the --dangerously-bypass… interactive mode shows a danger-confirm the prompt dismisses → codex exits) and claude’s banner is cropped. The app-mode session auto-restores a terminal, so the recording killAlls it for an empty opening canvas, then creates one Vaughn-themed terminal on camera, trimStart skipping the load-in. The clip grew into the full comment → agent → live-edit loop across two repos and two themes — every on-camera click telegraphed with a coral SVG arrow (the earlier glowing ring read as kolu’s own UI), sub-second pauses, and a held beat after the dock flips to awaiting so the status change is unmissable. ~45s, 3200×1800 (1600×900 ×2), ~3.9 MB mp4 / ~2.9 MB webm. Reusable patterns factored into helpers.ts.


Status: implemented — the welcome revamp in #1199 ; the hero demo + the folded-in home page in #1213 ; built on @kolu/surface-app #1154 .