summaryrefslogtreecommitdiff
path: root/vrsite/main.go
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 23:43:08 -0300
committerGabriel Schneider <[email protected]>2026-08-03 09:54:39 -0300
commit5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch)
treef12f4b700fff4ff6f7e818d2a542672ba0b8d76e /vrsite/main.go
parenta9263daee9413c5eff3c1f1bebcd224442fd8685 (diff)
downloadnotevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.tar.gz
notevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.zip
rebrand to notevi: one CLI over the jj sidecar
vr and vrsite become a single binary. vrsite/ folds into a web package in one module (0x4200.cafe/notevi); "notevi web" serves and exports exactly what vrsite did, and "notevi read/grep/note/query" is unchanged. The sidecar is renamed with it: notevi_log, notevi-log.jsonl, and the description "private: notevi log". The pre-rebrand names are still recognized, so an old repository opens and reads; it is renamed in place on the first write, or up front with "notevi migrate DIR...". That rename cannot be a single mv inside jj run. jj only auto-tracks a *new* file in the run working copy below a size limit it does not take from the command line, so writing a whole log under a name the change has never held is silently dropped while jj reports success. ensureLogFile creates the file empty first and lets every later byte be a modification of a tracked file, which snapshots at any size; that also fixes the same latent bug when importing a large legacy vr-log.jsonl. Adds a bem-te-vi mark (favicon and nav brand) and a README. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'vrsite/main.go')
-rw-r--r--vrsite/main.go237
1 files changed, 0 insertions, 237 deletions
diff --git a/vrsite/main.go b/vrsite/main.go
deleted file mode 100644
index e43a8eb..0000000
--- a/vrsite/main.go
+++ /dev/null
@@ -1,237 +0,0 @@
-// 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()
- }
- }
-}