Repository navigation
Conversation
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.
|
The following comment was made by an LLM, it may be inaccurate: |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| protected override renderSelf(buffer: OptimizedBuffer) { | ||
| super.renderSelf(buffer) |
There was a problem hiding this comment.
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.
|
Addressed the review feedback in f7849b0: |
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |


Issue for this PR
Closes #56747
Type of change
What does this PR do?
ui.dialog.promptalways echoed what was typed, so a plugin had no safe way to collect a password or token. This adds an optionalsecretflag toDialogPromptOptions; 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,renderSelfpasses 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 checkfrom the repo root, the fullpackages/tuisuite (1495 pass, 0 fail) andpackages/plugin(9 pass). New tests indialog-prompt.test.tsxcover 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 whileplainTextstill 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:
Checklist