summaryrefslogtreecommitdiff
path: root/vrsite/main.go
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 12:36:54 -0300
committerGabriel Schneider <[email protected]>2026-08-02 17:37:58 -0300
commite16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb (patch)
treede0db0c369522b5bf92ff32a8351de2224c5c9dc /vrsite/main.go
parent151ea12251ed876aa87bb486da1bc337e25369e9 (diff)
downloadnotevi-e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb.tar.gz
notevi-e16f78b255bcf46d8c6f9a4b9e76dd7a3817edeb.zip
Diffstat (limited to 'vrsite/main.go')
-rw-r--r--vrsite/main.go128
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