diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-02 12:36:54 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-02 17:37:58 -0300 |
| commit | e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb (patch) | |
| tree | de0db0c369522b5bf92ff32a8351de2224c5c9dc /vrsite/main.go | |
| parent | 151ea12251ed876aa87bb486da1bc337e25369e9 (diff) | |
| download | notevi-e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb.tar.gz notevi-e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb.zip | |
Diffstat (limited to 'vrsite/main.go')
| -rw-r--r-- | vrsite/main.go | 128 |
1 files changed, 68 insertions, 60 deletions
diff --git a/vrsite/main.go b/vrsite/main.go index 69c90a8..e43a8eb 100644 --- a/vrsite/main.go +++ b/vrsite/main.go @@ -7,8 +7,8 @@ // 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. +// 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 @@ -25,6 +25,8 @@ import ( "path/filepath" "strings" "time" + + "0x4200.cafe/vr/sidecar" ) //go:embed templates assets @@ -43,30 +45,33 @@ 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] + 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. 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 + 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 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. +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. @@ -80,8 +85,12 @@ flags: ` func main() { - logPath := flag.String("log", "", "vr trace log (jsonl)") - repo := flag.String("repo", "", "jj repo the trace was recorded in") + 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") @@ -99,42 +108,54 @@ func main() { 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 == "" && *watch { + fatal("-watch is for -out builds; the served app already follows the log") + } - if *out == "" { - if *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" } - serve(*addr, *logPath, repoAbs, *title, username(*who), strings.Split(*roots, ","), theme, light, themes) + 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(*logPath, repoAbs, *out, *title, theme, light, themes) } + 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", *logPath, *debounce) - watchLog(*logPath, *debounce, build) + fmt.Printf("vrsite: watching %s (rebuild after %s without changes)\n", log, *debounce) + watchLog(log, *debounce, build) } func username(flagValue string) string { @@ -152,27 +173,14 @@ func username(flagValue string) string { // ── 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 watchLog(log logStore, debounce time.Duration, rebuild func()) { + watchLogUntil(log, debounce, time.Second, nil, rebuild) } -func watchLogUntil(path string, debounce, poll time.Duration, done <-chan struct{}, rebuild func()) { - state, err := statLog(path) +func watchLogUntil(log logStore, debounce, poll time.Duration, done <-chan struct{}, rebuild func()) { + state, err := log.Generation() if err != nil { - fatal("watch %s: %v", path, err) + fatal("watch %s: %v", log, err) } ticker := time.NewTicker(poll) @@ -192,10 +200,10 @@ func watchLogUntil(path string, debounce, poll time.Duration, done <-chan struct return case <-ticker.C: - next, err := statLog(path) + next, err := log.Generation() if err != nil { if msg := err.Error(); msg != lastStatErr { - fmt.Fprintf(os.Stderr, "vrsite: watch %s: %v\n", path, err) + fmt.Fprintf(os.Stderr, "vrsite: watch %s: %v\n", log, err) lastStatErr = msg } continue |
