feat: add #{pane_last_input} — read-only live human-input signal#311
Conversation
A minimal, opt-in alternative to psmux#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 psmux#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.
There was a problem hiding this comment.
Pull request overview
Adds a minimal per-pane, read-only “human typing recency” primitive to psmux by tracking the time of the last printable, unmodified (no Ctrl/Alt) keystroke that came from a real client key path, and exposing it via a new format variable.
Changes:
- Add
Pane::last_human_input: Option<Instant>and initialize it for all pane creation paths. - Track printable human keystrokes in
forward_key_to_activevia a newis_human_text_keyhelper + unit tests for the classifier. - Expose
#{pane_last_input}in the format engine and document it for integrations.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests-rs/test_pane_last_input.rs | Adds unit tests for the human-text keystroke classifier. |
| src/types.rs | Adds a per-pane last_human_input timestamp field. |
| src/proxy_pane.rs | Initializes last_human_input for proxy panes. |
| src/popup.rs | Initializes last_human_input for popup panes. |
| src/pane.rs | Initializes last_human_input for standard pane creation/split paths. |
| src/input.rs | Adds is_human_text_key and updates forward_key_to_active to record last human input; wires in the new test module. |
| src/format.rs | Adds pane_last_input format variable expansion (ms since last human input). |
| docs/integration.md | Documents #{pane_last_input} for external tooling/integrations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if is_human_text_key(&key) { | ||
| let win = &mut app.windows[app.active_idx]; | ||
| if let Some(p) = active_pane_mut(&mut win.root, &win.active_path) { | ||
| p.last_human_input = Some(Instant::now()); | ||
| } | ||
| } |
|
Thanks for the follow-up, @quazardous . This is closer. Before merge, I think these are must-have so the contract is clear:
Main ask: the full PR language and naming should reflect “text input route signal,” not “human/activity detection.” CC: @psmux |
…eview psmux#311) Address tarikguney's review: this is a text-input *route* signal, not human/ activity detection. Drop all "human" wording and state the contract. - Rename: Pane.last_human_input → last_text_input; is_human_text_key → is_text_input_key; format var #{pane_last_input} → #{pane_last_text_input}. Reword field/fn/format comments, docs and tests in route terms. - Document the contract: interactive route (handle_key → forward_key_to_active) updates it; injected route (send-keys / send-paste / send-text → send_text_to_active) does NOT. Printable text only — Enter/arrows/shortcuts/ Ctrl/Alt excluded. Caveat: a bot injecting real key events through the interactive route also updates it (this measures the route, not the actor). - Fix (Copilot review): stamp exactly the panes that RECEIVE the key — every non-dead pane under sync-input, else the active pane only if alive — instead of always the active pane even when dead and ignoring sync fan-out. - Tests: keep strict key-classifier boundaries (printable vs control/Ctrl/Alt/ nav/F-key); route separation is structural (only forward_key_to_active writes the field) — noted in the test header.
|
Done, pushed. Reframed entirely as a text-input route signal:
On tests: the key-classifier boundaries are unit-tested strictly (printable vs control/Ctrl/Alt/nav/F-key). The route positive/negative isn't unit-testable in the current setup — the test suite uses empty windows (no lightweight |
|
Hi, thx for considering this little contribution :) |
|
Thanks for the follow-up, @quazardous — this is in good shape now. You addressed the core asks in code:
I’m approving this direction. One final cleanup request before merge: please align the PR title/body with the updated contract (they still contain old CC @psmux: implementation looks aligned and approved pending that metadata text cleanup. |
Add two read-only per-pane format variables for the last non-text key
received on the interactive input route:
- #{pane_last_special_key} -- canonical bind-key name (Escape, Enter,
Up, F9, C-c, M-a, ...)
- #{pane_last_special_key_ms} -- milliseconds since it arrived
Complement of #{pane_last_text_input} (#311): together they partition
all interactive keys into text vs non-text. Same route contract: set
only in forward_key_to_active (the injected route never updates it).
Also refactors the sync-input-aware pane selection into a shared
for_each_receiving_pane helper, deduplicating the text-input stamping.
Includes unit tests (key classification + naming) and E2E test.
Co-authored-by: David Berlioz <berliozdavid@gmail.com>
Minimal follow-up to #309. That PR (an option-driven file marker) was rightly declined as too much policy/lifecycle for core. This is the smallest primitive that still unblocks the use case, addressing each concern raised there.
What
One read-only per-pane format variable,
#{pane_last_input}= ms since the last printable human keystroke into that pane (empty until the first one).Why / how it answers the #309 objections
Option<Instant>on thePane, freed with the pane.forward_key_to_active(the sole client-keystroke path);send-keys/send-pastego throughsend_text_to_active, so injected input never updates it. Only printable text counts (is_human_text_key); Enter/navigation/shortcuts and Ctrl/Alt chords are excluded. This is the separationcapture-panecan't make.That's the bit that's impossible to build outside core today: no hook or control-mode event fires on input, and
bind-keyconsumes the key (fine for one chord, unworkable per-keystroke). With this primitive, the full feature can live as a plugin/external tool — psmux owns none of the policy.Diff
One field on
Pane+ its inits; ~6 lines inforward_key_to_active(+ a smallis_human_text_keyhelper); one#{pane_last_input}arm in the format engine; a classifier unit test; a doc note.Tests / validation
cargo test— classifier unit tests (tests-rs/test_pane_last_input.rs);cargo clippyclean.send-keys(injected input correctly ignored — the separation in action). The positive path (a real keystroke populating it) needs an attached client, so it's covered by the unit test + the single-line set site.