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