Skip to content

feat(tui): open a paste placeholder's text by clicking it, to edit or inline - #54463

Closed
Nowaker wants to merge 9 commits into
anomalyco:v1from
Nowaker:paste-edit-dialog
Closed

Nowaker wants to merge 9 commits into
anomalyco:v1from
Nowaker:paste-edit-dialog

Conversation

@Nowaker

@Nowaker Nowaker commented Oct 11, 2026 •

Copy link
Copy Markdown

Note: I'm aware v1 is now the maintenance branch and new features go to v2 only. This PR is here for others to use, and I'll submit the v2 version once I've migrated my whole plugin system to it.

Issue for this PR

Related: #8501.

Stacked on #54457 (atomic placeholder deletes), #54460 (undo history), #54458 (configurable paste summary, for relabelling an edited paste) and #53871 (clickable elements, for the click helper). Their commits are included; the change in this PR is the last five commits, from feat(tui): open a paste placeholder's text by clicking it, to edit or inline on.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Once a paste becomes [Pasted ~N lines], its text cannot be seen or changed without sending it or opening the whole prompt in $EDITOR.

Clicking a pasted-text placeholder opens a dialog with the text in the prompt's own editor and box: a textarea with the same input keybinds, mouse caret placement and grouped undo, inside the prompt's frame (its colored left bar, padding and bottom edge, now shared as PromptFrame in component/prompt/frame.tsx) without the agent line. enter saves and closes, as in the prompt it submits, and the prompt's input_newline keys (shift+enter, ctrl+j, ...) add a line. The editor is as tall as the text once wrapped at its width, up to the room left on screen; longer text scrolls inside it.

  • Closing it (enter, esc, ctrl+c, a click outside) saves: the part gets the edited text, and its label is rebuilt from prompt.paste_summary.format when the text changes it, such as its line count.
  • Two buttons sit above the editor: Keep as block saves the edit and keeps the placeholder, as enter does, and Insert inline replaces the placeholder with the text, as if it had been pasted without one. Click one, or press tab to move from the editor to Keep as block, Insert inline and back to the editor wherever the caret is (shift+tab goes the other way); the focused button highlights and enter presses it. up on the editor's first row also reaches them, left/right move between them and down goes back to the text.
  • The tab switch esc/enter save hint sits under the editor's box at its right end, where the prompt shows ctrl+p commands. Saving empty text removes the placeholder.
  • Undo in the prompt takes back either change.

Documented in tui.mdx (Clickable elements, Paste summary).

   Pasted text 4 lines

    Keep as block    Insert inline

   ┃
   ┃  zero
   ┃  one
   ┃  two
   ┃  three
   ┃
   ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
              tab switch  esc/enter save

How did you verify your code works?

  • test/component/prompt-paste.test.tsx drives the real TUI with a mouse click on the placeholder: an edit with a new line (shift+enter, ctrl+j) relabels it to [Pasted ~4 lines] and sends the edited text; enter saves and closes; up on the first row selects Inline, enter presses it, down goes back; a 6-line paste with two wrapped lines shows all 8 rows without scrolling; a 50-line paste keeps the buttons and the hint on screen at 16, 24 and 60 rows; tab and shift+tab cycle the editor and both buttons from any caret row, and enter presses the focused one; a click on Keep as block saves and keeps the placeholder; an edit keeping the line count keeps the label; Inline puts the text in the prompt and undo puts the placeholder back; undo takes back a dialog edit. The full-app harness moves to test/fixture/prompt-app.ts, shared with the undo tests. Another test checks the dialog's editor is drawn in the prompt's box: the bar, a padding row above and below, the edge, the text at the prompt's column, no agent line, and the hint right-aligned under the box.
  • bun typecheck and bun test in packages/tui.
  • In a real terminal through tmux: a click on the placeholder opened the dialog, typing zero and a new line then esc gave [Pasted ~4 lines], a click on Inline put the four lines in the prompt, and undo restored the placeholder. With the keyboard changes, a 6-line paste with two long lines opened with all 8 wrapped rows, shift+enter added a line, enter saved, and up then enter inlined it. The editor's box drew like the prompt's, with the bar in the same color. With the buttons, tab highlighted Keep as block, then Insert inline (the hint turned to esc save), then went back to the text; shift+tab twice and enter kept the edit as a block, and shift+tab then enter inlined it.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

This PR is AI generated, but under direct supervision and on request of @Nowaker.

ctrl+w (or alt+backspace) right after a "[Pasted ~5 lines]" placeholder
deleted only the last word of its label, leaving "[Pasted ~5 " behind
with the pasted text detached from it; ctrl+u and ctrl+k did the same
when the line edge cut into a placeholder. Backspace and delete already
removed it whole.

The cause is in @opentui/core: its extmark controller's deleteRange,
which every word and line delete goes through, trimmed a virtual
extmark instead of treating it as one unit. The fix is upstream as
anomalyco/opentui#1628; patches/@OpenTui%[email protected] backports
it to the pinned 0.4.5 until a release with it is adopted. deleteRange
now widens its range over every virtual extmark it cuts into.
… text

input_undo ran the textarea's own history, which steps back one
character at a time, so taking back a paragraph took dozens of
presses. It knew nothing of the prompt's parts: undoing the deletion
of a "[Pasted ~N lines]" placeholder brought its label back as plain
text with the pasted content gone. A submit cleared it, so text lost to
a stray submit or ctrl+u could only be found again through the
history.

The prompt now keeps its own history (src/prompt/undo.ts) and binds
input.undo and input.redo to it while focused. Typing is grouped by
word: a group ends where a new word starts, after a pause over a
second, when the caret moves, or between typing and deleting. A paste,
a word or line delete, a newline, clearing, recalling history and a
submit are each one step. Every entry records the text, the parts with
their pasted content, the mode and the caret, and undo restores them
together. The history lives outside the prompt component, so after a
submit undo brings back what was sent, even once the route has moved
to the new session, without touching the running turn.

prompt.undo_depth sets how many steps are kept (default 500). The
default keys are unchanged; keybinds.mdx shows how to move suspend off
ctrl+z and use ctrl+z / ctrl+shift+z.
…der says

A paste of three lines or more, or over 150 characters, always became
"[Pasted ~N lines]", which says nothing about what was pasted; only the
global toggle could change it.

prompt.paste_summary in tui.json sets the line and character
thresholds and the placeholder format, with {lines}, {characters},
{bytes} and an {excerpt} of the paste's start, whose length is
configurable. pasteSummary (src/prompt/paste.ts) builds it and keeps it
on one line. The defaults reproduce the previous rule and label
exactly; the toggle still turns summarizing off.
… and todos

Many parts of the TUI show something a keybind or slash command acts
on, but did nothing when clicked. Each now runs that command through
the keymap, or copies what it shows:

- prompt meta row: the agent, model and variant open their pickers; the
  "tab agents" and "ctrl+p commands" hints run agent.cycle and the
  command palette; "esc interrupt" runs session.interrupt, so two clicks
  interrupt like two esc presses;
- usage below the prompt and the sidebar Context lines open /status;
- the directory below the prompt and in the sidebar and home footers is
  copied;
- the model in an assistant footer opens the model picker on the model
  that turn ran on (DialogModel takes a current model);
- the sidebar title renames the session; an MCP row connects or
  disconnects that server without opening /mcps (the toggle, status
  refresh and in-flight guard move from DialogMcp into local.mcp); a
  Modified Files row opens /diff at that file (diff route `file`); a
  todo is copied and appended to the prompt;
- a session or message ID (ses_, msg_) is copied wherever it is drawn.
  ui/id-click.ts reads it back from the cells under the pointer inside
  the clicked renderable, and message and tool click handlers stand down
  when one is under the pointer;
- the home footer's MCP count opens /status and "Connect provider"
  opens /connect.

ui/click.ts runs an action only for a left press and release on the same
cell that leaves no text selected, so drag-to-copy keeps working on all
of these elements.
… inline

Once a paste became "[Pasted ~N lines]" its text could not be seen or
changed without sending it or opening the whole prompt in $EDITOR.

Clicking a pasted-text placeholder opens DialogPaste: the text in the
prompt's own editor, a textarea with the same input keybinds, mouse
caret placement and grouped undo, where enter adds a line. Closing it
(esc, ctrl+c, a click outside) saves: the part gets the edited text,
and the label is rebuilt with pasteLabel when the text changes it, such
as its line count. Inline replaces the placeholder with the text, and
saving empty text removes it. Both are one undo step in the prompt.

The full-app prompt test harness moves to test/fixture/prompt-app.ts,
now shared by the undo and paste tests, and gains clicks and frames.
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 11, 2026
Session record and five feature records for item 28: atomic
placeholder deletes (opentui#1628 backport), configurable paste
summary, raw paste on <leader>v, grouped prompt undo with fork ctrl+z
defaults, and the click-to-edit paste dialog, with their upstream v1
PRs anomalyco#54457-anomalyco#54460 and anomalyco#54463, key-byte measurements through Vibeterm's
tmux, and the vt-206 installs on desktop and m4max.

AI-Tool: opencode 1.18.34-vt-194-907b3bc518
AI-Model: anthropic/claude-opus-5-5
AI-Variant: high
AI-Platform: linux
AI-Harness: Vibeterm 9343cff
AI-Session-ID: ses_ed7634c31ffeH84yR0S99Ty8Nd
Clicking a paste placeholder in a tall terminal opened a dialog whose
Inline button was below the bottom of the screen. The dialog starts a
quarter of the way down (ui/dialog.tsx paddingTop), but the editor was
sized as the full height minus 14 rows, so the dialog ran past the
viewport by roughly a quarter of its height: 8 rows at 60 rows.

The editor now gets the rows left below that offset, less the dialog's
6 rows of chrome and a one-row margin, and no more than the text needs;
a longer paste scrolls inside it. A test pastes 50 lines at 16, 24 and
60 rows and clicks Inline; the 60-row case fails without the fix. The
paste and undo app tests get a 30s default timeout, since the first test
in each file also starts the whole app.
Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 11, 2026
Session record and five feature records for item 28: atomic
placeholder deletes (opentui#1628 backport), configurable paste
summary, raw paste on <leader>v, grouped prompt undo with fork ctrl+z
defaults, and the click-to-edit paste dialog, with their upstream v1
PRs anomalyco#54457-anomalyco#54460 and anomalyco#54463, key-byte measurements through Vibeterm's
tmux, and the vt-206 installs on desktop and m4max.

AI-Tool: opencode 1.18.34-vt-194-907b3bc518
AI-Model: anthropic/claude-opus-5-5
AI-Variant: high
AI-Platform: linux
AI-Harness: Vibeterm 9343cff
AI-Session-ID: ses_ed7634c31ffeH84yR0S99Ty8Nd
…ts wrapped text

Enter in the paste dialog now saves and closes it, the same as Esc and
the same key that submits the main prompt; a new line takes the main
prompt's input_newline keys (shift+enter, ctrl+j, ...), resolved from
the user's keybinds rather than hardcoded.

The Inline button moves above the editor, after the save action. Up on
the editor's first visual row focuses it, Enter (the input_submit keys)
presses it and Down (input.move.down keys) returns to the text. Focus
is shown by the button's highlight and a press briefly inverts it
before the dialog closes.

The editor's height comes from the text's visual line count after
wrapping at the editor's width (editorView.getTotalVirtualLineCount,
re-measured on content and size changes), still capped by the room
under the dialog's top offset. A 6-line paste with wrapped lines was
shown as 3 rows plus a scrollbar.
The paste dialog's editor now looks like the prompt it came from: the
prompt's left bar in the same color, its padding and element
background, and its half-row bottom edge, without the agent, model and
variant line.

The box moves out of the prompt into PromptFrame
(component/prompt/frame.tsx), which both the prompt and the dialog
render, so the two cannot drift. With no footer row the dialog pads
below the text as much as above it (padBottom). promptFrameRows gives
the rows the box adds, and the dialog's editor height subtracts them,
so a short paste still shows whole and Inline stays on screen.
…ed by tab

The paste dialog has two buttons above its editor: Keep as block saves
the edit and keeps the placeholder, the same as enter or esc, and
Insert inline replaces the placeholder with the text. Tab moves focus
from the editor to Keep as block, Insert inline and back to the editor
wherever the caret is, and shift+tab goes the other way; dialogs bind
tab literally, as dialog-update.tsx does. Up on the editor's first row
still reaches the buttons, left and right move between them, enter
presses the focused one (it flashes first) and down returns to the
text.

The save hint moves under the editor's box at its right end, next to a
tab hint, where the prompt shows ctrl+p commands; on Insert inline it
reads "esc save", since enter would inline. The editor's room subtracts
the extra hint row, so a short paste still shows whole and the buttons
stay on screen.
@Nowaker

Nowaker commented Nov 2, 2026

Copy link
Copy Markdown
Author

Closing: this targets the v1 TUI, which upstream now maintains for critical fixes only, so it is not going to be merged. The branch stays on Nowaker/opencode if anyone wants to pick it up.

@Nowaker Nowaker closed this Nov 2, 2026
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