Skip to content

feat(commands): neutral send_keys action to replay a key/text step list to a pane#5

Closed
Micaxes wants to merge 1 commit into
plotarmordev:mainfrom
Micaxes:feat/send-keys-command
Closed

feat(commands): neutral send_keys action to replay a key/text step list to a pane#5
Micaxes wants to merge 1 commit into
plotarmordev:mainfrom
Micaxes:feat/send-keys-command

Conversation

@Micaxes

@Micaxes Micaxes commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Adds a connector-facing send_keys command that surfaces tendwire's existing pane.send_keys / pane.send_text socket primitives, so a connector can answer a blocked TUI prompt (digit+enter, arrow toggles, or navigate + type a write-in) without ever touching herdr itself. The connector builds the sequence; tendwire relays it verbatim over the authoritative socket path.

Why

A connector that surfaces an agent's AskUserQuestion / ExitPlanMode prompt (e.g. to chat/Telegram) needs a neutral way to send the chosen answer back to the pane. Today the only mutating action is send_instruction, whose payload is a single line of text (Enter-terminated, escape/control sequences stripped) — it can't press a digit, toggle checkboxes with arrows, or navigate to a write-in field. send_keys fills that gap using primitives the socket backend already exposes.

Shape

  • commands.py: send_keys added to ALLOWED_ACTIONS. It carries params.steps — an ordered list where each step has exactly one of keys (a non-empty list of whitelisted neutral key tokens: enter/tab/space/backspace/escape/arrows/home/end/delete/pageup/pagedown/a digit/ctrl+<letter>) or text (validated identically to instruction.text, so a write-in can't smuggle escape/control sequences). New validate_pane_steps().
  • actions.py: _send_keys_result gives a dry-run preview (resolve target → dry_run); the real replay is socket-only, so a non-dry-run on the pure path reports backend_unsupported.
  • command_submission.py: non-dry-run send_keys flows through the existing mutating path (receipts + request_id idempotency) to a new _socket_send_keys_envelope that resolves the neutral target to a pane and replays the steps via _submit_private_pane_keys (a small inter-step delay lets the TUI repaint).

Neutrality

The target stays neutral (worker_id/worker_fingerprint/space_id/name); pane_id/terminal_id/session_id remain forbidden request fields. Key tokens are a fixed whitelist carrying no routing/identity data.

Tests

+11 tests (valid single/multi/write-in sequences, target/request_id requirements, rejection of empty/unknown/escape/both-keys-and-text steps, dry-run). 364 command/action/submission tests pass. The pre-existing test_herdr_socket / test_daemon / test_herdr_events failures in a socket-less environment are unrelated to this change (identical with and without it).

🤖 Generated with Claude Code

…ist to a pane)

Surfaces the existing pane.send_keys / pane.send_text socket primitives as a
connector-facing command so a connector can answer a TUI prompt (digit+enter,
arrow toggles, or navigate + type a write-in) without ever touching herdr
itself. The connector builds the sequence; tendwire relays it verbatim.

- commands.py: `send_keys` in ALLOWED_ACTIONS; carries `params.steps`, an ordered
  list where each step has exactly one of `keys` (whitelisted neutral key tokens)
  or `text` (validated like instruction.text). New validate_pane_steps().
- actions.py: dry-run preview via _send_keys_result (real delivery is the socket
  path); non-dry-run on the pure path reports backend_unsupported.
- command_submission.py: route non-dry-run send_keys through the mutating path
  (receipts + idempotency) to a new _socket_send_keys_envelope that resolves the
  neutral target to a pane and replays the steps via _submit_private_pane_keys.

Target stays neutral (worker_id/fingerprint/space/name); key tokens carry no
routing/identity data. +11 tests; 364 command/action/submission tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jerryfane

Copy link
Copy Markdown
Contributor

Cross-referencing from the connector half — I just reviewed herdres#150 (changes requested), and one finding really belongs to this PR's contract, so raising it here before the interface freezes:

Raw send_keys steps put the TUI calibration on the side that can't see the pane. With this contract, the connector must hard-code things like "a digit jumps to absolute row N", "the write-in row is N+1", "the Submit tab is one right away", and "the cursor starts at row 1" — and then replay them blind. Two failure modes follow:

  1. Staleness: if the prompt was answered at the workstation, superseded, or covered by a modal (we hit exactly this in prod — Claude's feedback-survey modal eating input), the replayed steps type into whatever is on screen. Tendwire relays the steps verbatim, so neither side verifies the prompt is still displayed.
  2. Silent drift: any Claude Code TUI change (layout, keybindings, default selections) makes the steps do the wrong thing with no error signal — the keys "succeed" either way.

Suggestion: since Tendwire already carries the decision in pending (meta.decision with decision_ref + option refs), accept a semantic action instead — answer_decision {decision_ref, selection} — and have Tendwire (the only party that can observe the pane) validate the prompt is still present and own the key calibration per Claude Code version. send_keys could stay as a lower-level escape hatch, but the decision-answer path deserves the validated shape. Happy to help spec it — much cheaper to settle now than after both halves land.

@plotarmordev plotarmordev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thank you for the contribution. I reviewed this against current main, including the now-merged semantic remote-decision pair (Tendwire #6 / Herdres #153). I do not think a generic connector-facing send_keys action should be added.\n\nRelease blockers:\n\n1. The action delegates backend calibration to an untrusted connector and exposes a low-level sequence of text, navigation, Enter, Escape, and ctrl+letter operations. That is materially broader than the neutral API boundary: a stale or buggy connector can mutate whatever TUI state currently owns the pane. Current main instead accepts a semantic answer_decision selection and keeps private key calibration in Tendwire.\n2. The request is not bound to the exact current pending revision, private binding fingerprint, and observed pane target. A prompt can change between connector observation and replay, allowing the sequence to answer or operate on a different screen.\n3. Multi-step replay is not transactionally knowable. If an early step applies and a later Herdr call returns an error, _socket_send_keys_envelope returns backend_failed rather than terminal uncertainty even though the pane was partially mutated. The stored receipt can therefore claim a definitive failure for an externally changed pane.\n4. The branch predates the current command envelope, receipt authority, pre-send classification, answer_pending, and answer_decision work. Applying it to current main conflicts in command_submission.py, core/actions.py, core/commands.py, and tests/test_commands.py; merging it would require a redesign rather than a conflict-only refresh.\n\nThe original use case is now covered by answer_decision: connectors send option references or write-in text, Tendwire revalidates the exact durable decision/source and owns the calibrated private pane operations. If another TUI interaction is needed later, it should be added as another semantic, revision-bound action rather than a public raw-key relay.

@plotarmordev

Copy link
Copy Markdown
Owner

Closing as superseded by the narrower semantic answer_decision contract in #6 and its paired Herdres implementation. The detailed review above records the safety and current-main integration reasons; no contributor branch history was modified.

@Micaxes
Micaxes deleted the feat/send-keys-command branch July 17, 2026 19:33
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