Skip to content

feat(tui): mask secret dialog prompt input - #56777

Open
SKar-2007 wants to merge 2 commits into
anomalyco:devfrom
SKar-2007:secret-prompt
Open

SKar-2007 wants to merge 2 commits into
anomalyco:devfrom
SKar-2007:secret-prompt

Conversation

@SKar-2007

@SKar-2007 SKar-2007 commented Oct 18, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #56747

Type of change

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

What does this PR do?

ui.dialog.prompt always echoed what was typed, so a plugin had no safe way to collect a password or token. This adds an optional secret flag to DialogPromptOptions; when it is set, the prompt draws each character as * instead of showing it.

The real value stays in the textarea's edit buffer and only the cells that were already drawn get rewritten to * during render, after the editor view has written them and before the frame is serialized. Editing behaves normally — cursor movement, backspace, selection and undo all operate on the real text — and the string handed back to the plugin on confirm is unchanged. An empty field still shows its placeholder, since a hint is not a secret. With the flag unset, renderSelf passes straight through, so existing prompts render exactly as before.

Review follow-up: selection could still resolve the real text, since copy-on-select and Ctrl+C read getSelectedText(). A masked field now returns an empty string from that method (f7849b0), with tests for both the masked and the normal case.

How did you verify your code works?

bun run check from the repo root, the full packages/tui suite (1495 pass, 0 fail) and packages/plugin (9 pass). New tests in dialog-prompt.test.tsx cover a masked prompt showing ***** and never the plaintext in the captured frame while Enter still resolves the real value, an empty secret prompt keeping its visible placeholder and masking typed text, selection in a masked field resolving to an empty string while plainText still holds the value, and a normal prompt still echoing and exposing its selection. The dialog-prompt tests were also re-run 5x for stability (35/35). The automated review ran this from source with a small plugin and confirmed the masked field and the resolved value.

Screenshots / recordings

Before and after from the automated review's run against this branch:

Before

After

Checklist

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

Add `secret?: boolean` to `DialogPromptOptions`. With it set, the TUI
prompt renders each entered character as `*` instead of echoing it, so
plugins can collect passwords or tokens without exposing them on screen.

The real value stays in the textarea's edit buffer and only the drawn
cells are masked during render, so cursor movement, editing shortcuts and
the string returned on confirm are unchanged. An empty field keeps its
placeholder visible.
@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label Oct 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

@opencode-agent opencode-agent Bot 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.

Masking works. I ran it from source with a small TUI plugin calling ui.dialog.prompt({ secret: true }). On the base the typed password shows in plain text. With this change the field shows **********, including interior spaces, and Enter still resolves the exact value. The dialog-prompt tests pass and type checks pass for packages/tui and packages/plugin.

Public API: adds the optional secret?: boolean field to DialogPromptOptions in @opencode/plugin/tui (backwards compatible; existing plugins are unaffected).

One concern worth fixing: the secret can still leave the field through selection. See the inline comment.

Related: #43132, #53781

Before

After

}

protected override renderSelf(buffer: OptimizedBuffer) {
super.renderSelf(buffer)

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.

Only the drawn cells are masked. Selection still reads the real buffer: TextareaRenderable.getSelectedText() returns editorView.getSelectedText(). When I dragged the mouse across the masked field, the TUI showed "Copied to clipboard". Copy-on-select (and Ctrl+C on a selection, which uses the same copy() path) therefore puts the plaintext secret on the clipboard, and with OSC 52 it is also sent through the terminal. Could the secret mode also override getSelectedText (e.g. return "" while _secret is set), so a masked field never yields its value through selection? A test for that would cover the main leak path the issue is worried about.

Copy-on-select and Ctrl+C resolve the selection through
TextareaRenderable.getSelectedText(), which read the real buffer. A masked
field now returns an empty string from that method so no copy path can leak
the value.
@SKar-2007

Copy link
Copy Markdown
Author

Addressed the review feedback in f7849b0: SecretTextareaRenderable.getSelectedText() now returns an empty string while the field is masked, which is the exact value copy-on-select and Ctrl+C resolve through Selection.getSelectedText(). Test coverage added — the masked prompt's selection resolves to "" while plainText still holds the real value, and a normal prompt still exposes its selection as before. The PR description has also been updated to follow the template.

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Oct 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

This branch has not been deployed

No deployments
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.

[FEATURE]: Plugin dialog prompt needs a masked (secret) input mode

1 participant