Guidance for coding agents (and people) changing this repository. Keep it short and true; update it when the facts change.
A read-only code viewer with a real terminal, written in TypeScript on Bun with zero third-party
dependencies. The server (src/server) serves one local page and a WebSocket per terminal; the
client (src/client) is vanilla TypeScript with a canvas terminal emulator, a virtualized code view,
a hand-written Markdown parser and a syntax highlighter. packaging/macos/main.swift is the native
Mac wrapper (AppKit + WebKit). The landing page is generated into docs/ by scripts/site.ts.
bun run test # builds the client, then runs the whole suite (the CI gate)
bun test test/foo.test.ts # one file; add -t "name" for one test
bun scripts/build.ts --client-only
bun src/server/main.ts . --no-open --port 4573 # run the app against this repo; prints the URL (keep its #t= token)
bun run site # regenerate docs/index.html after editing scripts/site.ts
bun run shots [name…] # retake landing page screenshots (macOS; shots in scripts/shots.list.ts)
bun run build:local # binaries + the app for this machine (needs Xcode CLT on a Mac)- There is no typecheck step:
tsconfig.jsonhastypes: []and CI runs onlybun run test. Verification is test-based; add or extend a test for every behaviour change. - Run one full suite at a time. Two concurrent
bun testruns make the watcher and git tests time out. - Linux-only failures reproduce locally with Docker:
docker run --rm -v "$PWD":/app -w /app oven/bun:1.4.4 bun test test/pty.test.ts(match the Bun version in.github/workflows/ci.yml). - Swift changes:
xcrun swiftc -typecheck -target arm64-apple-macos13.3 -framework Cocoa -framework WebKit packaging/macos/main.swift.
| Path | What lives there |
|---|---|
src/server/http.ts |
Routes, auth (tokenOk, authed), CSP, WebSocket upgrades, the open-only terminal token |
src/server/git.ts |
Every git call; neutralize() builds the hardened environment |
src/server/pty.ts |
Terminal sessions (replay ring, backpressure, exit grace) |
src/server/search.ts + search.worker.ts |
Search in files (globs, the worker that opens files) |
src/server/update.ts |
Signed updater (host pinning, size caps, semver) |
src/client/app.ts |
App shell: layout, keyboard shortcuts, palette commands, toasts, help |
src/client/codeview.ts, mdview.ts, diffview.ts |
The three file views; each owns select-all and copy while visible |
src/client/term/ |
Terminal emulator (terminal.ts), renderer, view (input, links, a11y log) |
src/client/tree.ts, search.ts, changes.ts, palette.ts |
Sidebar views and the palette (virtualized rows, aria-activedescendant); the palette's pick() lists anything (File History) and @ lists symbols |
src/client/symbols.ts, reference.ts |
Go to Symbol patterns per language (pure); path:line references and their parsing |
src/client/styles.css |
All styling; colour tokens at the top, dark and light |
test/helpers/fakedom.ts, selfake.ts |
Minimal fake DOM for client tests (see limits below) |
scripts/shots.ts, shots.list.ts, demo-repo.ts |
Landing page screenshots: a deterministic demo repo, an isolated server, packaging/macos/appshot.swift driving an offscreen WKWebView |
DESIGN.md |
Architecture and every review round with findings and fixes |
-
No dependencies. Do not add packages, CDN scripts or vendored libraries. Fonts in
vendor/fontsare the only third-party files. -
Read-only. Nothing may write to the viewed tree or the repository. Files open with
O_NOFOLLOWafterrealpathcontainment (resolveSafe,openRegular); keep every new endpoint on that path. -
Git hardening. Overrides go through
GIT_CONFIG_KEY_n/VALUE_n, never-c key=value(a driver nameda=bsplits at the first=). Any new git call goes throughGit.git(). -
Auth. The session token lives in the URL fragment, sessionStorage and the
x-cv-tokenheader (?t=only for WebSockets). Terminals get the open-only token. Compare tokens withtokenOk. Never log a query, a path outside the root or a token. -
Privacy. The only outbound request is the update check (
src/server/update.ts, GitHub hosts only). Adding another network call is a product decision, not a code change. -
Untrusted content. Markdown renders with
createElement/textContentonly; raw HTML is text, link schemes are allow-listed, remote images never load, and the CSP does not widen. -
Images are blob: URLs. They are fetched with the auth header and drawn from
blob:URLs, so the Mac window's content rules allowblob:http://127.0.0.1:<port>/(contentRulesinmain.swift, pinned bytest/images.test.ts). The server'sIMAGE_MIMEand the client'sIMAGE_EXT(mdimage.ts) list the same extensions. -
Terminal hot paths.
Terminal.writecopies ASCII runs (printAscii) and must match per-characterprint()exactly (asciiRuns = falseturns it off;test/terminal.perf.test.tscompares both). The renderer skips rows whose cells and overlay key are unchanged: anything new that changes a row's pixels goes intorowKey()or callsinvalidate(). Hidden terminals do not paint (TerminalView.setVisible, driven byTerminalPanel.activate/setShown). -
Terminal replay. Replay holds only the last 512 KB of output, which for a full-screen TUI may be diffs with no full paint.
PtySession.requestRepaintmakes the program redraw with a real size change; a same-size resize, or two in a row, does nothing. WebKit has no canvascontextlost/contextrestoredevents: repaint onTerminalView.onRevealinstead of relying on them. -
Theme switches do not animate. Set the theme with
setDocumentTheme(theme.ts): a transition started by a theme change freezes while WebKit is not rendering, leaving old colours on the new background. In the Mac app "system" must leavewindow.appearancenil, becauseprefers-color-schemefollows the window. -
Terminal input is the user's. On macOS ⌘-shortcuts belong to the app; on Linux Ctrl+key combinations stay with the shell unless the README lists them.
-
These rules are tested.
test/invariants.static.test.tschecks the ones above against the source,test/http.routes.test.tsapplies auth/Host/CSRF/header rules to every route it finds inhttp.ts, andtest/redos.matrix.test.tsruns every highlighter and symbol language against pathological input. A new innerHTML interpolation must be escaped or added to the reviewed list there with a reason; a new language is covered automatically (LANG_IDS,SYMBOL_LANGS).
- UI scale:
--ui-scale = codeSize / BASE_SIZE, applied with CSSzoomon chrome containers. Inside a zoomed box use percentages, notvh/vw(WebKit scales viewport units too). - Paint order: an in-flow sibling's text paints over a later sibling's background. Containers
that can be squeezed (
.editor) clip their overflow. - Virtualized lists render rows as HTML strings with
data-ifirst andidafter it; tests matchclass="…" data-i=. - Accessibility: keep
aria-activedescendanton the scroller, stable row ids,role="status"for counts andaria-liveregions for notifications. Respectprefers-reduced-motion. - The fake DOM does not materialise
innerHTMLchildren, has noclientHeight, canvas,insertAdjacentHTMLor selection API (getBoundingClientRectreturns a settablerect), and does not model nativeEventdispatch; stub what a test needs on the prototype, restore the original afterwards (test files share one process), and assert through the component's state. - Visual checks on macOS: a
WKWebViewsnapshot tool works headlessly ifrequestAnimationFrameis stubbed in aWKUserScriptat document start and entrance animations are set tonone. Snapshots write to the real settings file (tabs, zoom, panel): restore them afterwards.
README.md: features, the key table, Security and Privacy sections.SECURITY.md: anything that changes the threat model.DESIGN.md: a new round section with a findings → fixes table for every review or sizeable change.scripts/site.tsthenbun run site: the landing page (docs/index.htmlis generated, do not edit it by hand). Its feature tour has one screenshot per feature: after a UI change, retake the affected ones withbun run shots <name>and read every image before committing (no real user name, host or path may appear; the demo repo and prompt are neutral).- This file.
- Merge to
main(squash). 2. Bumpversioninpackage.json, commitRelease X.Y.Zwith a one-paragraph summary (scripts/release-notes.tsturns it into the GitHub release notes, which the update dialog shows; hard-wrapping is fine). 3.git tag vX.Y.Z && git push origin main --tags. The release workflow builds, signsSHA256SUMSand publishes; the landing page reads the release from GitHub at runtime.