diff --git a/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts b/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts index 525bcfc06..7634d154f 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts @@ -11,6 +11,7 @@ import { keyHint } from "./keybinding-hints.js"; export interface ExtensionInputOptions { tui?: TUI; timeout?: number; + secure?: boolean; } export class ExtensionInputComponent extends Container implements Focusable { @@ -61,6 +62,7 @@ export class ExtensionInputComponent extends Container implements Focusable { } this.input = new Input(); + this.input.secure = opts?.secure === true; if (placeholder) { this.input.placeholder = placeholder; } diff --git a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts b/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts index 85ba64d39..eb062ca41 100644 --- a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts @@ -1631,7 +1631,7 @@ export class InteractiveMode { this.hideExtensionInput(); resolve(undefined); }, - { tui: this.ui, timeout: opts?.timeout }, + { tui: this.ui, timeout: opts?.timeout, secure: opts?.secure }, ); this.editorContainer.clear(); diff --git a/packages/pi-tui/src/components/__tests__/input.test.ts b/packages/pi-tui/src/components/__tests__/input.test.ts index c47100492..581c2e14f 100644 --- a/packages/pi-tui/src/components/__tests__/input.test.ts +++ b/packages/pi-tui/src/components/__tests__/input.test.ts @@ -32,4 +32,15 @@ describe("Input", () => { input.focused = false; assert.equal(input.focused, false); }); + + it("secure mode obscures typed characters in render output", () => { + const input = new Input(); + input.secure = true; + input.focused = true; + input.handleInput("secret123"); + + const line = input.render(40)[0] ?? ""; + assert.ok(!line.includes("secret123"), "rendered line must not expose raw secret text"); + assert.ok(line.includes("*********"), "rendered line should include masked characters"); + }); }); diff --git a/packages/pi-tui/src/components/input.ts b/packages/pi-tui/src/components/input.ts index 627f3557c..78535ab3f 100644 --- a/packages/pi-tui/src/components/input.ts +++ b/packages/pi-tui/src/components/input.ts @@ -21,6 +21,8 @@ export class Input implements Component, Focusable { public onSubmit?: (value: string) => void; public onEscape?: () => void; public placeholder: string = ""; + /** When true, render obscured characters instead of the actual value. */ + public secure: boolean = false; /** Focusable interface - set by TUI when focus changes */ private _focused: boolean = false; @@ -446,6 +448,7 @@ export class Input implements Component, Focusable { // Calculate visible window const prompt = "> "; const availableWidth = width - prompt.length; + const renderValue = this.secure ? "*".repeat(this.value.length) : this.value; if (availableWidth <= 0) { return [prompt]; @@ -466,7 +469,7 @@ export class Input implements Component, Focusable { if (this.value.length < availableWidth) { // Everything fits (leave room for cursor at end) - visibleText = this.value; + visibleText = renderValue; } else { // Need horizontal scrolling // Reserve one character for cursor if it's at the end @@ -501,17 +504,17 @@ export class Input implements Component, Focusable { if (this.cursor < halfWidth) { // Cursor near start - visibleText = this.value.slice(0, findValidEnd(scrollWidth)); + visibleText = renderValue.slice(0, findValidEnd(scrollWidth)); cursorDisplay = this.cursor; } else if (this.cursor > this.value.length - halfWidth) { // Cursor near end const start = findValidStart(this.value.length - scrollWidth); - visibleText = this.value.slice(start); + visibleText = renderValue.slice(start); cursorDisplay = this.cursor - start; } else { // Cursor in middle const start = findValidStart(this.cursor - halfWidth); - visibleText = this.value.slice(start, findValidEnd(start + scrollWidth)); + visibleText = renderValue.slice(start, findValidEnd(start + scrollWidth)); cursorDisplay = halfWidth; } }