Imported from rupert648/pertmux (
AGENTS.md). Install upstream withnpx skills add rupert648/pertmux. Copyright stays with the author.
pertmux: Agent Guide
This document provides a technical overview of the pertmux codebase for AI agents and developers.
Project Overview
pertmux is a Rust TUI unified SWE dashboard that links GitLab/GitHub MRs to local branches/worktrees, tmux sessions, and coding agent instances. It provides a real-time view of session status, resource usage, and progress with integrated merge request tracking across multiple forges. The bottom panel provides worktrunk-powered worktree management with create/remove/merge actions. The architecture is pluggable — new coding agents can be added by implementing the CodingAgent trait, and new forges can be added by implementing the ForgeClient trait.
Architecture
The project uses a daemon/client architecture with Unix socket IPC. A background daemon (pertmux serve) owns all data fetching and state, while a lightweight TUI client (pertmux connect) connects to render the UI.
Daemon/Client Split
- Daemon (
daemon.rs): Runs persistently in background. Owns theAppstruct (which is notSenddue todyn CodingAgent), runs on the main tokio task. Performs all data fetching on configurable timers: tmux/agent (refresh_interval, default 2s), MR detail (mr_detail_interval, default 60s), worktrees (worktree_interval, default 30s), MR list (mr_list_interval, default 300s). Listens on/tmp/pertmux-{USER}.sock. - Client (
client.rs): Lightweight TUI. Owns all UI state (ClientState: selection indices, popup state, notifications). Connects to daemon via Unix socket, receivesDashboardSnapshotupdates, sends commands (Refresh,CreateWorktree, etc.). Navigation is instant with no daemon round-trip. - Protocol (
protocol.rs): DefinesDashboardSnapshot,ProjectSnapshot,ClientMsg,DaemonMsg. Framed withLengthDelimitedCodec+serde_json. Multi-client viatokio::sync::broadcast. Also carriesCodexHookEventpayloads frompertmux codex-hookto the daemon.
Data Flow
- Daemon startup: Loads config, validates projects, creates
App, performs initial fetch of MRs + tmux + worktrees. - Refresh loops: Daemon runs configurable tiered timers (
refresh_intervaldefault 2s tmux,mr_detail_intervaldefault 60s MR detail,worktree_intervaldefault 30s worktrees,mr_list_intervaldefault 300s MR list). After each refresh, broadcastsDashboardSnapshotto all connected clients. - Client connect: Connects to daemon socket. Fails with clear error if daemon not running. Receives initial snapshot immediately.
- Client commands: User actions (refresh, worktree create/remove/merge, MR selection) are sent as
ClientMsgto daemon. Daemon processes, refreshes relevant data, broadcasts updated snapshot. - Codex hook updates: Optional Codex hooks call
pertmux codex-hook, which reads the Codex hook JSON from stdin and sendsClientMsg::CodexHookto the daemon. The daemon performs a fast pane refresh, applies the hook status hint, and broadcasts a new snapshot. - tmux actions:
switch_to_pane()andfind_or_create_pane()run client-side — they only need data from the snapshot, not daemon state.
Module Guide
- main.rs: Entry point. Uses clap for subcommands:
serve→daemonize()(ordaemon::run()with--foreground),connect→client::run(),stop→client::stop(),status→client::status(),cleanup→client::cleanup(),install --codex-hooks→codex_hooks::install(), hiddencodex-hook→codex_hooks::send_from_stdin().serveself-daemonizes by default: re-execs with--foreground, redirecting stdout/stderr to/tmp/pertmux-daemon.log, detached viaprocess_group(0). Validates config and checks for existing daemon before forking. Requires explicit subcommand (no barepertmux). - daemon.rs: Background daemon. Unix socket listener with
LengthDelimitedCodecframing. Broadcast channel for multi-client snapshot fan-out.Arc<Mutex<DashboardSnapshot>>for latest snapshot (sent to new clients immediately). HandlesClientMsgcommands and runs tiered refresh intervals. Tracks client count viaArc<AtomicUsize>and accumulates MR changes inpending_for_offlinewhen no clients are connected — drains them into the initial snapshot on reconnect. - codex_hooks.rs: Codex hook integration.
install()writes/merges global~/.codex/hooks.jsonentries by default, or repo-local.codex/hooks.jsonentries with--local, forSessionStart,UserPromptSubmit, andStopusing the current pertmux executable path.send_from_stdin()is the hook receiver: it reads Codex's JSON hook payload, connects to the daemon socket, drains the initial snapshot, sendsClientMsg::CodexHook, and stays quiet on stdout so Codex hook output parsing is not disturbed. It exits successfully if the daemon is not running. - client.rs: TUI client. Connects to daemon (fails with error screen if not running), owns
ClientStatewith all UI state (selections, popup, notification). Event loop withtokio::select!on keyboard + daemon messages. Local navigation (j/k/Tab) with no round-trip. Project switching via fuzzy finder (fkey). Also providesstop(),status(), andcleanup()commands. On reconnect, ifpending_changesis non-empty, opensChangeSummarymodal; for live snapshots, shows toast notifications. - protocol.rs: IPC protocol. Defines
DashboardSnapshot,ProjectSnapshot,GlobalMrEntry(cross-project MR entries),ClientMsg(commands from client to daemon),DaemonMsg(responses/snapshots from daemon to client),PROTOCOL_VERSIONfor handshake validation. Also definesActivityEntry(feed items),ActivityKind(display colour), andActivityTarget(navigation destination —Pane { pane_id, pane_path }for agent events,MergeRequest { project_name, iid }for forge events). Thetargetfield is#[serde(default)]for backwards compatibility. - app.rs: Owns the
Appstruct, which holds data state (panes, projects, MRs, worktrees). Manages refresh cycle, linking, andsnapshot()method to produceDashboardSnapshot. UI-related methods (selection, popup) have moved toClientStateinclient.rs. - coding_agent/mod.rs: Defines the
CodingAgenttrait andagents_from_config()factory. The trait requiresname(),process_name(),query_status(), andsend_prompt(). Currently supports opencode, Claude Code, and Codex CLI. To add a new agent, implement the trait and register it here. - coding_agent/opencode.rs: opencode implementation of
CodingAgent. Querieshttp://127.0.0.1:{port}/session/statusfor session state. Requires opencode to be started with--port 0so it launches its HTTP server on a random port. Port discovery happens automatically via process tree inspection indiscovery.rs. - coding_agent/claude_code.rs: Claude Code implementation of
CodingAgent. Reads JSONL transcript files from~/.claude/projects/and~/.claude/transcripts/to determine session status (Busy/Idle) and extract session details (token usage, messages, model). No HTTP server or special flags required — Claude Code writes transcripts automatically. - coding_agent/codex.rs: Codex CLI implementation of
CodingAgent. Reads from two SQLite databases in~/.codex/:state_5.sqlite(threads table with session metadata — title, model, cwd, tokens) andlogs_2.sqlite(trace-level log entries for busy/idle detection). Matches panes to threads viacwdpath comparison. Prompts delivered via tmuxsend-keys. No HTTP server or special flags required. Optional Codex hooks installed bypertmux install --codex-hooksprovide immediate status hints:UserPromptSubmit→ Busy,Stop→ Idle,SessionStart→ refresh metadata. Once a Codex session has emitted hook status, the hook-derived status is prioritized over SQLite polling for that session. - tmux.rs: Wraps tmux CLI commands. Responsible for identifying coding agent panes (filtered by registered process names), switching focus between them, and
find_or_create_pane()which searches all sessions for matching paths before creating new windows (prefers project-named sessions). Whendefault_agent_commandis configured,find_or_create_pane()creates a horizontal split: LEFT pane runs the agent command viasend-keys, RIGHT pane is an empty terminal. - discovery.rs: Implements port discovery. It uses
sysinfoto find child processes andnetstat2to map those processes to active TCP listening ports. - config.rs: Defines
Config,AgentConfig,ProjectConfig,ProjectForgeenum,KeybindingsConfig,GitLabSourceConfig,GitHubSourceConfig, and per-agent config structs. Loads from TOML with-c/--configCLI flag or~/.config/pertmux.toml. Validates local_path existence, source configuration, token availability, project name uniqueness, and keybinding uniqueness at startup.default_worktree_with_promptis an optional command template (uses{{msg}}placeholder) that powers the "create worktree with prompt" feature. - db.rs: Manages read-only access to the opencode SQLite database. Fetches session details and enriches pane information for opencode agents.
- types.rs: Defines shared data structures like
AgentPane,SessionDetail, and thePaneStatusenum. - ui/mod.rs: Entry point
draw_client(frame, &ClientState). Constants (ACCENT,NOTIFICATION_DURATION),ProjectRenderDataadapter, layout orchestration. - ui/helpers.rs: Formatting (
truncate,shorten_path,format_tokens), status badges, merge status display, scroll computation. - mr_changes.rs: Defines
MrChangeandMrChangeTypefor tracking MR status changes.MrChangecarries project name, MR iid/title, and change type.MrChangeTypecovers pipeline failures/successes, new discussions, and approvals. ImplementsDisplayfor toast messages. - ui/components/: Modular rendering components —
list_panel(left panel with MR list or agent panes),detail_panel(right panel with MR detail or session info),mr_sections(MR and worktree block layouts),cards(individual MR/worktree cards),overview(project list with MR counts),pipeline(CI/CD dot visualization),popup(worktree actions, fuzzy filter, MR overview, activity feed popup),notification(toast overlay — rendersClientState.notification),change_summary(reconnect modal showing accumulated MR changes),activity_feed(inline activity log in the lower-right panel — label width is dynamic based on available width, GLOW_SECS = 1800 so entries stay visible for 30 minutes). - worktrunk.rs: Serde types for
wt list --format=jsonoutput (WtWorktree,WtCommit,WtMain, etc.). Async functions:fetch_worktrees(),create_worktree(),remove_worktree(),merge_worktree(). Includesformat_age()helper and 9 unit tests. - linking.rs: Defines
DashboardState,LinkedMergeRequest. Implementslink_all()which connects MRs ↔ branches ↔ worktrees ↔ tmux panes ↔ Claude. - forge_clients/mod.rs: Re-exports
GitLabClientandGitHubClient. Sub-modules:traits,types,gitlab,github. - forge_clients/traits.rs: Defines the
ForgeClienttrait with#[async_trait(?Send)]. Methods:fetch_mrs(),fetch_mr_detail(),fetch_ci_jobs(),fetch_notes(),fetch_discussions(),fetch_user_mrs(). All forge clients implement this trait. - forge_clients/types.rs: Shared types used across all forges:
ForgeUser,MergeRequestSummary,MergeRequestDetail,MergeRequestNote,PipelineJob,PipelineInfo,UserMrSummary(cross-project MR for global user feed). - forge_clients/gitlab/client.rs: GitLab implementation of
ForgeClient. UsesPRIVATE-TOKENheader auth, fetches from/api/v4endpoints.fetch_ci_jobsextracts pipeline ID fromhead_pipeline. - forge_clients/github/client.rs: GitHub implementation of
ForgeClient. UsesBearertoken auth withUser-Agentheader. Converts GitHub PR/check-run responses to shared types.fetch_ci_jobsuseshead_shato fetch check runs. Supports GitHub Enterprise via custom host. - forge_clients/github/types.rs: Raw GitHub API response types (internal):
GhPullRequest,GhUser,GhPrRef,GhCheckRunsResponse,GhCheckRun,GhIssueComment,GhIssueItem,GhPullRequestStub,GhRepository. - git.rs: Git worktree discovery.
discover_worktrees(path)runsgit worktree list --porcelainand returnsVec<WorktreeInfo>. - read_state.rs: Local SQLite DB for per-comment read/unread tracking.
ReadStateDbtracks seen notes and MR view timestamps.
Key Design Decisions
- Pluggable Agents: The
CodingAgenttrait abstracts process detection, status querying, pane enrichment, session detail fetching, and prompt delivery. Each agent handles its own discovery mechanism and communication channel internally. Thesend_prompt()trait method allows each agent to deliver prompts through its own mechanism (e.g. opencode uses its HTTP API, Claude Code uses tmux send-keys). - Multi-Forge Support:
ForgeClienttrait abstracts GitLab and GitHub behind a common interface.ProjectState.clientisBox<dyn ForgeClient>. Each forge handles its own API auth, response parsing, and state normalization (e.g. GitHub"open"→"opened", check runs → pipeline jobs). - Multi-Project Support:
[[project]]TOML array with per-project forge config (source = "gitlab"or"github"), local paths, and worktree state. Fuzzy finder (fkey) for project switching. Overview panel shows all projects with MR counts. - Worktrunk CLI Integration: Uses
wt list --format=json(NOT the library crate — author warns API is unstable).wtsupports-C <path>to target specific repos. Worktree actions (create/remove/merge) via popup dialogs. - Optional Config: Supports
-c/--configfor a TOML config file. Defaults to~/.config/pertmux.toml, falls back to built-in defaults if absent. - Startup Validation: Config
validate()checks local_path existence, source configuration, token availability, and project name uniqueness. Fails fast with clear error messages. - Read-Only DB Access: Opens the SQLite database with
SQLITE_OPEN_READ_ONLYto avoid locking issues or accidental corruption. - Smart Pane Focus:
find_or_create_pane()first searches ALL panes across ALL tmux sessions bypane_current_path(canonicalized). If no match, prefers a session whose name matches the project name (case-insensitive). Falls back to other-client heuristic, then current session. Whendefault_agent_commandis set, new windows are created as a horizontal split with the agent in the left pane and an empty terminal on the right; focus lands on the left (agent) pane. Without the config, behavior is a single pane (backwards compatible). - Create Worktree with Prompt: When
default_worktree_with_promptis configured, pressing'w'(configurable viaopen_worktree_with_prompt) opens a two-field modal: branch name and message. The message is substituted into the{{msg}}placeholder of the template to produce the command passed tofind_or_create_pane()as the agent command. The daemon creates the worktree via worktrunk; the client then opens the tmux pane with the filled command on the next snapshot update (pending_open_worktreeinClientState). - Responsive Layout: The UI adapts to landscape and portrait terminal dimensions.
- Process Tree Walking: Port discovery relies on finding the specific child process of the tmux pane that owns the API socket.
- MR-first layout: When a forge (
[gitlab]or[github]) is configured, the primary list entity is open MRs/PRs. Worktrees appear in a dedicated bottom section with navigation and actions. - Codex Hook Fast Path:
pertmux install --codex-hooksinstalls global Codex hook definitions in~/.codex/hooks.jsonthat callpertmux codex-hook;--localinstalls into the current repo's.codex/hooks.json. Codex still requires non-managed command hooks to be reviewed/trusted with/hooks(or run once with--dangerously-bypass-hook-trust). The hook path is prioritized over the SQLite polling heuristic for sessions that have emitted hook status, while SQLite polling remains the fallback for sessions without hooks or missed hook events. - Tiered refresh: Daemon runs configurable timers — tmux/agent (
refresh_intervaldefault 2s), MR detail (mr_detail_intervaldefault 60s), worktrees (worktree_intervaldefault 30s), MR list (mr_list_intervaldefault 300s). MR list also refreshed on manual 'r' or daemon startup. - Backwards compatibility: No forge config (
[gitlab]/[github]) = v1 behavior unchanged (agent-only mode). - Async runtime: tokio + crossterm EventStream.
CodingAgenttrait stays sync (not Send) — daemon keepsAppon main task. - Daemon/Client IPC:
tokio::net::UnixStreamwithtokio_util::codec::LengthDelimitedCodecframing andserde_jsonserialization. Multi-client viatokio::sync::broadcast. Client requires daemon to be running (no auto-start). - Socket path:
/tmp/pertmux-{USER}.sock. Stale socket cleaned up on daemon startup. - Self-daemonizing:
pertmux servevalidates config, checks for existing daemon, then re-execs itself with--foregroundin a new process group with stdout/stderr redirected to/tmp/pertmux-daemon.log. The parent prints the PID and exits immediately.--foregroundruns the daemon in the terminal for debugging. - Daemon lifecycle: Runs until killed or
pertmux stop. No idle timeout. Single daemon per user.
Dependencies
- ratatui: TUI framework for rendering.
- crossterm: Terminal abstraction for raw mode and event handling.
- ureq: Minimal, synchronous HTTP client for agent API calls.
- rusqlite: SQLite bindings (using the
bundledfeature). - serde / serde_json: Serialization for API responses, worktrunk JSON, and daemon/client IPC.
- sysinfo: Process management and tree traversal.
- netstat2: Socket-to-process mapping.
- dirs: Cross-platform path resolution for the database location.
- clap: CLI argument parsing (subcommands: serve, connect, stop, status, cleanup).
- toml: Configuration file parsing.
- anyhow: Error handling.
- tokio: Async runtime (full features). Used for daemon event loop and client I/O.
- tokio-util:
LengthDelimitedCodecfor daemon/client IPC framing. - bytes: Byte buffer for IPC messages.
- reqwest: Async HTTP client for forge APIs — GitLab and GitHub (json feature).
- async-trait: Async trait support for
ForgeClienttrait (#[async_trait(?Send)]). - futures: StreamExt for crossterm EventStream and IPC streams.
Landing Page & Docs Site
The docs/ directory contains the project website: a marketing landing page + full documentation site built with Astro + Starlight + Tailwind.
Tech Stack
- Astro 5: Static site generator (island architecture, near-zero JS)
- Starlight: Astro's docs theme (sidebar, search via Pagefind, dark/light mode)
- Tailwind 3: Utility CSS with custom config (
docs/tailwind.config.mjs) - Fonts: Outfit (sans), JetBrains Mono (mono) via Google Fonts
Structure
docs/src/pages/index.astro: Custom landing page (standalone, NOT Starlight layout)docs/src/components/: Landing page sections —Navbar,Hero,LinkingDiagram,FeatureGrid,GettingStarted,Footerdocs/src/content/docs/: Markdown docs rendered by Starlight with sidebar navigationgetting-started/: Installation, Quick Start, tmux Integrationconfiguration/: Config Reference, Multi-Project, Forge Setup, Agent Configfeatures/: MR Tracking, Worktree Management, Agent Monitoring, Pipeline Visualizationreference/: Keybindings, Architecture, CLI Commands, Extending
docs/src/styles/custom.css: Tailwind directives + Starlight theme overridesdocs/astro.config.mjs: Starlight sidebar config, social links, custom CSSdocs/tailwind.config.mjs: Custom colors (accent orange#FF8C00, gray palette), fonts, Starlight plugin
Routes
/— Landing page (custom Astro page, no Starlight layout)/getting-started/installation/— First docs page (Starlight routes docs at root, no/docs/prefix)- All docs pages follow Starlight's file-based routing from
src/content/docs/
Build & Dev
cd docs
npm install
npm run dev # Dev server on localhost:4321
npm run build # Static build to docs/dist/
npm run preview # Preview the build
Visual Identity
- Dark theme primary:
#0e1015(gray-950) - Cards/sections:
#17191e(gray-900) - Accent:
#FF8C00(orange, matches TUIACCENTcolor) - Screenshot placeholders exist in Hero and LinkingDiagram sections — replace with actual TUI screenshots/GIFs
Conventions
- Landing page is a STANDALONE Astro page — does NOT use Starlight's layout or components
- Internal doc cross-links use root-relative paths (
/getting-started/quick-start/), NOT/docs/prefix - All icons are inline SVGs — no icon library dependencies
- No React/Vue/framework components — pure Astro + HTML + Tailwind
Build & Run
- Install:
cargo install pertmux(from crates.io) orcargo install --path .(from source) - Build:
cargo build --release - Start daemon:
pertmux serve(backgrounds automatically;--foregroundto keep in terminal) - Connect client:
pertmux connect(daemon must be running) - Stop daemon:
pertmux stop - Check status:
pertmux status - Install Codex hooks:
pertmux install --codex-hooks(writes global~/.codex/hooks.json; use--localfor the current repo; trust with Codex/hooks) - Requirements: Must run inside a tmux session. Requires coding agent instances (e.g. opencode, Claude Code) to be running in other tmux panes to display data.
- Edition: Rust 2024.
CI (GitHub Actions)
Two separate workflows with path filters — only the relevant workflow runs when files change.
Rust (.github/workflows/rust.yml)
Triggers on changes to src/**, Cargo.toml, Cargo.lock.
- Format:
cargo fmt --all --check - Clippy:
cargo clippy --all-targets --all-features -- -D warnings(withSwatinem/rust-cache,shared-key: bundled) - Test:
cargo test --all-features(runs after fmt + clippy pass, shares the cache) - Uses
dtolnay/rust-toolchain@stable(not the unmaintainedactions-rs) rusqlitebundled feature compiles SQLite from C source — no system deps needed on runnersCARGO_INCREMENTAL: 0to avoid wasting cache space in CI
Docs (.github/workflows/docs.yml)
Triggers on changes to docs/**.
- Astro Check:
npx astro check(TypeScript diagnostics on.astrofiles, requires@astrojs/check+typescriptdevDependencies) - Build:
npm run build(runs after check passes) - Node 22,
npm ciwith cache scoped todocs/package-lock.json ASTRO_TELEMETRY_DISABLED: trueon build step
Both workflows use concurrency groups to cancel in-progress runs when new commits are pushed.
Important Paths & Endpoints
- Daemon socket:
/tmp/pertmux-{USER}.sock - Daemon log:
/tmp/pertmux-daemon.log - opencode Database:
~/.local/share/opencode/opencode.db - opencode API Endpoint:
http://127.0.0.1:{port}/session/status - Claude Code Transcripts:
~/.claude/projects/and~/.claude/transcripts/ - Codex State DB:
~/.codex/state_5.sqlite - Codex Logs DB:
~/.codex/logs_2.sqlite - GitLab API:
https://{host}/api/v4/projects/{project}/merge_requests - GitHub API:
https://api.github.com/repos/{owner}/{repo}/pulls(orhttps://{host}/api/v3/for GHE) - Read state DB:
~/.local/share/pertmux/read_state.db
Conventions
- Data state (panes, projects, MRs) resides in the
Appstruct (daemon-side). UI state (selection, popup, notification) resides inClientState(client-side). - UI rendering logic in
ui.rsshould be pure and not trigger side effects. - Status priority for display: Busy > Retry > Idle > Unknown.
link_all()is pure logic — receives pre-fetched data, no I/O except read_state queries.- All path comparisons use
std::fs::canonicalize()to handle symlinks. - GitLab token:
PERTMUX_GITLAB_TOKENenv var overrides config file token. GitHub token:PERTMUX_GITHUB_TOKEN. ProjectForgeis an enum (Gitlab,Github) — not a string. Validated at parse time.- Worktrunk integration uses CLI wrapper only (
wt list --format=json), NOT the library crate. - Do NOT use
--fullor--branchesflags onwt list(adds network calls). - Do NOT use
statuslinefield from wt output (contains ANSI escape codes). Usesymbolsfield instead. - No unsafe code. Manual validation with
anyhow(no validation crate). ACCENTcolor constant:Color::Rgb(255, 140, 0)(orange).- Toast notifications: Use
ClientState::notify(msg)to show a temporary toast overlay. It setsnotification: Option<(String, Instant)>and auto-expires afterNOTIFICATION_DURATION(2s). The notification component (ui/components/notification.rs) renders it in the bottom-right corner. Use for action feedback (e.g. "Refreshing...", "Creating worktree...", "Copied: branch-name"). The daemon'sActionResultresponse also triggers a toast with the result message. - Action keybindings are configurable via
[keybindings]in the TOML config (e.g.mr_overview,merge_worktreewhich now defaults toM,activity_feedwhich defaults toA,open_worktree_with_promptwhich defaults tow). Navigation keys (j/k/↑/↓/Tab/Enter/Esc/q) are not configurable. - Never add AI co-author trailers (e.g.
Co-authored-by: Sisyphus ...) to commits.
