detached f1ffcd8

Merge pull request #20 from bnema/chore/update-agent-guidance

docs(agent): simplify project guidance
commit
f1ffcd8e40355f87736ff93c99b9ba571a354750
author
bnema · · 2026-07-05 05:34 UTC
committer
GitHub · · 2026-07-05 05:34 UTC
merge of
fcf4de3 + 51db7d7 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
1 file · +54 -46
M AGENTS.md +54 -46
@@ -1,67 +1,75 @@
1-# CLAUDE.md
1+# vev agent guide
22
3-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
3+## Project
44
5-## What this is
6-
7-vev is a terminal multiplexer for Linux written in Go (deps: only `golang.org/x/sys`, `golang.org/x/term`). Sessions live in a per-user daemon; the client is a thin pipe. Rendering happens server-side with damage tracking so only minimal diffs cross the unix socket (or the SSH link on remote attach).
5+vev is a Linux terminal multiplexer written in Go. It has a per-user daemon, a thin client, server-side rendering, and minimal socket/SSH diffs.
86
97 ## Commands
108
119 ```sh
12-go build -o vev . # build
13-make test # go test ./... -race
14-go test ./internal/usecase/daemon -race -run TestName # single test
15-make lint # fails if goimports -l finds anything, then go vet
16-make mocks # regenerate mocks (mockery, config in .mockery.yml)
17-go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -bench=. -benchmem # microbenchmarks
10+go build -o vev .
11+make test
12+go test ./internal/usecase/daemon -race -run TestName
13+make lint
14+make mocks
15+go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -bench=. -benchmem
1816 ```
1917
20-- Format with **goimports**, not gofmt — lint enforces it.
21-- Daemon pprof is opt-in via `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon` (see `docs/performance.md`).
22-- Live logs are under `$XDG_STATE_HOME/vev` (or `~/.local/state/vev` when `XDG_STATE_HOME` is unset): `vev-daemon.log`, `vev-client.log`, `vev-stdio.log`, plus matching `*-crash.log` files.
23-- Feature branches live in git worktrees under `.worktrees/<branch-name>`.
18+- Format with **goimports**.
19+- `make test` runs `go test ./... -race`.
20+- `make lint` checks `goimports -l`, then `go vet`.
21+- Enable daemon pprof with `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon`.
22+- Logs live in `$XDG_STATE_HOME/vev`, or `~/.local/state/vev` when unset.
23+- Use feature worktrees under `.worktrees/<branch-name>`.
24+
25+## Architecture rules
26+
27+Hexagonal boundaries are enforced by `boundary_test.go`.
2428
25-## Architecture
29+- `pkg/` is reusable and must not import `internal/`.
30+- `internal/usecase/` must not import `internal/adapters/`.
31+- Usecases depend on `internal/ports` and `internal/domain`.
2632
27-Hexagonal, enforced mechanically by `boundary_test.go` at the repo root:
28-- `pkg/` must not import `internal/` (pure, reusable: `pkg/vt` VT emulator, `pkg/renderer` damage-diff renderer, `pkg/kv` store).
29-- `internal/usecase/` must not import `internal/adapters/`. Usecases depend only on `internal/ports` (interfaces + wire protocol) and `internal/domain`.
33+Layer map:
3034
31-Layers:
32-- `main.go` → `internal/app`: CLI parsing (`parseArgs`/`dispatch` in `run.go`), dependency wiring, daemon spawn/ensure logic. Hidden subcommands (e.g. `_stdio`) live here.
33-- `internal/ports`: all interfaces (`Transport`, `Listener`, `PTY`, `Terminal`, `Clock`, …) plus the wire protocol (`frame.go`, `wire.go`).
34-- `internal/usecase/daemon`: the server. Owns sessions → tabs → panes, one `pkg/vt.Screen` per pane (server-side authoritative terminal state), and one `pkg/renderer.Renderer` shadow per attached client (per-client diff state). `daemon/client.go` documents the lock ordering at the top of the file — read it before touching teardown paths (`clientGone`, `killSession`, `shutdownAll`).
35-- `internal/usecase/client`: the thin client — raw-mode ownership, stdin/resize pumps, writes received output bytes verbatim to the terminal. It interprets nothing.
36-- `internal/adapters`: `ipc` (unix socket framing, socket dir), `sshstdio` (remote attach), `pty`, `term`, `clock`.
37-- Other usecases (`keys`, `layout`, `copy`, `palette`, `picker`, `prompt`, `theme`, `ui`, …) are daemon-side features routed through `daemon/client.go`.
35+- `main.go` → `internal/app`: CLI parsing, wiring, daemon startup, hidden subcommands.
36+- `internal/ports`: interfaces and wire protocol.
37+- `internal/usecase/daemon`: sessions, tabs, panes, VT screens, renderer shadows, daemon features.
38+- `internal/usecase/client`: raw-mode thin client; writes output bytes verbatim and interprets nothing.
39+- `internal/adapters`: IPC, SSH stdio, PTY, terminal, and clock implementations.
3840
39-### Ports interfaces and mocks (mockery)
41+Before touching daemon teardown paths, read the lock-ordering notes at the top of `internal/usecase/daemon/client.go`.
4042
41-Every cross-layer abstraction is an interface in `internal/ports` (`Transport`, `Listener`, `PTY`, `PTYFactory`, `Terminal`, `Store`, `Clock`, `Timer`). Adapters implement them; usecases and `internal/app` consume them. This is the seam that makes the daemon and client testable without real sockets, PTYs, or wall-clock time (e.g. tests drive grace timers through the `Clock`/`Timer` ports instead of sleeping).
43+## Ports and mocks
4244
43-Mocks are generated by **mockery** (testify template), configured in `.mockery.yml`:
44-- `internal/ports` has `all: true` → every interface in the package gets a mock in `internal/ports/mocks/mocks.go` (package `portsmocks`, `MockTransport`, `MockPTY`, …).
45-- `internal/usecase/command.Context` gets a package-local `context_mock_test.go`.
45+- Define cross-layer interfaces only in `internal/ports`.
46+- Adapters implement ports; usecases and `internal/app` consume ports.
47+- Tests should prefer generated `portsmocks.MockX` with `.EXPECT()`.
48+- Hand-written fakes are okay only when mocks make the test harder to understand.
49+- After changing a port, run `make mocks` and commit the generated updates.
50+- Time-dependent daemon code must use `ports.Clock`/`ports.Timer`, not direct wall-clock APIs.
4651
47-The workflow when adding or changing an abstraction:
48-1. Add/modify the interface in `internal/ports` (never define the interface in a usecase or adapter).
49-2. Run `make mocks` — the generated file is committed, and forgetting this step leaves stale mocks that still compile silently.
50-3. In tests, prefer the generated `portsmocks.MockX` with `.EXPECT()` over hand-rolled stubs; hand-rolled fakes are acceptable only when a mock's call-expectation style fights the test (e.g. long-lived pumps).
52+## Tests
5153
52-New time-dependent daemon code must go through `ports.Clock`/`ports.Timer`, not `time.Now`/`time.NewTimer`, or it becomes untestable under the existing fake clock.
54+- Write Go tests as table-driven tests by default.
55+- Keep wire tests byte-for-byte and include truncated-prefix and trailing-garbage coverage.
56+- Prefer fake clocks over sleeps.
5357
54-### Wire protocol rules
58+## Wire protocol
5559
56-The IPC protocol lives entirely in `internal/ports` (`frame.go`, `wire.go`): frames are 4-byte big-endian length + 1 type byte + payload; messages are hand-marshalled big-endian.
60+Wire payload types/codecs live in `internal/ports/frame.go` and `internal/ports/wire.go`. Connection framing lives in `internal/adapters/ipc/transport.go`.
5761
58-- Client-originated message types are 1–15, server-originated 16+.
59-- Version negotiation is strict equality; the version must stay the **first field** of `Hello` so `PeekHelloVersion` keeps working on any version's payload. Bump `ProtocolVersion` when changing any message layout.
60-- `wire_test.go` contains byte-for-byte golden tests plus truncated-prefix / trailing-garbage invariants for every message. Any wire change must update the goldens and keep those invariants.
62+- IPC frames on a connection are 4-byte big-endian length, 1 type byte, then payload.
63+- Client message types are `1–15`; server message types are `16+`.
64+- Version negotiation requires strict equality.
65+- `Hello.Version` must stay first so `PeekHelloVersion` works.
66+- Bump `ProtocolVersion` for any message layout change.
6167
62-### Session/attach flow
68+## Session flow
6369
64-- Ephemeral sessions (numbered) die on detach; named sessions survive headless. The daemon starts on first use and exits when the last session ends.
65-- Local attach: client dials the unix socket (spawning the daemon if needed via `ensureDaemon` in `internal/app`), sends `Hello{Intent, Name, Size, …}`, gets `Welcome`, then pumps `Input`/`Resize` against `Output` frames.
66-- Remote attach: `vev attach user@host[:session]` runs `ssh -- host vev _stdio [session]`; the remote `_stdio` process proxies frames between ssh stdio and the remote daemon's unix socket. Same protocol end to end, so vev must be installed on the remote host.
67-- Only one client per session: a new attach displaces the previous one (`detachReplacedClient`).
70+- Ephemeral numbered sessions die on detach.
71+- Named sessions survive headless.
72+- The daemon starts on first use and exits when the last session ends.
73+- Local attach sends `Hello`, receives `Welcome`, then pumps `Input`/`Resize` and `Output` frames.
74+- Remote attach runs `ssh -- host vev _stdio [session]` and proxies the same protocol over stdio.
75+- Only one client may attach to a session; a new attach replaces the old client.
@@ -1,67 +1,75 @@
1-# CLAUDE.md1+# vev agent guide
2 2
3-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.3+## Project
4 4
5-## What this is5+vev is a Linux terminal multiplexer written in Go. It has a per-user daemon, a thin client, server-side rendering, and minimal socket/SSH diffs.
6-
7-vev is a terminal multiplexer for Linux written in Go (deps: only `golang.org/x/sys`, `golang.org/x/term`). Sessions live in a per-user daemon; the client is a thin pipe. Rendering happens server-side with damage tracking so only minimal diffs cross the unix socket (or the SSH link on remote attach).
8 6
9 ## Commands7 ## Commands
10 8
11 ```sh9 ```sh
12-go build -o vev . # build10+go build -o vev .
13-make test # go test ./... -race11+make test
14-go test ./internal/usecase/daemon -race -run TestName # single test12+go test ./internal/usecase/daemon -race -run TestName
15-make lint # fails if goimports -l finds anything, then go vet13+make lint
16-make mocks # regenerate mocks (mockery, config in .mockery.yml)14+make mocks
17-go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -bench=. -benchmem # microbenchmarks15+go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -bench=. -benchmem
18 ```16 ```
19 17
20-- Format with **goimports**, not gofmt — lint enforces it.18+- Format with **goimports**.
21-- Daemon pprof is opt-in via `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon` (see `docs/performance.md`).19+- `make test` runs `go test ./... -race`.
22-- Live logs are under `$XDG_STATE_HOME/vev` (or `~/.local/state/vev` when `XDG_STATE_HOME` is unset): `vev-daemon.log`, `vev-client.log`, `vev-stdio.log`, plus matching `*-crash.log` files.20+- `make lint` checks `goimports -l`, then `go vet`.
23-- Feature branches live in git worktrees under `.worktrees/<branch-name>`.21+- Enable daemon pprof with `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon`.
22+- Logs live in `$XDG_STATE_HOME/vev`, or `~/.local/state/vev` when unset.
23+- Use feature worktrees under `.worktrees/<branch-name>`.
24+
25+## Architecture rules
26+
27+Hexagonal boundaries are enforced by `boundary_test.go`.
24 28
25-## Architecture29+- `pkg/` is reusable and must not import `internal/`.
30+- `internal/usecase/` must not import `internal/adapters/`.
31+- Usecases depend on `internal/ports` and `internal/domain`.
26 32
27-Hexagonal, enforced mechanically by `boundary_test.go` at the repo root:33+Layer map:
28-- `pkg/` must not import `internal/` (pure, reusable: `pkg/vt` VT emulator, `pkg/renderer` damage-diff renderer, `pkg/kv` store).
29-- `internal/usecase/` must not import `internal/adapters/`. Usecases depend only on `internal/ports` (interfaces + wire protocol) and `internal/domain`.
30 34
31-Layers:35+- `main.go` → `internal/app`: CLI parsing, wiring, daemon startup, hidden subcommands.
32-- `main.go` → `internal/app`: CLI parsing (`parseArgs`/`dispatch` in `run.go`), dependency wiring, daemon spawn/ensure logic. Hidden subcommands (e.g. `_stdio`) live here.36+- `internal/ports`: interfaces and wire protocol.
33-- `internal/ports`: all interfaces (`Transport`, `Listener`, `PTY`, `Terminal`, `Clock`, …) plus the wire protocol (`frame.go`, `wire.go`).37+- `internal/usecase/daemon`: sessions, tabs, panes, VT screens, renderer shadows, daemon features.
34-- `internal/usecase/daemon`: the server. Owns sessions → tabs → panes, one `pkg/vt.Screen` per pane (server-side authoritative terminal state), and one `pkg/renderer.Renderer` shadow per attached client (per-client diff state). `daemon/client.go` documents the lock ordering at the top of the file — read it before touching teardown paths (`clientGone`, `killSession`, `shutdownAll`).38+- `internal/usecase/client`: raw-mode thin client; writes output bytes verbatim and interprets nothing.
35-- `internal/usecase/client`: the thin client — raw-mode ownership, stdin/resize pumps, writes received output bytes verbatim to the terminal. It interprets nothing.39+- `internal/adapters`: IPC, SSH stdio, PTY, terminal, and clock implementations.
36-- `internal/adapters`: `ipc` (unix socket framing, socket dir), `sshstdio` (remote attach), `pty`, `term`, `clock`.
37-- Other usecases (`keys`, `layout`, `copy`, `palette`, `picker`, `prompt`, `theme`, `ui`, …) are daemon-side features routed through `daemon/client.go`.
38 40
39-### Ports interfaces and mocks (mockery)41+Before touching daemon teardown paths, read the lock-ordering notes at the top of `internal/usecase/daemon/client.go`.
40 42
41-Every cross-layer abstraction is an interface in `internal/ports` (`Transport`, `Listener`, `PTY`, `PTYFactory`, `Terminal`, `Store`, `Clock`, `Timer`). Adapters implement them; usecases and `internal/app` consume them. This is the seam that makes the daemon and client testable without real sockets, PTYs, or wall-clock time (e.g. tests drive grace timers through the `Clock`/`Timer` ports instead of sleeping).43+## Ports and mocks
42 44
43-Mocks are generated by **mockery** (testify template), configured in `.mockery.yml`:45+- Define cross-layer interfaces only in `internal/ports`.
44-- `internal/ports` has `all: true` → every interface in the package gets a mock in `internal/ports/mocks/mocks.go` (package `portsmocks`, `MockTransport`, `MockPTY`, …).46+- Adapters implement ports; usecases and `internal/app` consume ports.
45-- `internal/usecase/command.Context` gets a package-local `context_mock_test.go`.47+- Tests should prefer generated `portsmocks.MockX` with `.EXPECT()`.
48+- Hand-written fakes are okay only when mocks make the test harder to understand.
49+- After changing a port, run `make mocks` and commit the generated updates.
50+- Time-dependent daemon code must use `ports.Clock`/`ports.Timer`, not direct wall-clock APIs.
46 51
47-The workflow when adding or changing an abstraction:52+## Tests
48-1. Add/modify the interface in `internal/ports` (never define the interface in a usecase or adapter).
49-2. Run `make mocks` — the generated file is committed, and forgetting this step leaves stale mocks that still compile silently.
50-3. In tests, prefer the generated `portsmocks.MockX` with `.EXPECT()` over hand-rolled stubs; hand-rolled fakes are acceptable only when a mock's call-expectation style fights the test (e.g. long-lived pumps).
51 53
52-New time-dependent daemon code must go through `ports.Clock`/`ports.Timer`, not `time.Now`/`time.NewTimer`, or it becomes untestable under the existing fake clock.54+- Write Go tests as table-driven tests by default.
55+- Keep wire tests byte-for-byte and include truncated-prefix and trailing-garbage coverage.
56+- Prefer fake clocks over sleeps.
53 57
54-### Wire protocol rules58+## Wire protocol
55 59
56-The IPC protocol lives entirely in `internal/ports` (`frame.go`, `wire.go`): frames are 4-byte big-endian length + 1 type byte + payload; messages are hand-marshalled big-endian.60+Wire payload types/codecs live in `internal/ports/frame.go` and `internal/ports/wire.go`. Connection framing lives in `internal/adapters/ipc/transport.go`.
57 61
58-- Client-originated message types are 1–15, server-originated 16+.62+- IPC frames on a connection are 4-byte big-endian length, 1 type byte, then payload.
59-- Version negotiation is strict equality; the version must stay the **first field** of `Hello` so `PeekHelloVersion` keeps working on any version's payload. Bump `ProtocolVersion` when changing any message layout.63+- Client message types are `1–15`; server message types are `16+`.
60-- `wire_test.go` contains byte-for-byte golden tests plus truncated-prefix / trailing-garbage invariants for every message. Any wire change must update the goldens and keep those invariants.64+- Version negotiation requires strict equality.
65+- `Hello.Version` must stay first so `PeekHelloVersion` works.
66+- Bump `ProtocolVersion` for any message layout change.
61 67
62-### Session/attach flow68+## Session flow
63 69
64-- Ephemeral sessions (numbered) die on detach; named sessions survive headless. The daemon starts on first use and exits when the last session ends.70+- Ephemeral numbered sessions die on detach.
65-- Local attach: client dials the unix socket (spawning the daemon if needed via `ensureDaemon` in `internal/app`), sends `Hello{Intent, Name, Size, …}`, gets `Welcome`, then pumps `Input`/`Resize` against `Output` frames.71+- Named sessions survive headless.
66-- Remote attach: `vev attach user@host[:session]` runs `ssh -- host vev _stdio [session]`; the remote `_stdio` process proxies frames between ssh stdio and the remote daemon's unix socket. Same protocol end to end, so vev must be installed on the remote host.72+- The daemon starts on first use and exits when the last session ends.
67-- Only one client per session: a new attach displaces the previous one (`detachReplacedClient`).73+- Local attach sends `Hello`, receives `Welcome`, then pumps `Input`/`Resize` and `Output` frames.
74+- Remote attach runs `ssh -- host vev _stdio [session]` and proxies the same protocol over stdio.
75+- Only one client may attach to a session; a new attach replaces the old client.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close