detached 51529aa

Merge pull request #11 from bnema/chore/add-agents-md

chore(docs): add agents md
commit
51529aa1afcf1c90eae2c8942a16496a29102ea5
author
bnema · · 2026-07-04 12:58 UTC
committer
GitHub · · 2026-07-04 12:58 UTC
merge of
c6f30c2 + e0cc36e · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
2 files · +67 -0
A AGENTS.md +66 -0
@@ -0,0 +1,66 @@
1+# CLAUDE.md
2+
3+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
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).
8+
9+## Commands
10+
11+```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
18+```
19+
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+- Feature branches live in git worktrees under `.worktrees/<branch-name>`.
23+
24+## Architecture
25+
26+Hexagonal, enforced mechanically by `boundary_test.go` at the repo root:
27+- `pkg/` must not import `internal/` (pure, reusable: `pkg/vt` VT emulator, `pkg/renderer` damage-diff renderer, `pkg/kv` store).
28+- `internal/usecase/` must not import `internal/adapters/`. Usecases depend only on `internal/ports` (interfaces + wire protocol) and `internal/domain`.
29+
30+Layers:
31+- `main.go` → `internal/app`: CLI parsing (`parseArgs`/`dispatch` in `run.go`), dependency wiring, daemon spawn/ensure logic. Hidden subcommands (e.g. `_stdio`) live here.
32+- `internal/ports`: all interfaces (`Transport`, `Listener`, `PTY`, `Terminal`, `Clock`, …) plus the wire protocol (`frame.go`, `wire.go`).
33+- `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`).
34+- `internal/usecase/client`: the thin client — raw-mode ownership, stdin/resize pumps, writes received output bytes verbatim to the terminal. It interprets nothing.
35+- `internal/adapters`: `ipc` (unix socket framing, socket dir), `sshstdio` (remote attach), `pty`, `term`, `clock`.
36+- Other usecases (`keys`, `layout`, `copy`, `palette`, `picker`, `prompt`, `theme`, `ui`, …) are daemon-side features routed through `daemon/client.go`.
37+
38+### Ports interfaces and mocks (mockery)
39+
40+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).
41+
42+Mocks are generated by **mockery** (testify template), configured in `.mockery.yml`:
43+- `internal/ports` has `all: true` → every interface in the package gets a mock in `internal/ports/mocks/mocks.go` (package `portsmocks`, `MockTransport`, `MockPTY`, …).
44+- `internal/usecase/command.Context` gets a package-local `context_mock_test.go`.
45+
46+The workflow when adding or changing an abstraction:
47+1. Add/modify the interface in `internal/ports` (never define the interface in a usecase or adapter).
48+2. Run `make mocks` — the generated file is committed, and forgetting this step leaves stale mocks that still compile silently.
49+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).
50+
51+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.
52+
53+### Wire protocol rules
54+
55+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.
56+
57+- Client-originated message types are 1–15, server-originated 16+.
58+- 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.
59+- `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.
60+
61+### Session/attach flow
62+
63+- Ephemeral sessions (numbered) die on detach; named sessions survive headless. The daemon starts on first use and exits when the last session ends.
64+- 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.
65+- 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.
66+- Only one client per session: a new attach displaces the previous one (`detachReplacedClient`).
@@ -0,0 +1,66 @@
1+# CLAUDE.md
2+
3+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
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).
8+
9+## Commands
10+
11+```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
18+```
19+
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+- Feature branches live in git worktrees under `.worktrees/<branch-name>`.
23+
24+## Architecture
25+
26+Hexagonal, enforced mechanically by `boundary_test.go` at the repo root:
27+- `pkg/` must not import `internal/` (pure, reusable: `pkg/vt` VT emulator, `pkg/renderer` damage-diff renderer, `pkg/kv` store).
28+- `internal/usecase/` must not import `internal/adapters/`. Usecases depend only on `internal/ports` (interfaces + wire protocol) and `internal/domain`.
29+
30+Layers:
31+- `main.go` → `internal/app`: CLI parsing (`parseArgs`/`dispatch` in `run.go`), dependency wiring, daemon spawn/ensure logic. Hidden subcommands (e.g. `_stdio`) live here.
32+- `internal/ports`: all interfaces (`Transport`, `Listener`, `PTY`, `Terminal`, `Clock`, …) plus the wire protocol (`frame.go`, `wire.go`).
33+- `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`).
34+- `internal/usecase/client`: the thin client — raw-mode ownership, stdin/resize pumps, writes received output bytes verbatim to the terminal. It interprets nothing.
35+- `internal/adapters`: `ipc` (unix socket framing, socket dir), `sshstdio` (remote attach), `pty`, `term`, `clock`.
36+- Other usecases (`keys`, `layout`, `copy`, `palette`, `picker`, `prompt`, `theme`, `ui`, …) are daemon-side features routed through `daemon/client.go`.
37+
38+### Ports interfaces and mocks (mockery)
39+
40+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).
41+
42+Mocks are generated by **mockery** (testify template), configured in `.mockery.yml`:
43+- `internal/ports` has `all: true` → every interface in the package gets a mock in `internal/ports/mocks/mocks.go` (package `portsmocks`, `MockTransport`, `MockPTY`, …).
44+- `internal/usecase/command.Context` gets a package-local `context_mock_test.go`.
45+
46+The workflow when adding or changing an abstraction:
47+1. Add/modify the interface in `internal/ports` (never define the interface in a usecase or adapter).
48+2. Run `make mocks` — the generated file is committed, and forgetting this step leaves stale mocks that still compile silently.
49+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).
50+
51+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.
52+
53+### Wire protocol rules
54+
55+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.
56+
57+- Client-originated message types are 1–15, server-originated 16+.
58+- 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.
59+- `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.
60+
61+### Session/attach flow
62+
63+- Ephemeral sessions (numbered) die on detach; named sessions survive headless. The daemon starts on first use and exits when the last session ends.
64+- 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.
65+- 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.
66+- Only one client per session: a new attach displaces the previous one (`detachReplacedClient`).
A CLAUDE.md +1 -0
@@ -0,0 +1,1 @@
1+AGENTS.md⏎̸
@@ -0,0 +1,1 @@
1+AGENTS.md⏎̸

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