cli-jaw 1.4.9 → 1.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,13 @@ import fs from 'node:fs';
9
9
  import { spawnSync } from 'node:child_process';
10
10
  import { resolve as resolvePath, dirname } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { parseCommand, executeCommand, getCompletionItems, getArgumentCompletionItems } from '../../src/cli/commands.js';
12
+ import { parseCommand, executeCommand } from '../../src/cli/commands.js';
13
+ import { appendNewlineToComposer, appendTextToComposer, backspaceComposer, clearComposer, consumePasteProtocol, flattenComposerForSubmit, getComposerDisplayText, getPlainCommandDraft, getTrailingTextSegment, setBracketedPaste, } from '../../src/cli/tui/composer.js';
14
+ import { classifyKeyAction } from '../../src/cli/tui/keymap.js';
15
+ import { applyResolvedAutocompleteState, clearAutocomplete, closeAutocomplete, makeSelectionKey, popupTotalRows, renderAutocomplete, resolveAutocompleteState, } from '../../src/cli/tui/overlay.js';
16
+ import { clipTextToCols, visualWidth } from '../../src/cli/tui/renderers.js';
17
+ import { cleanupScrollRegion, ensureSpaceBelow, resolveShellLayout, setupScrollRegion } from '../../src/cli/tui/shell.js';
18
+ import { createTuiStore } from '../../src/cli/tui/store.js';
13
19
  import { isGitRepo, captureFileSet, diffFileSets, detectIde, getIdeCli, openDiffInIde, getDiffStat, } from '../../src/ide/diff.js';
14
20
  const chatCwd = process.cwd();
15
21
  const isGit = isGitRepo(chatCwd);
@@ -290,70 +296,26 @@ else {
290
296
  const promptPrefix = ` ${accent}\u276F${c.reset} `;
291
297
  // ─── Scroll region: fixed footer at bottom ──
292
298
  const getRows = () => process.stdout.rows || 24;
293
- function setupScrollRegion() {
294
- const rows = getRows();
295
- // Set scroll region to rows 1..(rows-2), leaving bottom 2 for footer
296
- process.stdout.write(`\x1b[1;${rows - 2}r`);
297
- // Draw fixed footer at absolute positions
298
- process.stdout.write(`\x1b[${rows - 1};1H\x1b[2K ${c.dim}${hrLine()}${c.reset}`);
299
- process.stdout.write(`\x1b[${rows};1H\x1b[2K${footer}`);
300
- // Move cursor back into scroll region
301
- process.stdout.write(`\x1b[${rows - 2};1H`);
299
+ function renderBlockSeparator() {
300
+ process.stdout.write('\n');
301
+ console.log(` ${c.dim}${hrLine()}${c.reset}`);
302
302
  }
303
- function cleanupScrollRegion() {
304
- const rows = getRows();
305
- // Reset scroll region to full terminal
306
- process.stdout.write(`\x1b[1;${rows}r`);
307
- process.stdout.write(`\x1b[${rows};1H\n`);
303
+ function renderAssistantTurnStart() {
304
+ process.stdout.write('\n ');
308
305
  }
309
306
  function showPrompt() {
310
307
  if (typeof closeAutocomplete === 'function')
311
- closeAutocomplete();
308
+ closeAutocomplete(ac, (chunk) => process.stdout.write(chunk));
312
309
  prevLineCount = 1; // reset for fresh prompt
313
- console.log('');
314
- console.log(` ${c.dim}${hrLine()}${c.reset}`);
315
310
  process.stdout.write(promptPrefix);
316
311
  }
317
- // Phase 12.1.7: Calculate visual width (Korean/CJK = 2 columns, ANSI codes = 0)
318
- function visualWidth(str) {
319
- // Strip ANSI escape codes first
320
- const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
321
- let w = 0;
322
- for (const ch of stripped) {
323
- const cp = ch.codePointAt(0);
324
- if (cp === undefined) {
325
- w += 1;
326
- continue;
327
- }
328
- // CJK ranges: Hangul, CJK Unified, Fullwidth, etc
329
- if ((cp >= 0x1100 && cp <= 0x115F) || (cp >= 0x2E80 && cp <= 0x303E) ||
330
- (cp >= 0x3040 && cp <= 0x33BF) || (cp >= 0x3400 && cp <= 0x4DBF) ||
331
- (cp >= 0x4E00 && cp <= 0xA4CF) || (cp >= 0xA960 && cp <= 0xA97C) ||
332
- (cp >= 0xAC00 && cp <= 0xD7AF) || (cp >= 0xD7B0 && cp <= 0xD7FF) ||
333
- (cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0xFE30 && cp <= 0xFE6F) ||
334
- (cp >= 0xFF01 && cp <= 0xFF60) || (cp >= 0xFFE0 && cp <= 0xFFE6) ||
335
- (cp >= 0x20000 && cp <= 0x2FA1F)) {
336
- w += 2;
337
- }
338
- else {
339
- w += 1;
340
- }
341
- }
342
- return w;
312
+ function openPromptBlock() {
313
+ renderBlockSeparator();
314
+ showPrompt();
343
315
  }
344
- function clipTextToCols(str, maxCols) {
345
- if (maxCols <= 0)
346
- return '';
347
- let out = '';
348
- let w = 0;
349
- for (const ch of str) {
350
- const cw = visualWidth(ch);
351
- if (w + cw > maxCols)
352
- break;
353
- out += ch;
354
- w += cw;
355
- }
356
- return out;
316
+ function reopenPromptLine() {
317
+ process.stdout.write('\n');
318
+ showPrompt();
357
319
  }
358
320
  let prevLineCount = 1; // track how many terminal rows input occupied
359
321
  function redrawPromptLine() {
@@ -373,7 +335,7 @@ else {
373
335
  }
374
336
  process.stdout.write('\r');
375
337
  // Write the prompt + input (handle embedded newlines)
376
- const lines = inputBuf.split('\n');
338
+ const lines = getComposerDisplayText(composer).split('\n');
377
339
  const contPrefix = ` ${c.dim}· ${c.reset}`; // continuation line prefix
378
340
  let totalRows = 0;
379
341
  for (let i = 0; i < lines.length; i++) {
@@ -388,194 +350,48 @@ else {
388
350
  prevLineCount = totalRows;
389
351
  }
390
352
  // ─── State ───────────────────────────────
391
- let inputBuf = '';
353
+ const store = createTuiStore();
354
+ const composer = store.composer;
355
+ const pasteCapture = store.pasteCapture;
356
+ const panes = store.panes;
392
357
  let inputActive = true;
393
358
  let streaming = false;
394
359
  let commandRunning = false;
395
360
  const ESC_WAIT_MS = 70;
396
361
  let escPending = false;
397
362
  let escTimer = null;
398
- const ac = {
399
- open: false,
400
- stage: 'command',
401
- contextHeader: '',
402
- items: [],
403
- selected: 0,
404
- windowStart: 0,
405
- visibleRows: 0,
406
- renderedRows: 0,
407
- maxRowsCommand: 6,
408
- maxRowsArgument: 8,
409
- };
363
+ const ac = store.autocomplete;
410
364
  function getMaxPopupRows() {
411
365
  // Scroll region ends at rows-2 (rows-1/rows are fixed footer).
412
366
  // Prompt baseline at rows-2 can be lifted up to rows-3 lines.
413
367
  return Math.max(0, getRows() - 3);
414
368
  }
415
- function makeSelectionKey(item, stage) {
416
- if (!item)
417
- return '';
418
- const base = item.command ? `${item.command}:${item.name}` : item.name;
419
- return `${stage}:${base}`;
420
- }
421
- function popupTotalRows(state) {
422
- if (!state?.open)
423
- return 0;
424
- return (state.visibleRows || 0) + (state.contextHeader ? 1 : 0);
425
- }
426
- // ─ Phase 1c: use terminal natural scrolling to create space below ─
427
- // Prints \n within scroll region to push content up if needed,
428
- // then CSI A back to prompt row. No content is overwritten.
429
- function ensureSpaceBelow(n) {
430
- if (n <= 0)
431
- return;
432
- for (let i = 0; i < n; i++)
433
- process.stdout.write('\n');
434
- process.stdout.write(`\x1b[${n}A`);
435
- }
436
- function syncAutocompleteWindow() {
437
- if (!ac.items.length || ac.visibleRows <= 0) {
438
- ac.windowStart = 0;
439
- return;
440
- }
441
- ac.selected = Math.max(0, Math.min(ac.selected, ac.items.length - 1));
442
- const maxStart = Math.max(0, ac.items.length - ac.visibleRows);
443
- ac.windowStart = Math.max(0, Math.min(ac.windowStart, maxStart));
444
- if (ac.selected < ac.windowStart)
445
- ac.windowStart = ac.selected;
446
- if (ac.selected >= ac.windowStart + ac.visibleRows) {
447
- ac.windowStart = ac.selected - ac.visibleRows + 1;
448
- }
449
- }
450
- function resolveAutocompleteState(prevKey) {
451
- if (!inputBuf.startsWith('/') || inputBuf.includes('\n')) {
452
- return { open: false, items: [], selected: 0, visibleRows: 0 };
453
- }
454
- const body = inputBuf.slice(1);
455
- const firstSpace = body.indexOf(' ');
456
- let stage = 'command';
457
- let contextHeader = '';
458
- let items = [];
459
- if (firstSpace === -1) {
460
- items = getCompletionItems(inputBuf, 'cli');
461
- }
462
- else {
463
- const commandName = body.slice(0, firstSpace).trim().toLowerCase();
464
- if (!commandName)
465
- return { open: false, items: [], selected: 0, visibleRows: 0 };
466
- const rest = body.slice(firstSpace + 1);
467
- const endsWithSpace = /\s$/.test(rest);
468
- const tokens = rest.trim() ? rest.trim().split(/\s+/) : [];
469
- const partial = endsWithSpace ? '' : (tokens[tokens.length - 1] || '');
470
- const argv = endsWithSpace ? tokens : tokens.slice(0, -1);
471
- items = getArgumentCompletionItems(commandName, partial, 'cli', argv, {});
472
- if (items.length) {
473
- stage = 'argument';
474
- contextHeader = `${commandName} ▸ ${items[0]?.commandDesc || '인자 선택'}`;
475
- }
476
- }
477
- if (!items.length) {
478
- return { open: false, items: [], selected: 0, visibleRows: 0 };
479
- }
480
- const selected = (() => {
481
- if (!prevKey)
482
- return 0;
483
- const idx = items.findIndex(i => makeSelectionKey(i, stage) === prevKey);
484
- return idx >= 0 ? idx : 0;
485
- })();
486
- const maxRows = getMaxPopupRows();
487
- const headerRows = contextHeader ? 1 : 0;
488
- const maxItemRows = Math.max(0, maxRows - headerRows);
489
- const stageCap = stage === 'argument' ? ac.maxRowsArgument : ac.maxRowsCommand;
490
- const visibleRows = Math.min(stageCap, items.length, maxItemRows);
491
- if (visibleRows <= 0) {
492
- return { open: false, items: [], selected: 0, visibleRows: 0 };
493
- }
494
- return { open: true, stage, contextHeader, items, selected, visibleRows };
495
- }
496
- function clearAutocomplete() {
497
- if (ac.renderedRows <= 0)
498
- return;
499
- process.stdout.write('\x1b[s');
500
- for (let row = 1; row <= ac.renderedRows; row++) {
501
- process.stdout.write(`\x1b[${row}B\r\x1b[2K\x1b[${row}A`);
502
- }
503
- process.stdout.write('\x1b[u');
504
- ac.renderedRows = 0;
505
- }
506
- function closeAutocomplete() {
507
- clearAutocomplete();
508
- ac.open = false;
509
- ac.stage = 'command';
510
- ac.contextHeader = '';
511
- ac.items = [];
512
- ac.selected = 0;
513
- ac.windowStart = 0;
514
- ac.visibleRows = 0;
515
- }
516
- function formatAutocompleteLine(item, selected, stage) {
517
- const value = stage === 'argument' ? item.name : `/${item.name}`;
518
- const valueCol = stage === 'argument' ? 24 : 14;
519
- const valueText = value.length >= valueCol ? value.slice(0, valueCol) : value.padEnd(valueCol, ' ');
520
- const desc = item.desc || '';
521
- const raw = ` ${valueText} ${desc}`;
522
- const line = clipTextToCols(raw, (process.stdout.columns || 80) - 2);
523
- return selected ? `\x1b[7m${line}${c.reset}` : `${c.dim}${line}${c.reset}`;
524
- }
525
- function renderAutocomplete() {
526
- clearAutocomplete();
527
- if (!ac.open || ac.items.length === 0 || ac.visibleRows <= 0)
528
- return;
529
- syncAutocompleteWindow();
530
- const start = ac.windowStart;
531
- const end = Math.min(ac.items.length, start + ac.visibleRows);
532
- const headerRows = ac.contextHeader ? 1 : 0;
533
- process.stdout.write('\x1b[s');
534
- if (headerRows) {
535
- process.stdout.write('\x1b[1B\r\x1b[2K');
536
- const header = clipTextToCols(` ${ac.contextHeader}`, (process.stdout.columns || 80) - 2);
537
- process.stdout.write(`${c.dim}${header}${c.reset}`);
538
- process.stdout.write('\x1b[1A');
539
- }
540
- for (let i = start; i < end; i++) {
541
- const row = (i - start) + 1 + headerRows;
542
- process.stdout.write(`\x1b[${row}B\r\x1b[2K`);
543
- process.stdout.write(formatAutocompleteLine(ac.items[i], i === ac.selected, ac.stage));
544
- process.stdout.write(`\x1b[${row}A`);
545
- }
546
- ac.renderedRows = headerRows + (end - start);
547
- process.stdout.write('\x1b[u');
548
- }
549
369
  function redrawInputWithAutocomplete() {
550
370
  const prevItem = ac.items[ac.selected];
551
371
  const prevKey = makeSelectionKey(prevItem, ac.stage);
552
- const next = resolveAutocompleteState(prevKey);
553
- clearAutocomplete();
372
+ const next = resolveAutocompleteState({
373
+ draft: getPlainCommandDraft(composer),
374
+ prevKey,
375
+ maxPopupRows: getMaxPopupRows(),
376
+ maxRowsCommand: ac.maxRowsCommand,
377
+ maxRowsArgument: ac.maxRowsArgument,
378
+ });
379
+ clearAutocomplete(ac, (chunk) => process.stdout.write(chunk));
554
380
  // ─ Phase 1c: scroll to create space BELOW prompt (not above) ─
555
381
  if (next.open)
556
382
  ensureSpaceBelow(popupTotalRows(next));
557
383
  redrawPromptLine();
558
- if (!next.open) {
559
- ac.open = false;
560
- ac.stage = 'command';
561
- ac.contextHeader = '';
562
- ac.items = [];
563
- ac.selected = 0;
564
- ac.windowStart = 0;
565
- ac.visibleRows = 0;
566
- return;
567
- }
568
- ac.open = true;
569
- ac.stage = next.stage ?? 'command';
570
- ac.contextHeader = next.contextHeader || '';
571
- ac.items = next.items;
572
- ac.selected = next.selected;
573
- ac.visibleRows = next.visibleRows;
574
- syncAutocompleteWindow();
575
- renderAutocomplete();
384
+ applyResolvedAutocompleteState(ac, next);
385
+ renderAutocomplete(ac, {
386
+ write: (chunk) => process.stdout.write(chunk),
387
+ columns: process.stdout.columns || 80,
388
+ dimCode: c.dim,
389
+ resetCode: c.reset,
390
+ clipTextToCols,
391
+ });
576
392
  }
577
393
  function handleResize() {
578
- setupScrollRegion();
394
+ setupScrollRegion(footer, ` ${c.dim}${hrLine()}${c.reset}`, resolveShellLayout(process.stdout.columns || 80, getRows(), panes));
579
395
  if (!inputActive || commandRunning)
580
396
  return;
581
397
  redrawInputWithAutocomplete();
@@ -593,7 +409,7 @@ else {
593
409
  const result = await executeCommand(parsed, makeCliCommandCtx());
594
410
  if (result?.code === 'clear_screen') {
595
411
  console.clear();
596
- setupScrollRegion();
412
+ setupScrollRegion(footer, ` ${c.dim}${hrLine()}${c.reset}`, resolveShellLayout(process.stdout.columns || 80, getRows(), panes));
597
413
  }
598
414
  if (result?.text)
599
415
  console.log(` ${renderCommandText(result.text)}`);
@@ -617,8 +433,9 @@ else {
617
433
  }
618
434
  if (result?.code === 'exit') {
619
435
  exiting = true;
620
- cleanupScrollRegion();
436
+ cleanupScrollRegion(resolveShellLayout(process.stdout.columns || 80, getRows(), panes));
621
437
  console.log(` ${c.dim}Bye! \uD83E\uDD9E${c.reset}\n`);
438
+ setBracketedPaste(false);
622
439
  ws.close();
623
440
  process.stdin.setRawMode(false);
624
441
  process.exit(0);
@@ -631,20 +448,21 @@ else {
631
448
  if (!exiting) {
632
449
  commandRunning = false;
633
450
  inputActive = true;
634
- closeAutocomplete();
635
- showPrompt();
451
+ closeAutocomplete(ac, (chunk) => process.stdout.write(chunk));
452
+ openPromptBlock();
636
453
  }
637
454
  }
638
455
  }
639
456
  // ─── Raw stdin input ─────────────────────
640
457
  process.stdin.setRawMode(true);
458
+ setBracketedPaste(true);
641
459
  process.stdin.resume();
642
460
  process.stdin.setEncoding('utf8');
643
461
  function flushPendingEscape() {
644
462
  escPending = false;
645
463
  escTimer = null;
646
464
  if (ac.open) {
647
- closeAutocomplete();
465
+ closeAutocomplete(ac, (chunk) => process.stdout.write(chunk));
648
466
  redrawPromptLine();
649
467
  return;
650
468
  }
@@ -654,11 +472,11 @@ else {
654
472
  ws.send(JSON.stringify({ type: 'stop' }));
655
473
  console.log(`\n ${c.yellow}■ stopped${c.reset}`);
656
474
  inputActive = true;
657
- showPrompt();
475
+ openPromptBlock();
658
476
  }
659
477
  }
660
- process.stdin.on('data', (_key) => {
661
- let key = _key;
478
+ function handleKeyInput(rawKey) {
479
+ let key = rawKey;
662
480
  if (escPending) {
663
481
  if (escTimer)
664
482
  clearTimeout(escTimer);
@@ -668,7 +486,8 @@ else {
668
486
  key = `\x1b${key}`;
669
487
  }
670
488
  // Delay ESC standalone handling to distinguish it from ESC sequences.
671
- if (key === '\x1b') {
489
+ const action = classifyKeyAction(key);
490
+ if (action === 'escape-alone') {
672
491
  escPending = true;
673
492
  escTimer = setTimeout(flushPendingEscape, ESC_WAIT_MS);
674
493
  return;
@@ -676,126 +495,165 @@ else {
676
495
  // ESC and Ctrl+C always work, even when agent is running
677
496
  // Typing always works (for queue). Only Enter submission checks inputActive.
678
497
  // Phase 12.1.7: Option+Enter (ESC+CR/LF) → insert newline
679
- if (key === '\x1b\r' || key === '\x1b\n') {
498
+ if (action === 'option-enter') {
680
499
  if (commandRunning)
681
500
  return;
682
- inputBuf += '\n';
501
+ if (!inputActive) {
502
+ inputActive = true;
503
+ openPromptBlock();
504
+ }
505
+ appendNewlineToComposer(composer);
683
506
  redrawInputWithAutocomplete();
684
507
  return;
685
508
  }
686
509
  // Autocomplete navigation (raw ESC sequences)
687
- const isUpKey = key === '\x1b[A' || key === '\x1bOA';
688
- const isDownKey = key === '\x1b[B' || key === '\x1bOB';
689
- const isPageUpKey = key === '\x1b[5~';
690
- const isPageDownKey = key === '\x1b[6~';
691
- const isHomeKey = key === '\x1b[H' || key === '\x1b[1~' || key === '\x1bOH';
692
- const isEndKey = key === '\x1b[F' || key === '\x1b[4~' || key === '\x1bOF';
693
- if (ac.open && isUpKey) { // Up
510
+ if (ac.open && action === 'arrow-up') { // Up
694
511
  ac.selected = Math.max(0, ac.selected - 1);
695
512
  if (ac.selected < ac.windowStart)
696
513
  ac.windowStart = ac.selected;
697
- renderAutocomplete();
514
+ renderAutocomplete(ac, {
515
+ write: (chunk) => process.stdout.write(chunk),
516
+ columns: process.stdout.columns || 80,
517
+ dimCode: c.dim,
518
+ resetCode: c.reset,
519
+ clipTextToCols,
520
+ });
698
521
  return;
699
522
  }
700
- if (ac.open && isDownKey) { // Down
523
+ if (ac.open && action === 'arrow-down') { // Down
701
524
  const maxIdx = ac.items.length - 1;
702
525
  ac.selected = Math.min(maxIdx, ac.selected + 1);
703
526
  if (ac.selected >= ac.windowStart + ac.visibleRows) {
704
527
  ac.windowStart = ac.selected - ac.visibleRows + 1;
705
528
  }
706
- renderAutocomplete();
529
+ renderAutocomplete(ac, {
530
+ write: (chunk) => process.stdout.write(chunk),
531
+ columns: process.stdout.columns || 80,
532
+ dimCode: c.dim,
533
+ resetCode: c.reset,
534
+ clipTextToCols,
535
+ });
707
536
  return;
708
537
  }
709
- if (ac.open && isPageUpKey) {
538
+ if (ac.open && action === 'page-up') {
710
539
  const step = Math.max(1, ac.visibleRows);
711
540
  ac.selected = Math.max(0, ac.selected - step);
712
541
  if (ac.selected < ac.windowStart)
713
542
  ac.windowStart = ac.selected;
714
- renderAutocomplete();
543
+ renderAutocomplete(ac, {
544
+ write: (chunk) => process.stdout.write(chunk),
545
+ columns: process.stdout.columns || 80,
546
+ dimCode: c.dim,
547
+ resetCode: c.reset,
548
+ clipTextToCols,
549
+ });
715
550
  return;
716
551
  }
717
- if (ac.open && isPageDownKey) {
552
+ if (ac.open && action === 'page-down') {
718
553
  const step = Math.max(1, ac.visibleRows);
719
554
  const maxIdx = ac.items.length - 1;
720
555
  ac.selected = Math.min(maxIdx, ac.selected + step);
721
556
  if (ac.selected >= ac.windowStart + ac.visibleRows) {
722
557
  ac.windowStart = ac.selected - ac.visibleRows + 1;
723
558
  }
724
- renderAutocomplete();
559
+ renderAutocomplete(ac, {
560
+ write: (chunk) => process.stdout.write(chunk),
561
+ columns: process.stdout.columns || 80,
562
+ dimCode: c.dim,
563
+ resetCode: c.reset,
564
+ clipTextToCols,
565
+ });
725
566
  return;
726
567
  }
727
- if (ac.open && isHomeKey) {
568
+ if (ac.open && action === 'home') {
728
569
  ac.selected = 0;
729
570
  ac.windowStart = 0;
730
- renderAutocomplete();
571
+ renderAutocomplete(ac, {
572
+ write: (chunk) => process.stdout.write(chunk),
573
+ columns: process.stdout.columns || 80,
574
+ dimCode: c.dim,
575
+ resetCode: c.reset,
576
+ clipTextToCols,
577
+ });
731
578
  return;
732
579
  }
733
- if (ac.open && isEndKey) {
580
+ if (ac.open && action === 'end') {
734
581
  ac.selected = Math.max(0, ac.items.length - 1);
735
582
  if (ac.selected >= ac.windowStart + ac.visibleRows) {
736
583
  ac.windowStart = ac.selected - ac.visibleRows + 1;
737
584
  }
738
- renderAutocomplete();
585
+ renderAutocomplete(ac, {
586
+ write: (chunk) => process.stdout.write(chunk),
587
+ columns: process.stdout.columns || 80,
588
+ dimCode: c.dim,
589
+ resetCode: c.reset,
590
+ clipTextToCols,
591
+ });
739
592
  return;
740
593
  }
741
- if (ac.open && key === '\t') { // Tab accept (no execute)
594
+ if (ac.open && action === 'tab') { // Tab accept (no execute)
742
595
  const picked = ac.items[ac.selected];
743
596
  const pickedStage = ac.stage;
744
597
  if (picked) {
598
+ clearComposer(composer);
745
599
  if (pickedStage === 'argument') {
746
- inputBuf = picked.insertText || `/${picked.command || ''} ${picked.name}`.trim();
600
+ appendTextToComposer(composer, picked.insertText || `/${picked.command || ''} ${picked.name}`.trim());
747
601
  }
748
602
  else {
749
- inputBuf = `/${picked.name}${picked.args ? ' ' : ''}`;
603
+ appendTextToComposer(composer, `/${picked.name}${picked.args ? ' ' : ''}`);
750
604
  }
751
- closeAutocomplete();
605
+ closeAutocomplete(ac, (chunk) => process.stdout.write(chunk));
752
606
  redrawPromptLine();
753
607
  }
754
608
  return;
755
609
  }
756
- if (key === '\r' || key === '\n') {
610
+ if (action === 'enter') {
757
611
  if (ac.open) {
758
612
  const picked = ac.items[ac.selected];
759
613
  const pickedStage = ac.stage;
760
- closeAutocomplete();
614
+ closeAutocomplete(ac, (chunk) => process.stdout.write(chunk));
761
615
  if (picked) {
616
+ clearComposer(composer);
762
617
  if (pickedStage === 'argument') {
763
- inputBuf = picked.insertText || `/${picked.command || ''} ${picked.name}`.trim();
618
+ appendTextToComposer(composer, picked.insertText || `/${picked.command || ''} ${picked.name}`.trim());
764
619
  redrawPromptLine();
765
620
  return;
766
621
  }
767
622
  if (picked.args) {
768
- inputBuf = `/${picked.name} `;
623
+ appendTextToComposer(composer, `/${picked.name} `);
769
624
  redrawPromptLine();
770
625
  return;
771
626
  }
772
- inputBuf = `/${picked.name}`;
627
+ appendTextToComposer(composer, `/${picked.name}`);
773
628
  }
774
629
  }
775
630
  // Backslash continuation: \ at end → newline instead of submit
776
- if (inputBuf.endsWith('\\')) {
777
- inputBuf = inputBuf.slice(0, -1) + '\n';
631
+ const trailing = getTrailingTextSegment(composer);
632
+ if (trailing.text.endsWith('\\')) {
633
+ trailing.text = trailing.text.slice(0, -1);
634
+ appendNewlineToComposer(composer);
778
635
  redrawInputWithAutocomplete();
779
636
  return;
780
637
  }
781
638
  // Enter — submit
782
- const text = inputBuf.trim();
783
- inputBuf = '';
784
- closeAutocomplete();
639
+ const draft = getPlainCommandDraft(composer);
640
+ const text = flattenComposerForSubmit(composer).trim();
641
+ clearComposer(composer);
642
+ closeAutocomplete(ac, (chunk) => process.stdout.write(chunk));
785
643
  prevLineCount = 1;
786
- console.log(''); // newline after input
787
644
  if (!text) {
788
- showPrompt();
645
+ reopenPromptLine();
789
646
  return;
790
647
  }
648
+ renderBlockSeparator();
791
649
  // Phase 10: /file command
792
- if (text.startsWith('/file ')) {
650
+ if (draft !== null && text.startsWith('/file ')) {
793
651
  const parts = text.slice(6).trim().split(/\s+/);
794
652
  const fp = resolvePath(parts[0]);
795
653
  const caption = parts.slice(1).join(' ');
796
654
  if (!fs.existsSync(fp)) {
797
655
  console.log(` ${c.red}파일 없음: ${fp}${c.reset}`);
798
- showPrompt();
656
+ openPromptBlock();
799
657
  return;
800
658
  }
801
659
  const prompt = `[사용자가 파일을 보냈습니다: ${fp}]\n이 파일을 Read 도구로 읽고 분석해주세요.${caption ? `\n\n사용자 메시지: ${caption}` : ''}`;
@@ -807,7 +665,7 @@ else {
807
665
  inputActive = false;
808
666
  return;
809
667
  }
810
- const parsed = parseCommand(text);
668
+ const parsed = draft !== null ? parseCommand(text) : null;
811
669
  if (parsed) {
812
670
  inputActive = false;
813
671
  commandRunning = true;
@@ -821,14 +679,12 @@ else {
821
679
  ws.send(JSON.stringify({ type: 'send_message', text }));
822
680
  inputActive = false;
823
681
  }
824
- else if (key === '\x7f' || key === '\b') {
682
+ else if (action === 'backspace') {
825
683
  // Backspace
826
- if (inputBuf.length > 0) {
827
- inputBuf = inputBuf.slice(0, -1);
828
- redrawInputWithAutocomplete();
829
- }
684
+ backspaceComposer(composer);
685
+ redrawInputWithAutocomplete();
830
686
  }
831
- else if (key === '\x03') {
687
+ else if (action === 'ctrl-c') {
832
688
  // Ctrl+C — stop agent if running, otherwise exit
833
689
  if (!inputActive) {
834
690
  if (commandRunning)
@@ -836,33 +692,68 @@ else {
836
692
  ws.send(JSON.stringify({ type: 'stop' }));
837
693
  console.log(`\n ${c.yellow}■ stopped${c.reset}`);
838
694
  inputActive = true;
839
- showPrompt();
695
+ openPromptBlock();
840
696
  }
841
697
  else {
842
- cleanupScrollRegion();
698
+ cleanupScrollRegion(resolveShellLayout(process.stdout.columns || 80, getRows(), panes));
843
699
  console.log(`\n ${c.dim}Bye! \uD83E\uDD9E${c.reset}\n`);
700
+ setBracketedPaste(false);
844
701
  ws.close();
845
702
  process.stdin.setRawMode(false);
846
703
  process.exit(0);
847
704
  }
848
705
  }
849
- else if (key === '\x15') {
706
+ else if (action === 'ctrl-u') {
850
707
  // Ctrl+U — clear line
851
- inputBuf = '';
708
+ clearComposer(composer);
852
709
  redrawInputWithAutocomplete();
853
710
  }
854
- else if (key.charCodeAt(0) >= 32 || key.charCodeAt(0) > 127) {
711
+ else if (action === 'printable') {
855
712
  // Printable chars (including multibyte/Korean)
856
713
  // Phase 12.1.5: allow typing during agent run for queue
857
714
  if (!inputActive) {
858
715
  if (commandRunning)
859
716
  return;
860
717
  inputActive = true;
861
- showPrompt(); // new separator + prompt before queue input
718
+ openPromptBlock(); // new separator + prompt before queue input
862
719
  }
863
- inputBuf += key;
720
+ appendTextToComposer(composer, key);
864
721
  redrawInputWithAutocomplete();
865
722
  }
723
+ }
724
+ process.stdin.on('data', (_key) => {
725
+ let incoming = _key;
726
+ if (escPending) {
727
+ if (escTimer)
728
+ clearTimeout(escTimer);
729
+ escTimer = null;
730
+ escPending = false;
731
+ if (!incoming.startsWith('\x1b'))
732
+ incoming = `\x1b${incoming}`;
733
+ }
734
+ if (incoming === '\x1b') {
735
+ escPending = true;
736
+ escTimer = setTimeout(flushPendingEscape, ESC_WAIT_MS);
737
+ return;
738
+ }
739
+ if (commandRunning && !inputActive)
740
+ return;
741
+ const beforeDisplay = getComposerDisplayText(composer);
742
+ const tokens = consumePasteProtocol(incoming, pasteCapture, composer);
743
+ const afterDisplay = getComposerDisplayText(composer);
744
+ if (beforeDisplay !== afterDisplay) {
745
+ if (!inputActive) {
746
+ if (commandRunning)
747
+ return;
748
+ inputActive = true;
749
+ openPromptBlock();
750
+ }
751
+ redrawInputWithAutocomplete();
752
+ if (tokens.length === 0)
753
+ return;
754
+ }
755
+ for (const token of tokens)
756
+ handleKeyInput(token);
866
757
  });
867
758
  // ─── WS messages ─────────────────────────
868
759
  ws.on('message', (data) => {
@@ -877,8 +768,7 @@ else {
877
768
  }
878
769
  if (!streaming) {
879
770
  streaming = true;
880
- console.log('');
881
- process.stdout.write(' ');
771
+ renderAssistantTurnStart();
882
772
  }
883
773
  process.stdout.write((msg.text || '').replace(/\n/g, '\n '));
884
774
  break;
@@ -890,8 +780,8 @@ else {
890
780
  console.log('');
891
781
  }
892
782
  else if (msg.text) {
893
- console.log('');
894
- console.log(` ${msg.text.replace(/\n/g, '\n ')}`);
783
+ renderAssistantTurnStart();
784
+ console.log(msg.text.replace(/\n/g, '\n '));
895
785
  }
896
786
  // IDE diff: queue drain unconditional (mid-run /ide off safe)
897
787
  if (isGit && preFileSetQueue.length > 0) {
@@ -918,7 +808,7 @@ else {
918
808
  }
919
809
  streaming = false;
920
810
  inputActive = true;
921
- showPrompt();
811
+ openPromptBlock();
922
812
  break;
923
813
  case 'agent_status':
924
814
  // skip 'done' — redundant with agent_done, arrives late
@@ -971,12 +861,13 @@ else {
971
861
  catch { }
972
862
  });
973
863
  ws.on('close', () => {
974
- cleanupScrollRegion();
864
+ cleanupScrollRegion(resolveShellLayout(process.stdout.columns || 80, getRows(), panes));
975
865
  console.log(`\n ${c.dim}Disconnected${c.reset}\n`);
866
+ setBracketedPaste(false);
976
867
  process.stdin.setRawMode(false);
977
868
  process.exit(0);
978
869
  });
979
- setupScrollRegion();
980
- showPrompt();
870
+ setupScrollRegion(footer, ` ${c.dim}${hrLine()}${c.reset}`, resolveShellLayout(process.stdout.columns || 80, getRows(), panes));
871
+ openPromptBlock();
981
872
  }
982
873
  //# sourceMappingURL=chat.js.map