AGENTS.md RawHistoryFolder

vev agent guide

Project

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.

Commands

go build -o vev .
make test
go test ./internal/usecase/daemon -race -run TestName
make lint
make mocks
go test ./internal/adapters/ipc ./internal/usecase/daemon -run '^$' -bench=. -benchmem
  • Format with goimports.
  • Install dev tools once with go install golang.org/x/tools/cmd/goimports@latest and go install github.com/vektra/mockery/v3@v3.8.0.
  • Use Mockery v3 consistently across Go projects.
  • make test runs go test ./... -race.
  • make lint checks goimports -l, then go vet.
  • Enable daemon pprof with VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon.
  • Set VEV_LOG=debug|warn|error to change log verbosity (default info).
  • Logs live in $XDG_STATE_HOME/vev, or ~/.local/state/vev when unset.
  • Use feature worktrees under .worktrees/<branch-name>. Remove the worktree once its branch is merged or abandoned (git worktree remove <path>).
  • Create vev-owned private directories with pkg/safedir.EnsurePrivate, not os.MkdirAll.

Architecture rules

The exhaustive production matrix and separate test-import policy are enforced by boundary_test.go; package ownership is documented in docs/architecture.md.

  • pkg/ is reusable and never imports internal/.
  • Production use cases import only internal/ports, semantic internal/protocol packages, internal/domain, and approved sibling use cases.
  • Production use cases never import internal/protocol/wire, concrete adapters, internal/app, internal/persist, or internal/platform.
  • internal/ports owns application seams, not codecs, raw frames, environment policy, or worker implementations.

Layer map:

  • main.go → internal/app: CLI parsing, composition, daemon startup, and hidden subcommands.
  • internal/domain: pure shared values and terminal capability policy.
  • internal/protocol: typed session messages; protocol/catalogue owns remote JSON schema; protocol/wire owns binary encoding and raw carriage contracts.
  • internal/ports: typed application connections and infrastructure-facing use-case seams.
  • internal/adapters/sessionwire: typed message ↔ raw frame adaptation.
  • internal/usecase/daemon: sessions, tabs, panes, VT screens, renderer shadows, and daemon features.
  • internal/usecase/client: raw-mode thin client; writes output bytes verbatim. On remote attachments it also mirrors them in a local VT screen for predictive echo.
  • internal/adapters: IPC, UDP, SSH stdio, PTY, terminal, clock, and observability implementations.

Before touching daemon teardown paths, read the lock-ordering notes at the top of internal/usecase/daemon/client.go.

Ports and mocks

  • Define application cross-layer interfaces in internal/ports; raw frame carriage interfaces live in internal/protocol/wire.
  • Adapters implement ports and raw carriage contracts; use cases consume application connections through internal/ports and typed semantic values from internal/protocol, never raw carriage contracts.
  • Tests should prefer generated portsmocks.MockX with .EXPECT().
  • Hand-written fakes are okay only when mocks make the test harder to understand.
  • After changing a port, run make mocks and commit the generated updates.
  • Time-dependent daemon code must use ports.Clock/ports.Timer, not direct wall-clock APIs.

Tests

  • Write Go tests as table-driven tests by default.
  • Keep wire tests byte-for-byte and include truncated-prefix and trailing-garbage coverage.
  • Prefer fake clocks over sleeps.

Wire protocol

Typed messages and negotiated version live in internal/protocol. Remote discovery JSON lives in internal/protocol/catalogue. Protobuf schemas, generated wire envelopes, strict scanning, encoded bounds, and raw carriage interfaces live in internal/protocol/wire; internal/adapters/sessionwire owns semantic conversion and Output compression. Shared stream framing lives in internal/adapters/streamframe.

  • Stream frames are a 4-byte big-endian length followed by one complete serialized directional Protobuf envelope.
  • Client and server directions are separate closed oneof unions in internal/protocol/wire/schema/envelope.proto.
  • Every connection starts with the bounded preamble; magic, epoch, role, protocol version, and negotiated limits are validated before application messages.
  • Version negotiation requires strict equality. Bump internal/protocol.Version for semantic or message layout changes; bump wire.ProtocolEpoch only for an intentional clean break.
  • Generated *.pb.go files are never edited. Run go tool buf generate and make protocol-check after schema changes.

Session flow

  • Ephemeral numbered sessions survive detach while the daemon retains them, but are not persisted. With ephemeral.close-on-exit (default on) the daemon removes one when its last attachment ends definitively: a client Detach{Closed} (sent on SIGHUP/SIGTERM) or an expired park/suspension. Parked, parking, suspended, or live attachments keep it.
  • Named sessions survive headless and persist across daemon restarts.
  • The daemon starts on first use and survives an empty session registry: final session removal is not shutdown, so it can be reused; it ends only on an explicit shutdown request, kill --all, or process cancellation.
  • A bulk purge (kill --sessions) takes a transient admission gate and bounds each phase with its own deadline: admission drain, stopped/broken sweep, and a shared live-teardown budget. A repository delete that ignores cancellation is detached behind an exact name/incarnation/created-time fence and reported as a typed failure instead of holding purge admission or control.
  • A live unit whose teardown aborts before destructive ownership stays registered, and its snapshot-coordinator quarantine is rolled back so the surviving session resumes checkpoint scheduling and its attached clients are unfrozen.
  • Each connection has a 15-second handshake budget from connect through the initial committed publication.
  • Local and remote attach use the same typed Hello/Welcome session protocol. sessionwire translates typed traffic to Protobuf envelopes carried by IPC, QUIC, or SSH stdio.
  • Remote attach bootstraps an authenticated direct QUIC connection by default; vev host add --transport ssh <host> explicitly selects an SSH-only carriage.
  • A session owns shared PTYs, VT state, tabs, panes, PTY content geometry selected by the latest valid attachment claim, and ordered mutations. Each attachment owns its window/view, copy and overlay state, rendering/output state, and reconnect lifecycle; when the latest claimant detaches, the most recently claimed remaining attachment becomes authoritative.
  • Command requests have a 10-second result deadline and are tracked per connection.
source
1# vev agent guide
2
3## Project
4
5vev 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## Commands
8
9```sh
10go build -o vev .
11make test
12go test ./internal/usecase/daemon -race -run TestName
13make lint
14make mocks
15go test ./internal/adapters/ipc ./internal/usecase/daemon -run '^$' -bench=. -benchmem
16```
17
18- Format with **goimports**.
19- Install dev tools once with `go install golang.org/x/tools/cmd/goimports@latest` and `go install github.com/vektra/mockery/v3@v3.8.0`.
20- Use Mockery v3 consistently across Go projects.
21- `make test` runs `go test ./... -race`.
22- `make lint` checks `goimports -l`, then `go vet`.
23- Enable daemon pprof with `VEV_PPROF_ADDR=127.0.0.1:6060 vev --daemon`.
24- Set `VEV_LOG=debug|warn|error` to change log verbosity (default info).
25- Logs live in `$XDG_STATE_HOME/vev`, or `~/.local/state/vev` when unset.
26- Use feature worktrees under `.worktrees/<branch-name>`. Remove the worktree once its branch is merged or abandoned (`git worktree remove <path>`).
27- Create vev-owned private directories with `pkg/safedir.EnsurePrivate`, not `os.MkdirAll`.
28
29## Architecture rules
30
31The exhaustive production matrix and separate test-import policy are enforced by `boundary_test.go`; package ownership is documented in `docs/architecture.md`.
32
33- `pkg/` is reusable and never imports `internal/`.
34- Production use cases import only `internal/ports`, semantic `internal/protocol` packages, `internal/domain`, and approved sibling use cases.
35- Production use cases never import `internal/protocol/wire`, concrete adapters, `internal/app`, `internal/persist`, or `internal/platform`.
36- `internal/ports` owns application seams, not codecs, raw frames, environment policy, or worker implementations.
37
38Layer map:
39
40- `main.go` → `internal/app`: CLI parsing, composition, daemon startup, and hidden subcommands.
41- `internal/domain`: pure shared values and terminal capability policy.
42- `internal/protocol`: typed session messages; `protocol/catalogue` owns remote JSON schema; `protocol/wire` owns binary encoding and raw carriage contracts.
43- `internal/ports`: typed application connections and infrastructure-facing use-case seams.
44- `internal/adapters/sessionwire`: typed message ↔ raw frame adaptation.
45- `internal/usecase/daemon`: sessions, tabs, panes, VT screens, renderer shadows, and daemon features.
46- `internal/usecase/client`: raw-mode thin client; writes output bytes verbatim. On remote attachments it also mirrors them in a local VT screen for predictive echo.
47- `internal/adapters`: IPC, UDP, SSH stdio, PTY, terminal, clock, and observability implementations.
48
49Before touching daemon teardown paths, read the lock-ordering notes at the top of `internal/usecase/daemon/client.go`.
50
51## Ports and mocks
52
53- Define application cross-layer interfaces in `internal/ports`; raw frame carriage interfaces live in `internal/protocol/wire`.
54- Adapters implement ports and raw carriage contracts; use cases consume application connections through `internal/ports` and typed semantic values from `internal/protocol`, never raw carriage contracts.
55- Tests should prefer generated `portsmocks.MockX` with `.EXPECT()`.
56- Hand-written fakes are okay only when mocks make the test harder to understand.
57- After changing a port, run `make mocks` and commit the generated updates.
58- Time-dependent daemon code must use `ports.Clock`/`ports.Timer`, not direct wall-clock APIs.
59
60## Tests
61
62- Write Go tests as table-driven tests by default.
63- Keep wire tests byte-for-byte and include truncated-prefix and trailing-garbage coverage.
64- Prefer fake clocks over sleeps.
65
66## Wire protocol
67
68Typed messages and negotiated version live in `internal/protocol`. Remote discovery JSON lives in `internal/protocol/catalogue`. Protobuf schemas, generated wire envelopes, strict scanning, encoded bounds, and raw carriage interfaces live in `internal/protocol/wire`; `internal/adapters/sessionwire` owns semantic conversion and Output compression. Shared stream framing lives in `internal/adapters/streamframe`.
69
70- Stream frames are a 4-byte big-endian length followed by one complete serialized directional Protobuf envelope.
71- Client and server directions are separate closed `oneof` unions in `internal/protocol/wire/schema/envelope.proto`.
72- Every connection starts with the bounded preamble; magic, epoch, role, protocol version, and negotiated limits are validated before application messages.
73- Version negotiation requires strict equality. Bump `internal/protocol.Version` for semantic or message layout changes; bump `wire.ProtocolEpoch` only for an intentional clean break.
74- Generated `*.pb.go` files are never edited. Run `go tool buf generate` and `make protocol-check` after schema changes.
75
76## Session flow
77
78- Ephemeral numbered sessions survive detach while the daemon retains them, but are not persisted. With `ephemeral.close-on-exit` (default on) the daemon removes one when its last attachment ends definitively: a client `Detach{Closed}` (sent on SIGHUP/SIGTERM) or an expired park/suspension. Parked, parking, suspended, or live attachments keep it.
79- Named sessions survive headless and persist across daemon restarts.
80- The daemon starts on first use and survives an empty session registry: final session removal is not shutdown, so it can be reused; it ends only on an explicit shutdown request, `kill --all`, or process cancellation.
81- A bulk purge (`kill --sessions`) takes a transient admission gate and bounds each phase with its own deadline: admission drain, stopped/broken sweep, and a shared live-teardown budget. A repository delete that ignores cancellation is detached behind an exact name/incarnation/created-time fence and reported as a typed failure instead of holding purge admission or control.
82- A live unit whose teardown aborts before destructive ownership stays registered, and its snapshot-coordinator quarantine is rolled back so the surviving session resumes checkpoint scheduling and its attached clients are unfrozen.
83- Each connection has a 15-second handshake budget from connect through the initial committed publication.
84- Local and remote attach use the same typed `Hello`/`Welcome` session protocol. `sessionwire` translates typed traffic to Protobuf envelopes carried by IPC, QUIC, or SSH stdio.
85- Remote attach bootstraps an authenticated direct QUIC connection by default; `vev host add --transport ssh <host>` explicitly selects an SSH-only carriage.
86- A session owns shared PTYs, VT state, tabs, panes, PTY content geometry selected by the latest valid attachment claim, and ordered mutations. Each attachment owns its window/view, copy and overlay state, rendering/output state, and reconnect lifecycle; when the latest claimant detaches, the most recently claimed remaining attachment becomes authoritative.
87- Command requests have a 10-second result deadline and are tracked per connection.

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