Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ 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)
```

Expand Down Expand Up @@ -46,6 +47,7 @@ bun run build:local # binaries + the app for this machine (needs X
| `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 |

## Rules that are easy to break
Expand All @@ -64,6 +66,10 @@ bun run build:local # binaries + the app for this machine (needs X
hosts only). Adding another network call is a product decision, not a code change.
- **Untrusted content.** Markdown renders with `createElement`/`textContent` only; 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 allow `blob:http://127.0.0.1:<port>/` (`contentRules` in
`main.swift`, pinned by `test/images.test.ts`). The server's `IMAGE_MIME` and the client's
`IMAGE_EXT` (`mdimage.ts`) list the same extensions.
- **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.

Expand Down Expand Up @@ -91,7 +97,9 @@ bun run build:local # binaries + the app for this machine (needs X
- `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.ts` then `bun run site`: the landing page (`docs/index.html` is generated, do not
edit it by hand).
edit it by hand). Its feature tour has one screenshot per feature: after a UI change, retake the
affected ones with `bun 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.

## Releasing
Expand Down
39 changes: 39 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -835,3 +835,42 @@ Findings while building:
| `diff.orderFile` is read whole by diffcore; `diffFiles` passed `-O/dev/null`, but `log --follow` sets up its own diff and ignores `-O`, so a repository with `diff.orderFile=/dev/zero` held File History for the 30 s timeout. | `diff.orderFile=/dev/null` joins the fixed hardening config every git call gets; the malicious-repository test now also sets `diff.orderFile` and `diff.external` and runs `history()`. |
| File names are untrusted. Without bracketed paste (plain `sh`, many REPLs) a newline in a pasted reference ends the line, so "Insert Reference in Terminal" on a file named `x\nrm -rf ~` would have run a command. | References to paths with control or bidi characters are refused, for the terminal and the clipboard alike (`unsafePath`). |
| `@` re-scanned the file on every keystroke. | The symbol list is computed once per palette opening. |

## 19. Round 10: feature tour on the landing page, and images in the Mac app

The landing page gained "A tour of every feature": four chapters (Find your way, Read anything,
Review with git, Work with your agent) with one real screenshot per feature, in the visitor's colour
scheme. The screenshots are reproducible: `bun run shots` builds a deterministic demo repository
(`scripts/demo-repo.ts`: neutral authors and dates, a branch ahead of `main`, uncommitted edits),
serves it with an isolated config directory, `HOME` and prompt, and drives an offscreen WKWebView
(`packaging/macos/appshot.swift`) through the steps in `scripts/shots.list.ts`, cropping each shot to
what it shows. An inventory expert listed every feature from the source first; three reviewers then
checked the result independently: copy accuracy against the code, visual design and accessibility,
and security.

While taking the shots the user reported that JPEGs did not open in the app. The cause was not the
format: in the Mac app every image failed. Images are fetched with the auth header and drawn from a
`blob:` URL, and the window's content rule list blocked everything but `http://` and `ws://` on its
own port, so WebKit refused the blob load and the viewer showed "This image could not be decoded".
Browsers (the CLI, Linux) were unaffected, which is why tests and the headless shots never saw it.

| Finding | Fix |
| --- | --- |
| Mac app: every image and every Markdown image failed to load (content rules blocked `blob:`). | One more anchored allow rule, `^blob:http://127\.0\.0\.1:<port>/`. Only script on that origin can mint such URLs (a sandboxed `/api/raw` document's blobs are `blob:null/…`), and navigation to `blob:` is still cancelled by `decidePolicyFor`. `test/images.test.ts` pins the rule set to exactly http, ws and blob:http. Verified with the real rule list compiled into a WKWebView. |
| Common formats missing: `.jfif`, `.jpe`, `.apng`, TIFF and HEIC opened as a hex dump. | Added to `IMAGE_MIME` and the Markdown `IMAGE_EXT` (a test keeps the two lists equal). WebKit decodes TIFF/HEIC; elsewhere see the next row. |
| (Security review) A format the window cannot decode, or a damaged file, ended at an error notice with no way to see the bytes. | The image tab falls back to the hex dump under a notice (`client.imageview.test.ts`). |
| (Security review) `.svg` detection for the source toggle was case-sensitive. | `LOGO.SVG` gets the toggle too. |
| (Security review) A Markdown preview loads each image whole; a README pointing at a several-hundred-MB TIFF would buffer it all. | `/api/raw?max=N` refuses larger files with 413 before reading; previews pass 20 MiB and show the alt text instead. |
| (Security review) `bun scripts/demo-repo.ts <dir>` empties its directory, so a typo could erase a checkout. | The CLI refuses a directory that is not empty. |
| (Security review) The shot server inherited `GIT_*` (a hook's `GIT_DIR` would aim the terminal's git at the real repository); Ctrl-C left the server and temp dir behind; the WebView wrote to `~/Library/WebKit`. | `GIT_*` stripped from its environment, a SIGINT handler cleans up, and the WebView uses a non-persistent data store. |
| (Visual review) Card shots were captured too wide: code showed at about 8 px; the shortcut sheet showed app chrome through its edges; the context menu hid the Explorer letters it was meant to illustrate; crops cut lines in half. | Crops tightened to the content (whole lines, the sheet's first two groups, the menu under the last row); the status bar strip keeps its 1x size and scrolls sideways on phones. |
| (Visual review) Phones downloaded 1600 px images to show them at 300 px. | Every shot also has a half-size variant, served through `srcset`/`sizes`; the hero gets `fetchpriority="high"`. |
| (Visual review) In a card the image came before its heading for screen readers; separators sat inside `<kbd>`. | The `h4` comes first in the markup (CSS shows the picture on top); "F4 / ⇧F4" renders as two keys. |
| (Accuracy review) Alt texts said "1 of several", "the two changed lines", and named a letter the menu covered; File History claimed "every commit"; "seven review rounds" was stale. | Alt texts describe what each image shows; "up to 500 commits"; the round count is gone from the copy. Every shortcut and capability on the page was checked against the source. |
| Line helpers in the shot steps were off by one: the code view's `data-n` is 0-based. | `lineEl`/`clickLine` take 1-based line numbers. |
| (Final review) The tour missed features the inventory listed: several terminals and the panel's position, the hex dump, the welcome screen, updates, tabs, zoom and themes. | Four more shots (`panels`, `hexdump`, `welcome`, `updates`, the last with GitHub's answer stubbed inside the page so the shots never go online) and their copy; README gained a Layout bullet for the rest. The demo repository lives at a fixed `/tmp/codeviewer-demo/harbor`, because the welcome screen prints the folder's absolute path. |

The Linux app was not affected by the image bug: its Chromium is confined with
`--host-resolver-rules` and an unreachable `--proxy-server`, neither of which applies to `blob:`
URLs, which never reach the network stack. There, TIFF and HEIC (which Chromium cannot decode) now
open as the hex dump under a notice.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ signed build provenance; see [how to verify](https://codeviewer.21no.de/#verify)

![Codeviewer](docs/assets/screenshot-dark.jpg)

Every feature below has a screenshot in the [feature tour](https://codeviewer.21no.de/#tour).

- **Zero dependencies.** Bun + TypeScript only: the terminal emulator, syntax highlighter,
fuzzy finder, file watcher and UI are all written from scratch.
- **Standalone app.** On macOS it's a native app (AppKit + the system WebKit), installed from a
Expand Down Expand Up @@ -57,11 +59,21 @@ signed build provenance; see [how to verify](https://codeviewer.21no.de/#verify)
update pill in the status bar when a new version is out. Installing verifies the release's
Ed25519 signature and the file's SHA-256 before an atomic swap, then restarts. Turn off the
launch-time check with "Toggle Automatic Update Checks" or `CODEVIEWER_NO_UPDATE_CHECK=1`.
- **Images:** PNG, JPEG, GIF, WebP, AVIF, BMP, ICO and SVG open in an image viewer (Fit, 1:1, zoom
from 2% to 6400%, ⌘/Ctrl+wheel); the Mac app also shows TIFF and HEIC. SVGs switch to their source,
and other binary files open as a hex dump.
- **Live:** files that change on disk reload in place, the changed lines flash, the gutter marks
them, and the explorer shows dots on changed files.
- **Private:** no telemetry, crash reporting, account or cloud. The only outbound connection is the
launch-time update check against GitHub's releases API, and it can be switched off (see
[Privacy](#privacy)).
- **Layout:** pinned and preview tabs (a single click previews in italics, a double click pins),
tabs coloured by git status, breadcrumbs, several terminals as tabs, and a terminal panel at the
bottom or on the right that can be maximized ("Move Terminal Panel", "Maximize/Restore Terminal
Panel"). The title bar button cycles the System, Dark and Light themes. The palette also has
Copy Path, Copy Absolute Path, Close All Tabs and Clear Terminal. In a diff tab, ‹ › step to the
previous or next changed file and "Open file" opens the file itself; the Changes view switches
between "Uncommitted" and "vs main".
- **Keyboard and screen-reader friendly:** ⌘/ lists every shortcut in your platform's key names.
Explorer, search results, Changes and the palette expose their selection to assistive tech, the
terminal mirrors each finished line into a live region, splitters and diff gaps work from the keyboard,
Expand Down Expand Up @@ -105,6 +117,8 @@ Closing the app window quits codeviewer. Settings live in `~/.config/codeviewer/
| ⌘P / Ctrl+P | Go to file (`path:line` works) |
| ⌘⇧P / Ctrl+Shift+P | Command palette |
| ⌃\` | Toggle / focus terminal · ⌃⇧\` new terminal |
| ⌘J / Ctrl+J | Hide / show the terminal panel |
| ⌘K / Ctrl+Shift+K in the terminal | Clear the terminal |
| ⌘F · ⌃G | Find in file · go to line |
| ⌘⇧O / Ctrl+Shift+O | Go to symbol in the file (or `@` in ⌘P) |
| ⌥⌘C / Ctrl+Alt+C | Copy a `path:line` reference to the cursor or selected lines |
Expand Down Expand Up @@ -135,7 +149,8 @@ line, Find and Go to line always use the source. The preview updates live when t
- **Untrusted content:** the preview is built with `createElement`/`textContent` only. There is no raw HTML
(it is shown as text), links use an allowlist (`http`, `https`, `mailto`, `#fragment`, relative paths), and
relative links open inside the viewer.
- **Images:** relative images of a type the server serves (png, jpg, gif, webp, avif, bmp, ico, svg) load
- **Images:** relative images of a type the server serves (png, apng, jpg/jpeg/jpe/jfif, gif, webp, avif, bmp,
ico, svg, and tif/tiff/heic/heif where the window can decode them) load
through the authenticated raw endpoint; at most 100 images per render. Remote images (`http(s)`, `//host`,
`data:`) are never loaded: a placeholder shows the alt text and host, with a link to open it in your browser.
- **Limits:** files over 1 MiB open in source with a "Too large to preview · Render anyway" note; nesting,
Expand Down Expand Up @@ -199,6 +214,7 @@ bun run build:local # this machine only: dist/bin/codeviewer + its app (on a M
bun scripts/build.ts --slim # binaries only, no browser download
bun scripts/build.ts --browser <tag|latest> # re-pin the Linux browser (packaging/browser-pins.json)
bun run site # regenerate docs/index.html (the landing page; release data loads live from GitHub)
bun run shots # retake the landing page screenshots in a demo repository (macOS; then bun run site)
```

### Release signing (once)
Expand Down
4 changes: 3 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ with `createElement` and `textContent` only (never `innerHTML`), shows raw HTML
`http`, `https`, `mailto`, `#fragment` and relative link targets, prefixes heading ids with `md-`, and loads
no remote images: relative images are fetched through the authenticated raw endpoint as blob URLs (revoked on
re-render and close, image extensions only, at most 100 per render) and the Content-Security-Policy is not
widened. Report any way around these as a vulnerability.
widened. In the Mac app those blob URLs are the only addition to the window's network rules: it blocks every
URL except `http://` and `ws://` on its own `127.0.0.1` port and `blob:` URLs minted by that same origin. Report
any way around these as a vulnerability.

## Verifying a download

Expand Down
Binary file added docs/assets/features/blame-dark-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/blame-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/blame-light-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/blame-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/changes-dark-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/changes-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/changes-light-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/changes-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/commands-dark-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/commands-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/commands-light-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/commands-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/explorer-dark-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/explorer-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/explorer-light-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/explorer-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/find-dark-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/find-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/find-light-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/find-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/help-dark-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/help-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/help-light-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/help-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/hexdump-dark-half.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/features/hexdump-dark.jpg
Binary file added docs/assets/features/hexdump-light-half.jpg
Binary file added docs/assets/features/hexdump-light.jpg
Binary file added docs/assets/features/history-dark-half.jpg
Binary file added docs/assets/features/history-dark.jpg
Binary file added docs/assets/features/history-light-half.jpg
Binary file added docs/assets/features/history-light.jpg
Binary file added docs/assets/features/image-dark-half.jpg
Binary file added docs/assets/features/image-dark.jpg
Binary file added docs/assets/features/image-light-half.jpg
Binary file added docs/assets/features/image-light.jpg
Binary file added docs/assets/features/live-dark-half.jpg
Binary file added docs/assets/features/live-dark.jpg
Binary file added docs/assets/features/live-light-half.jpg
Binary file added docs/assets/features/live-light.jpg
Binary file added docs/assets/features/markdown-dark-half.jpg
Binary file added docs/assets/features/markdown-dark.jpg
Binary file added docs/assets/features/markdown-light-half.jpg
Binary file added docs/assets/features/markdown-light.jpg
Binary file added docs/assets/features/panels-dark-half.jpg
Binary file added docs/assets/features/panels-dark.jpg
Binary file added docs/assets/features/panels-light-half.jpg
Binary file added docs/assets/features/panels-light.jpg
Binary file added docs/assets/features/peek-dark-half.jpg
Binary file added docs/assets/features/peek-dark.jpg
Binary file added docs/assets/features/peek-light-half.jpg
Binary file added docs/assets/features/peek-light.jpg
Binary file added docs/assets/features/quick-open-dark-half.jpg
Binary file added docs/assets/features/quick-open-dark.jpg
Binary file added docs/assets/features/quick-open-light-half.jpg
Binary file added docs/assets/features/quick-open-light.jpg
Binary file added docs/assets/features/search-dark-half.jpg
Binary file added docs/assets/features/search-dark.jpg
Binary file added docs/assets/features/search-light-half.jpg
Binary file added docs/assets/features/search-light.jpg
Binary file added docs/assets/features/statusbar-dark-half.jpg
Binary file added docs/assets/features/statusbar-dark.jpg
Binary file added docs/assets/features/statusbar-light-half.jpg
Binary file added docs/assets/features/statusbar-light.jpg
Binary file added docs/assets/features/symbols-dark-half.jpg
Binary file added docs/assets/features/symbols-dark.jpg
Binary file added docs/assets/features/symbols-light-half.jpg
Binary file added docs/assets/features/symbols-light.jpg
Binary file added docs/assets/features/terminal-dark-half.jpg
Binary file added docs/assets/features/terminal-dark.jpg
Binary file added docs/assets/features/terminal-light-half.jpg
Binary file added docs/assets/features/terminal-light.jpg
Binary file added docs/assets/features/updates-dark-half.jpg
Binary file added docs/assets/features/updates-dark.jpg
Binary file added docs/assets/features/updates-light-half.jpg
Binary file added docs/assets/features/updates-light.jpg
Binary file added docs/assets/features/welcome-dark-half.jpg
Binary file added docs/assets/features/welcome-dark.jpg
Binary file added docs/assets/features/welcome-light-half.jpg
Binary file added docs/assets/features/welcome-light.jpg
Binary file modified docs/assets/og.png
Binary file added docs/assets/screenshot-dark-half.jpg
Binary file modified docs/assets/screenshot-dark.jpg
Binary file added docs/assets/screenshot-light-half.jpg
Binary file modified docs/assets/screenshot-light.jpg
Loading
Loading