Skip to content

Concepts

The full architectural deep-dive lives in DESIGN.md in the repo. This page is the short version — one-line definitions for everything you’ll see in the TUI and the TOML.

Concepts at a glance
Workspace
Named view over projects + sessions (committable TOML file)
Session
name + cwd + command + kind + env

Live   Idle   ? Untracked

Three states, each with a distinct TUI marker
Project
A directory on disk (referenced, never moved)
Multiplexer
tmux / zellij — owns terminal panes, pa drives it, never replaces it

A directory on disk with code or content you work on. Registered with portagenty at any of three tiers (global, workspace, or per-project portagenty.toml). Identified by its filesystem path.

A named, curated view over one or more projects plus the sessions you use to work on them. First-class file on disk (*.portagenty.toml), designed to be committable. A workspace is where “hierarchy on top of hierarchy” happens — same underlying projects, many possible views (recency, tags, custom groups — the latter two still on the roadmap).

One unit of execution: a shell, a process, an agent. Core schema is name + cwd + command, plus optional kind (display hint) and env (key-value env vars). A session belongs to a workspace.

Shown in the TUI as colored markers next to each row:

MarkerStateSourceEnter does
● (green)LiveWorkspace session, currently running in mpxattach
○ (dim)IdleWorkspace session, not yet startedcreate_and_attach
? (yellow)UntrackedRunning in mpx, not in workspace TOMLattach

Untracked = the tmux/zellij session you started manually last week that pa can see via list-sessions and let you re-attach to.

Tracked session lifecycle
Idle — in TOML, not running
Enter → create_and_attach
Live — running in mpx
x → kill → ○ IdleEnter → attach
Untracked session
?Untracked — in mpx, not in TOML
Enter → attach

Sessions you started manually outside pa. Visible via list-sessions.

Optional kind: field on a session: claude-code, opencode, editor, dev-server, shell, or other. The TUI shows a one-letter colored glyph (C / O / E / D) next to the state marker. For kind = "claude-code", pa launch --resume and pa claim --resume append --continue before launch so Claude picks up its prior conversation. Other kinds get a one-line hint.

tmux or zellij. The thing that owns the terminal panes and keeps them alive across detaches. portagenty drives it via its CLI — it doesn’t replace it. Each adapter (TmuxAdapter, ZellijAdapter) is a Rust implementation of the Multiplexer trait.

Three shapes a pa attach can take:

  • Takeover (default on tmux): detach any other clients on attach. Session keeps running; the other device’s client returns to its shell. Fixes the “screen size stuck to smaller client” multi-client issue.
  • Shared (--shared): attach without disturbing other clients. Pass --shared to pa launch.
  • Fresh (--fresh): kill any existing session with this name before launching, then create anew. Loses running state. Main use case: zellij takeover (see below).

pa claim is a short verb for takeover-attach; it defaults the session name to the first one in the workspace and passes through --fresh / --resume.

The default takeover semantics work cleanly on tmux — the mpx natively supports detach-client -a (kick others). On zellij, it’s a different story:

Default--shared--freshFrom inside (paclaim)
tmuxreal takeover, keeps stateexplicit sharedoverkill — wipes statedetach-client -a — instant
zellijde-facto shared (no kick)same as defaultthe only real takeoverprints limitation + --restart to kill

Zellij doesn’t expose per-client disconnection in any form (checked against 0.43.1 — no action disconnect-client, no equivalent). So “takeover” on zellij means “kill the session + recreate” — you lose running state but the other client drops because the session it was attached to is gone.

If you need hard takeover without losing state, either use tmux for that workspace (press m on the row in the session list to switch) or accept shared clients (zellij is designed for them).

Sessions + project registrations can be declared at:

  1. Global$XDG_CONFIG_HOME/portagenty/config.toml. Machine-local, not committed.
  2. Workspace — any *.portagenty.toml. Meant to commit.
  3. Per-projectportagenty.toml at a project root. Meant to commit.

Merge rule on session-name collision: workspace > per-project > global. Closer to the user’s current intent wins.

Workspace
>
Per-project
>
Global
on collision

$XDG_STATE_HOME/portagenty/state.toml. Records a bounded LRU of recent launches. Machine-local, not committed. Feeds the picker’s recency sort and the session list’s “LAST” column.

When you run bare pa from a directory with no walkable *.portagenty.toml, pa shows a picker: a ratatui home screen listing every workspace registered in your global config, sorted by recency (most-recent on top, never-launched alphabetical below). A bottom “live sessions on this machine” row gives you an ad-hoc browse mode — attach to any live tmux/zellij session without authoring TOML.

Auto-registration: pa init and the onboarding wizard both append the new workspace to [[workspace]] in the global config, so future pa invocations see it from anywhere.

Navigation follows Android-back semantics:

  • Esc from the session TUI → always returns to the picker.
  • Esc / q from the picker → exit pa.
  • q / Ctrl+C from anywhere → exit pa directly.

The picker is also a “jump to another workspace” affordance for walk-up users: enter via walk-up, press Esc once, and you’re on the home screen with every other registered workspace one keypress away.

The explorer encodes state with color, not just glyphs:

  • Title bar shows a colored mpx badge — cyan [tmux], magenta [zellij] — plus session count and an untracked-count badge in yellow when live sessions exist outside your workspace definition.
  • Session rows color the name itself, not just the marker: green for Live, dim for NotStarted, yellow for Untracked.
  • Kind glyphs get per-kind colors (blue C for claude-code, cyan O for opencode, magenta E for editor, green D for dev-server).
  • Attached-client count on tmux live rows: [live · 2 clients] / [live · 1 client] / [live · detached]. Zellij doesn’t expose per-session client counts, so those stay [live].
  • Recency shows twice: the picker lists “X ago” per workspace, the session list adds a LAST column on live rows at widths ≥ 80 cols.

At widths below 60 columns, each session row renders as a two-line card — marker + name + status on line 1, indented dim cmd · path on line 2 — so the essentials stay readable on a phone keyboard in portrait. The footer’s keybind hints shorten to fit (Esc: back · q: quit at the narrowest). See Termux for the full mobile story.

Multiplexer session names are workspace-scoped: a session "shell" in workspace "my-project" becomes my-project-shell in the mpx. This prevents the collision where two workspaces both defining a "shell" session would silently share the same tmux/zellij session.

The TUI display name stays unprefixed — you see shell, the mpx sees my-project-shell. The mapping is handled by workspace_session_name() in the sanitize module.

Press n in the workspace picker to open a centered search overlay. Type to fuzzy-search your filesystem for project folders. Tiered backends fire in order:

  1. Recency — recent launches from state.toml (instant).
  2. Zoxide — frecency scores, if installed.
  3. plocate / locate / Everything CLI — pre-built indexes.
  4. fd — live walk respecting .gitignore.
  5. Stdlib walker — always-available fallback (depth-capped, ignores .git, node_modules, target, etc.).

Each tier is silently skipped when its tool isn’t installed. Results are deduped and ranked by nucleo (Helix’s pure-Rust fuzzy matcher). Enter on a candidate either opens an existing workspace there or scaffolds a new one with a confirm prompt.

Find pipeline — fastest to broadest
1Recency — state.toml, instant, ~10 paths
2Zoxide — frecency scores, instant, ~50 paths
3plocate / locate / Everything — pre-built index
4fd — .gitignore-aware live walk
5Stdlib walker — always available, depth-capped

dedup on canonical path → nucleo fuzzy rank → top N

Press Ctrl+T inside the find overlay to switch to a filesystem tree view. Directories expand on Enter (lazy read_dir, cached), and collapse on /Backspace. Shift+Enter selects the highlighted directory. A marquee breadcrumb shows the current path. Used by both the n (new workspace) flow and the e → c (edit cwd) flow.

Press e on any tracked session row to edit it without leaving the TUI. A five-stage state machine walks you through: pick a field (name, cwd, command, kind, env) → type a new value or pick from a list. CWD editing opens the find/tree overlay for visual browse. All writes are comment-preserving via toml_edit.

When pa walks up from $PWD and finds a workspace file that isn’t in the global [[workspace]] registry, it silently appends the new path. This makes folder moves transparent: you don’t need to manually re-register a workspace after moving its parent directory.

Claude Code stores conversations at ~/.claude/projects/<path-encoded-cwd>/. Two scenarios break this:

  • Folder moves — move a project and the encoded path changes. --continue can’t find old sessions.
  • WSL vs Windows — the same project gets different encoded paths in each environment. Context is siloed.
  • Content poisoning — even when you bridge the storage dirs, WSL-authored conversations have /mnt/c/… paths baked into the content itself. A Windows Claude resuming that content hits Read errors on the first path reference.
  • Stable id field (UUIDv4) in every new workspace TOML. Committed, survives git clone, gives external tools a path-independent handle.
  • previous_paths auto-maintenance. When walk-up re-registers a workspace at a new location (i.e. the folder was moved), pa appends the old directory to the TOML’s previous_paths list. The trail of past locations travels with the repo.
  • pa convos shim. Forwards to pconv with the workspace’s TOML auto-injected, so lookups scope to this workspace’s projects + previous_paths.
  • pa init --with-agent-hooks scaffolds .mcp.json + .claude/skills/ so Claude Code agents self-discover portaconv when they enter the workspace.

portaconv reads each agent CLI’s conversation storage (Claude Code first) read-only and emits paste-ready markdown — optionally with WSL↔Windows path rewriting (--rewrite wsl-to-win / win-to-wsl). Also ships as an MCP server (pconv mcp serve) so any MCP-aware agent can query past conversations directly.

You don’t try to resume in place. You extract what you said, paste it into a fresh session on whatever host is in front of you, and the new session picks up the thread — with the paths already rewritten to the new OS.

Terminal window
# List this workspace's Claude Code sessions (scoped via pa)
pa convos list
# Dump a specific session as paste-ready markdown
pa convos dump <session-id>
# Same, with WSL paths rewritten to Windows before pasting
pa convos dump <session-id> --rewrite wsl-to-win

See the dedicated Portaconv integration concept page for the full move-recovery + paste-ready story, and pa convos / pa init --with-agent-hooks command reference for the surface.

If Claude Code adds native project-ID or path-migration support, the id + previous_paths fields become redundant for that use case but remain useful as general workspace-identity anchors.

When you press Enter to attach, pa restores your terminal and prints a one-line banner before the multiplexer takes over:

pa → zellij session "claude"
detach: Ctrl+O then d · re-attach: pa claim claude

Informational only — pa never rebinds your mpx keys. The detach chord shown is the multiplexer’s default; if you’ve customized it, use your own chord. (Opinionated mpx-config belongs in a dev- environment scaffold, not in the launcher.)