Skip to content

feat: export live human text-input via @human-input-marker option#309

Closed
quazardous wants to merge 1 commit into
psmux:masterfrom
quazardous:feat/human-input-marker
Closed

feat: export live human text-input via @human-input-marker option#309
quazardous wants to merge 1 commit into
psmux:masterfrom
quazardous:feat/human-input-marker

Conversation

@quazardous

Copy link
Copy Markdown
Contributor

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-pane can'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 via handle_key → forward_key_to_active, while send-keys/paste-buffer go through commands.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:

psmux set-option -t mysession @human-input-marker /path/to/typing.marker

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:

  • Text only — printable chars. Control codes, Enter, Tab, navigation keys, and any Ctrl/Alt-modified key are ignored (moving around a pane isn't "typing"). Classifier: is_human_text_key.
  • Throttled to ~once per 200 ms, so fast typing doesn't hammer the filesystem — the signal is "typed recently", not a keylog (file content is just a timestamp).
  • Opt-in, zero-cost when unset — no file I/O happens unless the option points at a path. No behavior change for anyone not using it.

Implementation

  • note_human_input(app, key) is called at the top of forward_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 by send-keys/paste-buffer. That physical separation is the whole point.
  • is_human_text_key is split out as a pure function for unit testing.
  • One new AppState field (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.

Heads-up unrelated to this PR: src/input.rs on master references tests-rs/test_issue284_pageup_wsl.rs via #[path = ...] mod, but that file isn't in the repo, so cargo test currently fails to compile the test target on a clean checkout. I stubbed it locally to run these tests; you may want to add or remove that mod declaration separately.

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.

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.
quazardous added a commit to quazardous/aiball that referenced this pull request May 21, 2026
… (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).
@quazardous

Copy link
Copy Markdown
Contributor Author

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,
David

@psmux

psmux commented May 23, 2026

Copy link
Copy Markdown
Owner

@tarikguney Any thoughts? what do you think?

@tarikguney

tarikguney commented May 23, 2026

Copy link
Copy Markdown
Collaborator

@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:

  • It is not full human-presence detection; it is "recent printable text keypresses."
  • Real user activity often includes Enter, navigation, and shortcuts, which are intentionally excluded.
  • It is session-level, so it can’t identify which pane is receiving human input in multi-pane workflows.
  • It introduces file-path lifecycle concerns (permissions, path hygiene, cleanup).
  • Consumers still must tune polling windows, so state remains heuristic.

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.

@psmux

psmux commented May 23, 2026

Copy link
Copy Markdown
Owner

@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.

@psmux psmux closed this May 23, 2026
@quazardous

Copy link
Copy Markdown
Contributor Author

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.

@psmux

psmux commented May 23, 2026

Copy link
Copy Markdown
Owner

@quazardous yes, have a look at https://github.com/psmux/psmux-plugins

@quazardous

Copy link
Copy Markdown
Contributor Author

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 capture-pane can't (it only sees the merged render).

A plugin can't do that today: no hook or control-mode event fires on input, and bind-key consumes the key (fine for a single AFK chord, but unworkable for every printable key — a shell fork per keystroke).

Mini-hack to unblock pluginization: expose one read-only, per-pane signal — e.g. #{pane_last_input} = ms since the last printable keystroke. No file, no policy, no heuristic in core (just a timestamp you already track); plugins own all the policy. That answers each concern raised (file lifecycle, per-pane, heuristic) and lets the whole feature live as a plugin.

tarikguney pushed a commit that referenced this pull request May 24, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants