Skip to content

Prompt.select renders duplicate lines when choice descriptions cause terminal line wrapping #5978

@kingstarfly

Description

@kingstarfly

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

  1. 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)
  1. Resize your terminal to < 80 columns (narrow enough that descriptions wrap)

  2. Run: npx tsx repro.ts

  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions