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 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 setup
36
+ peakypanes
37
37
  ```
38
38
 
39
- > npm packages are currently published for macOS and Linux.
40
- > Windows users should install from the GitHub release or build with Go.
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 setup
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
- - `←/→` project, `↑/↓` session, `⇧↑/⇧↓` window, `?` help
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 `?` help):
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
- - `c` close project (kills all running sessions in project)
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
- - `K` kill session
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
- - `space` toggle window list
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, `/` filter, `q` quit
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.1",
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.1",
16
- "peakypanes-darwin-arm64": "0.0.1",
17
- "peakypanes-linux-x64": "0.0.1",
18
- "peakypanes-linux-arm64": "0.0.1"
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())