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 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,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
- - `←/→` project, `↑/↓` session, `⇧↑/⇧↓` window, `?` help
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 `?` help):
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
- - `c` close project (kills all running sessions in project)
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
- - `K` kill session
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
- - `space` toggle window list
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, `/` filter, `q` quit
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.1",
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.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.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())