// vrsite — the reading room for vr trace logs. // // A vr log records who read what, where, and what they thought about it. This // program projects that log onto the jj change the code was read at: the repo // tree with per-agent coverage colors, tree-sitter highlighted sources with // coverage-tinted line numbers (colors mix where agents overlap), and notes — // both the ones agents left and the ones you write in the browser. // // It runs as a live web app (default) or writes a static export (-out). // Either way the log is the only state: notes are appended to it, and the // repository is only ever read, never modified. // // Theme comes from a Zed theme JSON; per-agent colors come from the theme's // player (collaborator) palette. Every other theme this machine has — Zed's // and Helix's both — is offered in the picker, previewed as you move through // it, and remembered in the browser. package main import ( "embed" "flag" "fmt" "os" "os/user" "path/filepath" "strings" "time" ) //go:embed templates assets var tfs embed.FS func fatal(format string, a ...any) { fmt.Fprintf(os.Stderr, "vrsite: "+format+"\n", a...) os.Exit(1) } const usageText = `vrsite — the reading room for vr trace logs. vr logs every file a coding agent reads, greps and notes, one JSON line each, keyed to the jj change the code was read at. vrsite serves that log: the repo tree with per-agent read coverage, syntax-highlighted sources, the notes agents left, the conversation each note was written in, and jj queries over the repo. usage: vrsite -log FILE -repo DIR [-addr HOST:PORT] [-user NAME] serve it, at http://127.0.0.1:4200 by default vrsite -log FILE -repo DIR -out DIR [-watch] write a static copy instead — no server, so no comments, no live activity, no jj queries; the focus toggle, filters and file finder still work, and it holds the one change the log points at starting on a new project: 1. the project must be a jj repo — in an existing git one: jj git init --colocate 2. give the agents vr and one log for the repo: export VR_LOG=$PWD/vr-log.jsonl and tell them to read with it; constrain/AGENTS.md in the vr repo is a drop-in brief that does, and constrain/ can also enforce it 3. let them work, then: vrsite -log $PWD/vr-log.jsonl -repo $PWD A log belongs to one repository. Entries carry the jj change id they were recorded at, and vrsite reads the code at those changes, so a log from another repo has nothing to show here. Point -repo at the repository the agents ran in. Notes written in the browser are appended to the same log, under -user, at the change being read. Nothing else is ever written: the repository is only read, and the app keeps no state of its own. Session transcripts (the conversation behind a note) are looked up by session id under -transcripts; Claude Code and Codex are found automatically. The site is built with the two themes -theme and -light-theme name. Every other theme installed on this machine — Zed families and Helix themes both — is offered in the picker at the bottom of the tree: it previews as you move through it, and the browser remembers what you settled on. flags: ` func main() { logPath := flag.String("log", "", "vr trace log (jsonl)") repo := flag.String("repo", "", "jj repo the trace was recorded in") addr := flag.String("addr", "127.0.0.1:4200", "address to serve on") who := flag.String("user", "", "name your browser notes are logged under (default: $USER)") out := flag.String("out", "", "write a static site here instead of serving") themePath := flag.String("theme", defaultTheme("dark"), "zed theme json for the dark scheme (default: discovered, else embedded One Dark)") themeName := flag.String("theme-name", "", "theme name inside the family") lightPath := flag.String("light-theme", defaultTheme("light"), "zed theme json for the light scheme (default: discovered, else embedded One Light)") lightName := flag.String("light-theme-name", "", "theme name inside the light family") title := flag.String("title", "vr trace", "site title") roots := flag.String("transcripts", strings.Join(defaultTranscriptRoots(), ","), "comma-separated roots to look for harness session transcripts in") watch := flag.Bool("watch", false, "with -out: watch the trace log and rebuild after changes settle") debounce := flag.Duration("debounce", 10*time.Minute, "quiet period before a watched rebuild") flag.Usage = func() { fmt.Fprint(flag.CommandLine.Output(), usageText) flag.PrintDefaults() } flag.Parse() if *logPath == "" || *repo == "" { missing := "-log and -repo are required" if *logPath != "" { missing = "-repo is required: the repository the log was recorded in" } else if *repo != "" { missing = "-log is required: the jsonl file vr appends to" } fmt.Fprintf(os.Stderr, "vrsite: %s\n\n", missing) flag.Usage() os.Exit(2) } repoAbs, err := filepath.Abs(*repo) if err != nil { fatal("%v", err) } if *debounce < 0 { fatal("debounce must not be negative") } theme := loadTheme(*themePath, *themeName, "dark") light := loadTheme(*lightPath, *lightName, "light") themes := newThemeSet(theme, light) if *out == "" { if *watch { fatal("-watch is for -out builds; the served app already follows the log") } serve(*addr, *logPath, repoAbs, *title, username(*who), strings.Split(*roots, ","), theme, light, themes) return } build := func() { buildSite(*logPath, repoAbs, *out, *title, theme, light, themes) } build() if !*watch { return } fmt.Printf("vrsite: watching %s (rebuild after %s without changes)\n", *logPath, *debounce) watchLog(*logPath, *debounce, build) } func username(flagValue string) string { if flagValue != "" { return flagValue } if u := os.Getenv("USER"); u != "" { return u } if u, err := user.Current(); err == nil && u.Username != "" { return u.Username } return "web" } // ── watching the log (static export only) ───────────────────────────────── type logState struct { size int64 modTime time.Time } func statLog(path string) (logState, error) { info, err := os.Stat(path) if err != nil { return logState{}, err } return logState{size: info.Size(), modTime: info.ModTime()}, nil } func watchLog(path string, debounce time.Duration, rebuild func()) { watchLogUntil(path, debounce, time.Second, nil, rebuild) } func watchLogUntil(path string, debounce, poll time.Duration, done <-chan struct{}, rebuild func()) { state, err := statLog(path) if err != nil { fatal("watch %s: %v", path, err) } ticker := time.NewTicker(poll) defer ticker.Stop() var timer *time.Timer var timerC <-chan time.Time defer func() { if timer != nil { timer.Stop() } }() lastStatErr := "" for { select { case <-done: return case <-ticker.C: next, err := statLog(path) if err != nil { if msg := err.Error(); msg != lastStatErr { fmt.Fprintf(os.Stderr, "vrsite: watch %s: %v\n", path, err) lastStatErr = msg } continue } lastStatErr = "" if next == state { continue } state = next if timer == nil { timer = time.NewTimer(debounce) timerC = timer.C } else { if !timer.Stop() { select { case <-timer.C: default: } } timer.Reset(debounce) } fmt.Printf("vrsite: change detected; rebuild scheduled after %s without changes\n", debounce) case <-timerC: fmt.Println("vrsite: rebuilding after trace changes settled") timer = nil timerC = nil rebuild() } } }