Description
When using Prompt.select with choices that have long descriptions that wrap across terminal lines, navigating up/down causes duplicate prompt lines to appear instead of updating in place.
Reproduction
- Create a file
repro.ts:
import { Prompt } from "@effect/cli"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const choice = yield* Prompt.select({
message: "Select option:",
choices: [
{ title: "opt-1", value: "a", description: "This is a very long description that will wrap to multiple lines in a narrow terminal window causing rendering issues" },
{ title: "opt-2", value: "b", description: "Another long description that exceeds typical terminal width and causes wrapping when displayed" },
{ title: "opt-3", value: "c", description: "Yet another verbose description to demonstrate the rendering bug in the select prompt" },
],
})
yield* Effect.log(\`Selected: \${choice}\`)
})
program.pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain)
-
Resize your terminal to < 80 columns (narrow enough that descriptions wrap)
-
Run: npx tsx repro.ts
-
Press up/down arrows to navigate between options
Expected: Prompt updates in place, single line for "Select option:"
Actual: Each navigation prints a new prompt line:
? Select option: ›
? Select option: ›
? Select option: ›
? Select option: ›
Root Cause
In packages/cli/src/internal/prompt/select.ts, the handleClear function calculates lines to erase incorrectly:
const text = "\n".repeat(Math.min(options.choices.length, options.maxPerPage)) + options.message
const clearOutput = InternalAnsiUtils.eraseText(text, columns)
Problem 1: Uses empty newlines to represent choices. eraseText counts 1 row per empty line, but actual rendered choices have content (prefix + title + description) that may wrap to multiple terminal rows.
Problem 2: The clear handler ignores state:
clear: () => handleClear(opts) // state ignored
But descriptions are only rendered for the selected choice (in renderChoiceDescription):
if (!choice.disabled && choice.description && isSelected) { ... }
Without knowing state, handleClear can't determine which choice line includes a description and will wrap.
Expected Behavior
The prompt should clear and redraw cleanly when navigating, regardless of description length or terminal width.
Environment
- @effect/cli version: 0.72.1
- Platform: macOS/Linux
- Terminal: Any terminal with width < total line length
Description
When using
Prompt.selectwith choices that have long descriptions that wrap across terminal lines, navigating up/down causes duplicate prompt lines to appear instead of updating in place.Reproduction
repro.ts:Resize your terminal to < 80 columns (narrow enough that descriptions wrap)
Run:
npx tsx repro.tsPress up/down arrows to navigate between options
Expected: Prompt updates in place, single line for "Select option:"
Actual: Each navigation prints a new prompt line:
Root Cause
In
packages/cli/src/internal/prompt/select.ts, thehandleClearfunction calculates lines to erase incorrectly:Problem 1: Uses empty newlines to represent choices.
eraseTextcounts 1 row per empty line, but actual rendered choices have content (prefix + title + description) that may wrap to multiple terminal rows.Problem 2: The
clearhandler ignores state:But descriptions are only rendered for the selected choice (in
renderChoiceDescription):Without knowing
state,handleClearcan't determine which choice line includes a description and will wrap.Expected Behavior
The prompt should clear and redraw cleanly when navigating, regardless of description length or terminal width.
Environment