summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--README.md125
-rw-r--r--constrain/AGENTS.md14
-rw-r--r--constrain/README.txt57
-rw-r--r--constrain/claude-headless-settings.json4
-rw-r--r--constrain/claude-settings.json4
-rwxr-xr-xconstrain/notevi-only-guard.sh (renamed from constrain/vr-only-guard.sh)6
-rw-r--r--constrain/notevi-only.rules (renamed from constrain/vr-only.rules)8
-rw-r--r--doc.txt36
-rw-r--r--go.mod24
-rw-r--r--go.sum (renamed from vrsite/go.sum)0
-rw-r--r--main.go84
-rw-r--r--query-doc.txt18
-rw-r--r--sidecar/store.go295
-rw-r--r--sidecar/store_test.go134
-rw-r--r--vrsite/go.mod26
-rw-r--r--web/app_test.go (renamed from vrsite/app_test.go)12
-rw-r--r--web/assets/app.js (renamed from vrsite/assets/app.js)20
-rw-r--r--web/assets/bash.scm (renamed from vrsite/assets/bash.scm)0
-rw-r--r--web/assets/bemtevi.svg31
-rw-r--r--web/assets/c.scm (renamed from vrsite/assets/c.scm)0
-rw-r--r--web/assets/cpp.scm (renamed from vrsite/assets/cpp.scm)0
-rw-r--r--web/assets/go.scm (renamed from vrsite/assets/go.scm)0
-rw-r--r--web/assets/htmx.min.js (renamed from vrsite/assets/htmx.min.js)0
-rw-r--r--web/assets/javascript-params.scm (renamed from vrsite/assets/javascript-params.scm)0
-rw-r--r--web/assets/javascript.scm (renamed from vrsite/assets/javascript.scm)0
-rw-r--r--web/assets/json.scm (renamed from vrsite/assets/json.scm)0
-rw-r--r--web/assets/jsx.scm (renamed from vrsite/assets/jsx.scm)0
-rw-r--r--web/assets/one.json (renamed from vrsite/assets/one.json)2
-rw-r--r--web/assets/python.scm (renamed from vrsite/assets/python.scm)0
-rw-r--r--web/assets/rust.scm (renamed from vrsite/assets/rust.scm)0
-rw-r--r--web/assets/style.css.tmpl (renamed from vrsite/assets/style.css.tmpl)6
-rw-r--r--web/assets/typescript.scm (renamed from vrsite/assets/typescript.scm)0
-rw-r--r--web/assets/zig.scm (renamed from vrsite/assets/zig.scm)0
-rw-r--r--web/bench-browser.mjs (renamed from vrsite/bench-browser.mjs)4
-rw-r--r--web/bench_test.go (renamed from vrsite/bench_test.go)2
-rw-r--r--web/css.go (renamed from vrsite/css.go)4
-rw-r--r--web/highlight_test.go (renamed from vrsite/highlight_test.go)2
-rw-r--r--web/jj.go (renamed from vrsite/jj.go)2
-rw-r--r--web/log.go (renamed from vrsite/log.go)2
-rw-r--r--web/markdown.go (renamed from vrsite/markdown.go)2
-rw-r--r--web/markdown_test.go (renamed from vrsite/markdown_test.go)4
-rw-r--r--web/model.go (renamed from vrsite/model.go)6
-rw-r--r--web/page.go (renamed from vrsite/page.go)2
-rw-r--r--web/permalink_test.go (renamed from vrsite/permalink_test.go)2
-rw-r--r--web/projects.go (renamed from vrsite/projects.go)63
-rw-r--r--web/projects_test.go (renamed from vrsite/projects_test.go)16
-rw-r--r--web/render.go (renamed from vrsite/render.go)2
-rw-r--r--web/serve.go (renamed from vrsite/serve.go)25
-rw-r--r--web/static.go (renamed from vrsite/static.go)8
-rw-r--r--web/templates/frag-activity.tmpl (renamed from vrsite/templates/frag-activity.tmpl)0
-rw-r--r--web/templates/frag-change.tmpl (renamed from vrsite/templates/frag-change.tmpl)0
-rw-r--r--web/templates/frag-jj.tmpl (renamed from vrsite/templates/frag-jj.tmpl)0
-rw-r--r--web/templates/frag-note.tmpl (renamed from vrsite/templates/frag-note.tmpl)0
-rw-r--r--web/templates/layout.tmpl (renamed from vrsite/templates/layout.tmpl)9
-rw-r--r--web/templates/page-change.tmpl (renamed from vrsite/templates/page-change.tmpl)0
-rw-r--r--web/templates/page-file.tmpl (renamed from vrsite/templates/page-file.tmpl)0
-rw-r--r--web/templates/page-files.tmpl (renamed from vrsite/templates/page-files.tmpl)0
-rw-r--r--web/templates/page-index.tmpl (renamed from vrsite/templates/page-index.tmpl)0
-rw-r--r--web/templates/page-jj.tmpl (renamed from vrsite/templates/page-jj.tmpl)0
-rw-r--r--web/templates/page-note.tmpl (renamed from vrsite/templates/page-note.tmpl)0
-rw-r--r--web/templates/page-notes.tmpl (renamed from vrsite/templates/page-notes.tmpl)0
-rw-r--r--web/templates/page-session.tmpl (renamed from vrsite/templates/page-session.tmpl)10
-rw-r--r--web/templates/projects.tmpl (renamed from vrsite/templates/projects.tmpl)15
-rw-r--r--web/theme.go (renamed from vrsite/theme.go)2
-rw-r--r--web/themes.go (renamed from vrsite/themes.go)2
-rw-r--r--web/themes_test.go (renamed from vrsite/themes_test.go)2
-rw-r--r--web/transcript.go (renamed from vrsite/transcript.go)24
-rw-r--r--web/transcript_test.go (renamed from vrsite/transcript_test.go)22
-rw-r--r--web/tree.go (renamed from vrsite/tree.go)2
-rw-r--r--web/web.go (renamed from vrsite/main.go)90
-rw-r--r--web/web_test.go (renamed from vrsite/main_test.go)2
72 files changed, 888 insertions, 347 deletions
diff --git a/.gitignore b/.gitignore
index 2a686c7..6dada09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
-/vr
-/vrsite/vrsite
+/notevi
/site
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3ca6eff
--- /dev/null
+++ b/README.md
@@ -0,0 +1,125 @@
+<img src="web/assets/bemtevi.svg" width="96" align="right" alt="">
+
+# notevi
+
+A code reader that remembers. Read files and grep through `notevi`, pin notes to
+line ranges, and every one of those calls is appended as a single JSON line to
+an **append-only JSONL log** kept in a **private jj commit**. Not your working
+copy. Not your history.
+
+Two front ends over that one log — a CLI and a local web app. Both read it, both
+write to it. Use either. Use only one.
+
+The bem-te-vi calls all day and keeps a bright crown patch hidden under a plain
+black cap. Same idea: the notes are always there, never in the way.
+
+## Install
+
+```sh
+go build -o ~/.local/bin/notevi .
+```
+
+Needs `jj` (the repo must be a jj workspace) and `rg` for working-copy greps.
+
+## From the shell
+
+```sh
+notevi read src/lib.rs # whole file, line-numbered
+notevi read src/lib.rs:120-180 # just the range
+notevi grep 'fn resolve' src/ # regex over the working copy
+
+notevi note -f src/lib.rs:143 -t invariant "holds only under the bank lock"
+notevi note -reply 12 "no — see the early return above"
+
+notevi query -op note # replay the log, oldest first
+notevi query -file accounts_db # everything touching a path
+```
+
+Add `-r REV` to any of them to work at another revision — any jj revset, default
+`@`. Every entry records the full change **and** commit id, so a note stays
+pinned to the exact bytes it was about even after the change is rewritten.
+Notes take an optional `-t KIND` (`fn`, `struct`, `invariant`, whatever you
+want) and can reply to each other by number.
+
+## From the browser
+
+```sh
+notevi web # dashboard: every sidecar under ~
+notevi web -repo . # one repo, http://127.0.0.1:4200
+```
+
+**This is not a viewer.** Click a line, type, and the note is appended to the
+same JSONL log `notevi note` writes to — same numbering, same permalinks, at the
+exact change and commit you are looking at, attributed to your username. Reply
+to notes, thread them, filter by file, text, kind, author, model, session,
+change or date range. Never open the CLI and you lose nothing.
+
+What it shows:
+
+- the repo tree with per-reader coverage, and sources highlighted by tree-sitter
+ with coverage-tinted line numbers — the colors mix where readers overlap
+- every note pinned to its lines, plus a notes-only mode across the whole repo
+- who is reading right now, refreshed every few seconds
+- ad-hoc jj queries over the repo
+- every Zed and Helix theme installed on the machine, previewed live as you
+ arrow through the picker
+
+The dashboard scans `~` for repositories, and can start a sidecar in a jj repo
+or add colocated jj to a Git one — so a project can begin in the browser too.
+It writes no registry and no scan cache.
+
+```sh
+notevi web -repo . -out site # static export: hand it to someone
+```
+
+The export is a plain directory of HTML. Tree, coverage, highlighting, notes,
+filters and the file finder all still work with no server; writing notes, live
+activity and jj queries do not, because there is nothing running.
+
+## The log
+
+One file, `notevi-log.jsonl`, only ever appended to. Lines are never edited and
+never deleted: a note you got wrong is superseded by a later note, not rewritten,
+so the trace stays a faithful record of what was believed when. Which also means
+you can skip the tooling entirely:
+
+```sh
+jj file show -r 'exactly(notevi_log, 1)' -- notevi-log.jsonl |
+ jq -c 'select(.op == "note") | {file, start, text}'
+```
+
+That file lives in one anonymous commit whose parent is `root()`, described
+`private: notevi log`. The repo-local revset alias `notevi_log` names it and
+`git.private-commits` keeps it from being pushed. Each append is a new revision
+of that commit, so the evolog is the log's own history:
+
+```sh
+jj evolog -r 'exactly(notevi_log, 1)' --reversed -p --git
+```
+
+Appends are serialized and go through an isolated `jj run`; your `@` is never
+snapshotted or moved. There is no bookmark and no push — **a fresh clone will
+not have your notes.** Back up the jj repo, or export the site.
+
+## If a coding agent is the one running it
+
+`notevi` notices. It reads the harness environment (Claude Code, Codex, Hermes),
+falling back to walking the process tree, and adds session id, model and
+reasoning effort to every entry it writes. Nothing to configure; a human at a
+shell just gets entries without those fields.
+
+That turns the trace into a record of what an agent actually looked at before it
+told you something. The web view colors coverage per reader, so overlapping
+work is visible, and each note links to the conversation turn it came from.
+
+`constrain/` goes further and makes `notevi` the *only* way to read: a Claude
+Code `PreToolUse` hook and a codex execpolicy ruleset that deny
+`cat`/`rg`/`sed`/`jj file show`/`git show`/… and point the agent at
+`notevi -doc`. Agents converge on it with zero instructions. See
+`constrain/README.txt`.
+
+## Coming from `vr` / `vrsite`
+
+Same tool, one binary. Old sidecars (`vr_log`, `vr-log.jsonl`) are still
+recognized and renamed in place on first write; `notevi migrate DIR...` does it
+up front.
diff --git a/constrain/AGENTS.md b/constrain/AGENTS.md
index 871fc6c..93df43b 100644
--- a/constrain/AGENTS.md
+++ b/constrain/AGENTS.md
@@ -1,14 +1,14 @@
# Reading code here
-Files are read with the `vr` tool — run `vr -doc` once for full usage.
+Files are read with the `notevi` tool — run `notevi -doc` once for full usage.
-`vr` stores the trace in the repo's private local jj sidecar named `vr_log`.
-Do not set `VR_LOG`, pass `-log`, or create a working-tree `vr-log.jsonl`.
+`notevi` stores the trace in the repo's private local jj sidecar named
+`notevi_log`. Do not create a working-tree log file; there is no path to set.
-- `vr read FILE:START-END` — line-numbered read; prefer ranges over whole files
-- `vr grep PATTERN [PATH]` — regex search; scope with a path
-- `vr read -r REV FILE`, `vr grep -r REV PATTERN PATH` — at another jj change
-- `vr note -f FILE:10-42 -t struct TEXT...` — record observations as you read
+- `notevi read FILE:START-END` — line-numbered read; prefer ranges over whole files
+- `notevi grep PATTERN [PATH]` — regex search; scope with a path
+- `notevi read -r REV FILE`, `notevi grep -r REV PATTERN PATH` — at another jj change
+- `notevi note -f FILE:10-42 -t struct TEXT...` — record observations as you read
Navigate top-down: grep for the symbol, read the enclosing range, note what
you learn. Every entry records the full current jj change and commit IDs; the
diff --git a/constrain/README.txt b/constrain/README.txt
index 4da2733..6d4b882 100644
--- a/constrain/README.txt
+++ b/constrain/README.txt
@@ -1,4 +1,4 @@
-Constrain agents to read files only through vr — enforcement comes from
+Constrain agents to read files only through notevi — enforcement comes from
harness config; AGENTS.md is navigation guidance only, never the constraint.
── files here ──────────────────────────────────────────────────────────────
@@ -6,26 +6,28 @@ claude-settings.json project install: <repo>/.claude/settings.json
(hook path uses $CLAUDE_PROJECT_DIR)
claude-headless-settings.json no-install variant for `claude -p --settings`
(hook path is absolute into this dir)
-vr-only-guard.sh PreToolUse hook: blocks cat/rg/sed/... and
+notevi-only-guard.sh PreToolUse hook: blocks cat/rg/sed/... and
jj/git content reads; its error message points
- the agent at `vr -doc`, so agents converge
+ the agent at `notevi -doc`, so agents converge
even with zero instructions
-vr-only.rules codex execpolicy rules (allow vr, forbid readers)
-AGENTS.md how-to-navigate-with-vr guidance for the repo
+notevi-only.rules codex execpolicy rules (allow notevi, forbid
+ readers)
+AGENTS.md how-to-navigate-with-notevi guidance for the repo
── one-time setup ──────────────────────────────────────────────────────────
-put both on PATH: go build -o ~/.local/bin/vr . (repo root)
- cd vrsite && go build -o ~/.local/bin/vrsite .
- (vr is what agents call; vrsite is how you read the trace)
+put it on PATH: go build -o ~/.local/bin/notevi . (repo root)
+
+ one binary now: agents call `notevi read/grep/note`, and you
+ read the trace back with `notevi web`
── running a constrained CLAUDE investigation ──────────────────────────────
From the target jj repo dir:
- claude -p --settings <vr-repo>/constrain/claude-headless-settings.json \
- --allowedTools 'Bash(vr)' 'Bash(vr:*)' \
+ claude -p --settings <notevi-repo>/constrain/claude-headless-settings.json \
+ --allowedTools 'Bash(notevi)' 'Bash(notevi:*)' \
< prompt.txt > report.md
- (<vr-repo> is wherever this repo lives; the settings file's hook path is
+ (<notevi-repo> is wherever this repo lives; the settings file's hook path is
absolute into this directory, so it must be spelled out in full)
- --allowedTools on the CLI is required headless: allow rules inside
@@ -36,42 +38,45 @@ From the target jj repo dir:
eaten by the flag's list parsing.
── running a constrained CODEX investigation ───────────────────────────────
- cp vr-only.rules ~/.codex/rules/ # activate (GLOBAL: constrains
- # every codex session while there)
+ cp notevi-only.rules ~/.codex/rules/ # activate (GLOBAL: constrains
+ # every codex session while there)
codex exec -s danger-full-access "$(cat prompt.txt)" > report.md
- rm ~/.codex/rules/vr-only.rules # deactivate when done
+ rm ~/.codex/rules/notevi-only.rules # deactivate when done
- danger-full-access is required: workspace-write blocks jj metadata writes,
- including the sidecar rewrite and `vr read`'s working-copy snapshot.
+ including the sidecar rewrite and `notevi read`'s working-copy snapshot.
- enforcement is pre-exec by codex's execpolicy engine, even through
`zsh -lc` wrappers; validate rules with:
- codex execpolicy check --rules vr-only.rules -- cat foo.txt
+ codex execpolicy check --rules notevi-only.rules -- cat foo.txt
── shared trace + rendering ────────────────────────────────────────────────
- - vr creates one anonymous sidecar change directly under jj's root() on the
- first append. The repo-local alias vr_log names it; git.private-commits
- protects it from accidental pushes. No path or environment setup exists.
+ - notevi creates one anonymous sidecar change directly under jj's root() on
+ the first append. The repo-local alias notevi_log names it;
+ git.private-commits protects it from accidental pushes. No path or
+ environment setup exists.
+ - A repository last written by the older `vr` tool is renamed in place the
+ first time notevi touches it; `notevi migrate <repo>...` does it up front.
- Multiple agents in the repo share that sidecar. Rewrites are serialized,
note ids are assigned under the same lock, and the project @ is untouched.
- The sidecar has no bookmark and is local-only. It does not survive a fresh
clone; include the jj repository in backups when the trace matters.
- In the prompt, tell the agent to leave pinned notes
- (vr note -f FILE:START-END -t kind "...") — that is the payload.
- - Read traces afterwards from one process. Bare `vrsite` scans below ~ at
+ (notevi note -f FILE:START-END -t kind "...") — that is the payload.
+ - Read traces afterwards from one process. Bare `notevi web` scans below ~ at
startup and when Refresh is pressed, then offers every repo with a sidecar:
- vrsite
+ notevi web
Its explicit buttons can start an empty private sidecar in a discovered jj
repo or initialize colocated jj in a discovered Git repo. The project index
is only in memory; those two requested metadata changes are the only writes.
- To bypass the dashboard and serve one repository directly:
- vrsite -repo <repo> -title "..."
+ notevi web -repo <repo> -title "..."
or take a static copy to hand around (one change, no server features):
- vrsite -repo <repo> -out site -title "..."
- `vrsite -h` explains both, and what a log needs to be worth reading.
+ notevi web -repo <repo> -out site -title "..."
+ `notevi web -h` explains both, and what a log needs to be worth reading.
── known holes (accepted) ──────────────────────────────────────────────────
Scripting runtimes (python/node/perl) can still open files — uncomment their
-rules in vr-only.rules / extend the hook to close, at the cost of breaking
+rules in notevi-only.rules / extend the hook to close, at the cost of breaking
legitimate scripts. Neither harness constrains its own non-shell internals
beyond what the deny rules cover. Codex's rules file is global-only; there is
no per-project rules mechanism (probed, none exists as of codex 0.145).
diff --git a/constrain/claude-headless-settings.json b/constrain/claude-headless-settings.json
index 30aff9b..b22704c 100644
--- a/constrain/claude-headless-settings.json
+++ b/constrain/claude-headless-settings.json
@@ -1,7 +1,7 @@
{
"permissions": {
"deny": ["Read", "Grep", "Glob"],
- "allow": ["Bash(vr)", "Bash(vr:*)"]
+ "allow": ["Bash(notevi)", "Bash(notevi:*)"]
},
"hooks": {
"PreToolUse": [
@@ -10,7 +10,7 @@
"hooks": [
{
"type": "command",
- "command": "/home/goblin/00-projects/01-tools/vr-agent-logger/constrain/vr-only-guard.sh"
+ "command": "/home/goblin/00-projects/01-tools/notevi/constrain/notevi-only-guard.sh"
}
]
}
diff --git a/constrain/claude-settings.json b/constrain/claude-settings.json
index 524ccbd..639befb 100644
--- a/constrain/claude-settings.json
+++ b/constrain/claude-settings.json
@@ -1,7 +1,7 @@
{
"permissions": {
"deny": ["Read", "Grep", "Glob"],
- "allow": ["Bash(vr)", "Bash(vr:*)"]
+ "allow": ["Bash(notevi)", "Bash(notevi:*)"]
},
"hooks": {
"PreToolUse": [
@@ -10,7 +10,7 @@
"hooks": [
{
"type": "command",
- "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/vr-only-guard.sh"
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notevi-only-guard.sh"
}
]
}
diff --git a/constrain/vr-only-guard.sh b/constrain/notevi-only-guard.sh
index 0de698f..729f4d4 100755
--- a/constrain/vr-only-guard.sh
+++ b/constrain/notevi-only-guard.sh
@@ -1,6 +1,6 @@
#!/bin/sh
# Claude Code PreToolUse hook (matcher: Bash): deny shell commands that read
-# files without going through vr. A guardrail, not a jail — it catches the
+# files without going through notevi. A guardrail, not a jail — it catches the
# common readers at command position, not every conceivable bypass.
cmd=$(jq -r '.tool_input.command // empty')
@@ -8,12 +8,12 @@ readers='cat|head|tail|less|more|sed|awk|cut|rg|grep|egrep|fgrep|find|fd|strings
pattern='(^|[;&|(`]|\$\()[[:space:]]*('$readers')([[:space:]]|$)'
if printf '%s' "$cmd" | grep -qE "$pattern"; then
- echo "blocked: read/search files only through vr (run 'vr -doc' for usage)" >&2
+ echo "blocked: read/search files only through notevi (run 'notevi -doc' for usage)" >&2
exit 2
fi
vcs='jj[[:space:]]+(file[[:space:]]+show|diff)|git[[:space:]]+(show|diff|grep|cat-file|blame|log)'
if printf '%s' "$cmd" | grep -qE "(^|[;&|(\`])[[:space:]]*($vcs)"; then
- echo "blocked: use 'vr read -r REV FILE' / 'vr grep -r REV' instead of raw jj/git reads" >&2
+ echo "blocked: use 'notevi read -r REV FILE' / 'notevi grep -r REV' instead of raw jj/git reads" >&2
exit 2
fi
exit 0
diff --git a/constrain/vr-only.rules b/constrain/notevi-only.rules
index 54d3392..f618f18 100644
--- a/constrain/vr-only.rules
+++ b/constrain/notevi-only.rules
@@ -1,12 +1,12 @@
-# codex execpolicy: force file reading through vr (which logs every read).
-# Activate: cp vr-only.rules ~/.codex/rules/
-# Deactivate: rm ~/.codex/rules/vr-only.rules
+# codex execpolicy: force file reading through notevi (which logs every read).
+# Activate: cp notevi-only.rules ~/.codex/rules/
+# Deactivate: rm ~/.codex/rules/notevi-only.rules
# The engine parses through `/bin/zsh -lc '...'` wrappers, so these match the
# inner command. Editing tools (apply_patch) are unaffected — this only
# constrains reading. Known hole: scripting runtimes can still open files;
# uncomment the last block to close it at the cost of breaking legit scripts.
-prefix_rule(pattern=["vr"], decision="allow")
+prefix_rule(pattern=["notevi"], decision="allow")
prefix_rule(pattern=["cat"], decision="forbidden")
prefix_rule(pattern=["head"], decision="forbidden")
diff --git a/doc.txt b/doc.txt
index 87b0db4..e3fe5fc 100644
--- a/doc.txt
+++ b/doc.txt
@@ -1,28 +1,30 @@
-vr — read code and take notes; every call is logged in a private jj sidecar.
+notevi — read code and take notes; every call is logged in a private jj sidecar.
read a file (line-numbered, relative to a jj revision):
- vr read FILE whole file
- vr read FILE:120-180 line range (or FILE:120 for one line)
- vr read -r REV FILE the file as it is in another jj change
+ notevi read FILE whole file
+ notevi read FILE:120-180 line range (or FILE:120 for one line)
+ notevi read -r REV FILE the file as it is in another jj change
search (regex):
- vr grep PATTERN [PATH] search the working copy
- vr grep -r REV PATTERN PATH search another change (scope with PATH)
+ notevi grep PATTERN [PATH] search the working copy
+ notevi grep -r REV PATTERN PATH search another change (scope with PATH)
take a note while reading (ids count up from 1 per session):
- vr note -f FILE:10-42 TEXT... about a line range
- vr note -t struct -f FILE:10-42 TEXT... about a struct/enum/const/fn/...
- vr note -r REV -f FILE TEXT... about code in another change
- vr note -reply 12 TEXT... reply to log-wide note #12
+ notevi note -f FILE:10-42 TEXT... about a line range
+ notevi note -t struct -f FILE:10-42 TEXT... about a struct/enum/const/fn/...
+ notevi note -r REV -f FILE TEXT... about code in another change
+ notevi note -reply 12 TEXT... reply to log-wide note #12
-vr query -op note shows both the session-local id and the log-wide id used by
--reply and vrsite /note/N permalinks.
+notevi query -op note shows both the session-local id and the log-wide id used
+by -reply and by the /note/N permalinks in `notevi web`.
REV is any jj revset; default @ (the working copy).
-The repo-local revset alias vr_log names the log change. vr creates it lazily
-as an anonymous child of root(); it never appears in the project working copy.
-Every new entry records the full target change id and exact target commit id.
-An existing repo-root vr-log.jsonl is read and imported on the first append;
-vr leaves that legacy working-tree file for you to remove after migration.
+The repo-local revset alias notevi_log names the log change. notevi creates it
+lazily as an anonymous child of root(); it never appears in the project working
+copy. Every new entry records the full target change id and exact target commit
+id. A sidecar left by the older vr tool is renamed in place the first time
+notevi writes to it, and an existing repo-root vr-log.jsonl is read and imported
+on the first append; notevi leaves that legacy working-tree file for you to
+remove after migration.
The sidecar has no bookmark and is deliberately local-only: a fresh clone will
not contain it, so include the jj repository in backups when notes matter.
diff --git a/go.mod b/go.mod
index 4510dd8..aabed6d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,23 @@
-module 0x4200.cafe/vr
+module 0x4200.cafe/notevi
-go 1.26
+go 1.26.5
+
+require (
+ github.com/BurntSushi/toml v1.6.0
+ github.com/tree-sitter/go-tree-sitter v0.25.0
+ github.com/tree-sitter/tree-sitter-bash v0.25.1
+ github.com/tree-sitter/tree-sitter-c v0.24.2
+ github.com/tree-sitter/tree-sitter-cpp v0.23.4
+ github.com/tree-sitter/tree-sitter-go v0.25.0
+ github.com/tree-sitter/tree-sitter-javascript v0.25.0
+ github.com/tree-sitter/tree-sitter-json v0.24.8
+ github.com/tree-sitter/tree-sitter-python v0.25.0
+ github.com/tree-sitter/tree-sitter-rust v0.24.2
+ github.com/tree-sitter/tree-sitter-typescript v0.23.2
+ github.com/yuin/goldmark v1.8.5
+)
+
+require (
+ github.com/mattn/go-pointer v0.0.1 // indirect
+ github.com/tree-sitter-grammars/tree-sitter-zig v1.1.2 // indirect
+)
diff --git a/vrsite/go.sum b/go.sum
index fd75698..fd75698 100644
--- a/vrsite/go.sum
+++ b/go.sum
diff --git a/main.go b/main.go
index 1d263cb..2924d1c 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,14 @@
-// vr — file reader / note taker for coding agents, logging every call.
+// notevi — read code, take notes, read the trace back; every call is logged.
//
-// Each operation appends one JSON line to a private jj sidecar recording who read what:
+// Named for the bem-te-vi, the kiskadee that announces itself all day and keeps
+// a bright crown patch hidden under plain feathers: the notes are there the
+// whole time, tucked out of the project's way in a private jj sidecar.
+//
+// Each operation appends one JSON line to that sidecar recording who read what:
// session id, agent, model, reasoning effort, working dir, full jj change and
// commit ids, file and line range. Reads and greps are relative to a jj
// revision (default @, the working copy) — the tool assumes a jj repo.
+// "notevi web" serves the same log as a browsable site.
//
// Agent identity comes from the caller's environment (Claude Code exports
// CLAUDE_CODE_SESSION_ID, AI_AGENT, CLAUDE_EFFORT into tool subprocesses); when
@@ -29,7 +34,8 @@ import (
"strings"
"time"
- "0x4200.cafe/vr/sidecar"
+ "0x4200.cafe/notevi/sidecar"
+ "0x4200.cafe/notevi/web"
)
type Entry struct {
@@ -76,6 +82,11 @@ func main() {
fmt.Print(queryDoc)
return
}
+ // web serves the log rather than appending to it, and brings its own flags.
+ if os.Args[1] == "web" {
+ web.Main(os.Args[2:])
+ return
+ }
detect()
switch os.Args[1] {
case "read":
@@ -86,36 +97,43 @@ func main() {
cmdNote(os.Args[2:])
case "query":
cmdQuery(os.Args[2:])
+ case "migrate":
+ cmdMigrate(os.Args[2:])
default:
usage()
}
}
func usage() {
- fmt.Fprint(os.Stderr, `vr — file reader / note taker for coding agents; logs every call.
+ fmt.Fprint(os.Stderr, `notevi — read code and take notes; logs every call, hides them in a sidecar.
usage:
- vr read [-r REV] FILE[:START[-END]]
+ notevi read [-r REV] FILE[:START[-END]]
print a file line-numbered, whole or a line range, at a jj revision
- vr grep [-r REV] PATTERN [PATH]
+ notevi grep [-r REV] PATTERN [PATH]
search the working copy (rg) or another revision (jj file list/show);
scope non-@ greps with PATH, they read every file through jj
- vr note [-r REV] [-f FILE[:START[-END]]] [-t KIND] [-reply NOTE] TEXT...
+ notevi note [-r REV] [-f FILE[:START[-END]]] [-t KIND] [-reply NOTE] TEXT...
record a note about a file range or a kind of thing (struct, enum,
const, ...), optionally replying to a log-wide note number; ids count
up from 1 per session
- vr query [-session S] [-op OP] [-file SUBSTR] [-n N]
+ notevi query [-session S] [-op OP] [-file SUBSTR] [-n N]
print logged entries, oldest first
+ notevi web [-repo DIR] [-out DIR] ...
+ browse the trace: repo tree, coverage, notes; "notevi web -h" for more
+ notevi migrate [DIR...]
+ rename a pre-notevi sidecar in place; run once per old repository
REV is any jj revset, default @ (working copy). The JSONL log lives in the
-private jj sidecar named by the repo-local revset alias vr_log.
-"vr -doc" prints the short agent-facing usage doc; "vr -doc-query" the query one.
+private jj sidecar named by the repo-local revset alias notevi_log.
+"notevi -doc" prints the short agent-facing usage doc; "notevi -doc-query" the
+query one.
`)
os.Exit(2)
}
func fatal(format string, a ...any) {
- fmt.Fprintf(os.Stderr, "vr: "+format+"\n", a...)
+ fmt.Fprintf(os.Stderr, "notevi: "+format+"\n", a...)
os.Exit(1)
}
@@ -252,7 +270,7 @@ func jjOut(args ...string) string {
}
// --ignore-working-copy skips the snapshot: change ids are stable across
-// snapshots, and this keeps `vr note`/`vr grep` working in sandboxes that
+// snapshots, and this keeps `notevi note`/`notevi grep` working in sandboxes that
// block .git writes (codex workspace-write). read/grep -r snapshot anyway
// via their own jj file show/list calls.
func revisionIDs(rev string) (change, commit string) {
@@ -290,7 +308,7 @@ func cmdRead(args []string) {
rev := fs.String("r", "@", "jj revision to read at")
fs.Parse(args)
if fs.NArg() != 1 {
- fatal("usage: vr read [-r REV] FILE[:START[-END]]")
+ fatal("usage: notevi read [-r REV] FILE[:START[-END]]")
}
file, start, end := parseFileRange(fs.Arg(0))
@@ -320,7 +338,7 @@ func cmdGrep(args []string) {
rev := fs.String("r", "@", "jj revision to search at")
fs.Parse(args)
if fs.NArg() < 1 || fs.NArg() > 2 {
- fatal("usage: vr grep [-r REV] PATTERN [PATH]")
+ fatal("usage: notevi grep [-r REV] PATTERN [PATH]")
}
pattern, path := fs.Arg(0), fs.Arg(1)
@@ -372,7 +390,7 @@ func cmdNote(args []string) {
fs.Parse(args)
text := strings.Join(fs.Args(), " ")
if text == "" {
- fatal("usage: vr note [-r REV] [-f FILE[:START[-END]]] [-t KIND] [-reply NOTE] TEXT...")
+ fatal("usage: notevi note [-r REV] [-f FILE[:START[-END]]] [-t KIND] [-reply NOTE] TEXT...")
}
if *reply < 0 {
fatal("reply note must be positive")
@@ -392,7 +410,7 @@ func cmdNote(args []string) {
fmt.Printf("note %d\n", e.NoteID)
}
-// hasNote uses the same numbering as vrsite permalinks: note operations count
+// hasNote uses the same numbering as the web permalinks: note operations count
// from one over the append-only log, independent of their writers' sessions.
func hasNote(entries []Entry, id int) bool {
n := 0
@@ -486,6 +504,40 @@ func cmdQuery(args []string) {
}
}
+// cmdMigrate renames pre-notevi sidecars. It takes directories rather than only
+// working on the current one so that a machine full of old repositories can be
+// brought over in one run.
+func cmdMigrate(args []string) {
+ fs := flag.NewFlagSet("migrate", flag.ExitOnError)
+ fs.Parse(args)
+ dirs := fs.Args()
+ if len(dirs) == 0 {
+ dirs = []string{gDir}
+ }
+ failed := false
+ for _, dir := range dirs {
+ s, err := sidecar.Open(dir)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "notevi: %s: %v\n", dir, err)
+ failed = true
+ continue
+ }
+ migrated, err := s.Migrate()
+ switch {
+ case err != nil:
+ fmt.Fprintf(os.Stderr, "notevi: %s: %v\n", s.Root(), err)
+ failed = true
+ case migrated:
+ fmt.Printf("%s: renamed to %s\n", s.Root(), s)
+ default:
+ fmt.Printf("%s: nothing to rename\n", s.Root())
+ }
+ }
+ if failed {
+ os.Exit(1)
+ }
+}
+
func fileRange(e Entry) string {
s := e.File
if e.Start > 0 {
diff --git a/query-doc.txt b/query-doc.txt
index 8d4f58a..f3f4b97 100644
--- a/query-doc.txt
+++ b/query-doc.txt
@@ -1,14 +1,14 @@
-vr query — inspect this jj repo's read/note sidecar (oldest first).
+notevi query — inspect this jj repo's read/note sidecar (oldest first).
- vr query everything
- vr query -n 20 last 20 entries
- vr query -op note only notes (or: read, grep)
- vr query -file accounts_db entries touching a file path substring
- vr query -session fa7cb1bf one session (id substring is enough)
+ notevi query everything
+ notevi query -n 20 last 20 entries
+ notevi query -op note only notes (or: read, grep)
+ notevi query -file accounts_db entries touching a file path substring
+ notevi query -session fa7cb1bf one session (id substring is enough)
Filters combine. The sidecar content is JSON lines and can also be read with:
- jj file show -r 'exactly(vr_log, 1)' -- vr-log.jsonl
+ jj file show -r 'exactly(notevi_log, 1)' -- notevi-log.jsonl
Its append history is:
- jj evolog -r 'exactly(vr_log, 1)' --reversed -p --git
+ jj evolog -r 'exactly(notevi_log, 1)' --reversed -p --git
Note rows show the session-local id first and the log-wide id used by
-`vr note -reply N` and vrsite permalinks in brackets.
+`notevi note -reply N` and by `notevi web` permalinks in brackets.
diff --git a/sidecar/store.go b/sidecar/store.go
index 76e626f..ce6c56f 100644
--- a/sidecar/store.go
+++ b/sidecar/store.go
@@ -1,4 +1,4 @@
-// Package sidecar stores vr's append-only JSONL log in a private jj change.
+// Package sidecar stores notevi's append-only JSONL log in a private jj change.
//
// The change is an anonymous direct child of root(), so it is visible and
// durable in the jj repository without becoming part of project history or the
@@ -18,18 +18,34 @@ import (
)
const (
- Alias = "vr_log"
- LogFile = "vr-log.jsonl"
- Description = "private: vr log"
+ Alias = "notevi_log"
+ LogFile = "notevi-log.jsonl"
+ Description = "private: notevi log"
+ lockFile = "notevi.lock"
)
-var ErrNoLog = errors.New("vr log has not been initialized")
+// naming is the set of repo-visible names one generation of the tool gave the
+// sidecar: the revset alias, the file inside the change, and the change's own
+// description.
+type naming struct{ alias, logFile, description string }
-// Store is a repository's vr log. Open is read-only with respect to jj; the
+var current = naming{Alias, LogFile, Description}
+
+// legacy is what the tool called itself before the notevi rebrand. Every read
+// path still recognizes it so an un-migrated repository opens; Migrate renames
+// it in place so nothing in the repository keeps the old name.
+var legacy = naming{"vr_log", "vr-log.jsonl", "private: vr log"}
+
+const legacyLockFile = "vr-agent-logger.lock"
+
+var ErrNoLog = errors.New("notevi log has not been initialized")
+
+// Store is a repository's notevi log. Open is read-only with respect to jj; the
// sidecar change and its config are created lazily by Append.
type Store struct {
- root string
- lockPath string
+ root string
+ configDir string
+ lockPath string
}
// Open finds the jj workspace containing dir.
@@ -43,32 +59,40 @@ func Open(dir string) (*Store, error) {
if err != nil {
return nil, err
}
+ configDir := filepath.Dir(strings.TrimSpace(configPath))
return &Store{
- root: root,
- lockPath: filepath.Join(filepath.Dir(strings.TrimSpace(configPath)), "vr-agent-logger.lock"),
+ root: root,
+ configDir: configDir,
+ lockPath: filepath.Join(configDir, lockFile),
}, nil
}
+// Descriptions lists every change subject that has ever named a notevi sidecar,
+// current first. Callers that discover sidecars by description need the older
+// ones too, so an un-migrated repository is still recognized as having a log.
+func Descriptions() []string { return []string{current.description, legacy.description} }
+
func (s *Store) Root() string { return s.root }
func (s *Store) String() string {
return fmt.Sprintf("jj sidecar %s:%s", Alias, LogFile)
}
-// Read returns the current log. Before the first sidecar write it falls back
-// to a repo-root legacy log, which makes migration transparent.
+// Read returns the current log. It reads a sidecar that still carries the
+// pre-rebrand names without migrating it, and before the first sidecar write it
+// falls back to a repo-root legacy log, which makes migration transparent.
func (s *Store) Read() ([]byte, error) {
var data []byte
err := s.withLock(syscall.LOCK_SH, func() error {
- rev, found, _, err := s.resolveExisting()
+ rev, n, found, _, err := s.locate()
if err != nil {
return err
}
if found {
- data, err = s.readRevision(rev)
+ data, err = s.readRevision(rev, n.logFile)
return err
}
- data, err = os.ReadFile(filepath.Join(s.root, LogFile))
+ data, err = os.ReadFile(filepath.Join(s.root, legacy.logFile))
if errors.Is(err, os.ErrNotExist) {
return ErrNoLog
}
@@ -77,12 +101,12 @@ func (s *Store) Read() ([]byte, error) {
return data, err
}
-// Generation is a cheap identity for the current contents, used by vrsite to
-// notice appends. A sidecar rewrite gets a new commit id.
+// Generation is a cheap identity for the current contents, used by the web app
+// to notice appends. A sidecar rewrite gets a new commit id.
func (s *Store) Generation() (string, error) {
var generation string
err := s.withLock(syscall.LOCK_SH, func() error {
- rev, found, _, err := s.resolveExisting()
+ rev, _, found, _, err := s.locate()
if err != nil {
return err
}
@@ -90,7 +114,7 @@ func (s *Store) Generation() (string, error) {
generation, err = s.logValue(rev, "commit_id")
return err
}
- info, err := os.Stat(filepath.Join(s.root, LogFile))
+ info, err := os.Stat(filepath.Join(s.root, legacy.logFile))
if errors.Is(err, os.ErrNotExist) {
generation = "empty"
return nil
@@ -104,7 +128,7 @@ func (s *Store) Generation() (string, error) {
return generation, err
}
-// Append serializes sidecar rewrites across vr processes. makePayload runs
+// Append serializes sidecar rewrites across notevi processes. makePayload runs
// while holding that lock and receives the complete current log, allowing note
// numbering and reply validation to be atomic with the append.
func (s *Store) Append(makePayload func(current []byte) ([]byte, error)) error {
@@ -113,17 +137,17 @@ func (s *Store) Append(makePayload func(current []byte) ([]byte, error)) error {
if err != nil {
return err
}
- sidecarData, err := s.readRevision(rev)
+ sidecarData, err := s.readRevision(rev, LogFile)
if err != nil {
return err
}
current := sidecarData
if len(current) == 0 {
- legacy, legacyErr := os.ReadFile(filepath.Join(s.root, LogFile))
- if legacyErr == nil {
- current = legacy
- } else if !errors.Is(legacyErr, os.ErrNotExist) {
- return legacyErr
+ old, oldErr := os.ReadFile(filepath.Join(s.root, legacy.logFile))
+ if oldErr == nil {
+ current = old
+ } else if !errors.Is(oldErr, os.ErrNotExist) {
+ return oldErr
}
}
payload, err := makePayload(current)
@@ -131,7 +155,7 @@ func (s *Store) Append(makePayload func(current []byte) ([]byte, error)) error {
return err
}
if len(payload) == 0 {
- return errors.New("refusing to append an empty vr log payload")
+ return errors.New("refusing to append an empty notevi log payload")
}
var appendData []byte
@@ -145,10 +169,97 @@ func (s *Store) Append(makePayload func(current []byte) ([]byte, error)) error {
if appendData[len(appendData)-1] != '\n' {
appendData = append(appendData, '\n')
}
- return s.runAppend(appendData)
+ return s.runAppend(rev, appendData)
})
}
+// Migrate renames a pre-rebrand sidecar in place: the file inside the change,
+// the change description, the repo-local revset alias, and the alias mentioned
+// in git.private-commits. It reports whether anything was renamed and is a
+// no-op on a repository that is already current or has no sidecar at all.
+func (s *Store) Migrate() (migrated bool, err error) {
+ err = s.withLock(syscall.LOCK_EX, func() error {
+ rev, n, found, configured, err := s.locate()
+ if err != nil {
+ return err
+ }
+ if !found || n != legacy {
+ // Still adopt an unconfigured current sidecar, so that a repo whose
+ // jj config was lost (moving a workspace orphans it) is repaired.
+ if found && !configured {
+ return s.configure(rev)
+ }
+ return nil
+ }
+ if err := s.rename(rev); err != nil {
+ return err
+ }
+ migrated = true
+ return nil
+ })
+ return migrated, err
+}
+
+// rename performs the legacy -> current renaming for an already-located sidecar.
+// The move is two runs, not one `mv`: see ensureLogFile.
+func (s *Store) rename(rev string) error {
+ listed, err := s.jj("file", "list", "-r", rev, "--", legacy.logFile)
+ if err != nil {
+ return err
+ }
+ if strings.TrimSpace(listed) != "" {
+ if err := s.ensureLogFile(rev); err != nil {
+ return err
+ }
+ if _, err := s.jjRun(rev, "cat "+legacy.logFile+" >> "+LogFile+" && rm "+legacy.logFile); err != nil {
+ return err
+ }
+ }
+ if _, err := s.jj("describe", "-r", rev, "-m", Description); err != nil {
+ return err
+ }
+ if err := s.configure(rev); err != nil {
+ return err
+ }
+ if _, err := s.jj("config", "get", "revset-aliases."+legacy.alias); err == nil {
+ if _, err := s.jj("config", "unset", "--repo", "revset-aliases."+legacy.alias); err != nil {
+ return err
+ }
+ }
+ os.Remove(filepath.Join(s.configDir, legacyLockFile)) //nolint:errcheck
+ return nil
+}
+
+// configure points the repo-local alias at rev and refreshes the push guard.
+func (s *Store) configure(rev string) error {
+ if _, err := s.jj("config", "set", "--repo", "revset-aliases."+Alias, strconv.Quote(rev)); err != nil {
+ return err
+ }
+ return s.guard()
+}
+
+// guard keeps git.private-commits naming the current alias, rewriting a legacy
+// mention rather than leaving both names in the expression.
+func (s *Store) guard() error {
+ private, err := s.jj("config", "get", "git.private-commits")
+ if err != nil {
+ return err
+ }
+ expr := strings.TrimSpace(private)
+ next := expr
+ if containsRevsetName(next, legacy.alias) {
+ next = replaceRevsetName(next, legacy.alias, Alias)
+ }
+ if !containsRevsetName(next, Alias) {
+ next = fmt.Sprintf("(%s) | %s", next, Alias)
+ }
+ if next == expr {
+ return nil
+ }
+ _, err = s.jj("config", "set", "--repo", "git.private-commits", strconv.Quote(next))
+ return err
+}
+
func (s *Store) withLock(kind int, fn func() error) error {
if err := os.MkdirAll(filepath.Dir(s.lockPath), 0o755); err != nil {
return err
@@ -166,57 +277,64 @@ func (s *Store) withLock(kind int, fn func() error) error {
}
func (s *Store) ensure() (string, error) {
- rev, found, configured, err := s.resolveExisting()
+ rev, n, found, configured, err := s.locate()
if err != nil {
return "", err
}
+ if found && n == legacy {
+ // The first append to a pre-rebrand repository renames it, so no
+ // repository keeps writing under the old name once notevi touches it.
+ return rev, s.rename(rev)
+ }
if !found {
if _, err := s.jj("new", "--no-edit", "root()", "-m", Description); err != nil {
return "", err
}
- ids, err := s.descriptionMatches()
+ ids, err := s.descriptionMatches(Description)
if err != nil {
return "", err
}
if len(ids) != 1 {
return "", fmt.Errorf("created %q sidecar but found %d matching root children", Description, len(ids))
}
- rev = ids[0]
+ rev, configured = ids[0], false
}
if !configured {
- if _, err := s.jj("config", "set", "--repo", "revset-aliases."+Alias, strconv.Quote(rev)); err != nil {
- return "", err
- }
+ return rev, s.configure(rev)
}
- private, err := s.jj("config", "get", "git.private-commits")
- if err != nil {
- return "", err
+ return rev, s.guard()
+}
+
+// locate finds this repository's sidecar under the current names, falling back
+// to the pre-rebrand ones, and reports which generation named it.
+func (s *Store) locate() (rev string, n naming, found, configured bool, err error) {
+ rev, found, configured, err = s.resolveExisting(current)
+ if err != nil || found {
+ return rev, current, found, configured, err
}
- if !containsRevsetName(private, Alias) {
- expr := fmt.Sprintf("(%s) | %s", strings.TrimSpace(private), Alias)
- if _, err := s.jj("config", "set", "--repo", "git.private-commits", strconv.Quote(expr)); err != nil {
- return "", err
- }
+ rev, found, configured, err = s.resolveExisting(legacy)
+ if !found {
+ return rev, current, false, false, err
}
- return rev, nil
+ return rev, legacy, found, configured, err
}
-func (s *Store) resolveExisting() (rev string, found, configured bool, err error) {
- configuredValue, configErr := s.jj("config", "get", "revset-aliases."+Alias)
+func (s *Store) resolveExisting(n naming) (rev string, found, configured bool, err error) {
+ configuredValue, configErr := s.jj("config", "get", "revset-aliases."+n.alias)
if configErr == nil {
- rev, err = s.logValue("exactly("+Alias+", 1)", "change_id")
+ rev, err = s.logValue("exactly("+n.alias+", 1)", "change_id")
if err != nil {
- return "", false, true, fmt.Errorf("repo-local revset alias %s is invalid: %w", Alias, err)
+ return "", false, true, fmt.Errorf("repo-local revset alias %s is invalid: %w", n.alias, err)
}
- valid := fmt.Sprintf("exactly(%s & root()+ & subject(exact:%s), 1)", Alias, strconv.Quote(Description))
+ valid := fmt.Sprintf("exactly(%s & root()+ & subject(exact:%s), 1)", n.alias, strconv.Quote(n.description))
if _, err := s.logValue(valid, "change_id"); err != nil {
- return "", false, true, fmt.Errorf("repo-local revset alias %s=%q does not name vr's sidecar: %w",
- Alias, strings.TrimSpace(configuredValue), err)
+ return "", false, true, fmt.Errorf("repo-local revset alias %s=%q does not name notevi's sidecar: %w",
+ n.alias, strings.TrimSpace(configuredValue), err)
}
return rev, true, true, nil
}
- ids, err := s.descriptionMatches()
+ ids, err := s.descriptionMatches(n.description)
if err != nil {
return "", false, false, err
}
@@ -226,12 +344,12 @@ func (s *Store) resolveExisting() (rev string, found, configured bool, err error
case 1:
return ids[0], true, false, nil
default:
- return "", false, false, fmt.Errorf("found %d root children described %q; cannot choose a vr sidecar", len(ids), Description)
+ return "", false, false, fmt.Errorf("found %d root children described %q; cannot choose a notevi sidecar", len(ids), n.description)
}
}
-func (s *Store) descriptionMatches() ([]string, error) {
- revset := fmt.Sprintf("root()+ & subject(exact:%s)", strconv.Quote(Description))
+func (s *Store) descriptionMatches(description string) ([]string, error) {
+ revset := fmt.Sprintf("root()+ & subject(exact:%s)", strconv.Quote(description))
out, err := s.jj("log", "-G", "-r", revset, "-T", "change_id ++ \"\\n\"")
if err != nil {
return nil, err
@@ -255,20 +373,44 @@ func (s *Store) logValue(revset, template string) (string, error) {
return value, nil
}
-func (s *Store) readRevision(rev string) ([]byte, error) {
- listed, err := s.jj("file", "list", "-r", rev, "--", LogFile)
+func (s *Store) readRevision(rev, logFile string) ([]byte, error) {
+ listed, err := s.jj("file", "list", "-r", rev, "--", logFile)
if err != nil {
return nil, err
}
if strings.TrimSpace(listed) == "" {
return nil, nil
}
- out, err := s.jj("file", "show", "-r", rev, "--", LogFile)
+ out, err := s.jj("file", "show", "-r", rev, "--", logFile)
return []byte(out), err
}
-func (s *Store) runAppend(data []byte) error {
- f, err := os.CreateTemp(filepath.Dir(s.lockPath), "vr-log-append-*")
+// ensureLogFile creates the log file, empty, if the sidecar does not have it.
+//
+// This is load-bearing, not tidiness. jj only auto-tracks a *new* file in the
+// run working copy while it is under snapshot.max-new-file-size, and the run
+// working copy does not take that limit from the command line, so writing a
+// whole log under a name the change has never held is silently dropped — the
+// commit is rewritten with the file missing and jj reports success. Writing the
+// file empty first is always under the limit; from then on every byte is a
+// modification of a tracked file, which snapshots at any size.
+func (s *Store) ensureLogFile(rev string) error {
+ listed, err := s.jj("file", "list", "-r", rev, "--", LogFile)
+ if err != nil {
+ return err
+ }
+ if strings.TrimSpace(listed) != "" {
+ return nil
+ }
+ _, err = s.jjRun(rev, ": > "+LogFile)
+ return err
+}
+
+func (s *Store) runAppend(rev string, data []byte) error {
+ if err := s.ensureLogFile(rev); err != nil {
+ return err
+ }
+ f, err := os.CreateTemp(s.configDir, "notevi-log-append-*")
if err != nil {
return err
}
@@ -281,13 +423,20 @@ func (s *Store) runAppend(data []byte) error {
if err := f.Close(); err != nil {
return err
}
- _, err = s.jj("--config", "snapshot.auto-track=\"all()\"",
- "--config", "snapshot.max-new-file-size=\"1GiB\"",
- "run", "--clean", "--root", "-r", "exactly("+Alias+", 1)", "--",
- "sh", "-c", "cat \"$1\" >> "+LogFile, "sh", tmp)
+ _, err = s.jjRun(rev, "cat \"$1\" >> "+LogFile, tmp)
return err
}
+// jjRun rewrites the sidecar change by running script against a clean checkout
+// of it. rev is a change id or alias; the working copy is never involved.
+func (s *Store) jjRun(rev, script string, scriptArgs ...string) (string, error) {
+ args := []string{"--config", "snapshot.auto-track=\"all()\"",
+ "--config", "snapshot.max-new-file-size=\"1GiB\"",
+ "run", "--clean", "--root", "-r", "exactly((" + rev + "), 1)", "--",
+ "sh", "-c", script, "sh"}
+ return s.jj(append(args, scriptArgs...)...)
+}
+
func (s *Store) jj(args ...string) (string, error) {
return jjOutput(s.root, args...)
}
@@ -324,6 +473,26 @@ func containsRevsetName(expr, name string) bool {
return false
}
+// replaceRevsetName swaps whole revset names, so that rewriting an expression
+// mentioning vr_log cannot corrupt an unrelated name that merely contains it.
+func replaceRevsetName(expr, name, replacement string) string {
+ var b strings.Builder
+ for i := 0; i < len(expr); {
+ if expr[i:min(i+len(name), len(expr))] == name {
+ before := i == 0 || !isNameByte(expr[i-1])
+ after := i+len(name) == len(expr) || !isNameByte(expr[i+len(name)])
+ if before && after {
+ b.WriteString(replacement)
+ i += len(name)
+ continue
+ }
+ }
+ b.WriteByte(expr[i])
+ i++
+ }
+ return b.String()
+}
+
func isNameByte(b byte) bool {
return b == '_' || b == '-' || b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9'
}
diff --git a/sidecar/store_test.go b/sidecar/store_test.go
index 85f50e1..8ce3192 100644
--- a/sidecar/store_test.go
+++ b/sidecar/store_test.go
@@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strconv"
"strings"
"sync"
"testing"
@@ -72,19 +73,19 @@ func TestAppendCreatesPrivateUnrelatedSidecar(t *testing.T) {
func TestLegacyLogIsReadThenImportedOnFirstAppend(t *testing.T) {
repo := testRepo(t)
- legacy := []byte("legacy\n")
- if err := os.WriteFile(filepath.Join(repo, LogFile), legacy, 0o644); err != nil {
+ data := []byte("legacy\n")
+ if err := os.WriteFile(filepath.Join(repo, legacy.logFile), data, 0o644); err != nil {
t.Fatal(err)
}
store, err := Open(repo)
if err != nil {
t.Fatal(err)
}
- if got, err := store.Read(); err != nil || string(got) != string(legacy) {
+ if got, err := store.Read(); err != nil || string(got) != string(data) {
t.Fatalf("pre-migration read = %q, %v", got, err)
}
if err := store.Append(func(current []byte) ([]byte, error) {
- if string(current) != string(legacy) {
+ if string(current) != string(data) {
t.Fatalf("migration callback saw %q", current)
}
return []byte("sidecar\n"), nil
@@ -96,6 +97,131 @@ func TestLegacyLogIsReadThenImportedOnFirstAppend(t *testing.T) {
}
}
+// A repository written by the pre-rebrand tool must keep working, and must stop
+// carrying the old name once notevi has touched it.
+func TestPreRebrandSidecarIsRenamedInPlace(t *testing.T) {
+ repo := testRepo(t)
+ rev := writeLegacySidecar(t, repo, "old\n")
+
+ store, err := Open(repo)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, err := store.Read(); err != nil || string(got) != "old\n" {
+ t.Fatalf("read of an un-migrated sidecar = %q, %v", got, err)
+ }
+
+ migrated, err := store.Migrate()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !migrated {
+ t.Fatal("Migrate reported nothing to do for a legacy sidecar")
+ }
+
+ if got := strings.TrimSpace(testJJ(t, repo, "config", "get", "revset-aliases."+Alias)); got != rev {
+ t.Fatalf("alias %s = %q, want the original change %q", Alias, got, rev)
+ }
+ if out, err := exec.Command("jj", "--ignore-working-copy", "-R", repo,
+ "config", "get", "revset-aliases."+legacy.alias).CombinedOutput(); err == nil {
+ t.Fatalf("legacy alias %s survived: %s", legacy.alias, out)
+ }
+ private := testJJ(t, repo, "config", "get", "git.private-commits")
+ if !containsRevsetName(private, Alias) || containsRevsetName(private, legacy.alias) {
+ t.Fatalf("private commits = %q, want %s and not %s", private, Alias, legacy.alias)
+ }
+ files := testJJ(t, repo, "file", "list", "-r", "exactly("+Alias+", 1)")
+ if !strings.Contains(files, LogFile) || strings.Contains(files, legacy.logFile) {
+ t.Fatalf("sidecar files = %q, want only %s", files, LogFile)
+ }
+ subject := strings.TrimSpace(testJJ(t, repo, "log", "-G",
+ "-r", "exactly("+Alias+", 1)", "-T", "description"))
+ if subject != Description {
+ t.Fatalf("sidecar description = %q, want %q", subject, Description)
+ }
+
+ // The trace survived the rename and the store keeps appending to it.
+ if err := store.Append(func(current []byte) ([]byte, error) {
+ if string(current) != "old\n" {
+ t.Fatalf("append after migration saw %q", current)
+ }
+ return []byte("new\n"), nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if got, err := store.Read(); err != nil || string(got) != "old\nnew\n" {
+ t.Fatalf("migrated log = %q, %v", got, err)
+ }
+ if again, err := store.Migrate(); err != nil || again {
+ t.Fatalf("second Migrate = %v, %v; want no-op", again, err)
+ }
+}
+
+// A real trace is hundreds of KB. jj silently declines to auto-track a *new*
+// file in the run working copy past a size limit it does not take from the
+// command line, so a rename that writes the whole log under a name the change
+// has never held loses it — with jj reporting success. Small fixtures do not
+// catch that; this one does.
+func TestLargeLogSurvivesRenameAndAppend(t *testing.T) {
+ repo := testRepo(t)
+ var seed strings.Builder
+ for i := range 5000 {
+ fmt.Fprintf(&seed, `{"time":"2026-07-26T10:00:00Z","op":"read","file":"src/%d.rs","start":1,"end":400}`+"\n", i)
+ }
+ if seed.Len() < 256<<10 {
+ t.Fatalf("fixture is only %d bytes; too small to exercise the limit", seed.Len())
+ }
+ writeLegacySidecar(t, repo, seed.String())
+
+ store, err := Open(repo)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := store.Migrate(); err != nil {
+ t.Fatal(err)
+ }
+ got, err := store.Read()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(got) != seed.String() {
+ t.Fatalf("renamed log is %d bytes, want %d", len(got), seed.Len())
+ }
+ if err := store.Append(func([]byte) ([]byte, error) { return []byte(`{"op":"note"}`), nil }); err != nil {
+ t.Fatal(err)
+ }
+ if got, err = store.Read(); err != nil || len(got) <= seed.Len() {
+ t.Fatalf("append after a large rename left %d bytes, %v", len(got), err)
+ }
+}
+
+// writeLegacySidecar builds the sidecar exactly as the pre-rebrand tool did and
+// returns its change id.
+func writeLegacySidecar(t *testing.T, repo, content string) string {
+ t.Helper()
+ testJJ(t, repo, "new", "--no-edit", "root()", "-m", legacy.description)
+ rev := strings.TrimSpace(testJJ(t, repo, "log", "-G", "-T", "change_id",
+ "-r", fmt.Sprintf("root()+ & subject(exact:%q)", legacy.description)))
+ tmp := filepath.Join(t.TempDir(), "seed")
+ if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ // Touch first, then fill: the same two steps the store uses, so a large
+ // fixture really lands in the sidecar instead of quietly seeding nothing.
+ testJJ(t, repo, "--config", `snapshot.auto-track="all()"`,
+ "run", "--clean", "--root", "-r", "exactly("+rev+", 1)", "--",
+ "sh", "-c", ": > "+legacy.logFile)
+ testJJ(t, repo, "--config", `snapshot.auto-track="all()"`,
+ "run", "--clean", "--root", "-r", "exactly("+rev+", 1)", "--",
+ "sh", "-c", "cat \"$1\" >> "+legacy.logFile, "sh", tmp)
+ if got := testJJ(t, repo, "file", "show", "-r", "exactly("+rev+", 1)", "--", legacy.logFile); got != content {
+ t.Fatalf("legacy fixture is %d bytes in the sidecar, want %d", len(got), len(content))
+ }
+ testJJ(t, repo, "config", "set", "--repo", "revset-aliases."+legacy.alias, strconv.Quote(rev))
+ testJJ(t, repo, "config", "set", "--repo", "git.private-commits", strconv.Quote("(none()) | "+legacy.alias))
+ return rev
+}
+
func TestConcurrentAppendsAreSerialized(t *testing.T) {
repo := testRepo(t)
store, err := Open(repo)
diff --git a/vrsite/go.mod b/vrsite/go.mod
deleted file mode 100644
index 08e34bc..0000000
--- a/vrsite/go.mod
+++ /dev/null
@@ -1,26 +0,0 @@
-module 0x4200.cafe/vrsite
-
-go 1.26.5
-
-require (
- 0x4200.cafe/vr v0.0.0
- github.com/BurntSushi/toml v1.6.0
- github.com/tree-sitter/go-tree-sitter v0.25.0
- github.com/tree-sitter/tree-sitter-bash v0.25.1
- github.com/tree-sitter/tree-sitter-c v0.24.2
- github.com/tree-sitter/tree-sitter-cpp v0.23.4
- github.com/tree-sitter/tree-sitter-go v0.25.0
- github.com/tree-sitter/tree-sitter-javascript v0.25.0
- github.com/tree-sitter/tree-sitter-json v0.24.8
- github.com/tree-sitter/tree-sitter-python v0.25.0
- github.com/tree-sitter/tree-sitter-rust v0.24.2
- github.com/tree-sitter/tree-sitter-typescript v0.23.2
- github.com/yuin/goldmark v1.8.5
-)
-
-require (
- github.com/mattn/go-pointer v0.0.1 // indirect
- github.com/tree-sitter-grammars/tree-sitter-zig v1.1.2 // indirect
-)
-
-replace 0x4200.cafe/vr => ..
diff --git a/vrsite/app_test.go b/web/app_test.go
index 876e253..8416099 100644
--- a/vrsite/app_test.go
+++ b/web/app_test.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"crypto/sha256"
@@ -14,7 +14,7 @@ import (
"testing"
"time"
- "0x4200.cafe/vr/sidecar"
+ "0x4200.cafe/notevi/sidecar"
)
func logLines(lines ...string) string { return strings.Join(lines, "\n") + "\n" }
@@ -403,8 +403,8 @@ func TestServerPages(t *testing.T) {
{"/style.css", `data-files="hidden"`},
{"/themes.css", `:root[data-theme="zed/one-light"]`},
{"/themes.js", "VRSITE_THEMES"},
- {"/app.js", "vrsite-mode"},
- {"/app.js", "vrsite-files-hidden"},
+ {"/app.js", "notevi-mode"},
+ {"/app.js", "notevi-files-hidden"},
{"/htmx.min.js", "htmx"},
} {
w := get(t, h, tc.path, false)
@@ -896,7 +896,7 @@ func TestServerPicksUpLogAppendsFromOtherProcesses(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- line := `{"time":"2026-07-26T10:00:00Z","op":"note","session":"s9","agent":"vr","change":"` +
+ line := `{"time":"2026-07-26T10:00:00Z","op":"note","session":"s9","agent":"notevi","change":"` +
change + `","file":"hello.txt","note_id":1,"text":"from another process"}`
if _, err := f.WriteString(line + "\n"); err != nil {
t.Fatal(err)
@@ -907,6 +907,6 @@ func TestServerPicksUpLogAppendsFromOtherProcesses(t *testing.T) {
os.Chtimes(logPath, future, future)
if !strings.Contains(get(t, h, "/notes", false).Body.String(), "from another process") {
- t.Error("an append by vr was not picked up")
+ t.Error("an append by notevi was not picked up")
}
}
diff --git a/vrsite/assets/app.js b/web/assets/app.js
index 854959a..5dd6ca0 100644
--- a/vrsite/assets/app.js
+++ b/web/assets/app.js
@@ -1,4 +1,4 @@
-// app.js — the client half of vrsite. Everything here works in the static
+// app.js — the client half of notevi web. Everything here works in the static
// export too; htmx only takes over the parts that need a server (filters,
// comments, live activity, jj queries).
"use strict";
@@ -11,7 +11,7 @@ const live = root.dataset.live === "1";
// tree markers), so the whole UI switches between them instead of layering.
function setMode(m) {
root.dataset.mode = m;
- localStorage.setItem("vrsite-mode", m);
+ localStorage.setItem("notevi-mode", m);
for (const b of document.querySelectorAll(".mode"))
b.setAttribute("aria-pressed", String(b.dataset.mode === m));
}
@@ -71,7 +71,7 @@ if (pick) {
{id: "dark", name: pick.dataset.dark, src: "built with · dark"},
{id: "light", name: pick.dataset.light, src: "built with · light"},
];
- let cur = localStorage.getItem("vrsite-theme") || "system";
+ let cur = localStorage.getItem("notevi-theme") || "system";
let active = null, filled = false, pending = null;
const wear = id => {
@@ -84,7 +84,7 @@ if (pick) {
const b = builtin.find(t => t.id === id);
if (b) return b.name;
const t = (window.VRSITE_THEMES || []).find(t => t.id === id);
- return t ? t.name : localStorage.getItem("vrsite-theme-name") ||
+ return t ? t.name : localStorage.getItem("notevi-theme-name") ||
id.replace(/^[^/]*\//, "").replace(/[-_]/g, " ");
};
btn.textContent = label(cur);
@@ -200,8 +200,8 @@ if (pick) {
if (menu.hidden) return;
if (keep && active) {
cur = active.dataset.id;
- localStorage.setItem("vrsite-theme", cur);
- localStorage.setItem("vrsite-theme-name", label(cur));
+ localStorage.setItem("notevi-theme", cur);
+ localStorage.setItem("notevi-theme-name", label(cur));
btn.textContent = label(cur);
for (const li of list.querySelectorAll("li.cur")) li.classList.remove("cur");
active.classList.add("cur");
@@ -254,7 +254,7 @@ if (pick) {
// ── nav tree ──────────────────────────────────────────────────────────────
const nav = document.getElementById("nav");
const tree = document.getElementById("tree");
-const state = JSON.parse(localStorage.getItem("vrsite-tree") || "{}");
+const state = JSON.parse(localStorage.getItem("notevi-tree") || "{}");
// The file tree can get out of the way without losing repository context or
// the navigation footer. Keep the choice across page loads in this browser.
@@ -265,9 +265,9 @@ if (fileList && fileToggle) {
root.dataset.files = hidden ? "hidden" : "shown";
fileToggle.setAttribute("aria-expanded", String(!hidden));
fileToggle.textContent = hidden ? "show files" : "hide files";
- localStorage.setItem("vrsite-files-hidden", hidden ? "1" : "0");
+ localStorage.setItem("notevi-files-hidden", hidden ? "1" : "0");
};
- setFilesHidden(localStorage.getItem("vrsite-files-hidden") === "1");
+ setFilesHidden(localStorage.getItem("notevi-files-hidden") === "1");
fileToggle.addEventListener("click", () => setFilesHidden(root.dataset.files !== "hidden"));
}
@@ -289,7 +289,7 @@ if (nav && tree) {
tree.addEventListener("toggle", e => {
if (tree.classList.contains("filtering")) return; // filter drives open state
state[e.target.dataset.path] = e.target.open ? 1 : 0;
- localStorage.setItem("vrsite-tree", JSON.stringify(state));
+ localStorage.setItem("notevi-tree", JSON.stringify(state));
}, true);
}).catch(() => {
tree.innerHTML = '<p class="meta">tree needs http — use the <a href="' +
diff --git a/vrsite/assets/bash.scm b/web/assets/bash.scm
index 4c7926a..4c7926a 100644
--- a/vrsite/assets/bash.scm
+++ b/web/assets/bash.scm
diff --git a/web/assets/bemtevi.svg b/web/assets/bemtevi.svg
new file mode 100644
index 0000000..e8f7367
--- /dev/null
+++ b/web/assets/bemtevi.svg
@@ -0,0 +1,31 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="bem-te-vi">
+ <title>bem-te-vi</title>
+ <!--
+ Placeholder mark: a stylized bem-te-vi (great kiskadee). Transparent
+ background, no external references, legible down to 16px. The orange sliver
+ at the crown is the patch the bird keeps hidden under its black cap — the
+ same joke the tool's name makes about its notes.
+ -->
+ <defs>
+ <path id="nv-body" d="M18 32C18 24 27 20 35 21 43 22 49 30 50 39L62 51 60 57 46 49C39 54 23 53 18 42Z"/>
+ <clipPath id="nv-bodyclip"><use href="#nv-body"/></clipPath>
+ <clipPath id="nv-headclip"><circle cx="21" cy="24" r="12"/></clipPath>
+ </defs>
+
+ <use href="#nv-body" fill="#8A5A34"/>
+ <g clip-path="url(#nv-bodyclip)">
+ <ellipse cx="24" cy="46" rx="21" ry="16" fill="#F5C518"/>
+ <ellipse cx="41" cy="39" rx="10" ry="5.5" transform="rotate(24 41 39)" fill="#7A4E2C"/>
+ </g>
+
+ <circle cx="21" cy="24" r="12" fill="#F4F1E8"/>
+ <g clip-path="url(#nv-headclip)">
+ <rect x="7" y="10" width="30" height="8" fill="#23201C"/>
+ <ellipse cx="21" cy="13.2" rx="5" ry="1.9" fill="#F08B1D"/>
+ <rect x="7" y="18" width="30" height="4" fill="#F4F1E8"/>
+ <rect x="7" y="22" width="30" height="6" fill="#23201C"/>
+ </g>
+ <circle cx="16" cy="24.5" r="2.4" fill="#F4F1E8"/>
+ <circle cx="16" cy="24.5" r="1.3" fill="#23201C"/>
+ <path d="M12 23.5.5 28 12 32.5Z" fill="#23201C"/>
+</svg>
diff --git a/vrsite/assets/c.scm b/web/assets/c.scm
index 789bf93..789bf93 100644
--- a/vrsite/assets/c.scm
+++ b/web/assets/c.scm
diff --git a/vrsite/assets/cpp.scm b/web/assets/cpp.scm
index e939a7a..e939a7a 100644
--- a/vrsite/assets/cpp.scm
+++ b/web/assets/cpp.scm
diff --git a/vrsite/assets/go.scm b/web/assets/go.scm
index 56f676c..56f676c 100644
--- a/vrsite/assets/go.scm
+++ b/web/assets/go.scm
diff --git a/vrsite/assets/htmx.min.js b/web/assets/htmx.min.js
index c62505f..c62505f 100644
--- a/vrsite/assets/htmx.min.js
+++ b/web/assets/htmx.min.js
diff --git a/vrsite/assets/javascript-params.scm b/web/assets/javascript-params.scm
index db300e3..db300e3 100644
--- a/vrsite/assets/javascript-params.scm
+++ b/web/assets/javascript-params.scm
diff --git a/vrsite/assets/javascript.scm b/web/assets/javascript.scm
index a5c8f01..a5c8f01 100644
--- a/vrsite/assets/javascript.scm
+++ b/web/assets/javascript.scm
diff --git a/vrsite/assets/json.scm b/web/assets/json.scm
index e82e125..e82e125 100644
--- a/vrsite/assets/json.scm
+++ b/web/assets/json.scm
diff --git a/vrsite/assets/jsx.scm b/web/assets/jsx.scm
index 708eba2..708eba2 100644
--- a/vrsite/assets/jsx.scm
+++ b/web/assets/jsx.scm
diff --git a/vrsite/assets/one.json b/web/assets/one.json
index 69ff0de..cb4ca24 100644
--- a/vrsite/assets/one.json
+++ b/web/assets/one.json
@@ -1,6 +1,6 @@
{
"name": "One",
- "author": "Zed Industries (vendored subset for vrsite fallback)",
+ "author": "Zed Industries (vendored subset for notevi fallback)",
"themes": [
{
"name": "One Dark",
diff --git a/vrsite/assets/python.scm b/web/assets/python.scm
index f64e673..f64e673 100644
--- a/vrsite/assets/python.scm
+++ b/web/assets/python.scm
diff --git a/vrsite/assets/rust.scm b/web/assets/rust.scm
index d23fa2c..d23fa2c 100644
--- a/vrsite/assets/rust.scm
+++ b/web/assets/rust.scm
diff --git a/vrsite/assets/style.css.tmpl b/web/assets/style.css.tmpl
index 5ea2a40..d555950 100644
--- a/vrsite/assets/style.css.tmpl
+++ b/web/assets/style.css.tmpl
@@ -73,6 +73,8 @@ nav > header {
}
nav > header a { color: var(--fg); }
nav .brand { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+/* the bem-te-vi; inline so the brand keeps ellipsing its title */
+nav .brand .mark { width: 1.15em; height: 1.15em; vertical-align: -0.22em; margin-right: 0.4em; }
#filetoggle {
display: block; margin: 0 0 6px auto;
background: none; color: var(--muted); border: 0;
@@ -430,8 +432,8 @@ aside summary { cursor: pointer; color: var(--muted); }
.t-tool .role { color: var(--note); }
.t-result .what { color: var(--muted); }
.t-result.bad .what { color: var(--note); }
-.t-vr { border-left-color: var(--cov); background: color-mix(in srgb, var(--cov) 7%, transparent); }
-.t-vr .role { color: var(--cov); font-weight: 600; }
+.t-notevi { border-left-color: var(--cov); background: color-mix(in srgb, var(--cov) 7%, transparent); }
+.t-notevi .role { color: var(--cov); font-weight: 600; }
/* A note is what you came to the conversation to find, so it breaks the run
of log lines: the note color's rail, a tinted band, a card instead of a
line. scroll-margin clears the sticky header — the anchored one is
diff --git a/vrsite/assets/typescript.scm b/web/assets/typescript.scm
index 96cf9c3..96cf9c3 100644
--- a/vrsite/assets/typescript.scm
+++ b/web/assets/typescript.scm
diff --git a/vrsite/assets/zig.scm b/web/assets/zig.scm
index a85383a..a85383a 100644
--- a/vrsite/assets/zig.scm
+++ b/web/assets/zig.scm
diff --git a/vrsite/bench-browser.mjs b/web/bench-browser.mjs
index 97b3a86..1e373a3 100644
--- a/vrsite/bench-browser.mjs
+++ b/web/bench-browser.mjs
@@ -1,5 +1,5 @@
#!/usr/bin/env node
-// Browser render benchmark for vrsite pages: drives headless Chrome over CDP
+// Browser render benchmark for notevi web pages: drives headless Chrome over CDP
// (no npm deps; node >= 22 for native WebSocket). Serves the site dir, loads
// each page N times, reports parse/paint/load timings, node count, and the
// cost of jumping to the bottom of the file.
@@ -41,7 +41,7 @@ await new Promise(r => server.listen(0, r));
const port = server.address().port;
// ── chrome + CDP ──────────────────────────────────────────────────────────
-const profile = path.join(tmpdir(), "vrsite-bench-profile");
+const profile = path.join(tmpdir(), "notevi-bench-profile");
const chrome = spawn(CHROME, [
"--headless=new", "--remote-debugging-port=0", `--user-data-dir=${profile}`,
"--no-first-run", "--disable-extensions", "--window-size=1600,1000", "about:blank",
diff --git a/vrsite/bench_test.go b/web/bench_test.go
index babf3d4..094eb47 100644
--- a/vrsite/bench_test.go
+++ b/web/bench_test.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"bytes"
diff --git a/vrsite/css.go b/web/css.go
index f1df936..e2637bc 100644
--- a/vrsite/css.go
+++ b/web/css.go
@@ -5,7 +5,7 @@
// Everything switchable is a custom property on :root. That is what lets
// themes.css hold nothing but one block per theme (writeThemesCSS) and the
// picker repaint the page by setting a single attribute.
-package main
+package web
import (
"encoding/json"
@@ -152,6 +152,6 @@ func writeThemesJS(w io.Writer, themes []*Theme) {
if err != nil {
b = []byte("[]")
}
- fmt.Fprintf(w, "// %d themes, written by vrsite from this machine's zed and helix installs\n", len(cards))
+ fmt.Fprintf(w, "// %d themes, written by notevi from this machine's zed and helix installs\n", len(cards))
fmt.Fprintf(w, "window.VRSITE_THEMES=%s;\n", strings.ReplaceAll(string(b), "},{", "},\n{"))
}
diff --git a/vrsite/highlight_test.go b/web/highlight_test.go
index e6897d1..9cb8e36 100644
--- a/vrsite/highlight_test.go
+++ b/web/highlight_test.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"strings"
diff --git a/vrsite/jj.go b/web/jj.go
index 0b5cbdf..1dd4a4d 100644
--- a/vrsite/jj.go
+++ b/web/jj.go
@@ -3,7 +3,7 @@
// These helpers never modify project history or its working copy
// (--ignore-working-copy means they never even snapshot it). Sidecar mutation
// is isolated in the shared sidecar package.
-package main
+package web
import (
"context"
diff --git a/vrsite/log.go b/web/log.go
index a0a4f3e..b45eec5 100644
--- a/vrsite/log.go
+++ b/web/log.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"fmt"
diff --git a/vrsite/markdown.go b/web/markdown.go
index 31581ae..476e67c 100644
--- a/vrsite/markdown.go
+++ b/web/markdown.go
@@ -10,7 +10,7 @@
// - fenced code goes through the same tree-sitter highlighter as the code
// view, so a snippet in a note is painted by the reader's theme exactly
// like the file it was copied from.
-package main
+package web
import (
"bytes"
diff --git a/vrsite/markdown_test.go b/web/markdown_test.go
index 72f2cca..2078bab 100644
--- a/vrsite/markdown_test.go
+++ b/web/markdown_test.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"net/http/httptest"
@@ -126,7 +126,7 @@ func TestMarkdownShowsAngleBracketsAsText(t *testing.T) {
}
}
-// A note is written by whoever ran vr, so the renderer is the last thing
+// A note is written by whoever ran notevi, so the renderer is the last thing
// standing between the log and the page.
func TestMarkdownDropsDangerousLinks(t *testing.T) {
m := testMarkdown(t)
diff --git a/vrsite/model.go b/web/model.go
index 10f3f1d..5e90ee1 100644
--- a/vrsite/model.go
+++ b/web/model.go
@@ -1,9 +1,9 @@
// model.go — the log projected into a browsable model.
//
-// Everything the site shows is derived here from the vr log alone: agents and
+// Everything the site shows is derived here from the notevi log alone: agents and
// their colors, per-file coverage bitmasks, notes, and live session activity.
// The private jj sidecar is the only state.
-package main
+package web
import (
"encoding/json"
@@ -437,7 +437,7 @@ func (s *Site) Facets() *Facets {
return fa
}
-// nextNoteID mirrors vr's numbering: ids count up from 1 within a session.
+// nextNoteID mirrors notevi's numbering: ids count up from 1 within a session.
func nextNoteID(entries []Entry, session string) int {
max := 0
for _, e := range entries {
diff --git a/vrsite/page.go b/web/page.go
index 1570d45..0425589 100644
--- a/vrsite/page.go
+++ b/web/page.go
@@ -4,7 +4,7 @@
// ".html" suffixes, no server-side features) and as the live web app
// (absolute routes, htmx-driven filters, comments and jj queries). Pages carry
// Live so a template can ask for the difference in one place.
-package main
+package web
import (
"html/template"
diff --git a/vrsite/permalink_test.go b/web/permalink_test.go
index c3d572f..44f9142 100644
--- a/vrsite/permalink_test.go
+++ b/web/permalink_test.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"os"
diff --git a/vrsite/projects.go b/web/projects.go
index 5808a92..1783a76 100644
--- a/vrsite/projects.go
+++ b/web/projects.go
@@ -1,11 +1,11 @@
-// projects.go — the stateless, multi-repository front door for vrsite.
+// projects.go — the stateless, multi-repository front door for the notevi site.
//
// The hub is an index and dispatcher, not another database. It reconstructs
-// its project list from .jj/.git markers and vr sidecars at startup or on an
+// its project list from .jj/.git markers and notevi sidecars at startup or on an
// explicit refresh. Per-project pages are still served by server, and the only
// writes are the two actions named by their buttons: initialize jj metadata or
-// create the normal private vr sidecar/config.
-package main
+// create the normal private notevi sidecar/config.
+package web
import (
"bytes"
@@ -27,7 +27,7 @@ import (
"syscall"
"time"
- "0x4200.cafe/vr/sidecar"
+ "0x4200.cafe/notevi/sidecar"
)
type projectKind string
@@ -187,7 +187,7 @@ func discoverProjects(root string) ([]*Project, string, error) {
for _, p := range groups {
if p.Kind != projectGit {
sort.Strings(p.Workspaces)
- active, err := hasVRSidecar(p.Path)
+ active, err := hasSidecar(p.Path)
if err != nil {
p.Err = err.Error()
} else if active {
@@ -242,16 +242,23 @@ func nameProject(p *Project, root string) {
}
}
+// sidecarChanges looks for every description a notevi sidecar has ever had, so
+// that a repository the tool has not written to since the rebrand is still
+// listed as having a log rather than offered as a fresh start.
func sidecarChanges(repo string) ([]string, error) {
- revset := fmt.Sprintf("root()+ & subject(exact:%s)", strconv.Quote(sidecar.Description))
- out, err := jjRun(repo, "log", "-G", "-r", revset, "-T", "change_id ++ \"\\n\"")
- if err != nil {
- return nil, err
+ var ids []string
+ for _, description := range sidecar.Descriptions() {
+ revset := fmt.Sprintf("root()+ & subject(exact:%s)", strconv.Quote(description))
+ out, err := jjRun(repo, "log", "-G", "-r", revset, "-T", "change_id ++ \"\\n\"")
+ if err != nil {
+ return nil, err
+ }
+ ids = append(ids, strings.Fields(string(out))...)
}
- return strings.Fields(string(out)), nil
+ return ids, nil
}
-func hasVRSidecar(repo string) (bool, error) {
+func hasSidecar(repo string) (bool, error) {
ids, err := sidecarChanges(repo)
if err != nil {
return false, err
@@ -262,7 +269,7 @@ func hasVRSidecar(repo string) (bool, error) {
case 1:
return true, nil
default:
- return false, fmt.Errorf("found %d private vr sidecars", len(ids))
+ return false, fmt.Errorf("found %d private notevi sidecars", len(ids))
}
}
@@ -313,6 +320,7 @@ func (h *projectHub) handler() http.Handler {
mux.HandleFunc("GET /themes.js", h.themeAsset)
mux.HandleFunc("GET /app.js", h.asset)
mux.HandleFunc("GET /htmx.min.js", h.asset)
+ mux.HandleFunc("GET /bemtevi.svg", h.asset)
return mux
}
@@ -364,7 +372,7 @@ func (h *projectHub) startAction(w http.ResponseWriter, r *http.Request) {
return
}
h.actionMu.Lock()
- err := ensureVRSidecar(p.Path)
+ err := ensureSidecar(p.Path)
h.actionMu.Unlock()
if err != nil {
homeRedirect(w, r, "", err)
@@ -401,7 +409,7 @@ func (h *projectHub) initGitAction(w http.ResponseWriter, r *http.Request) {
homeRedirect(w, r, "", err)
return
}
- homeRedirect(w, r, "jj initialized; the repository is ready for a vr sidecar", nil)
+ homeRedirect(w, r, "jj initialized; the repository is ready for a notevi sidecar", nil)
}
func localPost(w http.ResponseWriter, r *http.Request) bool {
@@ -487,7 +495,7 @@ func (h *projectHub) runtime(id string) (*projectRuntime, error) {
return nil, err
}
title := p.Name
- if h.title != "" && h.title != "vr projects" {
+ if h.title != "" && h.title != hubDefaultTitle {
title += " — " + h.title
}
sv := newServerAt(log, p.Path, title, h.user, "/p/"+id+"/", h.theme, h.light, h.themes)
@@ -503,12 +511,21 @@ func (h *projectHub) runtime(id string) (*projectRuntime, error) {
return rt, nil
}
-func ensureVRSidecar(repo string) error {
+func ensureSidecar(repo string) error {
+ // Rename a pre-rebrand sidecar first — before taking the lock below, which
+ // Migrate takes too — so everything after this only sees the current names.
+ store, err := sidecar.Open(repo)
+ if err != nil {
+ return err
+ }
+ if _, err := store.Migrate(); err != nil {
+ return err
+ }
config, err := jjRun(repo, "config", "path", "--repo")
if err != nil {
return err
}
- lockPath := filepath.Join(filepath.Dir(strings.TrimSpace(string(config))), "vr-agent-logger.lock")
+ lockPath := filepath.Join(filepath.Dir(strings.TrimSpace(string(config))), "notevi.lock")
if err := os.MkdirAll(filepath.Dir(lockPath), 0o755); err != nil {
return err
}
@@ -532,7 +549,7 @@ func ensureVRSidecar(repo string) error {
}
valid := fmt.Sprintf("exactly(%s & root()+ & subject(exact:%s), 1)", sidecar.Alias, strconv.Quote(sidecar.Description))
if _, err := jjLogValue(repo, valid, "change_id"); err != nil {
- return fmt.Errorf("repo-local revset alias %s=%q does not name vr's sidecar: %w",
+ return fmt.Errorf("repo-local revset alias %s=%q does not name notevi's sidecar: %w",
sidecar.Alias, strings.TrimSpace(string(configuredValue)), err)
}
id = resolved
@@ -546,7 +563,7 @@ func ensureVRSidecar(repo string) error {
case 1:
id = ids[0]
default:
- return fmt.Errorf("found %d root children described %q; cannot choose a vr sidecar", len(ids), sidecar.Description)
+ return fmt.Errorf("found %d root children described %q; cannot choose a notevi sidecar", len(ids), sidecar.Description)
}
}
if id == "" {
@@ -670,7 +687,7 @@ func (h *projectHub) asset(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
return
}
- w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
+ w.Header().Set("Content-Type", assetType(name))
w.Header().Set("Cache-Control", "max-age=300")
w.Write(b)
}
@@ -689,9 +706,9 @@ func serveProjects(addr string, hub *projectHub) {
}
}
hub.mu.RUnlock()
- fmt.Printf("vrsite: http://%s — %d projects, %d jj repos ready to start, %d git repos under %s\n",
+ fmt.Printf("notevi web: http://%s — %d projects, %d jj repos ready to start, %d git repos under %s\n",
addr, active, ready, git, hub.scanRoot)
- fmt.Println("vrsite: project discovery is in memory; refresh rescans the filesystem")
+ fmt.Println("notevi web: project discovery is in memory; refresh rescans the filesystem")
if err := http.ListenAndServe(addr, hub.handler()); err != nil {
fatal("%v", err)
}
diff --git a/vrsite/projects_test.go b/web/projects_test.go
index 79fc6bc..9f911c3 100644
--- a/vrsite/projects_test.go
+++ b/web/projects_test.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"encoding/json"
@@ -11,7 +11,7 @@ import (
"strings"
"testing"
- "0x4200.cafe/vr/sidecar"
+ "0x4200.cafe/notevi/sidecar"
)
func projectTestEnv(t *testing.T, home string) {
@@ -82,7 +82,7 @@ func initGitAt(t *testing.T, dir string) {
func testProjectHub(t *testing.T, home string) *projectHub {
t.Helper()
dark, light := loadTheme("", "", "dark"), loadTheme("", "", "light")
- h, err := newProjectHub(home, "vr projects", "tester", nil, dark, light, testThemes(dark, light))
+ h, err := newProjectHub(home, hubDefaultTitle, "tester", nil, dark, light, testThemes(dark, light))
if err != nil {
t.Fatal(err)
}
@@ -214,7 +214,7 @@ func TestProjectHubStartsAnEmptyPrivateSidecar(t *testing.T) {
if opened.Code != http.StatusOK || !strings.Contains(opened.Body.String(), "ready") {
t.Fatalf("new empty project page = %d: %s", opened.Code, opened.Body.String())
}
- active, err := hasVRSidecar(repo)
+ active, err := hasSidecar(repo)
if err != nil || !active {
t.Fatalf("sidecar = %v, %v", active, err)
}
@@ -249,11 +249,11 @@ func TestEnsureVRSidecarRefusesAConflictingAlias(t *testing.T) {
initJJAt(t, repo)
runAt(t, repo, "jj", "config", "set", "--repo", "revset-aliases."+sidecar.Alias, `"root()"`)
- err := ensureVRSidecar(repo)
- if err == nil || !strings.Contains(err.Error(), "does not name vr's sidecar") {
+ err := ensureSidecar(repo)
+ if err == nil || !strings.Contains(err.Error(), "does not name notevi's sidecar") {
t.Fatalf("ensure with conflicting alias = %v", err)
}
- if active, err := hasVRSidecar(repo); err != nil || active {
+ if active, err := hasSidecar(repo); err != nil || active {
t.Fatalf("conflicting alias created a sidecar: %v, %v", active, err)
}
alias := strings.TrimSpace(runAt(t, repo, "jj", "config", "get", "revset-aliases."+sidecar.Alias))
@@ -309,7 +309,7 @@ func TestProjectHubRefusesCrossOriginMutation(t *testing.T) {
if w.Code != http.StatusForbidden {
t.Fatalf("cross-origin start = %d", w.Code)
}
- if active, err := hasVRSidecar(repo); err != nil || active {
+ if active, err := hasSidecar(repo); err != nil || active {
t.Fatalf("cross-origin request created sidecar: %v, %v", active, err)
}
}
diff --git a/vrsite/render.go b/web/render.go
index 91e07b9..0bfbdce 100644
--- a/vrsite/render.go
+++ b/web/render.go
@@ -1,6 +1,6 @@
// render.go — source to HTML: tree-sitter highlighting, coverage classes and
// note anchors baked into one <ol> per chunk.
-package main
+package web
import (
"bytes"
diff --git a/vrsite/serve.go b/web/serve.go
index 746abac..93b3f05 100644
--- a/vrsite/serve.go
+++ b/web/serve.go
@@ -1,10 +1,10 @@
-// serve.go — vrsite as a live web application.
+// serve.go — notevi web as a live web application.
//
// The server owns nothing. Three rules keep it that way, and app_test.go holds
// each of them:
//
// - The log sidecar is the only thing written. Notes typed in the browser are
-// appended to it exactly as vr appends them; project history and the main
+// appended to it exactly as notevi appends them; project history and the main
// working copy are only ever read (see jj.go).
// - Memory is a cache, never a source. The model, the per-change
// projections, file lists and rendered pages are all derived from the log
@@ -19,7 +19,7 @@
// Pages are rendered per request from the same templates the static export
// uses; htmx swaps the parts that change on their own — filtered note lists,
// the activity of agents currently reading, jj queries.
-package main
+package web
import (
"bytes"
@@ -350,6 +350,7 @@ func (sv *server) handler() http.Handler {
mux.HandleFunc("GET /themes.js", sv.themeAsset)
mux.HandleFunc("GET /app.js", sv.asset)
mux.HandleFunc("GET /htmx.min.js", sv.asset)
+ mux.HandleFunc("GET /bemtevi.svg", sv.asset)
return mux
}
@@ -744,21 +745,29 @@ func (sv *server) asset(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
return
}
- w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
+ w.Header().Set("Content-Type", assetType(name))
w.Header().Set("Cache-Control", "max-age=300")
w.Write(b)
}
+// assetType covers what embedded assets/ actually holds: scripts and the mark.
+func assetType(name string) string {
+ if strings.HasSuffix(name, ".svg") {
+ return "image/svg+xml"
+ }
+ return "text/javascript; charset=utf-8"
+}
+
func serve(addr string, log logStore, repo, title, user string, roots []string, theme, light *Theme, themes *themeSet) {
sv := newServer(log, repo, title, user, theme, light, themes)
sv.tx = newTranscripts(roots)
site, _, def := sv.snap()
- fmt.Printf("vrsite: http://%s — %d notes, %d agents, repo %s at %s\n",
+ fmt.Printf("notevi web: http://%s — %d notes, %d agents, repo %s at %s\n",
addr, len(site.Notes), len(site.Agents), repo, short(def))
- fmt.Printf("vrsite: themes — %s dark, %s light, %d more to pick from (%d zed, %d helix)\n",
+ fmt.Printf("notevi web: themes — %s dark, %s light, %d more to pick from (%d zed, %d helix)\n",
theme.Name, light.Name, len(sv.themes.List), sv.themes.count("zed"), sv.themes.count("helix"))
- fmt.Printf("vrsite: comments are appended to %s; the project working copy is never written to\n", log)
- fmt.Printf("vrsite: session transcripts read from %s\n", strings.Join(sv.tx.roots, ", "))
+ fmt.Printf("notevi web: comments are appended to %s; the project working copy is never written to\n", log)
+ fmt.Printf("notevi web: session transcripts read from %s\n", strings.Join(sv.tx.roots, ", "))
if err := http.ListenAndServe(addr, sv.handler()); err != nil {
fatal("%v", err)
}
diff --git a/vrsite/static.go b/web/static.go
index 0a8eec0..3f3e766 100644
--- a/vrsite/static.go
+++ b/web/static.go
@@ -3,7 +3,7 @@
// Everything that needs a server (comments, live activity, jj queries) is
// omitted; the trace/notes toggle, the file finder and the note filters all
// keep working, since they run in the browser.
-package main
+package web
import (
"bytes"
@@ -62,7 +62,7 @@ func buildSite(log logStore, repoAbs, out, title string, theme, light *Theme, th
site.All = site.filesAt(strings.Split(strings.TrimRight(string(list), "\n"), "\n"), change)
commit := strings.TrimSpace(string(mustJJ(repoAbs, "log", "-r", site.Change, "--no-graph", "-T", "commit_id")))
- tmp, err := os.MkdirTemp("", "vrsite-")
+ tmp, err := os.MkdirTemp("", "notevi-")
if err != nil {
fatal("%v", err)
}
@@ -88,7 +88,7 @@ func buildSite(log logStore, repoAbs, out, title string, theme, light *Theme, th
stubs++
}
}
- fmt.Printf("vrsite: %d files (%d traced, %d not rendered), %d notes, %d agents in %s -> %s\n",
+ fmt.Printf("notevi web: %d files (%d traced, %d not rendered), %d notes, %d agents in %s -> %s\n",
len(site.All), len(site.Order), stubs, len(site.Notes), len(site.Agents),
time.Since(start).Round(time.Millisecond), out)
}
@@ -137,7 +137,7 @@ func writeSite(site *Site, out, srcDir string, themes *themeSet) {
fatal("css: %v", err)
}
cf.Close()
- for _, name := range []string{"app.js"} {
+ for _, name := range []string{"app.js", "bemtevi.svg"} {
b, err := tfs.ReadFile("assets/" + name)
if err != nil {
fatal("%v", err)
diff --git a/vrsite/templates/frag-activity.tmpl b/web/templates/frag-activity.tmpl
index 071c556..071c556 100644
--- a/vrsite/templates/frag-activity.tmpl
+++ b/web/templates/frag-activity.tmpl
diff --git a/vrsite/templates/frag-change.tmpl b/web/templates/frag-change.tmpl
index b33d746..b33d746 100644
--- a/vrsite/templates/frag-change.tmpl
+++ b/web/templates/frag-change.tmpl
diff --git a/vrsite/templates/frag-jj.tmpl b/web/templates/frag-jj.tmpl
index 61040dc..61040dc 100644
--- a/vrsite/templates/frag-jj.tmpl
+++ b/web/templates/frag-jj.tmpl
diff --git a/vrsite/templates/frag-note.tmpl b/web/templates/frag-note.tmpl
index 3b04e11..3b04e11 100644
--- a/vrsite/templates/frag-note.tmpl
+++ b/web/templates/frag-note.tmpl
diff --git a/vrsite/templates/layout.tmpl b/web/templates/layout.tmpl
index a11ef16..969cae6 100644
--- a/vrsite/templates/layout.tmpl
+++ b/web/templates/layout.tmpl
@@ -4,20 +4,21 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
+<link rel="icon" type="image/svg+xml" href="{{.Href "asset" "bemtevi.svg"}}">
<link rel="stylesheet" href="{{.Href "asset" "style.css"}}">
<script>
// the theme is worn before the first paint. A theme that is not one of the
// two the site was built with lives in themes.css, so pull that in too —
// from here, after style.css, so its :root block is the one that wins.
-{const d = document.documentElement, t = localStorage.getItem("vrsite-theme");
+{const d = document.documentElement, t = localStorage.getItem("notevi-theme");
if (t && t !== "system") d.dataset.theme = t;
if (t && !/^(system|dark|light)$/.test(t)) {
const l = document.createElement("link");
l.rel = "stylesheet"; l.id = "themescss"; l.href = "{{.Href "asset" "themes.css"}}";
document.head.append(l);
}
- d.dataset.mode = localStorage.getItem("vrsite-mode") || "trace";
- d.dataset.files = localStorage.getItem("vrsite-files-hidden") === "1" ? "hidden" : "shown";}
+ d.dataset.mode = localStorage.getItem("notevi-mode") || "trace";
+ d.dataset.files = localStorage.getItem("notevi-files-hidden") === "1" ? "hidden" : "shown";}
</script>
{{if .Live}}<script src="{{.Href "asset" "htmx.min.js"}}"></script>{{end}}
<script src="{{.Href "asset" "app.js"}}" defer></script>
@@ -26,7 +27,7 @@
<nav aria-label="repository files" id="nav" data-root="{{.Root}}" data-current="{{.Current}}" data-tree="{{.Href "tree"}}">
<header>
- <a class="brand" href="{{.Href ""}}">{{.Site.Title}}</a>
+ <a class="brand" href="{{.Href ""}}"><img class="mark" src="{{.Href "asset" "bemtevi.svg"}}" alt="" width="20" height="20">{{.Site.Title}}</a>
<div class="modes" role="group" aria-label="focus">
<button type="button" class="mode" data-mode="trace" title="reading coverage (t)">trace</button>
<button type="button" class="mode" data-mode="notes" title="notes and comments (t)">notes</button>
diff --git a/vrsite/templates/page-change.tmpl b/web/templates/page-change.tmpl
index 8641da7..8641da7 100644
--- a/vrsite/templates/page-change.tmpl
+++ b/web/templates/page-change.tmpl
diff --git a/vrsite/templates/page-file.tmpl b/web/templates/page-file.tmpl
index 39a8c44..39a8c44 100644
--- a/vrsite/templates/page-file.tmpl
+++ b/web/templates/page-file.tmpl
diff --git a/vrsite/templates/page-files.tmpl b/web/templates/page-files.tmpl
index 3f158fe..3f158fe 100644
--- a/vrsite/templates/page-files.tmpl
+++ b/web/templates/page-files.tmpl
diff --git a/vrsite/templates/page-index.tmpl b/web/templates/page-index.tmpl
index 93cad6b..93cad6b 100644
--- a/vrsite/templates/page-index.tmpl
+++ b/web/templates/page-index.tmpl
diff --git a/vrsite/templates/page-jj.tmpl b/web/templates/page-jj.tmpl
index 24df596..24df596 100644
--- a/vrsite/templates/page-jj.tmpl
+++ b/web/templates/page-jj.tmpl
diff --git a/vrsite/templates/page-note.tmpl b/web/templates/page-note.tmpl
index 8cd0e64..8cd0e64 100644
--- a/vrsite/templates/page-note.tmpl
+++ b/web/templates/page-note.tmpl
diff --git a/vrsite/templates/page-notes.tmpl b/web/templates/page-notes.tmpl
index ebd5865..ebd5865 100644
--- a/vrsite/templates/page-notes.tmpl
+++ b/web/templates/page-notes.tmpl
diff --git a/vrsite/templates/page-session.tmpl b/web/templates/page-session.tmpl
index 9a35e8d..1155b67 100644
--- a/vrsite/templates/page-session.tmpl
+++ b/web/templates/page-session.tmpl
@@ -1,4 +1,4 @@
-{{/* One session's conversation, with its vr log woven in by time. */}}
+{{/* One session's conversation, with its notevi log woven in by time. */}}
{{define "page-session"}}
{{$c := .Conv}}
<header class="filehead">
@@ -16,7 +16,7 @@
{{else}}
<p class="meta hint">no transcript found for this session — searched
{{if $c.Roots}}{{range $i, $r := $c.Roots}}{{if $i}}, {{end}}<code>{{$r}}</code>{{end}}{{else}}nowhere: <code>-transcripts</code> is empty{{end}} ·
- showing the vr log alone</p>
+ showing the notevi log alone</p>
{{end}}
{{if $c.Err}}<p class="err">{{$c.Err}}</p>{{end}}
</header>
@@ -41,9 +41,9 @@
<div class="notewrap">{{template "note" (dict "N" .Note "P" $)}}</div>
{{end}}
</div>
- {{else if eq .Role "vr"}}
- <div class="turn t-vr">
- <span class="role">vr</span><time>{{.Clock}}</time>
+ {{else if eq .Role "notevi"}}
+ <div class="turn t-notevi">
+ <span class="role">notevi</span><time>{{.Clock}}</time>
{{if .File}}<a class="what" href="{{$.Href "code" .File}}">{{.Text}}</a>
{{else}}<span class="what">{{.Text}}</span>{{end}}
</div>
diff --git a/vrsite/templates/projects.tmpl b/web/templates/projects.tmpl
index 3e3c670..ce75454 100644
--- a/vrsite/templates/projects.tmpl
+++ b/web/templates/projects.tmpl
@@ -4,25 +4,26 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
+<link rel="icon" type="image/svg+xml" href="/bemtevi.svg">
<link rel="stylesheet" href="/style.css">
<script>
// Match project pages: wear the saved theme before first paint, and load the
// installed-theme stylesheet now only when that saved choice requires it.
-{const d = document.documentElement, t = localStorage.getItem("vrsite-theme");
+{const d = document.documentElement, t = localStorage.getItem("notevi-theme");
if (t && t !== "system") d.dataset.theme = t;
if (t && !/^(system|dark|light)$/.test(t)) {
const l = document.createElement("link");
l.rel = "stylesheet"; l.id = "themescss"; l.href = "/themes.css";
document.head.append(l);
}
- d.dataset.mode = localStorage.getItem("vrsite-mode") || "trace";}
+ d.dataset.mode = localStorage.getItem("notevi-mode") || "trace";}
</script>
<script src="/app.js" defer></script>
</head>
<body class="projects">
<nav aria-label="projects" id="nav">
- <header><a class="brand" href="/">{{.Title}}</a></header>
- <p class="meta">one process · every local vr sidecar</p>
+ <header><a class="brand" href="/"><img class="mark" src="/bemtevi.svg" alt="" width="20" height="20">{{.Title}}</a></header>
+ <p class="meta">one process · every local notevi sidecar</p>
<input id="projectfind" class="find" type="search" placeholder="find project…" aria-label="find project" autocomplete="off" spellcheck="false">
<form method="post" action="/projects/refresh" class="project-refresh">
<button type="submit">refresh repositories</button>
@@ -51,7 +52,7 @@
<main class="projects-main">
<header class="filehead">
<h1>Projects</h1>
- <p class="meta">Choose a vr log, start one in a jj repository, or colocate jj with an existing Git repository.</p>
+ <p class="meta">Choose a notevi log, start one in a jj repository, or colocate jj with an existing Git repository.</p>
</header>
{{if .Message}}<p class="ok project-message">{{.Message}}</p>{{end}}
{{if .Err}}<p class="err project-message">{{.Err}}</p>{{end}}
@@ -69,12 +70,12 @@
<a class="project-action" href="/p/{{.ID}}/">open project</a>
</article>
{{end}}
- {{if not .Active}}<p class="meta hint">no vr sidecars found yet</p>{{end}}
+ {{if not .Active}}<p class="meta hint">no notevi sidecars found yet</p>{{end}}
</div>
</section>
<section>
- <h2>Ready to start <span class="meta">— {{len .Ready}} jj repositories without a vr sidecar</span></h2>
+ <h2>Ready to start <span class="meta">— {{len .Ready}} jj repositories without a notevi sidecar</span></h2>
<div class="project-grid">
{{range .Ready}}
<article class="project-card" data-project="{{.Name}} {{.Relative}}">
diff --git a/vrsite/theme.go b/web/theme.go
index 91eedbe..135af43 100644
--- a/vrsite/theme.go
+++ b/web/theme.go
@@ -4,7 +4,7 @@
// The two themes named on the command line are what the site is built with;
// themes.go finds the rest of this machine's themes for the picker, and they
// arrive here as the same *Theme.
-package main
+package web
import (
"encoding/json"
diff --git a/vrsite/themes.go b/web/themes.go
index f185fab..fd46799 100644
--- a/vrsite/themes.go
+++ b/web/themes.go
@@ -5,7 +5,7 @@
// Discovery is best-effort: a theme that will not parse is skipped, never
// fatal. What the site is built with stays what -theme and -light-theme say —
// these only widen what the picker can offer.
-package main
+package web
import (
"bytes"
diff --git a/vrsite/themes_test.go b/web/themes_test.go
index 4ef1cca..f50f969 100644
--- a/vrsite/themes_test.go
+++ b/web/themes_test.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"os"
diff --git a/vrsite/transcript.go b/web/transcript.go
index 713fe36..0678b80 100644
--- a/vrsite/transcript.go
+++ b/web/transcript.go
@@ -1,6 +1,6 @@
// transcript.go — the conversation a session's reading happened inside.
//
-// The vr log says a session read a file at 10:04; the harness that ran that
+// The notevi log says a session read a file at 10:04; the harness that ran that
// session kept the whole conversation on disk, in its own format. Here we find
// that file by session id and flatten both known formats into Turns, so a note
// can be opened at the moment it was written.
@@ -9,7 +9,7 @@
// to a line each — transcripts reach megabytes and none of that belongs in a
// page — and nothing outside a configured root is ever opened, with session
// ids checked before they touch the filesystem.
-package main
+package web
import (
"encoding/json"
@@ -32,16 +32,16 @@ const (
)
// A Turn is one moment in a session: something said, a tool called or answered,
-// or — woven in from the vr log — a read, a grep, a note.
+// or — woven in from the notevi log — a read, a grep, a note.
type Turn struct {
- Role string // user | assistant | thinking | tool | result | vr
- Kind string // qualifies Role: the vr op, or "error" on a failed tool
+ Role string // user | assistant | thinking | tool | result | notevi
+ Kind string // qualifies Role: the notevi op, or "error" on a failed tool
When time.Time
Text string // what was said
Tool string // tool name, on tool and result turns
Detail string // one compact line: the call's arguments, or its output
- File string // vr turns: the file the op touched
- Note *Note // vr note turns: the note itself, rendered as a card
+ File string // notevi turns: the file the op touched
+ Note *Note // notevi note turns: the note itself, rendered as a card
Here bool // the anchored moment
}
@@ -135,7 +135,7 @@ func under(root, path string) bool {
// find locates a session's transcript. Claude Code writes
// <root>/<mangled cwd>/<session>.jsonl; codex writes
-// <root>/YYYY/MM/DD/rollout-<time>-<thread>.jsonl and vr's session id is that
+// <root>/YYYY/MM/DD/rollout-<time>-<thread>.jsonl and notevi's session id is that
// thread id, so it is matched on the suffix. Both patterns are tried under
// every root — a root knows its own layout, we do not have to.
func (tx *transcripts) find(session, dir string) string {
@@ -486,7 +486,7 @@ func clip(s string, n int) string {
return strings.TrimRight(string(r[:n]), " \t") + "…"
}
-// ── one session: the transcript with the vr log woven in ──────────────────
+// ── one session: the transcript with the notevi log woven in ──────────────────
// A Conv is what the session page shows: the conversation, the log entries
// that happened during it, and where in a long one we are looking.
@@ -507,7 +507,7 @@ type Conv struct {
Incomplete bool // even "show all" had to stop
}
-// sessionTurns are the vr log's own moments for a session: what it read,
+// sessionTurns are the notevi log's own moments for a session: what it read,
// grepped and noted.
func (s *Site) sessionTurns(id string) []Turn {
var out []Turn
@@ -515,12 +515,12 @@ func (s *Site) sessionTurns(id string) []Turn {
if e.Session != id || e.Op == "note" {
continue
}
- out = append(out, Turn{Role: "vr", Kind: e.Op, When: parseTime(e.Time),
+ out = append(out, Turn{Role: "notevi", Kind: e.Op, When: parseTime(e.Time),
Text: describe(e), File: e.File})
}
for _, n := range s.Notes {
if n.Session == id {
- out = append(out, Turn{Role: "vr", Kind: "note", When: n.At, File: n.File, Note: n})
+ out = append(out, Turn{Role: "notevi", Kind: "note", When: n.At, File: n.File, Note: n})
}
}
return out
diff --git a/vrsite/transcript_test.go b/web/transcript_test.go
index 5e83ef4..1c3b3db 100644
--- a/vrsite/transcript_test.go
+++ b/web/transcript_test.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"fmt"
@@ -13,7 +13,7 @@ import (
// record per message with content blocks, codex wraps everything in a payload
// and keeps the model's history in response_items.
const claudeJSONL = `{"type":"user","timestamp":"2026-07-20T09:57:00.000Z","sessionId":"s1","cwd":"/repo","message":{"role":"user","content":"read lib.rs and tell me about the cache"}}
-{"type":"assistant","timestamp":"2026-07-20T09:57:30.000Z","message":{"role":"assistant","model":"claude-opus-5","content":[{"type":"thinking","thinking":"start with the struct","signature":"CAIS"},{"type":"text","text":"Reading it now."},{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"vr read src/lib.rs:1-30","description":"read the file"}}]}}
+{"type":"assistant","timestamp":"2026-07-20T09:57:30.000Z","message":{"role":"assistant","model":"claude-opus-5","content":[{"type":"thinking","thinking":"start with the struct","signature":"CAIS"},{"type":"text","text":"Reading it now."},{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"notevi read src/lib.rs:1-30","description":"read the file"}}]}}
{"type":"user","timestamp":"2026-07-20T09:59:10.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"1\tuse std::collections::HashMap;\n2\t\n3\tstruct Cache;\n"}]}}
{"type":"mode","timestamp":"2026-07-20T09:59:11.000Z","mode":"default"}
{"type":"assistant","timestamp":"2026-07-20T09:59:40.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_2","name":"Read","input":{"file_path":"/repo/src/main.rs"}}]}}
@@ -26,7 +26,7 @@ const codexJSONL = `{"timestamp":"2026-07-20T12:00:00.000Z","type":"session_meta
{"timestamp":"2026-07-20T12:00:02.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"<environment_context>\n <cwd>/repo</cwd>\n</environment_context>"}]}}
{"timestamp":"2026-07-20T12:00:03.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"review the cache"}]}}
{"timestamp":"2026-07-20T12:00:04.000Z","type":"response_item","payload":{"type":"reasoning","summary":[{"type":"summary_text","text":"start with lib.rs"}],"encrypted_content":"gAAAA"}}
-{"timestamp":"2026-07-20T12:00:05.000Z","type":"response_item","payload":{"type":"function_call","name":"exec_command","call_id":"call_1","arguments":"{\"cmd\":\"vr read src/lib.rs\",\"workdir\":\"/repo\"}"}}
+{"timestamp":"2026-07-20T12:00:05.000Z","type":"response_item","payload":{"type":"function_call","name":"exec_command","call_id":"call_1","arguments":"{\"cmd\":\"notevi read src/lib.rs\",\"workdir\":\"/repo\"}"}}
{"timestamp":"2026-07-20T12:00:06.000Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_1","output":"Chunk ID: abc123\nOutput:\n1\tstruct Cache;\n"}}
{"timestamp":"2026-07-20T12:00:07.000Z","type":"event_msg","payload":{"type":"agent_message","message":"the cache is two-level"}}
{"timestamp":"2026-07-20T12:00:08.000Z","type":"response_item","payload":{"type":"custom_tool_call","name":"apply_patch","call_id":"call_2","input":"*** Begin Patch\n*** Update File: src/lib.rs\n@@\n-old\n+new\n"}}
@@ -69,7 +69,7 @@ func TestParseClaudeTranscript(t *testing.T) {
"user read lib.rs and tell me about the cache",
"thinking start with the struct",
"assistant Reading it now.",
- "tool Bash vr read src/lib.rs:1-30",
+ "tool Bash notevi read src/lib.rs:1-30",
"result Bash 1\tuse std::collections::HashMap; … +2 lines",
"tool Read /repo/src/main.rs",
"result Read no such file",
@@ -99,7 +99,7 @@ func TestParseCodexTranscript(t *testing.T) {
want := []string{
"user review the cache",
"thinking start with lib.rs",
- "tool exec_command vr read src/lib.rs",
+ "tool exec_command notevi read src/lib.rs",
"result exec_command Chunk ID: abc123 … +2 lines",
"tool apply_patch *** Begin Patch … +4 lines",
"result apply_patch Exit code: 0",
@@ -259,11 +259,11 @@ func TestSessionPageInterleavesLogAndTranscript(t *testing.T) {
for _, want := range []string{
"read lib.rs and tell me about the cache", // the conversation
"It is a two-level map.",
- "vr read src/lib.rs:1-30", // a tool call, summarized
- "read hello.txt:1-2", // the vr log, woven in
- "second line matters", // the note itself
- `data-anchor="note-1"`, // and it is what the page lands on
- `class="turn t-note here"`, // marked as the anchored moment
+ "notevi read src/lib.rs:1-30", // a tool call, summarized
+ "read hello.txt:1-2", // the notevi log, woven in
+ "second line matters", // the note itself
+ `data-anchor="note-1"`, // and it is what the page lands on
+ `class="turn t-note here"`, // marked as the anchored moment
} {
if !strings.Contains(body, want) {
t.Errorf("session page is missing %q", want)
@@ -293,7 +293,7 @@ func TestSessionPageWithoutATranscript(t *testing.T) {
t.Error("the page does not say where it looked")
}
if !strings.Contains(body, "read hello.txt:1-2") || !strings.Contains(body, "second line matters") {
- t.Error("the vr timeline is missing from the fallback")
+ t.Error("the notevi timeline is missing from the fallback")
}
}
diff --git a/vrsite/tree.go b/web/tree.go
index e7891c6..85e5536 100644
--- a/vrsite/tree.go
+++ b/web/tree.go
@@ -1,5 +1,5 @@
// tree.go — the repository tree, one fragment shared by every page.
-package main
+package web
import (
"fmt"
diff --git a/vrsite/main.go b/web/web.go
index e43a8eb..ad7bd9c 100644
--- a/vrsite/main.go
+++ b/web/web.go
@@ -1,7 +1,7 @@
-// vrsite — the reading room for vr trace logs.
+// Package web is notevi's reading room: "notevi web" served or exported.
//
-// 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
+// A notevi log records who read what, where, and what they thought about it.
+// This package 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.
@@ -14,7 +14,7 @@
// 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
+package web
import (
"embed"
@@ -26,30 +26,36 @@ import (
"strings"
"time"
- "0x4200.cafe/vr/sidecar"
+ "0x4200.cafe/notevi/sidecar"
)
//go:embed templates assets
var tfs embed.FS
+// Titles for a single repository's trace and for the dashboard over all of them.
+const (
+ defaultTitle = "notevi trace"
+ hubDefaultTitle = "notevi projects"
+)
+
func fatal(format string, a ...any) {
- fmt.Fprintf(os.Stderr, "vrsite: "+format+"\n", a...)
+ fmt.Fprintf(os.Stderr, "notevi web: "+format+"\n", a...)
os.Exit(1)
}
-const usageText = `vrsite — the reading room for vr trace logs.
+const usageText = `notevi web — the reading room for notevi 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.
+notevi logs every file a coding agent reads, greps and notes, one JSON line
+each, keyed to the jj change the code was read at. This 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.
usage:
- vrsite [-scan-root DIR] [-addr HOST:PORT] [-user NAME]
+ notevi web [-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]
+ notevi web -repo DIR [-addr HOST:PORT] [-user NAME]
serve one repository directly, preserving the original single-project mode
- vrsite [-repo DIR] -out DIR [-watch]
+ notevi web [-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
@@ -58,12 +64,12 @@ 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
+ 3. give the agents notevi and tell them to read with it; constrain/AGENTS.md
+ in the notevi repo is a drop-in brief that does, and constrain/ enforces 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
+recorded at, and the site 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,
@@ -84,30 +90,32 @@ through it, and the browser remembers what you settled on.
flags:
`
-func main() {
+// Main runs the "notevi web" subcommand; args are everything after "web".
+func Main(args []string) {
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(), ","),
+ fs := flag.NewFlagSet("notevi web", flag.ExitOnError)
+ repo := fs.String("repo", "", "serve one jj repo directly (with -out, default: current directory)")
+ scanRoot := fs.String("scan-root", home, "live dashboard root to scan for jj and Git repositories")
+ addr := fs.String("addr", "127.0.0.1:4200", "address to serve on")
+ who := fs.String("user", "", "name your browser notes are logged under (default: $USER)")
+ out := fs.String("out", "", "write a static site here instead of serving")
+ themePath := fs.String("theme", defaultTheme("dark"), "zed theme json for the dark scheme (default: discovered, else embedded One Dark)")
+ themeName := fs.String("theme-name", "", "theme name inside the family")
+ lightPath := fs.String("light-theme", defaultTheme("light"), "zed theme json for the light scheme (default: discovered, else embedded One Light)")
+ lightName := fs.String("light-theme-name", "", "theme name inside the light family")
+ title := fs.String("title", defaultTitle, "site title")
+ roots := fs.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()
+ watch := fs.Bool("watch", false, "with -out: watch the trace log and rebuild after changes settle")
+ debounce := fs.Duration("debounce", 10*time.Minute, "quiet period before a watched rebuild")
+ fs.Usage = func() {
+ fmt.Fprint(fs.Output(), usageText)
+ fs.PrintDefaults()
}
- flag.Parse()
+ fs.Parse(args)
if *debounce < 0 {
fatal("debounce must not be negative")
}
@@ -122,8 +130,8 @@ func main() {
// path. A bare live invocation is the new, stateless project dashboard.
if *repo == "" && *out == "" {
hubTitle := *title
- if hubTitle == "vr trace" {
- hubTitle = "vr projects"
+ if hubTitle == defaultTitle {
+ hubTitle = hubDefaultTitle
}
hub, err := newProjectHub(*scanRoot, hubTitle, username(*who), strings.Split(*roots, ","), theme, light, themes)
if err != nil {
@@ -154,7 +162,7 @@ func main() {
if !*watch {
return
}
- fmt.Printf("vrsite: watching %s (rebuild after %s without changes)\n", log, *debounce)
+ fmt.Printf("notevi web: watching %s (rebuild after %s without changes)\n", log, *debounce)
watchLog(log, *debounce, build)
}
@@ -203,7 +211,7 @@ func watchLogUntil(log logStore, debounce, poll time.Duration, done <-chan struc
next, err := log.Generation()
if err != nil {
if msg := err.Error(); msg != lastStatErr {
- fmt.Fprintf(os.Stderr, "vrsite: watch %s: %v\n", log, err)
+ fmt.Fprintf(os.Stderr, "notevi web: watch %s: %v\n", log, err)
lastStatErr = msg
}
continue
@@ -225,10 +233,10 @@ func watchLogUntil(log logStore, debounce, poll time.Duration, done <-chan struc
}
timer.Reset(debounce)
}
- fmt.Printf("vrsite: change detected; rebuild scheduled after %s without changes\n", debounce)
+ fmt.Printf("notevi web: change detected; rebuild scheduled after %s without changes\n", debounce)
case <-timerC:
- fmt.Println("vrsite: rebuilding after trace changes settled")
+ fmt.Println("notevi web: rebuilding after trace changes settled")
timer = nil
timerC = nil
rebuild()
diff --git a/vrsite/main_test.go b/web/web_test.go
index b7c93c3..3986cdc 100644
--- a/vrsite/main_test.go
+++ b/web/web_test.go
@@ -1,4 +1,4 @@
-package main
+package web
import (
"os"