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.
- package/dist/bin/commands/chat.js +186 -295
- package/dist/bin/commands/chat.js.map +1 -1
- package/dist/bin/commands/skill.js +11 -21
- package/dist/bin/commands/skill.js.map +1 -1
- package/dist/lib/mcp-sync.js +77 -0
- package/dist/lib/mcp-sync.js.map +1 -1
- package/dist/src/cli/command-context.js +8 -19
- package/dist/src/cli/command-context.js.map +1 -1
- package/dist/src/cli/tui/composer.js +182 -0
- package/dist/src/cli/tui/composer.js.map +1 -0
- package/dist/src/cli/tui/keymap.js +32 -0
- package/dist/src/cli/tui/keymap.js.map +1 -0
- package/dist/src/cli/tui/overlay.js +156 -0
- package/dist/src/cli/tui/overlay.js.map +1 -0
- package/dist/src/cli/tui/panes.js +33 -0
- package/dist/src/cli/tui/panes.js.map +1 -0
- package/dist/src/cli/tui/renderers.js +40 -0
- package/dist/src/cli/tui/renderers.js.map +1 -0
- package/dist/src/cli/tui/shell.js +58 -0
- package/dist/src/cli/tui/shell.js.map +1 -0
- package/dist/src/cli/tui/store.js +12 -0
- package/dist/src/cli/tui/store.js.map +1 -0
- package/dist/src/core/config.js +13 -1
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/settings-merge.js +2 -2
- package/dist/src/core/settings-merge.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
304
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
553
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
475
|
+
openPromptBlock();
|
|
658
476
|
}
|
|
659
477
|
}
|
|
660
|
-
|
|
661
|
-
let 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
|
-
|
|
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 (
|
|
498
|
+
if (action === 'option-enter') {
|
|
680
499
|
if (commandRunning)
|
|
681
500
|
return;
|
|
682
|
-
|
|
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
|
-
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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
|
-
|
|
600
|
+
appendTextToComposer(composer, picked.insertText || `/${picked.command || ''} ${picked.name}`.trim());
|
|
747
601
|
}
|
|
748
602
|
else {
|
|
749
|
-
|
|
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 (
|
|
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
|
-
|
|
618
|
+
appendTextToComposer(composer, picked.insertText || `/${picked.command || ''} ${picked.name}`.trim());
|
|
764
619
|
redrawPromptLine();
|
|
765
620
|
return;
|
|
766
621
|
}
|
|
767
622
|
if (picked.args) {
|
|
768
|
-
|
|
623
|
+
appendTextToComposer(composer, `/${picked.name} `);
|
|
769
624
|
redrawPromptLine();
|
|
770
625
|
return;
|
|
771
626
|
}
|
|
772
|
-
|
|
627
|
+
appendTextToComposer(composer, `/${picked.name}`);
|
|
773
628
|
}
|
|
774
629
|
}
|
|
775
630
|
// Backslash continuation: \ at end → newline instead of submit
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
682
|
+
else if (action === 'backspace') {
|
|
825
683
|
// Backspace
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
redrawInputWithAutocomplete();
|
|
829
|
-
}
|
|
684
|
+
backspaceComposer(composer);
|
|
685
|
+
redrawInputWithAutocomplete();
|
|
830
686
|
}
|
|
831
|
-
else if (
|
|
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
|
-
|
|
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 (
|
|
706
|
+
else if (action === 'ctrl-u') {
|
|
850
707
|
// Ctrl+U — clear line
|
|
851
|
-
|
|
708
|
+
clearComposer(composer);
|
|
852
709
|
redrawInputWithAutocomplete();
|
|
853
710
|
}
|
|
854
|
-
else if (
|
|
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
|
-
|
|
718
|
+
openPromptBlock(); // new separator + prompt before queue input
|
|
862
719
|
}
|
|
863
|
-
|
|
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
|
-
|
|
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
|
-
|
|
894
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
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
|