peakypanes 0.0.1 → 0.0.3
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.md +123 -22
- package/package.json +7 -6
- package/scripts/agent-state/claude-hook.py +91 -0
- package/scripts/agent-state/codex-notify.py +55 -0
package/README.md
CHANGED
|
@@ -29,20 +29,20 @@ Define your tmux layouts in YAML, share them with your team via git, and get con
|
|
|
29
29
|
|
|
30
30
|
### Install
|
|
31
31
|
|
|
32
|
-
Using npm
|
|
32
|
+
**Using npm (recommended)**
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
npm i -g peakypanes
|
|
36
|
-
peakypanes
|
|
36
|
+
peakypanes
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
>
|
|
40
|
-
>
|
|
39
|
+
> [!TIP]
|
|
40
|
+
> Run `peakypanes setup` to check dependencies
|
|
41
41
|
|
|
42
|
-
Run once with npx
|
|
42
|
+
**Run once with npx**
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
npx -y peakypanes
|
|
45
|
+
npx -y peakypanes
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Using Go
|
|
@@ -53,12 +53,6 @@ go install github.com/regenrek/peakypanes/cmd/peakypanes@latest
|
|
|
53
53
|
|
|
54
54
|
### Usage
|
|
55
55
|
|
|
56
|
-
**Just run it:**
|
|
57
|
-
```bash
|
|
58
|
-
cd your-project
|
|
59
|
-
peakypanes
|
|
60
|
-
```
|
|
61
|
-
|
|
62
56
|
**Start a session (auto-detect layout):**
|
|
63
57
|
```bash
|
|
64
58
|
peakypanes start
|
|
@@ -246,33 +240,40 @@ The dashboard shows:
|
|
|
246
240
|
- Sessions on the left (with window counts and expandable windows)
|
|
247
241
|
- Live pane preview on the right (window bar at the bottom)
|
|
248
242
|
- Lightweight session thumbnails at the bottom (last activity per session)
|
|
243
|
+
- Quick reply bar (always visible) and target pane highlight for follow-ups
|
|
249
244
|
|
|
250
245
|
Navigation (always visible):
|
|
251
|
-
-
|
|
246
|
+
- `ctrl+a/ctrl+d` project, `ctrl+w/ctrl+s` session, `tab/⇧tab` pane (across windows), `ctrl+g` help
|
|
252
247
|
|
|
253
|
-
Key bindings (also shown in
|
|
248
|
+
Key bindings (also shown in the help view):
|
|
249
|
+
Keymap overrides are available in the global config (`~/.config/peakypanes/config.yml`).
|
|
254
250
|
|
|
255
251
|
Project
|
|
256
|
-
- `o` open project picker (creates session detached; stay in dashboard)
|
|
257
|
-
- `
|
|
252
|
+
- `ctrl+o` open project picker (creates session detached; stay in dashboard)
|
|
253
|
+
- `ctrl+b` close project (kills all running sessions in project)
|
|
258
254
|
|
|
259
255
|
Session
|
|
260
|
-
- `enter` attach/start session
|
|
261
|
-
- `n` new session (pick layout)
|
|
262
|
-
- `t` open in new terminal window
|
|
263
|
-
- `
|
|
256
|
+
- `enter` attach/start session (when reply is empty)
|
|
257
|
+
- `ctrl+n` new session (pick layout)
|
|
258
|
+
- `ctrl+t` open in new terminal window
|
|
259
|
+
- `ctrl+x` kill session
|
|
264
260
|
- rename session via command palette (`ctrl+p`)
|
|
265
261
|
|
|
266
262
|
Window
|
|
267
|
-
- `
|
|
263
|
+
- `ctrl+u` toggle window list
|
|
268
264
|
- rename window via command palette (`ctrl+p`)
|
|
269
265
|
|
|
266
|
+
Pane
|
|
267
|
+
- rename pane via command palette (`ctrl+p`)
|
|
268
|
+
|
|
270
269
|
Tmux (inside session)
|
|
271
270
|
- `prefix+g` open dashboard popup (tmux prefix is yours)
|
|
272
271
|
|
|
273
272
|
Other
|
|
274
273
|
- `ctrl+p` command palette
|
|
275
|
-
- `r` refresh, `e` edit config,
|
|
274
|
+
- `ctrl+r` refresh, `ctrl+e` edit config, `ctrl+f` filter, `ctrl+c` quit
|
|
275
|
+
|
|
276
|
+
Quick reply details: the input is always active—type and press `enter` to send to the highlighted pane. Use `esc` to clear. `tab/⇧tab` still cycles panes while the input is focused.
|
|
276
277
|
|
|
277
278
|
### Dashboard Config (optional)
|
|
278
279
|
|
|
@@ -285,10 +286,56 @@ dashboard:
|
|
|
285
286
|
idle_seconds: 20
|
|
286
287
|
show_thumbnails: true
|
|
287
288
|
preview_mode: grid # grid | layout
|
|
289
|
+
attach_behavior: new_terminal # current | new_terminal | detached
|
|
290
|
+
keymap:
|
|
291
|
+
project_left: ["ctrl+a"]
|
|
292
|
+
project_right: ["ctrl+d"]
|
|
293
|
+
session_up: ["ctrl+w"]
|
|
294
|
+
session_down: ["ctrl+s"]
|
|
295
|
+
pane_next: ["tab"]
|
|
296
|
+
pane_prev: ["shift+tab"]
|
|
297
|
+
toggle_windows: ["ctrl+u"]
|
|
298
|
+
command_palette: ["ctrl+p"]
|
|
299
|
+
help: ["ctrl+g"]
|
|
300
|
+
quit: ["ctrl+c"]
|
|
288
301
|
status_regex:
|
|
289
302
|
success: "(?i)done|finished|success|completed|✅"
|
|
290
303
|
error: "(?i)error|failed|panic|❌"
|
|
291
304
|
running: "(?i)running|in progress|building|installing|▶"
|
|
305
|
+
agent_detection:
|
|
306
|
+
codex: true
|
|
307
|
+
claude: true
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`attach_behavior` controls what the “attach/start” action does (default `new_terminal`): `current` switches the terminal running PeakyPanes into the session, `new_terminal` opens a fresh terminal to attach, and `detached` only creates the session.
|
|
311
|
+
|
|
312
|
+
### Agent Status Detection (Codex & Claude Code)
|
|
313
|
+
|
|
314
|
+
PeakyPanes can read per-pane JSON state files to show accurate running/idle/done status for Codex CLI and Claude Code TUI sessions. This is **on by default** and falls back to regex/idle detection if no state file is present. You can disable it via `dashboard.agent_detection`.
|
|
315
|
+
|
|
316
|
+
State files are written under `${XDG_RUNTIME_DIR:-/tmp}/peakypanes/agent-state` and keyed by `TMUX_PANE` (override with `PEAKYPANES_AGENT_STATE_DIR`).
|
|
317
|
+
|
|
318
|
+
**Codex CLI (TUI)**
|
|
319
|
+
|
|
320
|
+
Add a `notify` command in your Codex config to call the PeakyPanes hook script (Codex passes one JSON arg):
|
|
321
|
+
|
|
322
|
+
```toml
|
|
323
|
+
# ~/.codex/config.toml
|
|
324
|
+
notify = ["python3", "/absolute/path/to/peakypanes/scripts/agent-state/codex-notify.py"]
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Tip: with `npm i -g peakypanes`, the scripts live under `$(npm root -g)/peakypanes/scripts/agent-state/`.
|
|
328
|
+
Note: Codex `notify` only fires on turn completion, so running state still relies on regex/idle detection between turns.
|
|
329
|
+
|
|
330
|
+
**Claude Code (TUI)**
|
|
331
|
+
|
|
332
|
+
Configure hooks to run the PeakyPanes hook script (Claude passes JSON on stdin). Recommended events:
|
|
333
|
+
`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `Stop`, `SessionEnd`.
|
|
334
|
+
|
|
335
|
+
Example hook command (wire it to each event above in Claude Code):
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
python3 /absolute/path/to/peakypanes/scripts/agent-state/claude-hook.py
|
|
292
339
|
```
|
|
293
340
|
|
|
294
341
|
### Tmux Config & Key Bindings
|
|
@@ -315,6 +362,45 @@ settings:
|
|
|
315
362
|
3. Project entry in `~/.config/peakypanes/config.yml`
|
|
316
363
|
4. Built-in `dev-3` layout (fallback)
|
|
317
364
|
|
|
365
|
+
## Testing
|
|
366
|
+
|
|
367
|
+
Run the unit tests with coverage:
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
go test ./... -coverprofile /tmp/peakypanes.cover
|
|
371
|
+
go tool cover -func /tmp/peakypanes.cover | tail -n 1
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Race tests:
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
go test ./... -race
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Tmux integration tests (requires tmux; opt-in):
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
PEAKYPANES_INTEGRATION=1 go test ./internal/tmuxctl -run Integration -count=1
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Manual npm smoke run (fresh HOME/XDG config):
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
scripts/fresh-run
|
|
390
|
+
scripts/fresh-run 0.0.2 --with-project
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
GitHub Actions runs gofmt checks, go vet, go test with coverage, race, and tmux integration tests on Linux.
|
|
394
|
+
|
|
395
|
+
## Release
|
|
396
|
+
|
|
397
|
+
See `RELEASE-DOCS.md` for the full release checklist (tests, tag, GoReleaser, npm publish).
|
|
398
|
+
|
|
399
|
+
## Windows
|
|
400
|
+
> npm packages are currently published for macOS and Linux.
|
|
401
|
+
> Windows users should install from the GitHub release or build with Go.
|
|
402
|
+
|
|
403
|
+
|
|
318
404
|
## For Teams
|
|
319
405
|
|
|
320
406
|
1. Run `peakypanes init --local` in your project
|
|
@@ -325,3 +411,18 @@ settings:
|
|
|
325
411
|
## License
|
|
326
412
|
|
|
327
413
|
MIT
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
## Links
|
|
417
|
+
|
|
418
|
+
- X/Twitter: [@kregenrek](https://x.com/kregenrek)
|
|
419
|
+
- Bluesky: [@kevinkern.dev](https://bsky.app/profile/kevinkern.dev)
|
|
420
|
+
|
|
421
|
+
## Courses
|
|
422
|
+
- Learn Cursor AI: [Ultimate Cursor Course](https://www.instructa.ai/en/cursor-ai)
|
|
423
|
+
- Learn to build software with AI: [AI Builder Hub](https://www.instructa.ai)
|
|
424
|
+
|
|
425
|
+
## See my other projects:
|
|
426
|
+
|
|
427
|
+
* [codefetch](https://github.com/regenrek/codefetch) - Turn code into Markdown for LLMs with one simple terminal command
|
|
428
|
+
* [instructa](https://github.com/orgs/instructa/repositories) - Instructa Projects
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peakypanes",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Tmux layout manager with YAML based configuration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/regenrek/peakypanes",
|
|
@@ -12,17 +12,18 @@
|
|
|
12
12
|
"peakypanes": "bin/peakypanes.js"
|
|
13
13
|
},
|
|
14
14
|
"optionalDependencies": {
|
|
15
|
-
"peakypanes-darwin-x64": "0.0.
|
|
16
|
-
"peakypanes-darwin-arm64": "0.0.
|
|
17
|
-
"peakypanes-linux-x64": "0.0.
|
|
18
|
-
"peakypanes-linux-arm64": "0.0.
|
|
15
|
+
"peakypanes-darwin-x64": "0.0.3",
|
|
16
|
+
"peakypanes-darwin-arm64": "0.0.3",
|
|
17
|
+
"peakypanes-linux-x64": "0.0.3",
|
|
18
|
+
"peakypanes-linux-arm64": "0.0.3"
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"bin/peakypanes.js",
|
|
25
|
-
"README.md"
|
|
25
|
+
"README.md",
|
|
26
|
+
"scripts/agent-state/"
|
|
26
27
|
],
|
|
27
28
|
"publishConfig": {
|
|
28
29
|
"access": "public"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def state_dir() -> Path:
|
|
10
|
+
root = os.environ.get("PEAKYPANES_AGENT_STATE_DIR")
|
|
11
|
+
if root:
|
|
12
|
+
return Path(root)
|
|
13
|
+
runtime = os.environ.get("XDG_RUNTIME_DIR") or "/tmp"
|
|
14
|
+
return Path(runtime) / "peakypanes" / "agent-state"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def write_state(pane_id: str, state: str, tool: str, payload: dict) -> None:
|
|
18
|
+
path = state_dir()
|
|
19
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
record = {
|
|
21
|
+
"state": state,
|
|
22
|
+
"tool": tool,
|
|
23
|
+
"updated_at_unix_ms": int(time.time() * 1000),
|
|
24
|
+
"pane_id": pane_id,
|
|
25
|
+
}
|
|
26
|
+
session_id = payload.get("session_id") or payload.get("sessionId")
|
|
27
|
+
if session_id:
|
|
28
|
+
record["session_id"] = session_id
|
|
29
|
+
transcript = payload.get("transcript_path") or payload.get("transcriptPath")
|
|
30
|
+
if transcript:
|
|
31
|
+
record["transcript_path"] = transcript
|
|
32
|
+
target = path / f"{pane_id}.json"
|
|
33
|
+
tmp = target.with_suffix(".json.tmp")
|
|
34
|
+
tmp.write_text(json.dumps(record))
|
|
35
|
+
tmp.replace(target)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def normalize_event(payload: dict) -> str:
|
|
39
|
+
for key in ("event", "hook", "type", "name"):
|
|
40
|
+
value = payload.get(key)
|
|
41
|
+
if isinstance(value, str) and value.strip():
|
|
42
|
+
return value.strip().lower()
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def map_event_to_state(event: str, payload: dict) -> str:
|
|
47
|
+
if event in {"sessionstart", "session_start"}:
|
|
48
|
+
return "idle"
|
|
49
|
+
if event in {"userpromptsubmit", "user_prompt_submit"}:
|
|
50
|
+
return "running"
|
|
51
|
+
if event in {"pretooluse", "pre_tool_use"}:
|
|
52
|
+
return "running"
|
|
53
|
+
if event in {"posttooluse", "post_tool_use"}:
|
|
54
|
+
return "running"
|
|
55
|
+
if event in {"permissionrequest", "permission_request"}:
|
|
56
|
+
return "waiting"
|
|
57
|
+
if event in {"notification"}:
|
|
58
|
+
return "waiting"
|
|
59
|
+
if event in {"stop"}:
|
|
60
|
+
return "idle"
|
|
61
|
+
if event in {"sessionend", "session_end"}:
|
|
62
|
+
reason = payload.get("reason") or payload.get("end_reason")
|
|
63
|
+
if isinstance(reason, str) and reason.lower() in {"error", "failed", "failure"}:
|
|
64
|
+
return "error"
|
|
65
|
+
exit_code = payload.get("exit_code") or payload.get("exitCode")
|
|
66
|
+
if isinstance(exit_code, int) and exit_code != 0:
|
|
67
|
+
return "error"
|
|
68
|
+
return "done"
|
|
69
|
+
return ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main() -> int:
|
|
73
|
+
pane_id = os.environ.get("TMUX_PANE", "").strip()
|
|
74
|
+
if not pane_id:
|
|
75
|
+
return 0
|
|
76
|
+
try:
|
|
77
|
+
payload = json.load(sys.stdin)
|
|
78
|
+
except json.JSONDecodeError:
|
|
79
|
+
return 0
|
|
80
|
+
event = normalize_event(payload)
|
|
81
|
+
if not event:
|
|
82
|
+
return 0
|
|
83
|
+
state = map_event_to_state(event, payload)
|
|
84
|
+
if not state:
|
|
85
|
+
return 0
|
|
86
|
+
write_state(pane_id, state, "claude", payload)
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def state_dir() -> Path:
|
|
10
|
+
root = os.environ.get("PEAKYPANES_AGENT_STATE_DIR")
|
|
11
|
+
if root:
|
|
12
|
+
return Path(root)
|
|
13
|
+
runtime = os.environ.get("XDG_RUNTIME_DIR") or "/tmp"
|
|
14
|
+
return Path(runtime) / "peakypanes" / "agent-state"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def write_state(pane_id: str, state: str, tool: str, payload: dict) -> None:
|
|
18
|
+
path = state_dir()
|
|
19
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
record = {
|
|
21
|
+
"state": state,
|
|
22
|
+
"tool": tool,
|
|
23
|
+
"updated_at_unix_ms": int(time.time() * 1000),
|
|
24
|
+
"pane_id": pane_id,
|
|
25
|
+
}
|
|
26
|
+
if payload.get("turn-id"):
|
|
27
|
+
record["turn_id"] = payload.get("turn-id")
|
|
28
|
+
if payload.get("thread-id"):
|
|
29
|
+
record["thread_id"] = payload.get("thread-id")
|
|
30
|
+
if payload.get("cwd"):
|
|
31
|
+
record["cwd"] = payload.get("cwd")
|
|
32
|
+
target = path / f"{pane_id}.json"
|
|
33
|
+
tmp = target.with_suffix(".json.tmp")
|
|
34
|
+
tmp.write_text(json.dumps(record))
|
|
35
|
+
tmp.replace(target)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> int:
|
|
39
|
+
pane_id = os.environ.get("TMUX_PANE", "").strip()
|
|
40
|
+
if not pane_id:
|
|
41
|
+
return 0
|
|
42
|
+
if len(sys.argv) < 2:
|
|
43
|
+
return 0
|
|
44
|
+
try:
|
|
45
|
+
payload = json.loads(sys.argv[-1])
|
|
46
|
+
except json.JSONDecodeError:
|
|
47
|
+
return 0
|
|
48
|
+
if payload.get("type") != "agent-turn-complete":
|
|
49
|
+
return 0
|
|
50
|
+
write_state(pane_id, "idle", "codex", payload)
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
raise SystemExit(main())
|