From 5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 2 Aug 2026 23:43:08 -0300 Subject: rebrand to notevi: one CLI over the jj sidecar vr and vrsite become a single binary. vrsite/ folds into a web package in one module (0x4200.cafe/notevi); "notevi web" serves and exports exactly what vrsite did, and "notevi read/grep/note/query" is unchanged. The sidecar is renamed with it: notevi_log, notevi-log.jsonl, and the description "private: notevi log". The pre-rebrand names are still recognized, so an old repository opens and reads; it is renamed in place on the first write, or up front with "notevi migrate DIR...". That rename cannot be a single mv inside jj run. jj only auto-tracks a *new* file in the run working copy below a size limit it does not take from the command line, so writing a whole log under a name the change has never held is silently dropped while jj reports success. ensureLogFile creates the file empty first and lets every later byte be a modification of a tracked file, which snapshots at any size; that also fixes the same latent bug when importing a large legacy vr-log.jsonl. Adds a bem-te-vi mark (favicon and nav brand) and a README. Co-Authored-By: Claude Opus 5 (1M context) --- vrsite/transcript.go | 602 --------------------------------------------------- 1 file changed, 602 deletions(-) delete mode 100644 vrsite/transcript.go (limited to 'vrsite/transcript.go') diff --git a/vrsite/transcript.go b/vrsite/transcript.go deleted file mode 100644 index 713fe36..0000000 --- a/vrsite/transcript.go +++ /dev/null @@ -1,602 +0,0 @@ -// 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 -// 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. -// -// Two rules keep this cheap and safe. Tool calls and their output are reduced -// 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 - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - "sync" - "time" -) - -const ( - maxTranscript = 32 << 20 // refuse to parse more than this in one file - transcriptCacheMax = 8 // parsed transcripts are large; keep few - briefLine = 160 // one line of a tool call or its output - maxText = 6000 // one message - convWindow = 80 // turns rendered around the anchored moment - convMax = 1500 // turns rendered by "show all" -) - -// 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. -type Turn struct { - Role string // user | assistant | thinking | tool | result | vr - Kind string // qualifies Role: the vr 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 - Here bool // the anchored moment -} - -func (t Turn) Clock() string { - if t.When.IsZero() { - return "" - } - return t.When.Local().Format("15:04:05") -} - -// A Transcript is one harness conversation, already normalized. -type Transcript struct { - Path string - Harness string // claude-code | codex - Turns []Turn - Err string -} - -// transcripts finds and parses harness transcripts, remembering what it has -// already parsed. Roots are the only places it will look. -type transcripts struct { - roots []string - - mu sync.Mutex - cache map[string]*Transcript // path|mtime|size -> parsed -} - -func newTranscripts(roots []string) *transcripts { - tx := &transcripts{cache: map[string]*Transcript{}} - for _, r := range roots { - r = strings.TrimSpace(r) - if r == "" { - continue - } - if abs, err := filepath.Abs(r); err == nil { - r = abs - } - tx.roots = append(tx.roots, filepath.Clean(r)) - } - return tx -} - -// defaultTranscriptRoots are where the two harnesses we know about keep their -// conversations. -func defaultTranscriptRoots() []string { - home, err := os.UserHomeDir() - if err != nil { - return nil - } - return []string{filepath.Join(home, ".claude", "projects"), filepath.Join(home, ".codex", "sessions")} -} - -// safeSession refuses ids that could name something we did not mean to open: -// the id goes into a filesystem path, so it must be one plain component. -func safeSession(id string) error { - if id == "" { - return fmt.Errorf("no session id") - } - if len(id) > 128 { - return fmt.Errorf("session id is too long") - } - for _, r := range id { - ok := r == '-' || r == '_' || r == ':' || r == '.' || - '0' <= r && r <= '9' || 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' - if !ok { - return fmt.Errorf("session id contains %q", r) - } - } - if strings.Contains(id, "..") { - return fmt.Errorf("session id contains %q", "..") - } - return nil -} - -// mangleDir is how Claude Code names a project directory after its cwd: -// /home/u/0x4200.cafe -> -home-u-0x4200-cafe. -func mangleDir(dir string) string { - return strings.NewReplacer("/", "-", ".", "-").Replace(dir) -} - -// under states the rule the globs below already obey: a transcript we open -// lies inside a configured root. It is checked anyway, so the rule has one -// place to fail rather than living in the shape of two patterns. -func under(root, path string) bool { - rel, err := filepath.Rel(root, path) - if err != nil { - return false - } - return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) -} - -// find locates a session's transcript. Claude Code writes -// //.jsonl; codex writes -// /YYYY/MM/DD/rollout-