// 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 a private jj // sidecar, never to the project working copy. // // 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" "0x4200.cafe/vr/sidecar" ) //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 [-scan-root DIR] [-addr HOST:PORT] [-user NAME] serve every discovered project from one dashboard; scans ~ by default vrsite -repo DIR [-addr HOST:PORT] [-user NAME] serve one repository directly, preserving the original single-project mode vrsite [-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. open the dashboard; it discovers jj and Git repositories below ~ 2. initialize colocated jj or start the private sidecar from its explicit button — no project registry or scan result is written to disk 3. give the agents vr and tell them to read with it; constrain/AGENTS.md in the vr repo is a drop-in brief that does, and constrain/ can enforce it 4. let them work, then refresh the dashboard when a new repo appears 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 private jj sidecar, under -user, at the exact change and commit being read. The project working copy is never changed, and the app keeps no state of its own. The sidecar has no bookmark and is local-only. It is not included in a fresh clone, so back up the jj repository or export the site when the trace matters. 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() { home, err := os.UserHomeDir() if err != nil { home = "." } repo := flag.String("repo", "", "serve one jj repo directly (with -out, default: current directory)") scanRoot := flag.String("scan-root", home, "live dashboard root to scan for jj and Git repositories") 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 *debounce < 0 { fatal("debounce must not be negative") } theme := loadTheme(*themePath, *themeName, "dark") light := loadTheme(*lightPath, *lightName, "light") themes := newThemeSet(theme, light) if *out == "" && *watch { fatal("-watch is for -out builds; the served app already follows the log") } // Explicit -repo and every static export retain the original one-repository // path. A bare live invocation is the new, stateless project dashboard. if *repo == "" && *out == "" { hubTitle := *title if hubTitle == "vr trace" { hubTitle = "vr projects" } hub, err := newProjectHub(*scanRoot, hubTitle, username(*who), strings.Split(*roots, ","), theme, light, themes) if err != nil { fatal("discover projects: %v", err) } serveProjects(*addr, hub) return } repoPath := *repo if repoPath == "" { repoPath = "." } repoAbs, err := filepath.Abs(repoPath) if err != nil { fatal("%v", err) } log, err := sidecar.Open(repoAbs) if err != nil { fatal("open jj sidecar: %v", err) } if *out == "" { serve(*addr, log, repoAbs, *title, username(*who), strings.Split(*roots, ","), theme, light, themes) return } build := func() { buildSite(log, repoAbs, *out, *title, theme, light, themes) } build() if !*watch { return } fmt.Printf("vrsite: watching %s (rebuild after %s without changes)\n", log, *debounce) watchLog(log, *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) ───────────────────────────────── func watchLog(log logStore, debounce time.Duration, rebuild func()) { watchLogUntil(log, debounce, time.Second, nil, rebuild) } func watchLogUntil(log logStore, debounce, poll time.Duration, done <-chan struct{}, rebuild func()) { state, err := log.Generation() if err != nil { fatal("watch %s: %v", log, 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 := log.Generation() if err != nil { if msg := err.Error(); msg != lastStatErr { fmt.Fprintf(os.Stderr, "vrsite: watch %s: %v\n", log, 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() } } }