Development
lazyshell is a Go project with no code generation and no exotic build step:
go build, go test, golangci-lint.
Build and test
$ make build # go build -o bin/lazyshell ./cmd/lazyshell
$ make test # go test -race ./...
$ make lint # golangci-lint run
CI runs the same commands on ubuntu-latest and macos-latest, plus a
performance budget (go test -run TestPerfBudget ./...). The
golangci-lint version is pinned in the workflow so CI and make lint
cannot disagree.
Package layout
cmd/
lazyshell/ main entrypoint
spike-pty/ phase-1 pty spike (kept for reference, not part of the binary)
pkg/
app/ bootstrap: load config, build SessionManager, run gui.Run()
session/ SessionManager: CRUD (New, Kill, List); Session{cmd, ptmx, scrollback,
status}; Env() (launch-time) and Stats() (per-OS CPU/RSS/disk sampling)
screen/ terminal emulator backing the output panel (vim/htop/less support)
gui/ gocui init, layout, keybindings, mouse, panels, tabs, help, theme,
notify, stats
tasks/ TaskManager (display/reading goroutines only)
agent/ AI agent state detection (config-free + hooks-driven)
hook/ authoritative hooks channel for agent sessions
config/ user config + project config (lazyshell.yml) loading
keys/ keybinding definitions
i18n/ strings/translations
version/ --version metadata (goreleaser-injected)
docs/
adr/ architecture decision records
repports/ historical analysis reports
Architecture decisions
-
Base library:
github.com/jesseduffield/gocui, withlazycore/pkg/boxlayoutfor the sessions-list / output-panel split and portrait-mode stacking on narrow terminals. -
Keybindings: flat
Binding{ViewName, Key, Modifier, Handler}passed tog.SetKeybinding, lazydocker-style — no lazygit-style controller/context-stack pattern. -
Async tasks:
pkg/tasksowns only display/reading goroutines, never the underlying shell process. Switching session selection cancels the reader for the previous session, not its pty: the shell keeps running regardless of what's on screen. -
PTY handling:
github.com/creack/pty, one pty per session,pty.Setsizepropagated from the panel's computed layout size on resize, and a full terminal emulator — not just ANSI stripping — so full-screen apps genuinely work inside a session. -
Process lifecycle:
pkg/session.Managerowns themap[sessionID]*Sessionand keeps shells alive across selection changes, decoupled frompkg/tasks. -
Agent coupling stays contained: outside
notify.goandstats.go, nothing inpkg/sessionorpkg/guiknows about agent-specific formats — it all lives inpkg/agentandpkg/hook.
ADRs
ROADMAP.md tells the phase-by-phase story: what is built, what is still open, and why.
Contributing
- Scope. Windows is deliberately out of scope (no Unix pty). So is detach/daemon mode ("agents keep running with the laptop closed"), unless real demand surfaces.
- Agent control API (agents creating panels or reading other sessions' output via a socket): decided against for now. The hooks socket is inbound and declarative only; an outbound control verb is a deliberately separate, not-yet-taken decision.
-
Before opening a pull request:
make testandmake lintmust pass, and the race detector must stay green.