feat: export live human text-input via @human-input-marker option#309
feat: export live human text-input via @human-input-marker option#309quazardous wants to merge 1 commit into
@human-input-marker option#309Conversation
Add an opt-in session option `@human-input-marker <file>`. While set, psmux touches that file (mtime) on every real human TEXT keystroke routed to a pane — and never on send-keys/paste-buffer, which take a separate internal path (commands.rs -> send_text_to_active, not handle_key -> forward_key_to_active). This lets external tools tell a human typing apart from programmatic injection, live, even while the child app is busy streaming — something capture-pane polling can't do (rendered output merges the two streams and only updates at idle). The two byte streams are physically distinct paths inside psmux, so no heuristic is needed. - Text only: printable chars; control codes, Enter/Tab/nav, and any Ctrl/Alt-modified key are ignored. - Throttled to ~once per 200ms so fast typing doesn't hammer the FS. - Opt-in, zero-cost when unset. note_human_input() is called at the top of forward_key_to_active (the sole client-keystroke path). Classifier split into the pure is_human_text_key() for unit testing. Tests in tests-rs/test_issue281_human_input.rs. Used by claude-loop (github.com/quazardous/aiball) on Windows to drive its "human present" status without a nested-PTY proxy.
… (psmux#309) (#13) cli.ts sets the `@human-input-marker` session option to the loop's existing `human-typing` marker path at new-session. psmux builds with the feature (psmux/psmux#309) then touch that marker natively on real human keystrokes — busy included, no nested ConPTY — and the existing humanIsTyping / setTmuxStatus / pushState readers consume it unchanged. Version-safe: older psmux + tmux store the unknown option and ignore it, so the pane-diff fallback still applies (no regression). Complements the ConPTY proxy (strategy B); see docs/PTY-PROXY-WINDOWS.md (strategy A section updated to reflect the upstream PR).
|
hi, here is the human behind the PR. the goal is to trigger something when the tty really receive human input. for now I've created a proxy that does the trick but I thought that maybe I'm not the only one wanting this feat :) Best regards, |
|
@tarikguney Any thoughts? what do you think? |
|
@psmux Thanks for tagging me. I need a bit more time to think this through. My initial view is that I’m not convinced we should bring this responsibility into psmux core. Features like this can add ongoing complexity over time, and I want us to be explicit about whether we’re committed to maintaining that surface area long-term. To be clear, I do see the benefit this PR adds: reliable separation of human printable typing from injected input, with an opt-in low-overhead signal. But I also see scope gaps and operational costs:
So my current position is: useful idea, but I want to think more about ownership and long-term maintenance burden before supporting merging it into core. |
|
@tarikguney I think that's fair enough 👍 . @quazardous , appreciate the contribution. Perhaps you could try to make this an external Psmux plugin so people who need this can install it. |
|
Yes I understand your POV. And on my side the level of customisation is growing too. Is a plugin something framed / structured ? Claude code missed it in the repo exploration. |
|
@quazardous yes, have a look at https://github.com/psmux/psmux-plugins |
|
Follow-up on the "make it a plugin" suggestion — I looked into the plugin surface and there's one real blocker worth flagging. The need isn't a status widget. claude-loop auto-injects keystrokes into the pane and must yield the instant a human types. So it has to tell three streams apart — human keystrokes vs. its own injected input vs. the app's output — which A plugin can't do that today: no hook or control-mode event fires on input, and Mini-hack to unblock pluginization: expose one read-only, per-pane signal — e.g. |
A minimal, opt-in alternative to #309 (which proposed an option-driven file marker, declined as too much policy for core). This exposes only a read-only per-pane format variable: milliseconds since the last printable HUMAN keystroke into that pane (empty until the first one). - Set in forward_key_to_active (the sole client-keystroke path) for printable text only — Enter/nav/shortcuts and Ctrl/Alt chords excluded (is_human_text_key). send-keys/paste-buffer go through send_text_to_active, so injected input never updates it: the signal is human typing, which capture-pane can't isolate. - Stored on the Pane (one Option<Instant> field) → freed with the pane, no file lifecycle, no policy/heuristic in core. Consumers own the policy (treat "value < N ms" as "typing now"). - Per-pane (not session-level), addressing the multi-pane gap raised on #309. This is the smallest primitive that lets external tools / plugins build live human-input detection on top, without psmux owning any of the policy. Tests in tests-rs/test_pane_last_input.rs (classifier); doc in docs/integration.md.
Problem
Some integrations need to know when a human is actively typing into a pane, as distinct from input injected programmatically via
send-keys/paste-buffer.capture-panecan't answer this: it only shows rendered output, where a human's keystroke echo and the application's own output are already merged into the same cells. Polling it also misses typing that happens while the app is busy streaming (the screen is changing for other reasons). The only place the two input sources are physically distinct is inside psmux itself — a human's keys arrive viahandle_key → forward_key_to_active, whilesend-keys/paste-buffergo throughcommands.rs → send_text_to_active. Two different code paths.What this adds
An opt-in session option,
@human-input-marker, pointing at a file path:While it's set, psmux touches that file (updates its mtime) on every real human text keystroke routed to a pane in that session — and never on
send-keys/paste-buffer. An external tool then treats "marker mtime within the last N seconds" as "a human is typing now", live, even while the child app is busy.Design choices:
Enter,Tab, navigation keys, and anyCtrl/Alt-modified key are ignored (moving around a pane isn't "typing"). Classifier:is_human_text_key.Implementation
note_human_input(app, key)is called at the top offorward_key_to_active— the single path real client keystrokes take to a pane. Programmatic injection doesn't go through it, so the marker is never touched bysend-keys/paste-buffer. That physical separation is the whole point.is_human_text_keyis split out as a pure function for unit testing.AppStatefield (last_human_input: Option<Instant>) for the throttle.Tests
tests-rs/test_issue281_human_input.rs(6 tests): the classifier (text vs control/Ctrl/Alt/nav), that the marker is touched only on text input, only when the option is set, and that rapid typing is throttled.Motivation / consumer
Built for claude-loop (an autonomous Claude Code wrapper) running on Windows under psmux: it needs to yield to a human typing in the pane. The alternative was a nested-ConPTY proxy interposed between psmux and the child — which works but pays a double-conhost translation tax. Since psmux already has the human/injection separation natively, exposing it directly is far cleaner. Generic enough for any tool that wants a live "human present" signal.