-
-
Notifications
You must be signed in to change notification settings - Fork 529
Description
What version of Effect is running?
effect 3.19.14, effect/cli 0.73.0
What steps can reproduce the bug?
Prompt.select with long choice titles that wrap in the terminal:
import { Prompt } from "@effect/cli"
import { Effect } from "effect"
import { NodeRuntime } from "@effect/platform-node"
const program = Prompt.select({
message: "Choose an action:",
choices: [
{ title: "1. Short choice", value: "a" },
{ title: "2. Another short choice", value: "b" },
{
title:
"3. Albinoni & Pachelbel by Georg Philipp Telemann, Antonio Vivaldi, Johann Pachelbel, Franz Xaver Richter, Pierre Cochand and Kammerorchester Ensemble Classico",
value: "c",
},
{ title: "4. Planet Jarre (Deluxe-Version) by Jean-Michel Jarre", value: "d" },
{ title: "5. Skip this track", value: "e" },
],
})
NodeRuntime.runMain(program.pipe(Effect.tap(Effect.log)))Run this in a terminal narrower than ~170 columns (so choice 3 wraps), then press the down arrow repeatedly.
Prompt.text with a long pasted value:
import { Prompt } from "@effect/cli"
import { Effect } from "effect"
import { NodeRuntime } from "@effect/platform-node"
const program = Prompt.text({
message: "Paste a long value:",
})
NodeRuntime.runMain(program.pipe(Effect.tap(Effect.log)))Paste a string longer than your terminal width (e.g. a 200-character token).
What is the expected behavior?
For Prompt.select, navigating with arrow keys should cleanly re-render the prompt in place. Only one ? Choose an action: › line should ever be visible:
? Choose an action: ›
1. Short choice
2. Another short choice
3. Albinoni & Pachelbel by Georg Philipp Telemann, Antonio Vivaldi, Johann
Pachelbel, Franz Xaver Richter, Pierre Cochand and Kammerorchester ...
4. Planet Jarre (Deluxe-Version) by Jean-Michel Jarre
❯ 5. Skip this trackFor Prompt.text, pasting a long value should show a single prompt line.
? Paste a long value: › u3IY3.SqPJqP87aNDKu...
What do you see instead?
For Prompt.select, each arrow key press leaves behind an un-erased copy of the prompt line:
? Choose an action: ›
? Choose an action: ›
? Choose an action: ›
? Choose an action: ›
? Choose an action: ›
1. Short choice
2. Another short choice
3. Albinoni & Pachelbel by Georg Philipp Telemann, Antonio Vivaldi, Johann
Pachelbel, Franz Xaver Richter, Pierre Cochand and Kammerorchester ...
4. Planet Jarre (Deluxe-Version) by Jean-Michel Jarre
❯ 5. Skip this trackFor Prompt.text, pasting a long value duplicates the prompt line for each re-render:
? Paste a long value: › u3IY3.SqPJqP87aNDKu...
? Paste a long value: › u3IY3.SqPJqP87aNDKu...
? Paste a long value: › u3IY3.SqPJqP87aNDKu...
? Paste a long value: › u3IY3.SqPJqP87aNDKu...Additional information
I traced this to the handleClear functions in packages/cli/src/internal/prompt/select.ts and packages/cli/src/internal/prompt/text.ts. Both use InternalAnsiUtils.eraseText(text, columns) to calculate how many terminal rows to erase, but the text they pass doesn't reflect the actual rendered output.
Prompt.select (select.ts ~line 185):
const text = "\n".repeat(Math.min(options.choices.length, options.maxPerPage)) + options.messageThis uses empty strings (from \n repeats) for each choice, so eraseText calculates exactly 1 row per choice. But renderNextFrame renders the full choice titles via renderChoices, which can wrap to multiple terminal rows when a title exceeds terminal width. The row count mismatch means eraseLines doesn't erase enough rows, leaving the top prompt line behind on each re-render.
Prompt.text (text.ts renderClearScreen):
const fullLine = `? ${options.message} \u203a ${inputValue}`
const clearOutput = InternalAnsiUtils.eraseText(fullLine, columns)This calculates wrapping with line.length / columns, but renderNextFrame renders with Doc.render({ style: "pretty", options: { lineWidth: columns } }). The pretty printer's layout decisions (which accounts for Doc structure, annotations, and nested documents) can produce different line breaks than the simple character-count division.
The fix for select would be to include the actual choice title text in the string passed to eraseText (instead of bare newlines). The fix for text may require using the actual rendered output length (post-Doc.render) for the erase calculation.
Environment
- @effect/cli version: 0.73.0
- effect version: 3.19.14
- Runtime: Bun
- Platform: Linux