Add hook system.
This commit is contained in:
@@ -278,6 +278,11 @@ in principle, but a terminal grid is small.
|
||||
- **Saved layouts**: whole tabs — panes, splits, ratios, per-pane directories
|
||||
and scripts — opened in one go, parameterised by `{{name}}`, authored by
|
||||
arranging a tab and saving it. See [Layouts](#layouts)
|
||||
- **Pane status in the tab strip**, driven by OSC 9;4, so a tab can say whether
|
||||
it is working, waiting on you, or finished while you were elsewhere. See
|
||||
[Agent status](#agent-status)
|
||||
- **Renaming a tab**: `Ctrl+Shift+R`, right-click or double-click a tab row.
|
||||
A typed name pins the label; clearing it hands the label back to the panes
|
||||
|
||||
### Shortcuts
|
||||
|
||||
@@ -289,6 +294,7 @@ in principle, but a terminal grid is small.
|
||||
| `Ctrl+Shift+W` | close the focused pane (closes the tab with its last one) |
|
||||
| `Ctrl+Shift+←/→/↑/↓` | move the focused pane within its view |
|
||||
| `Ctrl+Shift+V` | paste into a terminal (bracketed-paste aware, refuses unsafe pastes) |
|
||||
| `Ctrl+Shift+R` | rename the current tab (empty name = follow the terminal) |
|
||||
| `Ctrl+PageUp/PageDown` | previous / next tab |
|
||||
| `Alt+1`..`Alt+8` | jump to tab N, `Alt+9` jumps to the last |
|
||||
|
||||
@@ -300,6 +306,97 @@ competes with the content's own mouse handling.
|
||||
Both add-pane shortcuts are also buttons in every pane header, which is how you
|
||||
open a web view without remembering the chord.
|
||||
|
||||
## Agent status
|
||||
|
||||
Playpen is mostly used to keep several Claude Code sessions side by side, and
|
||||
the question a sidebar full of them has to answer is "which of these needs me?".
|
||||
So a tab can carry a dot:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| purple, pulsing | working |
|
||||
| amber | waiting for you — a permission prompt, or a question |
|
||||
| green | **finished while you were looking at another tab** |
|
||||
| red | stopped on an error |
|
||||
| none | idle |
|
||||
|
||||
The green one is the point of the feature. A session that has gone back to idle
|
||||
looks exactly like one that never ran, so a tab that finishes work while it is
|
||||
not the visible one latches green and stays that way until you actually visit
|
||||
it. Nothing else tells you a tab is worth going back to.
|
||||
|
||||
Panes report this individually and the tab shows the most urgent of them, so a
|
||||
four-pane tab still reduces to one dot. The pane headers carry their own dots to
|
||||
say which pane inside it was the one asking.
|
||||
|
||||
### How it gets there
|
||||
|
||||
Programs report state with **OSC 9;4** — the ConEmu progress protocol, the same
|
||||
one Windows Terminal and Ghostty use for taskbar progress — and their title with
|
||||
OSC 0/2. Nothing about this is Claude-specific: a build that reports progress
|
||||
lights the same dot.
|
||||
|
||||
Claude Code has no idea about any of this, so a hook tells it:
|
||||
|
||||
```sh
|
||||
mise run install-claude-hooks # merges into ~/.claude/settings.json
|
||||
mise run uninstall-claude-hooks
|
||||
```
|
||||
|
||||
That installs `hooks/playpen-status.sh` into `~/.claude/hooks/` and wires it to
|
||||
five events: `UserPromptSubmit` → working (and sets the tab title from your
|
||||
prompt), `Notification` → waiting, `Stop` / `SessionStart` / `SessionEnd` →
|
||||
idle. The red state is deliberately not wired to anything: the obvious
|
||||
candidate, `PostToolUseFailure`, fires for tool errors Claude then goes on to
|
||||
recover from, so a tab would turn red constantly during ordinary work. The
|
||||
script still accepts `error` for anything of your own worth flagging that way.
|
||||
|
||||
Existing hooks are left alone — every entry is tagged with a marker
|
||||
comment, so re-running replaces Playpen's own entries rather than stacking
|
||||
duplicates, and the previous file is kept at `settings.json.playpen-backup`.
|
||||
Hooks are read at startup, so open a new session to pick them up.
|
||||
|
||||
The hook writes to `/dev/tty`, not to a socket or a daemon. A hook runs as a
|
||||
child of Claude Code, so its controlling terminal *is* the pty of the pane
|
||||
Claude is running in — the bytes land in that pane and no other, with nothing to
|
||||
configure and no way for two concurrent sessions to be mistaken for each other.
|
||||
It writes there rather than to stdout because Claude Code parses hook stdout as
|
||||
the hook's JSON result; an escape sequence written there would corrupt the hook
|
||||
protocol instead of reaching the terminal.
|
||||
|
||||
A tab's title follows your prompt for as long as Claude holds the foreground.
|
||||
Once it exits, the shell's own prompt sets the title back, which is the right
|
||||
answer — there is no task any more. A name you type yourself outranks both.
|
||||
|
||||
### Inside a VM
|
||||
|
||||
Claude in a microVM (`smolvm`, `krunvm`, anything libkrun-based) still works,
|
||||
because the guest console passes these bytes through to the host pty unchanged:
|
||||
|
||||
```
|
||||
$ # written to /dev/tty inside the guest, observed on the host pty:
|
||||
b'\x1b]9;4;3\x07' b'\x1b]0;hello-from-guest\x07' b'\x1b]9;4;0\x07'
|
||||
```
|
||||
|
||||
This is the reason for choosing escape sequences over a socket. A unix socket on
|
||||
the host is not reachable from inside a guest, and matching a message to a pane
|
||||
by cwd breaks as soon as guest paths stop corresponding to host ones. A console
|
||||
you already have is the one channel guaranteed to cross that boundary.
|
||||
|
||||
The guest has its own `$HOME` and so its own `~/.claude/settings.json`. Install
|
||||
the hook inside the image rather than on the host — the script is dependency-free
|
||||
POSIX `sh` and behaves identically on both sides:
|
||||
|
||||
```sh
|
||||
smolvm machine exec --name NAME -- mkdir -p /root/.claude/hooks
|
||||
# copy hooks/playpen-status.sh to /root/.claude/hooks/ via a mount or a heredoc,
|
||||
# then merge the same five events into the guest's settings.json.
|
||||
```
|
||||
|
||||
`jq` is used for the title when present and a `sed` fallback covers a minimal
|
||||
image that has no `jq`; if the title can't be read the state still reports and
|
||||
the tab simply keeps the name it had.
|
||||
|
||||
## Not implemented
|
||||
|
||||
This is a proof of concept, and the following are deliberately absent:
|
||||
|
||||
Reference in New Issue
Block a user