Fix hooks.
This commit is contained in:
@@ -279,7 +279,7 @@ in principle, but a terminal grid is small.
|
||||
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
|
||||
it is working, waiting on you, or finished and still unanswered. 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
|
||||
@@ -345,18 +345,45 @@ 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** |
|
||||
| green | **finished, and you haven't answered it** |
|
||||
| red | stopped on an error |
|
||||
| none | idle |
|
||||
|
||||
Each colour is carried at three sizes, so it survives being glanced at rather
|
||||
than read: an 11px dot on the row, a bar down the row's leading edge, and a wash
|
||||
behind the whole row. Working gets the bar without the wash — it is the resting
|
||||
state of a busy afternoon, and tinting half the sidebar all day would only teach
|
||||
you to stop looking. Inside a tab, a pane that is asking colours its own frame
|
||||
and header the same way, which is what picks it out of a four-way split.
|
||||
|
||||
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.
|
||||
looks exactly like one that never ran, so a pane that finishes latches green and
|
||||
stays that way until you deal with it. Nothing else tells you a tab is worth
|
||||
going back to.
|
||||
|
||||
**Opening the tab clears the row.** A pane's own dot clears when you go to that
|
||||
pane or type in it. The two answer slightly different questions: the row's is
|
||||
"should I go there?", which visiting settles whether or not you then deal with
|
||||
everything inside, and a pane's is "have you dealt with me?", which only you can
|
||||
answer. In a split that difference is the whole point — you open a flagged tab,
|
||||
the row goes quiet, and the panes you haven't been to yet are still marked.
|
||||
|
||||
Work that finishes while you are sitting in the tab flags it too, deliberately.
|
||||
Having a tab on screen when a session stopped says only that the pixels were in
|
||||
front of you; watching it finish and then moving on to something else is the
|
||||
case this is most needed for. It goes quiet as soon as you answer that pane, or
|
||||
the next time you come back to the tab.
|
||||
|
||||
Green also outranks purple. A tab holding three sessions goes green as soon as
|
||||
any one of them finishes rather than waiting for the last one to stop, because
|
||||
"one of these is ready for you" is the news. The two states that actually want
|
||||
something from you — amber and red — outrank it in turn.
|
||||
|
||||
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.
|
||||
four-pane tab still reduces to one dot. The pane headers carry their own dots,
|
||||
so once the row has brought you to the tab, they say which pane inside it was
|
||||
the one asking; the row stays lit until every finished pane in it has been
|
||||
answered.
|
||||
|
||||
### How it gets there
|
||||
|
||||
@@ -385,13 +412,28 @@ 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.
|
||||
The hook writes to a pty, not to a socket or a daemon: 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.
|
||||
|
||||
Finding that pty is the one genuinely fiddly part, and `/dev/tty` — the obvious
|
||||
answer, and what this used to do — is the wrong one. **Claude Code starts each
|
||||
hook in its own session**, so a hook has no controlling terminal at all and
|
||||
opening `/dev/tty` fails with `ENXIO`. It fails invisibly, too: `[ -w /dev/tty ]`
|
||||
returns true regardless, because it stats a path whose mode is `0666` rather
|
||||
than opening it, so guarding on that reports success and then writes into
|
||||
nothing. Every state change was being dropped on the floor with no error
|
||||
anywhere.
|
||||
|
||||
The pty is only one hop away, though — it is on Claude Code's own standard file
|
||||
descriptors. So the script tries its controlling terminal by *opening* it, and
|
||||
failing that walks up `/proc` for the nearest ancestor holding a pty, with a
|
||||
`ps -o tty=` fallback for systems without `/proc`. The nearest ancestor is the
|
||||
right answer even with something in between: a Claude running inside tmux inside
|
||||
a pane finds tmux's pty, which is where its output actually goes.
|
||||
|
||||
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
|
||||
@@ -403,7 +445,7 @@ 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:
|
||||
$ # written to the guest's console, observed on the host pty:
|
||||
b'\x1b]9;4;3\x07' b'\x1b]0;hello-from-guest\x07' b'\x1b]9;4;0\x07'
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user