Skip to content

feat: add #{pane_last_input} — read-only live human-input signal#311

Merged
tarikguney merged 2 commits into
psmux:masterfrom
quazardous:feat/pane-last-input
May 24, 2026
Merged

feat: add #{pane_last_input} — read-only live human-input signal#311
tarikguney merged 2 commits into
psmux:masterfrom
quazardous:feat/pane-last-input

Conversation

@quazardous

Copy link
Copy Markdown
Contributor

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

psmux display-message -t dev -p '#{pane_last_input}'   # "740", or "" if none yet

Why / how it answers the #309 objections

  • No file, no lifecycle — it's one Option<Instant> on the Pane, freed with the pane.
  • No policy / heuristic in core — core only exposes a timestamp; the consumer decides ("value < N ms" = typing now).
  • Per-pane, not session-level (the multi-pane gap you flagged).
  • Human typing only — set in forward_key_to_active (the sole client-keystroke path); send-keys/send-paste go through send_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 separation capture-pane can'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-key consumes 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 in forward_key_to_active (+ a small is_human_text_key helper); 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 clippy clean.
  • Verified live: the var is recognized and stays empty after 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.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_active via a new is_human_text_key helper + 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.

Comment thread src/input.rs Outdated
Comment on lines +1892 to +1897
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());
}
}
@tarikguney

tarikguney commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the follow-up, @quazardous . This is closer. Before merge, I think these are must-have so the contract is clear:

  • Please frame this as a text input route signal, not human detection.
  • Please remove “human” wording from all names and text (field names, format name, comments, docs, tests). The naming should describe route/class only.
  • Please define it explicitly: interactive text route = handle_key -> forward_key_to_active (updates), injected text route = send-keys / send-paste / send-text (does not update).
  • Please document key scope clearly: printable text counts; Enter/arrows/shortcuts do not.
  • Please add one caveat: if a bot sends real key events through the interactive route, this signal will also update.
  • Please keep tests strict to this contract (interactive positive path, injected negative path, key-classifier boundaries).

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

Copy link
Copy Markdown
Contributor Author

Done, pushed. Reframed entirely as a text-input route signal:

  • Removed all "human" wording from names/text. last_human_inputlast_text_input, is_human_text_keyis_text_input_key, and the format var → #{pane_last_text_input}.
  • Documented the contract: interactive route (handle_key → forward_key_to_active) updates it; injected route (send-keys / send-paste / send-textsend_text_to_active) does not. Printable text only — Enter/arrows/shortcuts/Ctrl/Alt excluded. Added the caveat that a bot injecting real key events through the interactive route also updates it (measures the route, not the actor).
  • Also fixed @copilot's catch: 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.

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 Pane; a real one needs a PTY/child), so I can't drive forward_key_to_active vs send_text_to_active from a unit test. I verified the injected-negative via integration (display-message -p '#{pane_last_text_input}' stays empty after send-keys); route separation is otherwise structural (only forward_key_to_active writes the field). Happy to add a process-spawning integration test if you'd prefer that over the structural guarantee.

@quazardous

Copy link
Copy Markdown
Contributor Author

Hi, thx for considering this little contribution :)

@tarikguney

Copy link
Copy Markdown
Collaborator

Thanks for the follow-up, @quazardous — this is in good shape now.

You addressed the core asks in code:

  • reframed as a text-input route signal (not human/activity detection),
  • renamed symbols to the route-based contract (last_text_input, is_text_input_key, #{pane_last_text_input}),
  • clarified interactive vs injected route behavior,
  • tightened key-scope boundaries,
  • and fixed sync/dead-pane stamping so only receiving panes are updated.

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 human / pane_last_input wording). After that, this is good to go from my side.

CC @psmux: implementation looks aligned and approved pending that metadata text cleanup.

@tarikguney tarikguney merged commit 3f44a4e into psmux:master May 24, 2026
3 checks passed
psmux added a commit that referenced this pull request May 27, 2026
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>
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