projmux 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ # npm Distribution
2
+
3
+ `projmux` remains a Go CLI. npm is a distribution channel that installs a
4
+ small Node.js shim plus one platform-specific Go binary package.
5
+
6
+ The public npm package `projmux` is the root shim package. Release builds use
7
+ this package layout:
8
+
9
+ | package | contents |
10
+ | --- | --- |
11
+ | `projmux` | `npm/projmux.js` shim and optional dependencies |
12
+ | `@projmux/linux-x64` | `linux/amd64` `bin/projmux` |
13
+ | `@projmux/linux-arm64` | `linux/arm64` `bin/projmux` |
14
+ | `@projmux/darwin-x64` | `darwin/amd64` `bin/projmux` |
15
+ | `@projmux/darwin-arm64` | `darwin/arm64` `bin/projmux` |
16
+
17
+ The shim sets `PROJMUX_INSTALLER=npm` before executing the real binary so
18
+ `projmux update status` and the Settings About screen can present
19
+ npm-specific guidance. npm is only an update/install source label here; the
20
+ keybinding flow remains `projmux shell` first, then `projmux setup` and
21
+ `projmux init` only for terminals that swallow shortcuts.
22
+
23
+ ## Local Packaging
24
+
25
+ Build and dry-run pack all npm packages:
26
+
27
+ ```bash
28
+ make npm-pack
29
+ ```
30
+
31
+ or:
32
+
33
+ ```bash
34
+ scripts/package-npm.sh --version 0.4.0 --out /tmp/projmux-npm --pack
35
+ ```
36
+
37
+ The script stages package directories under `dist/npm` by default. It builds
38
+ the Go binary for each supported platform, copies package metadata and docs,
39
+ updates package versions in the staged copies, then runs `npm pack --dry-run`
40
+ when `--pack` is set.
41
+
42
+ ## Publish Order
43
+
44
+ The platform packages must be published before the root package:
45
+
46
+ ```text
47
+ @projmux/linux-x64
48
+ @projmux/linux-arm64
49
+ @projmux/darwin-x64
50
+ @projmux/darwin-arm64
51
+ projmux
52
+ ```
53
+
54
+ Publishing the scoped platform packages requires control of the `@projmux`
55
+ npm scope. Configure npm Trusted Publishing for every package before merging a
56
+ release PR:
57
+
58
+ | npm package | GitHub organization/user | repository | workflow filename |
59
+ | --- | --- | --- | --- |
60
+ | `@projmux/linux-x64` | `crevissepartners` | `projmux` | `release.yml` |
61
+ | `@projmux/linux-arm64` | `crevissepartners` | `projmux` | `release.yml` |
62
+ | `@projmux/darwin-x64` | `crevissepartners` | `projmux` | `release.yml` |
63
+ | `@projmux/darwin-arm64` | `crevissepartners` | `projmux` | `release.yml` |
64
+ | `projmux` | `crevissepartners` | `projmux` | `release.yml` |
65
+
66
+ Leave the npm trusted publisher environment field empty unless the workflow is
67
+ later moved behind a GitHub deployment environment.
68
+
69
+ Tag releases publish npm packages from GitHub Actions after release archives
70
+ are uploaded. The workflow runs:
71
+
72
+ ```bash
73
+ scripts/package-npm.sh --version "${GITHUB_REF_NAME#v}" --out dist/npm
74
+ ```
75
+
76
+ then publishes each staged package with `npm publish --access public`.
77
+ The npm publish job uses GitHub Actions OIDC (`id-token: write`) instead of a
78
+ long-lived `NPM_TOKEN` secret. PR CI runs `make npm-pack` so package staging and
79
+ dry-run packing fail before release.
80
+
81
+ ## Non-Goals
82
+
83
+ The npm installer must not install system dependencies, edit shell startup
84
+ files, or mutate tmux config. Those actions stay behind explicit
85
+ `projmux doctor`, `projmux init`, Settings About update actions, or future
86
+ opt-in install commands.
@@ -0,0 +1,111 @@
1
+ # Picker UI Plan
2
+
3
+ ## Goal
4
+
5
+ The project switcher needs a richer picker surface than a single-line fzf row.
6
+ The target interaction is a card-like list where each item can show a title plus
7
+ small contextual lines such as session state, window/pane summary, branch, or
8
+ path. Search should stay focused on stable identity text, especially the project
9
+ or session title, instead of matching every contextual preview line.
10
+
11
+ ## Current Contract
12
+
13
+ The picker contract is split in two layers:
14
+
15
+ - `internal/ui/picker` owns backend-neutral items, actions, preview metadata,
16
+ backend selection, title-focused filtering, and the default native runner.
17
+ - `internal/ui/fzf` adapts that model into the historical fzf command line.
18
+
19
+ The fzf fallback backend sends one logical row per item:
20
+
21
+ ```text
22
+ <visible label>\t<selection value>
23
+ ```
24
+
25
+ fzf is configured with:
26
+
27
+ - `--delimiter "\t"`
28
+ - `--with-nth 1`
29
+ - `--exit-0`
30
+ - optional `--preview` and `--preview-window`
31
+
32
+ The app depends on fzf returning the selected row and then extracts the hidden
33
+ value after the first tab. This contract is simple and stable, but it limits each
34
+ row to one visible line.
35
+
36
+ ## fzf Capability Check
37
+
38
+ The installed fzf version supports multi-line items with `--read0`. That means a
39
+ single item can contain newline characters when input records are NUL-delimited.
40
+ This can render card-like rows.
41
+
42
+ The simple fzf option path is not enough for the desired search behavior:
43
+
44
+ - `--read0` can display multi-line items.
45
+ - `--nth` can restrict search to selected fields.
46
+ - `--with-nth` can transform the displayed fields.
47
+ - In practice, once `--with-nth` is used to show a card field, fzf searches the
48
+ transformed visible text. Context lines become searchable.
49
+
50
+ So fzf can support "multi-line cards", but not "multi-line cards with title-only
51
+ search" through a small option-only extension while preserving the current
52
+ selection contract.
53
+
54
+ ## Viable Paths
55
+
56
+ ### 1. fzf card approximation
57
+
58
+ Use `--read0` and NUL-delimited multi-line entries. This is the smallest change,
59
+ but contextual card text will participate in search unless the visible card is
60
+ kept title-only. This does not meet the intended search model.
61
+
62
+ This path is acceptable only as a temporary visual experiment.
63
+
64
+ ### 2. fzf custom filtering
65
+
66
+ Run fzf in a more controlled mode where query changes reload a filtered list
67
+ from `projmux`, and `projmux` performs title-focused matching. This keeps fzf as
68
+ the renderer but moves filtering into the app.
69
+
70
+ Tradeoffs:
71
+
72
+ - More shell quoting and reload complexity.
73
+ - More edge cases around selection identity and tracking.
74
+ - Still constrained by fzf's list layout and event model.
75
+
76
+ This is viable, but it is a bridge rather than a clean long-term model.
77
+
78
+ ### 3. Native picker TUI
79
+
80
+ Introduce a picker abstraction and implement a native terminal UI for card rows,
81
+ title-focused search, stable selection identity, and app-owned key handling. fzf
82
+ remains the default backend until parity is reached.
83
+
84
+ This best matches the desired product direction:
85
+
86
+ - card rows are first-class data, not encoded fzf strings
87
+ - search fields are explicit
88
+ - preview/context fields can be visible but non-searchable
89
+ - future key behavior can be tested without relying on fzf internals
90
+
91
+ ## Implemented Direction
92
+
93
+ Do not extend the current fzf row format again as the main implementation. The
94
+ previous hidden-field attempt showed that small fzf encoding changes can break
95
+ selection and navigation in subtle ways.
96
+
97
+ Current implementation:
98
+
99
+ - Picker-domain model exists as `picker.Item` with `Title`, `Value`,
100
+ `SearchText`, `MetaLines`, `Badges`, and `PreviewTarget`.
101
+ - `picker.Options` carries backend-neutral actions, preview metadata, prompt,
102
+ footer, initial query, and multiline intent.
103
+ - Native is the default backend and renders the popup/sidebar surfaces.
104
+ - `PROJMUX_PICKER_BACKEND=fzf` opts into the external fzf runner. Native supports
105
+ multiline item rendering, title-focused search via `SearchText`, numeric
106
+ selection, and shared close actions.
107
+ - Switcher popup/sidebar use the selected backend for preview and key action
108
+ parity, including native preview panes, raw-key navigation, and sidebar focus
109
+ tracking.
110
+
111
+ fzf can stay as the stable fallback while the native picker continues to mature.
@@ -0,0 +1,106 @@
1
+ # PR Guideline
2
+
3
+ Audience: every contributor — humans and agents alike. Agents working in this
4
+ repo (`claude` / `codex` panes, the team-lead session, etc.) MUST follow these
5
+ rules; the conventions here are what `release-please` parses for the next
6
+ release notes, so a sloppy PR title silently breaks the changelog.
7
+
8
+ For the surrounding workflow (worktree, validation gates, post-merge install)
9
+ see [AGENTS.md](../AGENTS.md). This document covers only the PR itself.
10
+
11
+ ## PR title — Conventional Commits
12
+
13
+ Default merge method is **squash**, so the PR title becomes the only commit
14
+ subject that lands on `main`. Format:
15
+
16
+ ```
17
+ <type>(<optional scope>): <imperative summary>
18
+ ```
19
+
20
+ Examples:
21
+
22
+ ```
23
+ feat(ai): add codex split picker keybinding
24
+ fix(ai): prepend agent bin dir to PATH so node-managed CLIs find node
25
+ docs(readme): drop Releases and Configuration sections
26
+ chore: bump release-please manifest to 0.3.0
27
+ refactor(picker): collapse duplicate fzf bootstrap code
28
+ ```
29
+
30
+ Rules:
31
+
32
+ - Subject is in the imperative ("add", "fix", "drop"), no trailing period.
33
+ - Keep the title under ~70 characters when possible. Long detail goes in the
34
+ body.
35
+ - The scope is optional but recommended for non-trivial diffs (`ai`, `picker`,
36
+ `tmux`, `readme`, `ci`, etc.).
37
+ - A `!` after the type or scope marks a breaking change:
38
+ `feat(ai)!: rename PROJMUX_NOTIFY_HOOK to PROJMUX_NOTIFY_BIN`.
39
+ - Or include a `BREAKING CHANGE: <description>` footer in the body. Either form
40
+ bumps the major version on the next release-please run.
41
+
42
+ ### Allowed types
43
+
44
+ | type | use for | release impact |
45
+ | --- | --- | --- |
46
+ | `feat` | user-visible new behavior or capability | minor bump |
47
+ | `fix` | bug fix that ships to users | patch bump |
48
+ | `perf` | measurable runtime/memory improvement | patch bump |
49
+ | `refactor` | code restructure with no user-visible change | none |
50
+ | `docs` | docs-only change | none |
51
+ | `test` | adding or restructuring tests | none |
52
+ | `build` | build system, Makefile, dependencies | none |
53
+ | `ci` | CI workflow / GitHub Actions | none |
54
+ | `chore` | release plumbing, tooling, repo housekeeping | none |
55
+ | `style` | formatting only, no logic change | none |
56
+
57
+ If the change includes both a feat and a fix, split it into two PRs. release-please
58
+ classifies the whole PR by its title type, not by content.
59
+
60
+ ## PR body
61
+
62
+ Use this template:
63
+
64
+ ```markdown
65
+ ## Summary
66
+ - 1–3 bullets describing what changed and why.
67
+
68
+ ## Test plan
69
+ - [ ] make fmt-check
70
+ - [ ] make test
71
+ - [ ] manual verification step (if relevant)
72
+ ```
73
+
74
+ Notes:
75
+
76
+ - **Why** matters more than **what**. Diff already shows the what.
77
+ - Reference issues with `Closes #<n>` so they auto-close on merge.
78
+ - Mention follow-ups explicitly when scope was deliberately deferred.
79
+
80
+ ## Branch protection in effect
81
+
82
+ `main` is governed by ruleset `main-protect`:
83
+
84
+ - Direct push to `main` is blocked. Even repository admin must use a PR.
85
+ - Required status check: the CI `Test` job. The PR cannot merge until it is
86
+ green.
87
+ - Admin bypass is `pull_request` mode — admin can self-merge without
88
+ approvals, but the PR itself is mandatory.
89
+ - Linear history is enforced. The merge methods exposed are
90
+ `merge` / `squash` / `rebase`; **default is squash** and that is what the
91
+ team-lead session uses unless the change explicitly needs preserved history.
92
+ - Force pushes and branch deletions on `main` are blocked.
93
+
94
+ `gh pr merge <num> --squash --delete-branch` is the canonical merge command.
95
+ Use `--auto` if you want the merge queued automatically once CI passes.
96
+
97
+ ## Release-please coupling
98
+
99
+ Every PR title that lands on `main` is parsed by `release-please-action`.
100
+ A `feat:` or `fix:` PR adds an entry to the next release notes; `chore:` /
101
+ `docs:` / `refactor:` etc. do not. To force a release of accumulated non-user
102
+ changes, open a `chore` PR titled `chore: release X.Y.Z` (or wait for any
103
+ real change). The `internal/version/version.go` constant carries the
104
+ `x-release-please-version` marker so release-please bumps it automatically.
105
+
106
+ Do not hand-author CHANGELOG.md or version bumps. release-please owns both.
@@ -0,0 +1,50 @@
1
+ # Repository Layout
2
+
3
+ ## Planned layout
4
+
5
+ ```text
6
+ projmux/
7
+ cmd/
8
+ projmux/
9
+ internal/
10
+ app/
11
+ config/
12
+ core/
13
+ candidates/
14
+ pins/
15
+ preview/
16
+ sessions/
17
+ integrations/
18
+ filesystem/
19
+ git/
20
+ kube/
21
+ tmux/
22
+ state/
23
+ ui/
24
+ fzf/
25
+ render/
26
+ version/
27
+ docs/
28
+ scripts/
29
+ test/
30
+ integration/
31
+ e2e/
32
+ ```
33
+
34
+ ## Notes
35
+
36
+ - `cmd/projmux` contains only CLI wiring.
37
+ - `internal/core` contains product behavior that should be testable without tmux.
38
+ - `internal/integrations/tmux` should be the only place that knows tmux command strings and output formats.
39
+ - `internal/ui/fzf` may depend on shelling out to `fzf`, but should call typed core services.
40
+ - `scripts/` is for development tooling only, not product logic.
41
+
42
+ ## Early implementation order
43
+
44
+ 1. `internal/core/sessions`
45
+ 2. `internal/core/candidates`
46
+ 3. `internal/core/pins`
47
+ 4. `internal/state`
48
+ 5. `internal/integrations/tmux`
49
+ 6. `internal/ui/fzf`
50
+ 7. CLI command wiring
@@ -0,0 +1,95 @@
1
+ # Roadmap
2
+
3
+ ## Done (0.4.x)
4
+
5
+ The 0.4 line filled in the operational surface around the session-management
6
+ core that 0.3 had landed.
7
+
8
+ ### Setup and install
9
+
10
+ - `projmux setup` — TTY raw-mode probe that reports which projmux key
11
+ sequences (`Alt-1..5`, `Ctrl-N`, `Ctrl-Shift-{R,L,M}`, `Ctrl-M`,
12
+ `Alt-Shift-{Left,Right}`) reach the process and which the terminal
13
+ swallows.
14
+ - `projmux init [terminal]` — auto-merges projmux's CSI-u + chord
15
+ bindings into a terminal config. Adapters: Ghostty (with the
16
+ `config` / `config.ghostty` candidate split and symlink guard) and
17
+ Windows Terminal (WSL + native).
18
+
19
+ ### Diagnostics
20
+
21
+ - `projmux doctor` — runtime dependency report. Enforces minimum tmux
22
+ 3.4 and fzf 0.65.0 (`stale` status when present but below the floor).
23
+
24
+ ### Focus
25
+
26
+ - `projmux focus` — unified switch-client dispatch. Resolves a target
27
+ session against the live tmux inventory, redirects an existing client
28
+ if one is attached, otherwise emits a desktop notification. Used by
29
+ the status-bar notify click and by the AI reply-ready handler.
30
+
31
+ ### Notify queue
32
+
33
+ - `projmux notify push|list|ack` — persistent JSON-backed queue at
34
+ `<state>/projmux/notify.json` with TTL, severity, source, and target
35
+ metadata.
36
+ - `projmux notify reconcile` — back-fills the queue from live pane
37
+ state by walking `tmux list-panes -a`.
38
+ - Producer wired to the attention state machine: a pane transitioning
39
+ to `reply` with an AI agent option set pushes an `ai:<session>:<pane>`
40
+ entry; the matching `clear` acks it.
41
+
42
+ ### Usage tracking
43
+
44
+ - `projmux usage` (and `status usage`) — authoritative 5h + weekly
45
+ utilisation for both Claude (OAuth `api/oauth/usage` endpoint with
46
+ 401 token refresh) and Codex (latest rollout `rate_limits` JSONL).
47
+ - Per-adapter throttle (Claude `5m`, default `30s`), 429 backoff
48
+ (`30m`–`60m` exponential), `--force` to bypass both. Snapshots
49
+ preserved on failure so a 429 does not erase prior rows.
50
+
51
+ ### Statusbar and HUD
52
+
53
+ - Two-line clickable status bar: row 0 is the existing
54
+ session/window/path/git/kube row, row 1 splits notify (left) and
55
+ usage (right).
56
+ - `projmux statusbar click` — single dispatcher for both mouse clicks
57
+ and the `prefix s {u,n,g,k,p,s}` keyboard chord. Window-list clicks
58
+ on tabs short-circuit to native `select-window`.
59
+ - `pwd` status click copies the current pane path into the tmux paste
60
+ buffer and shows a compact path popup instead of a transient
61
+ warning-coloured toast.
62
+ - HUD-style notify segment with severity+agent badge, midpoint dot
63
+ separators, and an age field.
64
+ - HUD-style usage segment with bars, last-sync age indicator (Claude),
65
+ and graceful degradation through six tiers as `--max-width` shrinks.
66
+
67
+ ## Next (0.5+)
68
+
69
+ Carried forward from earlier milestones — items still outstanding when
70
+ v0.4 shipped.
71
+
72
+ ### Picker UI
73
+
74
+ - Picker-domain model separate from fzf row encoding (kept fzf as the
75
+ stable fallback backend). Done in the 0.5 picker contract slice.
76
+ - Native picker backend for multi-line card rows and title-focused search.
77
+ Done in the 0.5 picker contract slice, later promoted to the default picker
78
+ backend with `PROJMUX_PICKER_BACKEND=fzf` kept as the explicit fallback.
79
+ - Port switcher popup/sidebar surfaces after parity tests cover
80
+ selection, preview, and key actions.
81
+
82
+ ### Picker dismissal
83
+
84
+ - Picker-agnostic popup close/toggle handling so AI picker dismissal
85
+ does not depend on fzf-specific key bindings. Done in the 0.5 picker
86
+ contract slice; fzf maps close actions to `abort`, and the native runner
87
+ consumes the same close action keys.
88
+
89
+ ### Docker install and E2E harness
90
+
91
+ - Initial Docker-backed Linux smoke suites are available through
92
+ `make test-integration`, `make test-install-smoke`, and `make test-e2e`.
93
+ They cover install/runtime substrate checks against real `tmux`; host-only
94
+ terminal, WSL, macOS, and GUI checks remain separate in
95
+ [docs/testing.md](testing.md).
@@ -0,0 +1,33 @@
1
+ # Shell Auto-Start
2
+
3
+ If you want every new interactive bash or zsh shell to drop you straight into
4
+ the projmux app, add a guarded hook to `~/.bashrc`, `~/.zshrc`, or the
5
+ equivalent interactive rc file for your shell:
6
+
7
+ ```sh
8
+ if [[ $- == *i* && -z "${TMUX:-}" ]] && command -v projmux >/dev/null 2>&1; then
9
+ exec projmux shell
10
+ fi
11
+ ```
12
+
13
+ The three guards each prevent a common breakage:
14
+
15
+ - `$- == *i*` — only fire for interactive shells. Without this you would
16
+ break `scp`, `ssh host cmd`, `git` over SSH, and any `bash -c '...'` or
17
+ `zsh -c '...'` invocation.
18
+ - `-z "${TMUX:-}"` — skip when already inside tmux. Without this the hook
19
+ recurses every time projmux opens a new pane.
20
+ - `command -v projmux >/dev/null 2>&1` — skip on machines where projmux is not
21
+ installed yet. Without this a fresh login on a new box hangs at a missing
22
+ binary.
23
+
24
+ To bypass the hook for one shell, set `TMUX` before launching:
25
+
26
+ ```sh
27
+ TMUX=1 bash
28
+ TMUX=1 zsh
29
+ ```
30
+
31
+ This is **opt-in** behavior. projmux does not assume you want every shell to
32
+ auto-start the app; the snippet above is here only as a known-safe starting
33
+ point for users who do.
@@ -0,0 +1,157 @@
1
+ # Statusbar
2
+
3
+ `projmux shell` configures tmux with `status 2` and renders a two-line
4
+ clickable status bar. The same dispatcher (`projmux statusbar click`)
5
+ handles both mouse clicks and the keyboard chord, so adding a new
6
+ segment only requires one wiring point.
7
+
8
+ ## Layout
9
+
10
+ ```
11
+ row 0 [#S] #{pane_current_path} ⎈ <ctx>/<ns> <git>  %H:%M
12
+ └────────── native tmux window list (one entry per window) ──────────┘
13
+ row 1 #[range=user|notify] <notify HUD pill> #[norange]
14
+ #[range=user|usage] <usage HUD bar> #[norange]
15
+ ```
16
+
17
+ - Row 0 keeps tmux's native `window-status-format` so clicking a tab
18
+ selects the window. The bind uses `if-shell -F
19
+ "#{==:#{mouse_status_range},window}"` to run `select-window -t =`
20
+ natively when the click lands on the window list, so the
21
+ mouse-target context resolves the clicked window directly. All
22
+ other ranges fall through to `run-shell projmux statusbar click
23
+ ...`, which dispatches by range id. The in-config short-circuit
24
+ is required because `#{mouse_window}` is empty for window-list
25
+ clicks on tmux 3.4+, so a `run-shell` handler can't recover the
26
+ target after the fact — the Go dispatcher's
27
+ `isWindowListRangeToken` fallback is now defense-in-depth only.
28
+ The session, pwd, kube, and git segments on this row are wrapped
29
+ in `#[range=user|<id>]` ranges and dispatched through the projmux
30
+ handler. The git segment shows the current branch or detached commit,
31
+ then compact state indicators when available: `*` for local changes,
32
+ `+N` for staged entries, and `↑N`/`↓N` for ahead/behind counts. Each
33
+ state token gets its own compact foreground color while preserving the
34
+ existing branch block background.
35
+ - Row 1 splits the line with `#[align=left]` (the pending AI notify
36
+ queue, capped at 80
37
+ cells) and `#[align=right]` (usage, capped at 120 cells). `notify` is the
38
+ explicit-ack pending queue; live pane attention badges are a separate
39
+ state surface. Both
40
+ segments degrade gracefully when the cell budget is tight; see
41
+ [notify-queue.md](notify-queue.md) and [usage-tracking.md](usage-tracking.md)
42
+ for the per-segment tier ladder.
43
+
44
+ A single tmux bind handles both lines:
45
+
46
+ ```tmux
47
+ bind-key -n MouseDown1Status if-shell -F "#{==:#{mouse_status_range},window}" \
48
+ { select-window -t = } \
49
+ { run-shell "'<projmux>' statusbar click \"#{mouse_status_range}\" --mouse-window \"#{mouse_window}\"" }
50
+ ```
51
+
52
+ `MouseDown1Status` fires from any line of a multi-line status bar with
53
+ `#{mouse_status_range}` resolving to the range under the cursor.
54
+
55
+ ## Range catalogue
56
+
57
+ | Range id | Row | Click action | Keyboard |
58
+ | -------- | --- | ----------------------------------------- | ------------- |
59
+ | `session` | 0 | `projmux tmux popup-toggle sessionizer-sidebar` | `prefix s s` |
60
+ | `pwd` | 0 | copy `#{pane_current_path}` to tmux buffer and show a compact `Path copied` popup | `prefix s p` |
61
+ | `kube` | 0 | `projmux tmux popup-toggle sessionizer` | `prefix s k` |
62
+ | `git` | 0 | `projmux tmux popup-toggle sessionizer` | `prefix s g` |
63
+ | `usage` | 1 | `display-popup -E -h 60% -w 80% -- projmux usage`, then wait for Enter | `prefix s u` |
64
+ | `notify` | 1 | `projmux focus --target <newest> --source status-bar --kind segment-click`, then ack on focus success | `prefix s n` |
65
+
66
+ `notify` reads the pending queue only. For a live pane-state view that is
67
+ independent of queued reminders, use `projmux attention list`. To explain why
68
+ a live reply badge and the queue disagree, use `projmux notify list --live`.
69
+ The notify segment renders the newest queued item as a single notification
70
+ block: project, state (`NEED`/`INFO`/`WARN`/`CRIT`), optional agent, text,
71
+ age, and `+N` for older pending entries. Window/pane ids are not shown in the
72
+ compact status segment.
73
+ `usage` deliberately opens the detailed `projmux usage` table popup; it is the
74
+ clear action surface for the compact HUD bar.
75
+
76
+ The path popup uses a short title, one-line copy status, the current path, and
77
+ an `Enter closes this popup` prompt. If the tmux buffer write fails, it keeps
78
+ the same compact surface with `Current path` as the title and a copy-unavailable
79
+ message. The notification HUD detail surface (`Alt-2` / `User2`) opens the
80
+ right-side notification popup with newest-first rows. The popup itself is
81
+ untitled; when decoration mode is `symbol` or `emoji`, the bell appears before
82
+ the fzf header text instead. Selecting a row still focuses and acknowledges
83
+ that notification.
84
+
85
+ Empty `#{mouse_status_range}` (a click on whitespace) falls through to
86
+ `select-window -t @<mouse_window>` when `--mouse-window` is non-empty,
87
+ otherwise it is a no-op. Unknown user range ids are non-specialized
88
+ placeholder surfaces and no-op until a handler is wired into the dispatcher.
89
+
90
+ ## Keyboard chord
91
+
92
+ ```tmux
93
+ bind-key s switch-client -T projmux-status
94
+ bind-key -T projmux-status u run-shell '#{q:projmux} statusbar click usage'
95
+ bind-key -T projmux-status n run-shell '#{q:projmux} statusbar click notify'
96
+ bind-key -T projmux-status g run-shell '#{q:projmux} statusbar click git'
97
+ bind-key -T projmux-status k run-shell '#{q:projmux} statusbar click kube'
98
+ bind-key -T projmux-status p run-shell '#{q:projmux} statusbar click pwd'
99
+ bind-key -T projmux-status s run-shell '#{q:projmux} statusbar click session'
100
+ ```
101
+
102
+ The chord routes through the same dispatcher as the mouse click, so
103
+ keyboard and mouse paths are functionally identical.
104
+
105
+ ## Click failure handling
106
+
107
+ Every status-bar click runs from tmux's `run-shell`. A non-zero exit
108
+ there triggers a tmux error popup, which is hostile UX for a casual
109
+ click. Each handler therefore swallows runtime failures and surfaces
110
+ them as `display-message` toasts:
111
+
112
+ - `notify` click whose focus dispatch exits 2 (target unresolved):
113
+ keep the entry pending, toast `notify target gone; ack to clear`.
114
+ - Any other focus failure: keep the entry, toast `focus failed:
115
+ <reason>`.
116
+ - `session`, `kube`, or `git` popup launch failure: toast
117
+ `statusbar <range>: popup failed`.
118
+ - `pwd` path popup failure: keep the copied path in the tmux paste
119
+ buffer when possible and fall back to a short `display-message`.
120
+ - `usage` popup failure: fall back to inlining the rendered table
121
+ into a single `display-message`.
122
+
123
+ `MouseX` / `MouseY` are accepted but not consumed today; the fields
124
+ are wired through so click telemetry can land without changing the
125
+ bind.
126
+
127
+ ## Customizing
128
+
129
+ The status bar is generated by `projmux tmux print-config` /
130
+ `print-app-config`. To change a segment, edit the generator (it
131
+ emits a deterministic block per segment), regenerate, and re-apply:
132
+
133
+ ```sh
134
+ projmux tmux apply
135
+ ```
136
+
137
+ Settings > Icons & Decorations controls the optional decoration mode used by
138
+ the path, git branch, and notification sidebar header. The persisted enum lives at
139
+ `~/.config/projmux/statusbar-decoration`; valid values are `off` (default,
140
+ font-safe), `symbol` (Nerd Font-style folder/GitHub/bell icons), and `emoji`.
141
+ Settings also updates tmux `@projmux_statusbar_decoration` for the live
142
+ server when run inside tmux.
143
+
144
+ To add a new clickable segment:
145
+
146
+ 1. Add a `statusbarRangeID` constant in
147
+ `internal/app/statusbar.go`.
148
+ 2. Wire its handler into `dispatchTable`.
149
+ 3. Update the generator so the segment is wrapped in
150
+ `#[range=user|<id>]...#[norange]` on the chosen row.
151
+ 4. Add a chord key under `projmux-status` if a keyboard binding is
152
+ wanted.
153
+
154
+ Custom user ranges can coexist with the built-in window-list range:
155
+ the dispatcher detects the `window` / `window|<idx>` token before
156
+ the user-range table, so a hostile range named `window` cannot
157
+ shadow the built-in.