[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

fix: handle multi-line select options (#413)

authored by

James Garbutt and committed by
GitHub
(Nov 16, 2025, 3:25 PM UTC) 4d1d83bf b0fa7d89

+162 -4
+5
.changeset/busy-baths-work.md
··· 1 + --- 2 + "@clack/prompts": patch 3 + --- 4 + 5 + Fixes rendering of multi-line messages and options in select prompt.
+14 -4
packages/prompts/src/select.ts
··· 72 72 maxItems?: number; 73 73 } 74 74 75 + const computeLabel = (label: string, format: (text: string) => string) => { 76 + if (!label.includes('\n')) { 77 + return format(label); 78 + } 79 + return label 80 + .split('\n') 81 + .map((line) => format(line)) 82 + .join('\n'); 83 + }; 84 + 75 85 export const select = <Value>(opts: SelectOptions<Value>) => { 76 86 const opt = ( 77 87 option: Option<Value>, ··· 80 90 const label = option.label ?? String(option.value); 81 91 switch (state) { 82 92 case 'disabled': 83 - return `${color.gray(S_RADIO_INACTIVE)} ${color.gray(label)}${ 93 + return `${color.gray(S_RADIO_INACTIVE)} ${computeLabel(label, color.gray)}${ 84 94 option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : '' 85 95 }`; 86 96 case 'selected': 87 - return `${color.dim(label)}`; 97 + return `${computeLabel(label, color.dim)}`; 88 98 case 'active': 89 99 return `${color.green(S_RADIO_ACTIVE)} ${label}${ 90 100 option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 91 101 }`; 92 102 case 'cancelled': 93 - return `${color.strikethrough(color.dim(label))}`; 103 + return `${computeLabel(label, (str) => color.strikethrough(color.dim(str)))}`; 94 104 default: 95 - return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; 105 + return `${color.dim(S_RADIO_INACTIVE)} ${computeLabel(label, color.dim)}`; 96 106 } 97 107 }; 98 108
+106
packages/prompts/test/__snapshots__/select.test.ts.snap
··· 84 84 ] 85 85 `; 86 86 87 + exports[`select (isCI = false) > renders multi-line option labels 1`] = ` 88 + [ 89 + "<cursor.hide>", 90 + "│ 91 + ◆ foo 92 + │ ● Option 0 93 + │ with multiple lines 94 + │ ○ Option 1 95 + └ 96 + ", 97 + "<cursor.backward count=999><cursor.up count=6>", 98 + "<cursor.down count=2>", 99 + "<erase.down>", 100 + "│ ○ Option 0 101 + │ with multiple lines 102 + │ ● Option 1 103 + └ 104 + ", 105 + "<cursor.backward count=999><cursor.up count=6>", 106 + "<cursor.down count=1>", 107 + "<erase.down>", 108 + "◇ foo 109 + │ Option 1", 110 + " 111 + ", 112 + "<cursor.show>", 113 + ] 114 + `; 115 + 87 116 exports[`select (isCI = false) > renders option hints 1`] = ` 88 117 [ 89 118 "<cursor.hide>", ··· 201 230 │  foo foo foo foo foo foo  202 231 │ foo foo foo foo 203 232 │", 233 + " 234 + ", 235 + "<cursor.show>", 236 + ] 237 + `; 238 + 239 + exports[`select (isCI = false) > wraps long messages 1`] = ` 240 + [ 241 + "<cursor.hide>", 242 + "│ 243 + ◆ foo foo foo foo foo foo foo 244 + │ foo foo foo foo foo foo 245 + │ foo foo foo foo foo foo foo 246 + │ ● opt0 247 + │ ○ opt1 248 + └ 249 + ", 250 + "<cursor.backward count=999><cursor.up count=7>", 251 + "<cursor.down count=1>", 252 + "<erase.down>", 253 + "◇ foo foo foo foo foo foo foo 254 + │ foo foo foo foo foo foo 255 + │ foo foo foo foo foo foo foo 256 + │ opt0", 204 257 " 205 258 ", 206 259 "<cursor.show>", ··· 319 372 ] 320 373 `; 321 374 375 + exports[`select (isCI = true) > renders multi-line option labels 1`] = ` 376 + [ 377 + "<cursor.hide>", 378 + "│ 379 + ◆ foo 380 + │ ● Option 0 381 + │ with multiple lines 382 + │ ○ Option 1 383 + └ 384 + ", 385 + "<cursor.backward count=999><cursor.up count=6>", 386 + "<cursor.down count=2>", 387 + "<erase.down>", 388 + "│ ○ Option 0 389 + │ with multiple lines 390 + │ ● Option 1 391 + └ 392 + ", 393 + "<cursor.backward count=999><cursor.up count=6>", 394 + "<cursor.down count=1>", 395 + "<erase.down>", 396 + "◇ foo 397 + │ Option 1", 398 + " 399 + ", 400 + "<cursor.show>", 401 + ] 402 + `; 403 + 322 404 exports[`select (isCI = true) > renders option hints 1`] = ` 323 405 [ 324 406 "<cursor.hide>", ··· 436 518 │  foo foo foo foo foo foo  437 519 │ foo foo foo foo 438 520 │", 521 + " 522 + ", 523 + "<cursor.show>", 524 + ] 525 + `; 526 + 527 + exports[`select (isCI = true) > wraps long messages 1`] = ` 528 + [ 529 + "<cursor.hide>", 530 + "│ 531 + ◆ foo foo foo foo foo foo foo 532 + │ foo foo foo foo foo foo 533 + │ foo foo foo foo foo foo foo 534 + │ ● opt0 535 + │ ○ opt1 536 + └ 537 + ", 538 + "<cursor.backward count=999><cursor.up count=7>", 539 + "<cursor.down count=1>", 540 + "<erase.down>", 541 + "◇ foo foo foo foo foo foo foo 542 + │ foo foo foo foo foo foo 543 + │ foo foo foo foo foo foo foo 544 + │ opt0", 439 545 " 440 546 ", 441 547 "<cursor.show>",
+37
packages/prompts/test/select.test.ts
··· 211 211 212 212 expect(output.buffer).toMatchSnapshot(); 213 213 }); 214 + 215 + test('wraps long messages', async () => { 216 + output.columns = 40; 217 + 218 + const result = prompts.select({ 219 + message: 'foo '.repeat(20).trim(), 220 + options: [{ value: 'opt0' }, { value: 'opt1' }], 221 + input, 222 + output, 223 + }); 224 + 225 + input.emit('keypress', '', { name: 'return' }); 226 + 227 + const value = await result; 228 + 229 + expect(value).toEqual('opt0'); 230 + expect(output.buffer).toMatchSnapshot(); 231 + }); 232 + 233 + test('renders multi-line option labels', async () => { 234 + const result = prompts.select({ 235 + message: 'foo', 236 + options: [ 237 + { value: 'opt0', label: 'Option 0\nwith multiple lines' }, 238 + { value: 'opt1', label: 'Option 1' }, 239 + ], 240 + input, 241 + output, 242 + }); 243 + 244 + input.emit('keypress', '', { name: 'down' }); 245 + input.emit('keypress', '', { name: 'return' }); 246 + 247 + await result; 248 + 249 + expect(output.buffer).toMatchSnapshot(); 250 + }); 214 251 });