peakypanes 0.0.1 → 0.0.2
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 +105 -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,25 +240,26 @@ 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+h/ctrl+l` project, `ctrl+k/ctrl+j` session, `ctrl+u/ctrl+d` window, `tab/⇧tab` pane, `ctrl+g` help
|
|
252
247
|
|
|
253
|
-
Key bindings (also shown in
|
|
248
|
+
Key bindings (also shown in the help view):
|
|
254
249
|
|
|
255
250
|
Project
|
|
256
|
-
- `o` open project picker (creates session detached; stay in dashboard)
|
|
257
|
-
- `
|
|
251
|
+
- `ctrl+o` open project picker (creates session detached; stay in dashboard)
|
|
252
|
+
- `ctrl+b` close project (kills all running sessions in project)
|
|
258
253
|
|
|
259
254
|
Session
|
|
260
|
-
- `enter` attach/start session
|
|
261
|
-
- `n` new session (pick layout)
|
|
262
|
-
- `t` open in new terminal window
|
|
263
|
-
- `
|
|
255
|
+
- `enter` attach/start session (when reply is empty)
|
|
256
|
+
- `ctrl+n` new session (pick layout)
|
|
257
|
+
- `ctrl+t` open in new terminal window
|
|
258
|
+
- `ctrl+x` kill session
|
|
264
259
|
- rename session via command palette (`ctrl+p`)
|
|
265
260
|
|
|
266
261
|
Window
|
|
267
|
-
- `
|
|
262
|
+
- `ctrl+w` toggle window list
|
|
268
263
|
- rename window via command palette (`ctrl+p`)
|
|
269
264
|
|
|
270
265
|
Tmux (inside session)
|
|
@@ -272,7 +267,9 @@ Tmux (inside session)
|
|
|
272
267
|
|
|
273
268
|
Other
|
|
274
269
|
- `ctrl+p` command palette
|
|
275
|
-
- `r` refresh, `e` edit config,
|
|
270
|
+
- `ctrl+r` refresh, `ctrl+e` edit config, `ctrl+f` filter, `ctrl+q` quit (or `ctrl+c`)
|
|
271
|
+
|
|
272
|
+
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
273
|
|
|
277
274
|
### Dashboard Config (optional)
|
|
278
275
|
|
|
@@ -289,6 +286,38 @@ dashboard:
|
|
|
289
286
|
success: "(?i)done|finished|success|completed|✅"
|
|
290
287
|
error: "(?i)error|failed|panic|❌"
|
|
291
288
|
running: "(?i)running|in progress|building|installing|▶"
|
|
289
|
+
agent_detection:
|
|
290
|
+
codex: true
|
|
291
|
+
claude: true
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Agent Status Detection (Codex & Claude Code)
|
|
295
|
+
|
|
296
|
+
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`.
|
|
297
|
+
|
|
298
|
+
State files are written under `${XDG_RUNTIME_DIR:-/tmp}/peakypanes/agent-state` and keyed by `TMUX_PANE` (override with `PEAKYPANES_AGENT_STATE_DIR`).
|
|
299
|
+
|
|
300
|
+
**Codex CLI (TUI)**
|
|
301
|
+
|
|
302
|
+
Add a `notify` command in your Codex config to call the PeakyPanes hook script (Codex passes one JSON arg):
|
|
303
|
+
|
|
304
|
+
```toml
|
|
305
|
+
# ~/.codex/config.toml
|
|
306
|
+
notify = ["python3", "/absolute/path/to/peakypanes/scripts/agent-state/codex-notify.py"]
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Tip: with `npm i -g peakypanes`, the scripts live under `$(npm root -g)/peakypanes/scripts/agent-state/`.
|
|
310
|
+
Note: Codex `notify` only fires on turn completion, so running state still relies on regex/idle detection between turns.
|
|
311
|
+
|
|
312
|
+
**Claude Code (TUI)**
|
|
313
|
+
|
|
314
|
+
Configure hooks to run the PeakyPanes hook script (Claude passes JSON on stdin). Recommended events:
|
|
315
|
+
`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PermissionRequest`, `Stop`, `SessionEnd`.
|
|
316
|
+
|
|
317
|
+
Example hook command (wire it to each event above in Claude Code):
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
python3 /absolute/path/to/peakypanes/scripts/agent-state/claude-hook.py
|
|
292
321
|
```
|
|
293
322
|
|
|
294
323
|
### Tmux Config & Key Bindings
|
|
@@ -315,6 +344,45 @@ settings:
|
|
|
315
344
|
3. Project entry in `~/.config/peakypanes/config.yml`
|
|
316
345
|
4. Built-in `dev-3` layout (fallback)
|
|
317
346
|
|
|
347
|
+
## Testing
|
|
348
|
+
|
|
349
|
+
Run the unit tests with coverage:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
go test ./... -coverprofile /tmp/peakypanes.cover
|
|
353
|
+
go tool cover -func /tmp/peakypanes.cover | tail -n 1
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Race tests:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
go test ./... -race
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Tmux integration tests (requires tmux; opt-in):
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
PEAKYPANES_INTEGRATION=1 go test ./internal/tmuxctl -run Integration -count=1
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Manual npm smoke run (fresh HOME/XDG config):
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
scripts/fresh-run
|
|
372
|
+
scripts/fresh-run 0.0.2 --with-project
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
GitHub Actions runs gofmt checks, go vet, go test with coverage, race, and tmux integration tests on Linux.
|
|
376
|
+
|
|
377
|
+
## Release
|
|
378
|
+
|
|
379
|
+
See `RELEASE-DOCS.md` for the full release checklist (tests, tag, GoReleaser, npm publish).
|
|
380
|
+
|
|
381
|
+
## Windows
|
|
382
|
+
> npm packages are currently published for macOS and Linux.
|
|
383
|
+
> Windows users should install from the GitHub release or build with Go.
|
|
384
|
+
|
|
385
|
+
|
|
318
386
|
## For Teams
|
|
319
387
|
|
|
320
388
|
1. Run `peakypanes init --local` in your project
|
|
@@ -325,3 +393,18 @@ settings:
|
|
|
325
393
|
## License
|
|
326
394
|
|
|
327
395
|
MIT
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
## Links
|
|
399
|
+
|
|
400
|
+
- X/Twitter: [@kregenrek](https://x.com/kregenrek)
|
|
401
|
+
- Bluesky: [@kevinkern.dev](https://bsky.app/profile/kevinkern.dev)
|
|
402
|
+
|
|
403
|
+
## Courses
|
|
404
|
+
- Learn Cursor AI: [Ultimate Cursor Course](https://www.instructa.ai/en/cursor-ai)
|
|
405
|
+
- Learn to build software with AI: [AI Builder Hub](https://www.instructa.ai)
|
|
406
|
+
|
|
407
|
+
## See my other projects:
|
|
408
|
+
|
|
409
|
+
* [codefetch](https://github.com/regenrek/codefetch) - Turn code into Markdown for LLMs with one simple terminal command
|
|
410
|
+
* [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.2",
|
|
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.2",
|
|
16
|
+
"peakypanes-darwin-arm64": "0.0.2",
|
|
17
|
+
"peakypanes-linux-x64": "0.0.2",
|
|
18
|
+
"peakypanes-linux-arm64": "0.0.2"
|
|
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())
|