Skip to content

feat(terminal): paste into the input line and insert a line without Enter - #1853

Merged
devswha merged 10 commits into
mainfrom
feat/terminal-paste-insert
Oct 25, 2026
Merged

devswha merged 10 commits into
mainfrom
feat/terminal-paste-insert

Conversation

@devswha

@devswha devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner

This PR adds a WS contract (insert / insert-result, feature terminal-insert) and needs a real review before merging.

Refs #1047.

What it does for the user

On a phone, the terminal's key bar gets a Paste key, and the terminal's input line gets an Insert button beside Send.

  • Paste reads the clipboard from that tap alone and puts the text into the input line at the caret, never into the grid. A read that answers late is dropped when, meanwhile, the line or its caret changed, a composition ran, the pane was put out of sight, or the pane, mode, input lease or connection changed. A clipboard that refuses or is empty says so and points to the keyboard's own paste. In direct typing, Paste changes nothing and shows "Tap Input line to paste." in the key bar's own row.
  • Insert types one line into the terminal without Enter, so a path or a flag can be put at the prompt and edited there. The line clears only once the PC confirms that herdr has the text, and only the part that was sent. A refusal, a dropped connection or an older PC keeps the draft with a note, and nothing is resent automatically or through a fallback. Insert refuses several lines (herdr reports no paste mode of the program, so a guessed bracketed paste could run each line in a shell) and every control character, the paste markers included.
  • The input line shows its label ("Input line") and each action's word above its round target.

Server side (server/core/input.ts): the server checks the text's bounds, refuses observe connections, held or unready panes, a password prompt on screen and stale claims, and re-checks all of these after every wait and at the write. It then makes a single pane.send_text call with herdr's guard. It sends no Enter, no keys, no focus, no resize and no --takeover.

Why

Putting text at the prompt without running it (a path to edit, a flag to add) had no way in from a phone: the input line always sent Enter, and direct typing has no reliable paste. No widely used coding-agent app (the Codex and Claude apps, Paseo, T3 Code) offers a phone terminal draft with a separate no-Enter insert. agent-of-empires' compose sheet has Insert beside Send, but it clears its draft on a synchronous boolean, which this design rejects: the draft clears on the bridge's acknowledgement, as Send's does (DESIGN.md entry).

Rebase notes

The branch was cut about 180 commits ago, so it was squashed and rebased onto current main. Later commits on this PR are the review fixes and test updates for main's changes:

  • insertTerminalText moves into src/lib/useTerminalSends.ts, next to sendTerminalLine, following main's split of PaneTerminal.tsx into hooks. pasteTarget and the paste guidance stay in PaneTerminal.tsx, which owns their refs.
  • The new strings live in src/lib/i18n/composer.*.ts and terminal.*.ts.
  • The input slot's height keeps main's split padding (2 * --space-1) and adds the label row. The slot is taller than on main, so in line mode on touch screens the grid gets a little shorter.
  • fix(terminal-input): send a long input-line line to the pane's agent as its prompt #1791 (a long input-line line goes to the agent as a prompt, server/core/composer.ts) is untouched: Send behaves as on main.

How it was verified

  • bun run check fast on the rebased branch: workflow syntax, generated files, typecheck and build pass in every run. One full run passed all unit tests (6397 pass, 0 fail). The PC is shared by many test runs, and later runs each failed one unrelated timing-bound test (install-star.test.ts on an inotify EMFILE, portal.test.ts on a 2s bound). Both pass alone, and this branch does not touch them. CI's Fast checks are the clean run.
  • server/terminal-insert.contract.test.ts on the isolated herdr test session: 9/9 pass. It covers the insert frame typed with no Enter, the refusals (several lines, escapes, a program that turned bracketed paste off, observe, held, secret prompt, stale claim) and the write guard's race.
  • Unit tests for ws, compose, terminalDraft, shared/terminal-insert, i18n, ci-browser and conventions: 181 pass. The new ws.test.ts case (an Insert waiting on a liveness probe while the pane's input lease changes) fails with the fix removed.
  • scripts/terminal-paste-insert-demo-regression.ts (enrolled in ci-browser.sh) passes on the rebased branch, with 141 captures at 390 and 1440, light and dark, and Korean. On the rebase it now uses the ovis:* storage keys, and it measures Paste's 44px target through the key bar's expander from fix(terminal): space the phone's input line and key bar evenly #1527. I looked at the 390 rest state, the 1440 in-flight insert (text at the prompt, no Enter) and the Korean direct-typing guidance (one line in the key bar's row).

Review

  • A code-review agent asked for changes over one HIGH: the Enter key sat in the base i18n dictionary, so i18n.test.ts failed. Fixed. Its MEDIUM about a control-character refusal reading "the terminal is not ready" is fixed with a message of its own. Its LOW about a late snapshot answering "Update this PC" is fixed: Insert now answers capabilities_unknown, as submit does.
  • Codex (gpt-6-astra) found no HIGH. Two of its MEDIUMs are fixed: an Insert that waited on a liveness probe while the pane's input lease changed is now refused (new ws.test.ts case, which fails without the fix), and a paste whose pane was put out of sight is dropped.

Known limits (not in this PR)

  • Password prompt race. The server's password-prompt check reads the screen and then writes in a separate herdr call, so a prompt that appears between the two calls is not caught. Closing that gap needs a conditional write in herdr itself.
  • iOS Safari, unverified. Whether the Paste callout fires a selectionchange, which would drop the read, is not checked on iOS Safari.
  • Held messages, unverified. The server does not consult a pane's held messages before an insert.
  • HerdrSocket.canInsert() is used only by tests.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 8fe7da9 and 63cbbcd.

Files selected for processing: 36
  • DESIGN.md
  • changelog.d/terminal-paste-insert.md
  • docs/development.md
  • scripts/ci-browser.sh
  • scripts/ci-browser.test.ts
  • scripts/key-bar-customization-demo-regression.ts
  • scripts/terminal-input-regression.ts
  • scripts/terminal-input-spacing-regression.ts
  • scripts/terminal-paste-insert-demo-regression.ts
  • server/core/input.ts
  • server/core/server-core.ts
  • server/core/socket.ts
  • server/terminal-insert.contract.test.ts
  • shared/protocol.ts
  • shared/terminal-insert.test.ts
  • shared/terminal-insert.ts
  • site/demo/transport.ts
  • src/components/KeyBar.css
  • src/components/KeyBar.tsx
  • src/components/PaneTerminal.tsx
  • src/components/TerminalInput.css
  • src/components/TerminalInput.tsx
  • src/lib/compose.test.ts
  • src/lib/compose.ts
  • src/lib/i18n.test.ts
  • src/lib/i18n/composer.ja.ts
  • src/lib/i18n/composer.ko.ts
  • src/lib/i18n/composer.zh.ts
  • src/lib/i18n/terminal.ja.ts
  • src/lib/i18n/terminal.ko.ts
  • src/lib/i18n/terminal.zh.ts
  • src/lib/terminalDraft.test.ts
  • src/lib/terminalDraft.ts
  • src/lib/useTerminalSends.ts
  • src/lib/ws.test.ts
  • src/lib/ws.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

📝 Summary

Summary by CodeRabbit

  • New Features
    • On phones, tap Paste to place clipboard text at the cursor in the terminal input line. Paste is discarded if the input or terminal state changes before the clipboard responds.
    • Insert sends a single line to the terminal without pressing Enter. The draft clears only after confirmation; refusals or uncertain results preserve it and display a status note.
    • Added translated paste and insert messages in Japanese, Korean, and Simplified Chinese.
  • Documentation
    • Documented paste and insert behavior and added browser regression coverage for the new controls.

The change adds a WebSocket operation for inserting one line into a terminal without submitting it. It adds terminal input-line Insert and touch-key-bar Paste actions, stale-result checks, user feedback, and regression coverage for the protocol and UI behavior.

Changes: Terminal Paste and Insert

Layer / File(s) Summary
Insert contract and server handling
shared/protocol.ts, shared/terminal-insert.ts, server/core/*, server/terminal-insert.contract.test.ts, site/demo/transport.ts
The protocol adds insert requests and results, with shared text limits and validation. Server handlers check connection and pane state, reject unsafe input, and send text without Enter. Contract tests cover accepted text, refusals, and write-time state changes.
Client insert lifecycle
src/lib/ws.ts, src/lib/ws.test.ts, src/lib/useTerminalSends.ts, src/lib/compose.ts, src/lib/terminalDraft.ts, src/lib/*test.ts
The client tracks input leases and insert acknowledgements, and does not send when capability or readiness checks fail. The send hook maps results to notes. Draft versions prevent late acknowledgements from clearing newer text.
Input-line Insert and Paste UI
src/components/TerminalInput.*, src/components/KeyBar.*, src/components/PaneTerminal.tsx, src/lib/i18n/*, scripts/terminal-input-regression.ts, scripts/terminal-input-spacing-regression.ts, scripts/key-bar-customization-demo-regression.ts, DESIGN.md, changelog.d/terminal-paste-insert.md
The input line adds Insert and tap-triggered clipboard Paste. Paste is applied only when the captured input state remains current. The key bar displays paste guidance and status notes. Related styling, translations, and regression checks are updated.
Demo browser regression and CI
scripts/terminal-paste-insert-demo-regression.ts, scripts/ci-browser.sh, scripts/ci-browser.test.ts, docs/development.md
The browser regression covers paste races, insert acknowledgements, refusal cases, and display variants. Browser CI and the development command list include the script.

Priority: Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Merge Risk: Minimal · up to 63cbb

No actionable merge-blocking issue was established. The Paste guidance is reachable, and the Insert regression checks for grid resizing.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 57.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 31 files. (5 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly summarizes the two main changes: terminal Paste into the input line and Insert without Enter.
Description check Passed The description explains the user behavior, implementation scope, rationale, known limits, issue reference, and validation results. It includes WebSocket contract-test coverage and relevant regression…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the user behavior, implementation scope, rationale, known limits, issue reference, and validation results. It includes WebSocket contract-test coverage and relevant regression checks, although it uses headings different from the template.

Full details: Docstring Coverage

Explanation

Docstring coverage is 57.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 31 files. (5 skipped: 5 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

CodeRabbit's review limit was reached on this PR, so no CodeRabbit review ran. Per AGENTS.md, here is the review that stands in for it on this WS-contract change, with how each finding was resolved.

Code-review agent (omo-native-code-reviewer) on the rebased diff: REQUEST_CHANGES

  • HIGH: "Enter" sat in the base i18n dictionaries, but its only user is TerminalInput.tsx, so i18n.test.ts failed. Fixed: it moved to src/lib/i18n/composer.*.ts.
  • MEDIUM: invalid_insert_text (a control character, such as an escape copied with coloured output) read "the terminal is not ready", although a retry is refused the same way. Fixed: it has a note of its own, covered in compose.test.ts.
  • MEDIUM: HerdrSocket.canInsert() is used only by tests. Kept as a small, tested query, and noted in the PR body.
  • LOW: Insert said "Update this PC" when the snapshot was only late. Fixed: it returns capabilities_unknown, as submit does, covered in ws.test.ts.
  • LOW: the input slot is taller (a label row). Stated in the PR body and DESIGN.md.
  • LOW, unverified: iOS Safari's selectionchange on its Paste callout, and held messages beside an insert. Listed under known limits.
  • Checked correct: the rebase port (nothing lost or duplicated), the browser-script count, the demo feature list, fix(terminal-input): send a long input-line line to the pane's agent as its prompt #1791 untouched, and the server path (no Enter, keys, focus, resize or --takeover; escapes and line breaks refused; checks re-run at the write).

Codex (gpt-6-astra): no HIGH

  • MEDIUM: an Insert waiting on a liveness probe could go into a pane whose input lease changed meanwhile (reproduced by Codex). Fixed: HerdrSocket.insert keeps the lease from the tap and sends nothing when it changed. The new ws.test.ts case fails without the fix.
  • MEDIUM: a paste could land in a pane that was put out of sight while the clipboard answered. Fixed: pasteTarget requires the pane to be active and presented.
  • MEDIUM: the server's password-prompt check reads the screen, then writes in a separate herdr call. Not fixed here: closing that gap needs a conditional write in herdr. Listed under known limits.

A real review is still needed before merging: this PR adds a WS contract (insert / insert-result, feature terminal-insert).

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass review-work for 8a05cb3: REVIEW FAILED on one flaky assertion. The production code holds up. This PR adds a WS contract and CodeRabbit was rate-limited, so this comment records the independent review.

Blocker: the new "Insert takes no grid back" assertion (scripts/terminal-input-regression.ts:385) is flaky in a required lane. It failed in 1 of 4 runs at this head, with 10 resizes counted where 9 were expected. A copy with resize logging reproduced it on its second run. The extra resize comes from the test's own earlier lens switches refitting the grid (rows 37↔33), and it lands 21 ms after the count is taken at :372. Insert itself sends no resize. Please wait for the grid to settle before taking that count, for example by waiting for no resize within a frame or two, or for the fitted row count. The test runs through ui-regression.ts:2287, so it is part of the browser lane.

Checked and passing

  • Tests: 201 unit tests pass, and so does the typecheck. The contract tests pass 9/9 on an isolated herdr session, and the paste/insert demo regression passes. Two mutants on the control-character refusal (check removed, ESC allowed) are both killed.
  • Compatibility:
    • Against an older PC, the client waits for the terminal-insert feature flag and sends nothing without it. It answers capabilities_unknown or unsupported.
    • An older client never sends insert.
  • Draft handling: the draft is cleared only after confirmation, and only the sent prefix. An edit that breaks the prefix keeps the whole draft.
  • Refusals:
    • A stale insert is refused on both sides: by the lease check on the client, and by a recheck on the server after every wait.
    • Observers get read_only.
    • Any line break and any control character except Tab is refused, so a bracketed paste is never guessed.
  • Other behaviour:
    • Every listed clipboard race drops the read.
    • herdr's focus never moves: insert is a single pane.send_text.
    • Every string is translated, and the design precedent is named.
    • There are no fixed sleeps.

Non-blocking

  • HerdrSocket.canInsert() (ws.ts:918) is used only by tests.
  • TerminalInput.tsx grows to 310 lines; the paste logic could split out.
  • The 45 s insert deadline repeats the submit deadline's value instead of using the same constant.
  • The held-input note says "Not sent" where the other notes say "Not inserted".
  • The demo regression waits on full English sentences.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Thanks. Both test failures from the merge pass review and this run's ui-regression lane are fixed in test-only commits; no production code changed:

  • 0fd16d7 (the review's blocker): "Insert takes no grid back" now takes its resize count only once the last resize sent matches the rows drawn, so a late refit from the earlier lens switches is no longer counted as Insert's. ui-regression.ts ran 3 times locally, and the terminal-input check passed all 3 times.
  • a0cf07b: CI's ui-regression lane failed in main's fix(terminal): space the phone's input line and key bar evenly #1527 spacing check (terminal-input-spacing-regression.ts:92, 30 / 4). This branch puts the line's label above the field, so the field is now measured together with its label, as the send button already is with its word. The room under the field still has to match the line's padding above. The regression passes locally.

Typecheck and the conventions and ci-browser tests pass. The non-blocking notes are left for a follow-up.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass review-work for a0cf07b: one mismatch left. The flake is fixed: the resize count now waits for the grid's refit, which is a condition and not a time, and terminal-input-regression passed 3 runs in a row.

Blocker: Insert accepts TAB, but the description says it refuses every control character. shared/terminal-insert.ts:23-34 leaves U+0009 out of the refusal: insertTextProblem("a\tb") returns null. The contract test at server/terminal-insert.contract.test.ts:229-235 also sends a TAB and expects it to succeed. Typed into a shell, a TAB triggers completion instead of inserting a literal tab, so the line that lands at the prompt may not be what the user saw in the field. Please do one of these:

  • refuse TAB as well, and update the tests that expect it to go through; or
  • if TAB is meant to pass, say "every control character except Tab" in the description and docs, and give the reason.

Checked and passing:

  • Tests: a fresh contract run passes 9/9 on an isolated herdr session.
  • Paste: pasted text goes into the draft, and a late clipboard read is dropped on any change.
  • Insert: the draft clears only the part the PC acknowledged. Nothing is replayed after a disconnect.
  • Compatibility: the client checks the terminal-insert capability, so an older PC gets nothing.
  • Observers: guarded on both sides.
  • Locales and design: every locale is covered, and the design precedent is named.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Thanks. Insert now refuses a TAB, as the description says (f704f23): a shell takes TAB as completion, so the prompt could show something other than the field. A TAB now gets the same invalid_insert_text refusal and "control character" note as the other control characters, and Send is unchanged. The unit test expects the refusal and fails with the old rule. The contract test's success case now uses a space, and its refusal case adds git\tstatus. Contract tests: 9/9 on an isolated herdr session.

This push also fixes the "Browser regression (other scripts)" failure (f4aad65). Main's key-bar-customization-demo-regression.ts lists the touch key bar's fixed keys, and Paste now follows the Input line toggle there. The demo passes locally. ui-contexts-regression.ts (the #1527 spacing check included) also passed locally on a0cf07b.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Codex review on Sol 6.1 Fast (xhigh), on the production diff after the TAB change: no HIGH or MEDIUM findings. Its one LOW is fixed in f3c6424: a paste restores the caret one frame later, and that frame could focus the old line after another pane had been picked in the split, which selects that pane again. The frame now re-checks the paste target and the draft first. Typecheck passes. The paste/insert demo regression is queued locally, and CI's browser lane runs it too.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

CI on f3c6424 passed Fast checks, Integration tests, Native Windows install and Browser regression (other scripts), so the key-bar demo fix is confirmed there. The ui-regression lane hit the Insert resize count again (10 !== 9). The settle condition in 0fd16d7 was not enough: PaneTerminal's observer fits and sends the resize together after its 120 ms rest, so a refit still waiting out that rest is invisible to "last resize == rows drawn".

63cbbcd moves the Insert check to right after the input line's Send. There the test already shows that the grid's own ResizeObserver never fired, and that stillness is now asserted beside the resize count, so a late refit can no longer land in it. The Insert assertions themselves are unchanged. Locally, ui-regression passed with the moved check. Two more runs are under way, and CI runs it as well.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

All required checks pass on 63cbbcd: Fast checks, Integration tests, Browser regression (ui-regression), Browser regression (other scripts) and Native Windows install. The Insert resize count passed in CI's ui-regression lane and in two full local runs. Every point from both merge-pass reviews is addressed above. A real review is still needed for the new WS contract.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass: one rebase needed. #1874 is merged on main. scripts/ci-browser.test.ts no longer pins the browser lane's count; it checks the scripts on disk. This branch still edits the old toHaveLength(…) line, so it now conflicts with main. Rebase onto main and drop this PR's change to scripts/ci-browser.test.ts, keeping its run_script line in scripts/ci-browser.sh. The new test checks that line automatically. This is the last rebase a lane count will cause. The merge pass checks that the rebase changed only that file, then re-queues the PR. A rebase needs no new CodeRabbit review.

@devswha
devswha force-pushed the feat/terminal-paste-insert branch from 63cbbcd to 04121ec Compare October 24, 2026 16:22
@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main after #1874 and pushed 04121ec (was 63cbbcd). I dropped this PR's change to scripts/ci-browser.test.ts and kept its run_script line in scripts/ci-browser.sh. Apart from that file, the branch's patch is identical to before (git patch-id matches). scripts/ci-browser.test.ts passes 7/7, and bun run check fast passed before the push. This is a rebase only, so it doesn't need another CodeRabbit review.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass: this PR now conflicts with main in scripts/ci-browser.sh after the batch with #1831, #1890, #1895, #1897 and #1830. Please rebase onto main and push. The merge pass rechecks only the rebase delta and requeues it.

@devswha
devswha force-pushed the feat/terminal-paste-insert branch from 04121ec to d8c0095 Compare October 24, 2026 20:31
@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main after the batch (#1831, #1890, #1895, #1897, #1830) and pushed d8c0095 (was 04121ec). The only conflict was in scripts/ci-browser.sh, and both lines are kept: main's chat-local-send-follow-demo-regression.ts and this PR's terminal-paste-insert-demo-regression.ts. Nothing else changed. bun run check fast passed before the push. This is a rebase only, so it doesn't need another CodeRabbit review.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass: this PR conflicts with main in scripts/ci-browser.sh again, after #1837 and #1899 merged (001e8e3). Please rebase onto main and push. The merge pass rechecks only the rebase delta and requeues it.

@devswha
devswha force-pushed the feat/terminal-paste-insert branch from d8c0095 to da02cd9 Compare October 24, 2026 21:14
@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main after #1837 and #1899 and pushed da02cd9 (was d8c0095). The only conflict was the scripts/ci-browser.sh line again, and both are kept: main's chat-tool-paths-demo-regression.ts and this PR's terminal-paste-insert-demo-regression.ts. The branch's change to that file is still its one added line. bun run check fast passed before the push. This is a rebase only, so it doesn't need another CodeRabbit review.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass: #1852 merged (4865b7d), and this PR now conflicts with main in src/components/PaneTerminal.tsx. Please rebase onto main and push. The merge pass rechecks only the rebase delta and requeues it.

devswha and others added 9 commits October 25, 2026 06:58
…usal

Review follow-ups on the input line's Insert and Paste:

- HerdrSocket.insert keeps the pane's input lease from the tap and sends
  nothing when it changed while the insert waited on the snapshot or a
  liveness probe, so a draft never lands in a reattached pane.
- It returns capabilities_unknown, as submit does, when the snapshot is
  late, instead of telling the user to update the PC.
- invalid_insert_text (a control character, such as an escape copied with
  coloured output) gets its own note instead of "the terminal is not ready".
- A paste that waited on the clipboard is dropped when its pane was put out
  of sight (another pane picked in the split).
- "Enter" moves to the composer part of the dictionaries, where its only
  user lives.

Refs #1047
…gression

Main renamed browser storage keys to ovis:*; the demo seeded and read the old herdr-web-ui:* keys, which the app no longer reads and the conventions test refuses.

Refs #1047
…pander

Main (#1527) draws key-bar keys at --key-h and reaches the 44px target through .key::before; the demo now checks that a tap half a pixel inside that band, above and below Paste's middle, lands on it.

Refs #1047
… out

The merge pass saw "Insert takes no grid back" fail in 1 of 4 runs: the
lens switches before it refit the grid, and that refit's resize could
land just after the count was taken. The count now starts once the last
resize sent matches the rows drawn, so only a resize Insert itself sent
can change it.

Refs #1047
Main's #1527 checks that the input line's field and send button have as
much room below as above. This branch puts the line's label above the
field (as the send button carries its word above its face), so the field
is measured with its label: the room above is the line's padding over the
label, which still has to match the room under the field.

Refs #1047
Insert types the line at the prompt as the field shows it, but a shell
takes a tab as completion, so what landed could differ from what the user
saw. A tab is now refused with the other control characters
(invalid_insert_text, "the line holds a control character"), as the PR
description already said; Send is unchanged.

Refs #1047
The touch key bar's fixed keys are now the Input line toggle and Paste;
the key-bar customization regression from main lists them both ahead of
the user's keys.

Refs #1047
A paste puts the caret after its text one frame later. If another pane
was picked in the split, or the line was written again, before that
frame, focusing the old line would select its pane again and move the
caret. The frame now checks the paste target and the draft first.

Refs #1047
Waiting for the last resize to match the drawn rows did not settle the
grid: PaneTerminal's observer fits and sends together after its 120 ms
rest, so a refit from the earlier lens switches could still land in the
count (CI: 10 !== 9). Insert is now checked right after the input line's
Send, where the grid's own observer is shown never to fire, and that
stillness is asserted beside the resize count.

Refs #1047
@devswha
devswha force-pushed the feat/terminal-paste-insert branch from da02cd9 to c11d0ff Compare October 24, 2026 21:59
@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main after #1852 and pushed c11d0ff (was da02cd9). The one conflict was the useTerminalSends dependency list in PaneTerminal.tsx, where both sides only added a name. Main's cancelTouchRef and this PR's insertTerminalText are both kept, in the same places. Nothing else changed. bun run check fast (typecheck included) passed before the push. This is a rebase only, so it doesn't need another CodeRabbit review.

@devswha

devswha commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

Merge pass review-work report. Overall: PASSED. This PR adds a WS contract and touches held input. CodeRabbit didn't review it, so per AGENTS.md the independent review is recorded here and in the two earlier comments.

Lane Verdict Confidence
QA (code-review agent's matrix + the merge pass's reruns; artifacts kept) PASS at f4aad65 -
Code review (omo-native-code-reviewer, programming + remove-ai-slops) at 8a05cb3 FAIL: flaky "Insert takes no grid back" assertion -
Gate round 1 (a0cf07b, flake fixed) REJECT: Insert accepted TAB HIGH
Gate round 2 (f4aad65) APPROVE HIGH
Gate re-check of the f3c6424 delta (no focus taken back after a paste once the pane changed) APPROVE MODERATE-HIGH

QA

  • 201 unit tests pass, and the typecheck passes.
  • The contract tests pass 9/9 on an isolated herdr.
  • The paste/insert and key-bar customization demo regressions pass.
  • terminal-input-regression passed 3 runs in a row after the flake fix. The resize count now waits for the grid's refit, a condition rather than a fixed time.
  • Both control-character mutants are killed.

Behaviour checked

  • Compatibility: the client checks the terminal-insert capability, so an older PC gets nothing. An older client never sends insert.

  • Confirmed clearing: the draft clears only the acknowledged prefix. An edit that breaks that prefix keeps the whole draft.

  • Refusals:

    • A stale insert is refused on both sides.
    • Observers get read_only.
    • All 65 C0, DEL and C1 control characters are refused, TAB included. A shell takes TAB as completion, so the prompt wouldn't show the line as written. The refusal is readable and the draft is kept.
    • Line breaks are refused, so a bracketed paste is never guessed.
  • Clipboard races: every listed race drops the read.

  • No side effects: herdr's focus never moves, and nothing is replayed after a disconnect.

  • Translations and precedent: all locales are covered, and the design precedent is named.

  • f3c6424: after a paste, focus and the caret aren't restored if another pane was picked or the line was written again before the frame. A plain paste still gets both back. 59 tests pass and the typecheck passes.

  • 63cbbcd (test only): Insert's resize check now runs in the still grid that Send just used. The merge pass reran terminal-input-regression twice: both exit 0.

  • 04121ec, rebase after test(ci): check browser lane assignment from the scripts on disk, not a pinned count #1874: this PR no longer edits scripts/ci-browser.test.ts. The lane-count commit is dropped, and one patch no longer touches that file. Per git range-diff, the other 9 commits are unchanged.

Follow-ups (MINOR)

  • No test holds a frame to pin the new pre-frame paste guards.
  • HerdrSocket.canInsert() is used only by tests.
  • TerminalInput.tsx is at about 310 LOC.
  • The 45 s insert deadline repeats the submit deadline's value.
  • The demo regression waits on English sentences.

All required checks are green and no review thread is open.

CodeRabbit did not review this PR (rate-limited or skipped). The reviews above stand in for it.

@devswha
devswha merged commit b9be12c into main Oct 25, 2026
19 checks passed
@devswha
devswha deleted the feat/terminal-paste-insert branch October 25, 2026 00:55
devswha added a commit that referenced this pull request Nov 5, 2026
Fixes #2493

## What
An opt-in **Paste and send** icon button (lucide `ClipboardPaste`,
beside Attach) in the chat composer. It reads **this device's**
clipboard on the press (never the PC's) and sends its text through
Send's own path, so it is queued while the agent works, refused or kept
in the box exactly as a typed message would be.

The issue proposed no setting; per the maintainer's rule that the
composer's row stays minimal, it is behind a new device setting,
**Settings → Chat → Composer → Paste and send button**
(`composerPasteSend`, off by default). With it off, nothing changes.

Behaviour:
- Enabled only for an empty box with no files, while connected and not
sending; one read per press (a double press reads once).
- The text is put in the box and then sent by `send()` once the box is
drawn with it, so draft protection, the cross-tab send lock,
acknowledgement and the comment carry all apply unchanged. A refused or
unconfirmed send leaves the pasted text in the box; nothing is retried.
- A read that answers after the pane/PC, the connection or the box
changed sends nothing.
- Empty, refused/unavailable, or over-limit clipboards are said in the
composer note; nothing is cut to fit.

Design: the Codex and Claude apps have no such button; it follows Ovis's
own terminal key bar Paste (#1853). DESIGN.md is updated.

## Screenshots (demo app, 1280 and 390, dark and light)
Desktop dark / light:
<img
src="https://raw.githubusercontent.com/devswha/ovis/9ae724d9e7df24ebd0851ec4f83c0b85d1e18baf/desktop-dark-composer.png"
width="640">
<img
src="https://raw.githubusercontent.com/devswha/ovis/9ae724d9e7df24ebd0851ec4f83c0b85d1e18baf/desktop-light-composer.png"
width="640">

Phone 390 dark / light:
<img
src="https://raw.githubusercontent.com/devswha/ovis/9ae724d9e7df24ebd0851ec4f83c0b85d1e18baf/phone-dark-composer.png"
width="390"> <img
src="https://raw.githubusercontent.com/devswha/ovis/9ae724d9e7df24ebd0851ec4f83c0b85d1e18baf/phone-light-composer.png"
width="390">

Empty clipboard note (desktop dark, phone light):
<img
src="https://raw.githubusercontent.com/devswha/ovis/9ae724d9e7df24ebd0851ec4f83c0b85d1e18baf/desktop-dark-empty-note.png"
width="640">
<img
src="https://raw.githubusercontent.com/devswha/ovis/9ae724d9e7df24ebd0851ec4f83c0b85d1e18baf/phone-light-empty-note.png"
width="390">

Settings (desktop dark, phone light):
<img
src="https://raw.githubusercontent.com/devswha/ovis/9ae724d9e7df24ebd0851ec4f83c0b85d1e18baf/desktop-dark-settings.png"
width="640">
<img
src="https://raw.githubusercontent.com/devswha/ovis/9ae724d9e7df24ebd0851ec4f83c0b85d1e18baf/phone-light-settings.png"
width="320">

## Validation
- Failing first: `src/components/ComposerPasteSend.test.tsx` (11 cases)
and the new `settings.test.ts` case failed on origin/main (12 fail),
pass now.
- `bun test` on ComposerPasteSend, settings, paletteSettings,
conventions, i18n, changelog-fold, ComposerRestored and
ComposerCrossTab: all pass.
- `bun run check fast` (run on old-desktop through the heavy runner): ok
(typecheck, build, unit 11680 pass / 0 fail).
- Manual QA in a real Chromium on the demo app at 1280 and 390 (mobile
emulation), dark and light, with clipboard permission granted: off by
default; once on, the button sits beside Attach on the controls row,
sends the clipboard text exactly once (counted on the socket's submit
frames), is disabled over a draft, says so for an empty clipboard, and
the Settings toggle reads on; no page errors.
- Skipped: Mac app (WKWebView) clipboard prompt, Integration and browser
regressions (left to CI).
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.

1 participant