[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(multi-line): only submit on double-return at end of value (#569)

authored by

James Garbutt and committed by
GitHub
(Jun 17, 2026, 8:13 PM +0100) e1b6ee71 f87933fb

+87 -20
+10
.changeset/ten-vans-stand.md
··· 1 + --- 2 + "@clack/core": patch 3 + --- 4 + 5 + fix: only submit multi-line prompt when double-return happens at end of input. 6 + 7 + Also fixes two minor things: 8 + 9 + - Initial value is used as initial user input for multi-line prompts 10 + - Cursor is placed at the end when there is initial input
+16 -2
packages/core/src/prompts/multi-line.ts
··· 72 72 } 73 73 const wasReturn = this.#lastKeyWasReturn; 74 74 this.#lastKeyWasReturn = true; 75 - if (wasReturn) { 75 + if (wasReturn && this.cursor === this.userInput.length) { 76 76 if (this.userInput[this.cursor - 1] === '\n') { 77 77 this._setUserInput( 78 78 this.userInput.slice(0, this.cursor - 1) + this.userInput.slice(this.cursor) ··· 87 87 } 88 88 89 89 constructor(opts: MultiLineOptions) { 90 - super(opts, false); 90 + const initialUserInput = opts.initialUserInput ?? opts.initialValue; 91 + 92 + super( 93 + { 94 + ...opts, 95 + initialUserInput, 96 + }, 97 + false 98 + ); 99 + 100 + if (initialUserInput !== undefined) { 101 + this._cursor = initialUserInput.length; 102 + } 103 + 91 104 this.#showSubmit = opts.showSubmit ?? false; 92 105 93 106 this.on('key', (char, key) => { 94 107 if (key?.name && cursorActions.has(key.name as CursorAction)) { 108 + this.#lastKeyWasReturn = false; 95 109 this.#handleCursor(key.name as CursorAction); 96 110 return; 97 111 }
+61 -18
packages/core/test/prompts/multi-line.test.ts
··· 57 57 expect(result).to.equal('x'); 58 58 }); 59 59 60 + test('sets initial value from initialValue', async () => { 61 + const instance = new MultiLinePrompt({ 62 + input, 63 + output, 64 + render: () => 'foo', 65 + initialValue: 'bleep bloop', 66 + }); 67 + const resultPromise = instance.prompt(); 68 + input.emit('keypress', '', { name: 'return' }); 69 + input.emit('keypress', '', { name: 'return' }); 70 + const result = await resultPromise; 71 + expect(result).to.equal('bleep bloop'); 72 + }); 73 + 74 + test('sets initial value from initialUserInput', async () => { 75 + const instance = new MultiLinePrompt({ 76 + input, 77 + output, 78 + render: () => 'foo', 79 + initialUserInput: 'bleep bloop', 80 + }); 81 + const resultPromise = instance.prompt(); 82 + input.emit('keypress', '', { name: 'return' }); 83 + input.emit('keypress', '', { name: 'return' }); 84 + const result = await resultPromise; 85 + expect(result).to.equal('bleep bloop'); 86 + }); 87 + 60 88 describe('cursor', () => { 61 89 test('can get cursor', () => { 62 90 const instance = new MultiLinePrompt({ ··· 70 98 }); 71 99 72 100 describe('userInputWithCursor', () => { 73 - test('returns value on submit', () => { 101 + test('returns value on submit', async () => { 74 102 const instance = new MultiLinePrompt({ 75 103 input, 76 104 output, 77 105 render: () => 'foo', 78 106 }); 79 - instance.prompt(); 107 + const resultPromise = instance.prompt(); 80 108 input.emit('keypress', 'x', { name: 'x' }); 81 109 input.emit('keypress', '', { name: 'return' }); 82 110 input.emit('keypress', '', { name: 'return' }); 83 111 expect(instance.userInputWithCursor).to.equal('x'); 112 + const value = await resultPromise; 113 + expect(value).to.equal('x'); 114 + }); 115 + 116 + test('double return does not submit mid-value', async () => { 117 + const instance = new MultiLinePrompt({ 118 + input, 119 + output, 120 + render: () => 'foo', 121 + }); 122 + const resultPromise = instance.prompt(); 123 + input.emit('keypress', 'x', { name: 'x' }); 124 + input.emit('keypress', 'y', { name: 'y' }); 125 + input.emit('keypress', '', { name: 'left' }); 126 + input.emit('keypress', '', { name: 'return' }); 127 + input.emit('keypress', '', { name: 'return' }); 128 + expect(instance.userInput).to.equal('x\n\ny'); 129 + input.emit('keypress', '', { name: 'escape' }); 130 + await resultPromise; 84 131 }); 85 132 86 133 test('highlights cursor position', () => { ··· 257 304 input.emit('keypress', 'x', { name: 'x' }); 258 305 input.emit('keypress', '', { name: 'left' }); 259 306 input.emit('keypress', '', { name: 'backspace' }); 260 - input.emit('keypress', '', { name: 'return' }); 261 - input.emit('keypress', '', { name: 'return' }); 262 - const result = await resultPromise; 263 - expect(result).to.equal('x'); 307 + expect(instance.userInput).to.equal('x'); 308 + input.emit('keypress', '', { name: 'escape' }); 309 + await resultPromise; 264 310 }); 265 311 266 312 test('left moves left until start', async () => { ··· 274 320 input.emit('keypress', '', { name: 'left' }); 275 321 input.emit('keypress', '', { name: 'left' }); 276 322 input.emit('keypress', 'y', { name: 'y' }); 277 - input.emit('keypress', '', { name: 'return' }); 278 - input.emit('keypress', '', { name: 'return' }); 279 - const result = await resultPromise; 280 - expect(result).to.equal('yx'); 323 + expect(instance.userInput).to.equal('yx'); 324 + input.emit('keypress', '', { name: 'escape' }); 325 + await resultPromise; 281 326 }); 282 327 283 328 test('right moves right until end', async () => { ··· 312 357 input.emit('keypress', '', { name: 'left' }); 313 358 input.emit('keypress', '', { name: 'left' }); 314 359 input.emit('keypress', 'z', { name: 'z' }); 315 - input.emit('keypress', '', { name: 'return' }); 316 - input.emit('keypress', '', { name: 'return' }); 317 - const result = await resultPromise; 318 - expect(result).to.equal('xz\ny'); 360 + expect(instance.userInput).to.equal('xz\ny'); 361 + input.emit('keypress', '', { name: 'escape' }); 362 + await resultPromise; 319 363 }); 320 364 321 365 test('right moves across lines', async () => { ··· 351 395 input.emit('keypress', 'y', { name: 'y' }); 352 396 input.emit('keypress', '', { name: 'up' }); 353 397 input.emit('keypress', 'z', { name: 'z' }); 354 - input.emit('keypress', '', { name: 'return' }); 355 - input.emit('keypress', '', { name: 'return' }); 356 - const result = await resultPromise; 357 - expect(result).to.equal('xz\ny'); 398 + expect(instance.userInput).to.equal('xz\ny'); 399 + input.emit('keypress', '', { name: 'escape' }); 400 + await resultPromise; 358 401 }); 359 402 360 403 test('down moves down a line', async () => {