projmux 0.4.4 → 0.4.6
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.
- package/README-ko.md +58 -269
- package/README.md +57 -259
- package/docs/agent-workflow.md +56 -0
- package/docs/architecture.md +199 -0
- package/docs/assets/projmux-icon.png +0 -0
- package/docs/assets/projmux-shell-sidebar.gif +0 -0
- package/docs/cli.md +429 -0
- package/docs/configuration.md +127 -0
- package/docs/hooks.md +118 -0
- package/docs/install.md +99 -0
- package/docs/keybindings.md +337 -0
- package/docs/migration-plan.md +82 -0
- package/docs/notify-queue.md +199 -0
- package/docs/npm-distribution.md +86 -0
- package/docs/picker-ui-plan.md +111 -0
- package/docs/pr-guideline.md +106 -0
- package/docs/repo-layout.md +50 -0
- package/docs/roadmap.md +95 -0
- package/docs/shell-autostart.md +33 -0
- package/docs/statusbar.md +151 -0
- package/docs/testing.md +57 -0
- package/docs/upgrading.md +100 -0
- package/docs/usage-tracking.md +153 -0
- package/package.json +7 -5
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Notify queue
|
|
2
|
+
|
|
3
|
+
`projmux` keeps a persistent JSON queue of pending AI notifications.
|
|
4
|
+
`attention` is live tmux pane state; `notify` is the user's pending
|
|
5
|
+
notification source of truth. The queue is derived from live state and user
|
|
6
|
+
pushes, but an entry remains pending until explicit ack. Each entry can route
|
|
7
|
+
the status-bar notify segment or notify sidebar to the originating tmux pane
|
|
8
|
+
via `projmux focus`, and feeds the HUD pill rendered by
|
|
9
|
+
`projmux status notify`.
|
|
10
|
+
|
|
11
|
+
## File layout
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
${XDG_STATE_HOME:-$HOME/.local/state}/projmux/notify.json
|
|
15
|
+
${XDG_STATE_HOME:-$HOME/.local/state}/projmux/notify.json.lock
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The lock file is acquired via `O_CREATE|O_EXCL` with bounded retry
|
|
19
|
+
(default ~200 attempts, 2ms–50ms jittered backoff) and a 30s
|
|
20
|
+
stale-after window so a crashed writer never permanently wedges the
|
|
21
|
+
queue.
|
|
22
|
+
|
|
23
|
+
The queue file is a pretty-printed JSON array of `Notification`
|
|
24
|
+
objects, sorted newest-first on read. `expires_at` is freshness metadata;
|
|
25
|
+
expired entries are not filtered or deleted by `list`.
|
|
26
|
+
|
|
27
|
+
## Data model
|
|
28
|
+
|
|
29
|
+
`internal/core/notify`:
|
|
30
|
+
|
|
31
|
+
```go
|
|
32
|
+
type Notification struct {
|
|
33
|
+
ID string // stable key for dedupe / ack
|
|
34
|
+
Text string // capped at 80 runes
|
|
35
|
+
Severity string // info | warn | critical
|
|
36
|
+
Source string // ai | k8s | git | external
|
|
37
|
+
CreatedAt time.Time
|
|
38
|
+
ExpiresAt time.Time
|
|
39
|
+
Target // session, window, pane, socket
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Target struct {
|
|
43
|
+
Socket string // tmux -L socket path; empty = default
|
|
44
|
+
Session string
|
|
45
|
+
Window string // optional; "" means session-only
|
|
46
|
+
Pane string // optional; "" means window-level
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Defaults: `DefaultTTL = 600s`, `MaxTextLength = 80`. TTL is retained as a
|
|
51
|
+
freshness/display field, not a removal condition. `Severity` and
|
|
52
|
+
`Source` are validated against the constants above; an invalid value
|
|
53
|
+
returns `ErrInvalidSeverity` / `ErrInvalidSource` which the CLI maps to
|
|
54
|
+
exit code 2.
|
|
55
|
+
|
|
56
|
+
## CLI surface
|
|
57
|
+
|
|
58
|
+
### push
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
projmux notify push --text <s> --target <SESSION[:WINDOW[.PANE]]>
|
|
62
|
+
[--socket <s>] [--severity info|warn|critical]
|
|
63
|
+
[--source ai|k8s|git|external]
|
|
64
|
+
[--ttl <seconds>] [--id <s>] [--json]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Appends one entry. With `--id` an existing entry is overwritten in
|
|
68
|
+
place (text + timestamp refresh), enabling idempotent producers like
|
|
69
|
+
the AI reply-ready transition. `--ttl` accepts a positive integer
|
|
70
|
+
number of seconds. `--json` prints `{id, queued}` for scripting.
|
|
71
|
+
|
|
72
|
+
### list
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
projmux notify list [--live] [--json] [--limit N] [--ui table|sidebar]
|
|
76
|
+
[--severity ...] [--source ...]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Newest-first pending queue entries. Default output is the tab-aligned
|
|
80
|
+
table `ID AGE SEV SRC TARGET TEXT`. `--severity` / `--source` are
|
|
81
|
+
repeatable filters. Without `--live`, this command reads only the queue and
|
|
82
|
+
preserves the stable JSON array used by scripts.
|
|
83
|
+
|
|
84
|
+
`--ui=sidebar` opens the notify queue as an interactive right-side list when
|
|
85
|
+
run inside the tmux popup surface. Enter focuses the selected target pane and
|
|
86
|
+
acks the row after focus succeeds. `a` acks the selected row. `Ctrl-A` clears
|
|
87
|
+
all rows via `notify ack --all`. Rows are intentionally compact: the visible
|
|
88
|
+
label keeps notification text first, then age, project, window, and pane
|
|
89
|
+
metadata; id/source/severity/target remain searchable.
|
|
90
|
+
|
|
91
|
+
`--live` adds a non-mutating explanation view that reads
|
|
92
|
+
`tmux list-panes -a` and compares the queue with live reply-state panes. It
|
|
93
|
+
does not push, ack, or otherwise repair anything. Human output keeps the
|
|
94
|
+
queue table and adds a `STATE TARGET ID EXPLANATION TEXT` section; JSON
|
|
95
|
+
output becomes `{queue, live, rows, errors}`. Typical states:
|
|
96
|
+
|
|
97
|
+
- `live-manual-reply` — a live reply/green badge exists, but no queue entry
|
|
98
|
+
is expected because the pane has no AI agent metadata.
|
|
99
|
+
- `live-ai-reply-queued` — a live AI reply pane has the matching actionable
|
|
100
|
+
queue entry.
|
|
101
|
+
- `live-ai-reply-missing-queue` — a live AI reply pane lacks the derived
|
|
102
|
+
queue entry; run `projmux notify reconcile` to back-fill it.
|
|
103
|
+
- `queue-stale` — an `ai:` queue entry exists, but the live pane no longer
|
|
104
|
+
matches reply+agent state; it remains pending until explicit ack.
|
|
105
|
+
- `queue-only` — a non-AI/external queue entry is pending and has no live AI
|
|
106
|
+
reply-pane requirement.
|
|
107
|
+
|
|
108
|
+
To inspect live pane attention without queue context, use
|
|
109
|
+
`projmux attention list`.
|
|
110
|
+
|
|
111
|
+
### ack
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
projmux notify ack <id>
|
|
115
|
+
projmux notify ack --all
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Removes one entry by id, or flushes the queue. `--all` returns the
|
|
119
|
+
removed count.
|
|
120
|
+
|
|
121
|
+
### reconcile
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
projmux notify reconcile [--json]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Walks `tmux list-panes -a -F` with the producer-key format (session,
|
|
128
|
+
window id, pane id, attention state, AI state, agent, topic, socket
|
|
129
|
+
path), then:
|
|
130
|
+
|
|
131
|
+
- pushes/refreshes one `ai:<session>:<pane>` entry for every pane
|
|
132
|
+
whose attention state is `reply` AND whose agent option is non-empty;
|
|
133
|
+
- reports every existing queue entry whose id starts with `ai:` and whose
|
|
134
|
+
pane no longer matches that condition as stale, without acking it.
|
|
135
|
+
|
|
136
|
+
Soft-fails when tmux is not running (returns a populated `errors`
|
|
137
|
+
field rather than a non-zero exit) so the post-install hook does not
|
|
138
|
+
break. Run this as the recovery path when the on-disk queue has drifted
|
|
139
|
+
from live pane state.
|
|
140
|
+
|
|
141
|
+
Output: `reconcile: pushed N, acked M, kept K, stale S`.
|
|
142
|
+
|
|
143
|
+
Ack behavior is intentionally queue-local: only `ack` removes explicit ids or
|
|
144
|
+
flushes the queue. TTL is freshness metadata, and `reconcile` only repairs
|
|
145
|
+
derived `ai:` entries. Manual attention badges without agent metadata remain
|
|
146
|
+
live attention only.
|
|
147
|
+
|
|
148
|
+
## Producer (AI reply-ready)
|
|
149
|
+
|
|
150
|
+
`internal/app/notify_producer.go`. The attention state machine calls
|
|
151
|
+
`PushReplyReady` when a pane flips to `reply`. The producer reads
|
|
152
|
+
`@projmux_ai_pane_agent`, `@projmux_ai_pane_topic`, `#S`,
|
|
153
|
+
`#{window_id}`, `#{pane_id}`, `#{socket_path}` off the pane and writes
|
|
154
|
+
an entry with:
|
|
155
|
+
|
|
156
|
+
- id: `ai:<session>:<pane>`
|
|
157
|
+
- text: `<agent>: <topic>` (or `<agent>: ready` when no topic is set)
|
|
158
|
+
- severity: `info`, source: `ai`, freshness TTL: 10 minutes
|
|
159
|
+
|
|
160
|
+
When the pane leaves the reply state (manual `attention clear`,
|
|
161
|
+
`status set idle`, or a window close), `AckReplyReady` intentionally does not
|
|
162
|
+
remove the entry. The user consumes it through explicit ack. Store errors are
|
|
163
|
+
swallowed so the live tmux UI never blocks on disk IO.
|
|
164
|
+
|
|
165
|
+
Manual `projmux attention toggle` on a pane without an agent option
|
|
166
|
+
does NOT push — the queue is intentionally AI-driven; reconcile honours
|
|
167
|
+
the same contract.
|
|
168
|
+
|
|
169
|
+
## Consumer (status-bar click)
|
|
170
|
+
|
|
171
|
+
`internal/app/statusbar.go::handleNotify`. A click on the notify range
|
|
172
|
+
or the `prefix s n` chord reads the newest queue entry and dispatches:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
projmux focus --target <target> --source status-bar --kind segment-click [--socket <s>]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Outcomes:
|
|
179
|
+
|
|
180
|
+
- **Focus succeeded** — ack the entry. Focus is the single consume path for
|
|
181
|
+
routed notification clicks.
|
|
182
|
+
- **Focus exited 2 (target unresolved)** — keep the entry pending and toast
|
|
183
|
+
`notify target gone; ack to clear`.
|
|
184
|
+
- **Other failure** — keep the entry, toast `focus failed: <reason>`
|
|
185
|
+
so the user can retry without losing the row.
|
|
186
|
+
|
|
187
|
+
The handler never returns a non-zero error to tmux's `run-shell`; every
|
|
188
|
+
failure becomes a `display-message` toast so a transient miss does not
|
|
189
|
+
trigger a tmux error popup.
|
|
190
|
+
|
|
191
|
+
## Render (status segment)
|
|
192
|
+
|
|
193
|
+
`projmux status notify` is the HUD-style renderer wired to the tmux
|
|
194
|
+
status interval. See [statusbar.md](statusbar.md) for the layout and
|
|
195
|
+
degradation tiers. It shows the newest pending item as a single notification
|
|
196
|
+
block with project, state, optional agent, text, age, and an extra-count
|
|
197
|
+
marker. Window/pane ids remain routable metadata but are not displayed in the
|
|
198
|
+
compact HUD. The renderer is silent on every failure mode (no store, list
|
|
199
|
+
error, empty queue) so the status line never carries a stack trace.
|
|
@@ -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 opt-in native runner.
|
|
17
|
+
- `internal/ui/fzf` adapts that model into the historical fzf command line.
|
|
18
|
+
|
|
19
|
+
The default fzf backend still 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
|
+
- fzf remains the default backend and renders the same popup/sidebar surfaces.
|
|
104
|
+
- `PROJMUX_PICKER_BACKEND=native` opts into the native runner. It supports
|
|
105
|
+
multiline item rendering, title-focused search via `SearchText`, numeric
|
|
106
|
+
selection, and shared close actions.
|
|
107
|
+
- Switcher popup/sidebar still use the fzf backend for full preview and key
|
|
108
|
+
action parity. Native preview panes, raw-key navigation, and sidebar focus
|
|
109
|
+
tracking remain follow-up work.
|
|
110
|
+
|
|
111
|
+
fzf can stay as the stable fallback while the native picker reaches parity.
|
|
@@ -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
|
package/docs/roadmap.md
ADDED
|
@@ -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
|
+
- Opt-in native picker backend for multi-line card rows and
|
|
77
|
+
title-focused search. Done in the 0.5 picker contract slice via
|
|
78
|
+
`PROJMUX_PICKER_BACKEND=native`.
|
|
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.
|