deepclause-sdk 0.0.74 → 0.0.75

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/cli/tui.js CHANGED
@@ -7,10 +7,11 @@ import { listCommands } from './commands.js';
7
7
  import { getConfigPath, loadConfig, resolveModelSlot, setModel } from './config.js';
8
8
  import { run } from './run.js';
9
9
  import { formatToolArgs } from './tool-args.js';
10
- import { appendConductorSessionMessages, createSessionExecutionLogWriter, createLocalSkill, createConductorSession, getConductorSessionDetail, listConductorSessions, runConductorTurn, } from '../system/runtime/conductor.js';
10
+ import { appendConductorSessionMessages, createSessionExecutionLogWriter, createLocalSkill, createConductorSession, getConductorSessionDetail, listConductorSessions, mergeSessionUsage, runConductorTurn, } from '../system/runtime/conductor.js';
11
11
  import { getSystemAssetSourcePaths } from '../system/assets/index.js';
12
12
  import { createShellManager, describeShellExecutionBackend, } from '../system/runtime/shell-manager.js';
13
13
  import { buildToolCompletionEvent, buildToolFailureEvent, buildToolStartEvent, createShellToolEventBridge, } from '../system/runtime/shell-tool-events.js';
14
+ import { recordTokenUsage } from '../system/runtime/token-usage.js';
14
15
  const IGNORED_LIVE_LOG_TOOLS = new Set(['set_result', 'update_memory']);
15
16
  const BUILTIN_SLASH_COMMANDS = ['new', 'sessions', 'help', 'run', 'compile', 'skill-creator', 'set-model', 'cancel', 'exit', 'quit'];
16
17
  const MODEL_SLOTS = ['gateway', 'run', 'compile'];
@@ -305,6 +306,67 @@ export function releaseTuiInputStream(stream) {
305
306
  }
306
307
  stream.pause();
307
308
  }
309
+ class MouseParser {
310
+ buffer = '';
311
+ feed(data, onEvent) {
312
+ this.buffer += data.toString('utf8');
313
+ while (this.buffer.length > 0) {
314
+ const mouseStart = this.buffer.indexOf('\u001b[<');
315
+ if (mouseStart === -1) {
316
+ if (this.buffer.length > 0) {
317
+ this.buffer = '';
318
+ }
319
+ break;
320
+ }
321
+ this.buffer = this.buffer.slice(mouseStart);
322
+ const parsed = this.tryParseMouse();
323
+ if (parsed === null) {
324
+ if (this.buffer.length > 256) {
325
+ this.buffer = '';
326
+ }
327
+ break;
328
+ }
329
+ onEvent(parsed);
330
+ }
331
+ }
332
+ tryParseMouse() {
333
+ const mIdx = this.buffer.indexOf('M', 3);
334
+ const mIdxSgr = this.buffer.indexOf('m', 3);
335
+ const end = mIdx !== -1 && (mIdxSgr === -1 || mIdx < mIdxSgr) ? mIdx : mIdxSgr;
336
+ if (end === -1)
337
+ return null;
338
+ const payload = this.buffer.slice(3, end);
339
+ const isRelease = this.buffer[end] === 'm';
340
+ this.buffer = this.buffer.slice(end + 1);
341
+ const semicolon = payload.indexOf(';');
342
+ if (semicolon === -1)
343
+ return null;
344
+ const secondSemicolon = payload.indexOf(';', semicolon + 1);
345
+ if (secondSemicolon === -1)
346
+ return null;
347
+ const cb = Number.parseInt(payload.slice(0, semicolon), 10);
348
+ const col = Number.parseInt(payload.slice(semicolon + 1, secondSemicolon), 10);
349
+ const row = Number.parseInt(payload.slice(secondSemicolon + 1), 10);
350
+ if (Number.isNaN(cb) || Number.isNaN(col) || Number.isNaN(row))
351
+ return null;
352
+ const shift = (cb & 4) !== 0;
353
+ const meta = (cb & 8) !== 0;
354
+ const ctrl = (cb & 16) !== 0;
355
+ const buttonCode = cb & 3;
356
+ if (isRelease) {
357
+ return { button: 'release', row, column: col, shift, ctrl, meta };
358
+ }
359
+ if ((cb & 64) !== 0) {
360
+ return { button: buttonCode === 0 ? 'wheel-up' : 'wheel-down', row, column: col, shift, ctrl, meta };
361
+ }
362
+ const isMotion = (cb & 32) !== 0;
363
+ if (isMotion) {
364
+ return { button: 'motion', row, column: col, shift, ctrl, meta };
365
+ }
366
+ const buttonMap = { 0: 'left', 1: 'middle', 2: 'right' };
367
+ return { button: buttonMap[buttonCode] ?? 'left', row, column: col, shift, ctrl, meta };
368
+ }
369
+ }
308
370
  class FullscreenTui {
309
371
  workspaceRoot;
310
372
  options;
@@ -337,6 +399,13 @@ class FullscreenTui {
337
399
  };
338
400
  currentPreview = null;
339
401
  currentAbortController = null;
402
+ toolAbortController = null;
403
+ toolAbortSignalRef = {};
404
+ mouseParser = null;
405
+ mouseDataHandler = null;
406
+ mouseEscapeState = 'idle';
407
+ selection = null;
408
+ isDraggingSelection = false;
340
409
  inputValue = '';
341
410
  cursor = 0;
342
411
  busy = false;
@@ -356,6 +425,8 @@ class FullscreenTui {
356
425
  lastRenderAt = 0;
357
426
  previousFrameLines = null;
358
427
  previousFrameSize = null;
428
+ resizeHandler = null;
429
+ wrapCache = new Map();
359
430
  constructor(workspaceRoot, options = {}) {
360
431
  this.workspaceRoot = workspaceRoot;
361
432
  this.options = options;
@@ -366,12 +437,22 @@ class FullscreenTui {
366
437
  await this.refreshCommands();
367
438
  await this.refreshSessions({ createIfMissing: true });
368
439
  this.statusLine = `Ready. Shell ${this.getShellBackend().backendLabel} in ${path.relative(this.workspaceRoot, this.shellWorkspacePath) || '.'}. F10 or Ctrl+G opens menus. Left/right changes pane when the menu is closed.`;
369
- emitKeypressEvents(input);
370
440
  if (input.isTTY) {
371
441
  input.setRawMode(true);
372
442
  }
443
+ emitKeypressEvents(input);
373
444
  input.on('keypress', this.onKeypress);
374
- output.write('\u001b[?1049h\u001b[2J\u001b[H');
445
+ this.mouseParser = new MouseParser();
446
+ this.mouseDataHandler = (chunk) => {
447
+ this.mouseParser.feed(chunk, (event) => this.handleMouseEvent(event));
448
+ };
449
+ input.prependListener('data', this.mouseDataHandler);
450
+ this.resizeHandler = () => {
451
+ this.wrapCache.clear();
452
+ this.requestRender();
453
+ };
454
+ output.on('resize', this.resizeHandler);
455
+ output.write('\u001b[?1049h\u001b[?1002h\u001b[?1006h\u001b[2J\u001b[H');
375
456
  this.requestRender();
376
457
  await new Promise((resolve) => {
377
458
  this.closeResolver = resolve;
@@ -386,16 +467,315 @@ class FullscreenTui {
386
467
  clearInterval(this.animationTimer);
387
468
  this.animationTimer = null;
388
469
  }
389
- input.off('keypress', this.onKeypress);
470
+ if (this.mouseDataHandler) {
471
+ input.removeListener('data', this.mouseDataHandler);
472
+ this.mouseDataHandler = null;
473
+ }
474
+ this.mouseParser = null;
475
+ input.removeListener('keypress', this.onKeypress);
476
+ if (this.resizeHandler) {
477
+ output.off('resize', this.resizeHandler);
478
+ this.resizeHandler = null;
479
+ }
390
480
  releaseTuiInputStream(input);
391
481
  this.previousFrameLines = null;
392
482
  this.previousFrameSize = null;
393
- output.write('\u001b[?25h\u001b[?1049l');
483
+ output.write('\u001b[?25h\u001b[?1006l\u001b[?1002l\u001b[?1049l');
394
484
  }
395
485
  }
396
486
  onKeypress = (text, key) => {
487
+ if (key.sequence === '\x1b[<') {
488
+ this.mouseEscapeState = 'consuming';
489
+ return;
490
+ }
491
+ if (this.mouseEscapeState === 'consuming') {
492
+ if (text === 'M' || text === 'm') {
493
+ this.mouseEscapeState = 'idle';
494
+ }
495
+ return;
496
+ }
397
497
  void this.handleKeypress(text, key);
398
498
  };
499
+ handleMouseEvent(event) {
500
+ if (event.button === 'wheel-up' || event.button === 'wheel-down') {
501
+ if (event.shift)
502
+ return;
503
+ const direction = event.button === 'wheel-up' ? 'up' : 'down';
504
+ const paneKind = this.paneKindAtPosition(event.row, event.column);
505
+ if (paneKind) {
506
+ this.focusedPane = paneKind;
507
+ }
508
+ this.scrollFocusedPane(direction, 3);
509
+ return;
510
+ }
511
+ if (event.button === 'left' && !event.shift && !event.ctrl && !event.meta) {
512
+ const menuIndex = this.menuIndexAtColumn(event.column);
513
+ if (menuIndex !== null && event.row === 1) {
514
+ this.clearSelection();
515
+ if (this.uiMode !== 'menu') {
516
+ this.uiMode = 'menu';
517
+ }
518
+ this.menuState.activeIndex = menuIndex;
519
+ this.menuState.selectedIndex = 0;
520
+ this.menuState.typeahead = '';
521
+ this.requestRender();
522
+ return;
523
+ }
524
+ if (this.uiMode === 'menu' && event.row >= 2) {
525
+ const menuItemIndex = this.menuItemAtRow(event.row);
526
+ if (menuItemIndex !== null) {
527
+ this.menuState.selectedIndex = menuItemIndex;
528
+ this.requestRender();
529
+ void this.activateCurrentMenuItem();
530
+ return;
531
+ }
532
+ }
533
+ const paneKind = this.paneKindAtPosition(event.row, event.column);
534
+ if (!paneKind) {
535
+ this.clearSelection();
536
+ this.requestRender();
537
+ return;
538
+ }
539
+ const panePos = this.paneContentPosition(paneKind, event.row, event.column);
540
+ if (!panePos) {
541
+ this.clearSelection();
542
+ this.focusedPane = paneKind;
543
+ this.requestRender();
544
+ return;
545
+ }
546
+ this.selection = { pane: paneKind, startRow: panePos.bodyRow, startCol: panePos.innerCol, endRow: panePos.bodyRow, endCol: panePos.innerCol };
547
+ this.isDraggingSelection = true;
548
+ this.focusedPane = paneKind;
549
+ this.requestRender();
550
+ return;
551
+ }
552
+ if (event.button === 'motion' && !event.shift && this.isDraggingSelection && this.selection) {
553
+ const paneKind = this.paneKindAtPosition(event.row, event.column);
554
+ if (paneKind && paneKind === this.selection.pane) {
555
+ const panePos = this.paneContentPosition(paneKind, event.row, event.column);
556
+ if (panePos) {
557
+ this.selection.endRow = panePos.bodyRow;
558
+ this.selection.endCol = panePos.innerCol;
559
+ this.requestRender();
560
+ }
561
+ }
562
+ return;
563
+ }
564
+ if (event.button === 'release' && !event.shift && this.isDraggingSelection) {
565
+ this.isDraggingSelection = false;
566
+ if (this.selection) {
567
+ const text = this.extractSelectionText();
568
+ if (text) {
569
+ this.copyToClipboard(text);
570
+ this.statusLine = 'Copied selection to clipboard.';
571
+ }
572
+ }
573
+ this.clearSelection();
574
+ this.requestRender();
575
+ return;
576
+ }
577
+ if (event.button === 'release') {
578
+ this.isDraggingSelection = false;
579
+ return;
580
+ }
581
+ }
582
+ clearSelection() {
583
+ if (this.selection) {
584
+ this.selection = null;
585
+ }
586
+ this.isDraggingSelection = false;
587
+ }
588
+ paneContentPosition(paneKind, screenRow, screenCol) {
589
+ const columns = output.columns ?? 120;
590
+ const rows = output.rows ?? 40;
591
+ if (screenRow < 2 || screenRow >= rows - 1)
592
+ return null;
593
+ if (this.uiMode !== 'command')
594
+ return null;
595
+ const contentHeight = rows - 3;
596
+ const widths = computePaneWidths(columns);
597
+ const rightHeights = computeRightPaneHeights(contentHeight);
598
+ const contentRow = screenRow - 2;
599
+ let paneLeft;
600
+ let paneWidth;
601
+ let paneTop;
602
+ switch (paneKind) {
603
+ case 'sessions':
604
+ paneLeft = 0;
605
+ paneWidth = widths.left;
606
+ paneTop = 0;
607
+ break;
608
+ case 'messages':
609
+ paneLeft = widths.left + 1;
610
+ paneWidth = widths.center;
611
+ paneTop = 0;
612
+ break;
613
+ case 'process':
614
+ paneLeft = widths.left + 1 + widths.center + 1;
615
+ paneWidth = widths.right;
616
+ paneTop = 0;
617
+ break;
618
+ case 'context':
619
+ paneLeft = widths.left + 1 + widths.center + 1;
620
+ paneWidth = widths.right;
621
+ paneTop = rightHeights.process;
622
+ break;
623
+ }
624
+ if (screenCol < paneLeft + 2 || screenCol > paneLeft + paneWidth - 1)
625
+ return null;
626
+ const bodyRow = contentRow - paneTop - 1;
627
+ if (bodyRow < 0)
628
+ return null;
629
+ const innerCol = screenCol - paneLeft - 2;
630
+ return { bodyRow, innerCol };
631
+ }
632
+ extractSelectionText() {
633
+ if (!this.selection)
634
+ return '';
635
+ const sel = this.selection;
636
+ const paneBody = this.getPaneBody(sel.pane);
637
+ const columns = output.columns ?? 120;
638
+ const widths = computePaneWidths(columns);
639
+ const width = sel.pane === 'sessions' ? widths.left : sel.pane === 'messages' ? widths.center : widths.right;
640
+ const innerWidth = Math.max(1, width - 2);
641
+ const wrappedLines = wrapBodyLines(paneBody, innerWidth);
642
+ const startRow = Math.min(sel.startRow, sel.endRow);
643
+ const endRow = Math.max(sel.startRow, sel.endRow);
644
+ const startCol = sel.startRow <= sel.endRow ? sel.startCol : sel.endCol;
645
+ const endCol = sel.startRow <= sel.endRow ? sel.endCol : sel.startCol;
646
+ if (startRow === endRow) {
647
+ const line = wrappedLines[startRow] ?? '';
648
+ return this.stripAnsi(line.slice(startCol, endCol + 1));
649
+ }
650
+ const parts = [];
651
+ const firstLine = wrappedLines[startRow] ?? '';
652
+ parts.push(this.stripAnsi(firstLine.slice(startCol)));
653
+ for (let row = startRow + 1; row < endRow; row++) {
654
+ parts.push(this.stripAnsi(wrappedLines[row] ?? ''));
655
+ }
656
+ const lastLine = wrappedLines[endRow] ?? '';
657
+ parts.push(this.stripAnsi(lastLine.slice(0, endCol + 1)));
658
+ return parts.join('\n');
659
+ }
660
+ stripAnsi(text) {
661
+ return text.replace(/\u001b\[[0-9;]*m/g, '');
662
+ }
663
+ copyToClipboard(text) {
664
+ const osc52 = `\u001b]52;c;${Buffer.from(text).toString('base64')}\u0007`;
665
+ output.write(osc52);
666
+ }
667
+ applySelectionHighlight(lines, widths, rightHeights, _contentHeight) {
668
+ const sel = this.selection;
669
+ if (!sel)
670
+ return;
671
+ const startRow = Math.min(sel.startRow, sel.endRow);
672
+ const endRow = Math.max(sel.startRow, sel.endRow);
673
+ const startCol = sel.startRow <= sel.endRow ? sel.startCol : sel.endCol;
674
+ const endCol = sel.startRow <= sel.endRow ? sel.endCol : sel.startCol;
675
+ let paneLeft;
676
+ let paneWidth;
677
+ let paneTop;
678
+ switch (sel.pane) {
679
+ case 'sessions':
680
+ paneLeft = 0;
681
+ paneWidth = widths.left;
682
+ paneTop = 0;
683
+ break;
684
+ case 'messages':
685
+ paneLeft = widths.left + 1;
686
+ paneWidth = widths.center;
687
+ paneTop = 0;
688
+ break;
689
+ case 'process':
690
+ paneLeft = widths.left + 1 + widths.center + 1;
691
+ paneWidth = widths.right;
692
+ paneTop = 0;
693
+ break;
694
+ case 'context':
695
+ paneLeft = widths.left + 1 + widths.center + 1;
696
+ paneWidth = widths.right;
697
+ paneTop = rightHeights.process;
698
+ break;
699
+ }
700
+ const innerDisplayStart = paneLeft + 1;
701
+ const innerDisplayEnd = paneLeft + paneWidth - 2;
702
+ for (let bodyRow = startRow; bodyRow <= endRow; bodyRow++) {
703
+ const screenLineIdx = 1 + paneTop + 1 + bodyRow;
704
+ if (screenLineIdx >= lines.length)
705
+ break;
706
+ const line = lines[screenLineIdx];
707
+ const rowStartCol = bodyRow === startRow ? innerDisplayStart + startCol : innerDisplayStart;
708
+ const rowEndCol = bodyRow === endRow ? innerDisplayStart + endCol + 1 : innerDisplayEnd + 1;
709
+ const clampedStartCol = Math.max(innerDisplayStart, rowStartCol);
710
+ const clampedEndCol = Math.min(innerDisplayEnd + 1, rowEndCol);
711
+ if (clampedStartCol >= clampedEndCol)
712
+ continue;
713
+ const strStart = displayColumnToStringIndex(line, clampedStartCol);
714
+ const strEnd = displayColumnToStringIndex(line, clampedEndCol);
715
+ const before = line.slice(0, strStart);
716
+ const segment = line.slice(strStart, strEnd);
717
+ const after = line.slice(strEnd);
718
+ const highlighted = this.invertAnsiColors(segment);
719
+ lines[screenLineIdx] = `${before}${highlighted}${after}`;
720
+ }
721
+ }
722
+ invertAnsiColors(text) {
723
+ return `\u001b[7m${text}\u001b[27m`;
724
+ }
725
+ paneKindAtPosition(row, column) {
726
+ const columns = output.columns ?? 120;
727
+ const rows = output.rows ?? 40;
728
+ if (row < 2 || row >= rows - 1)
729
+ return null;
730
+ if (this.uiMode !== 'command')
731
+ return null;
732
+ const widths = computePaneWidths(columns);
733
+ const rightHeights = computeRightPaneHeights(rows - 3);
734
+ const contentRow = row - 1;
735
+ if (column <= widths.left)
736
+ return 'sessions';
737
+ if (column <= widths.left + 1 + widths.center)
738
+ return 'messages';
739
+ if (contentRow <= rightHeights.process)
740
+ return 'process';
741
+ return 'context';
742
+ }
743
+ menuIndexAtColumn(column) {
744
+ const menuLabels = ['Session', 'Skills', 'Files', 'Run', 'View', 'Help'];
745
+ let pos = 1 + measureDisplayWidth(' DeepClause ');
746
+ for (let i = 0; i < menuLabels.length; i++) {
747
+ const labelWidth = measureDisplayWidth(` [${i + 1}] ${menuLabels[i]} `);
748
+ if (column >= pos && column < pos + labelWidth)
749
+ return i;
750
+ pos += labelWidth;
751
+ }
752
+ return null;
753
+ }
754
+ menuItemAtRow(row) {
755
+ if (this.uiMode !== 'menu')
756
+ return null;
757
+ const contentRow = row - 2;
758
+ if (contentRow < 2)
759
+ return null;
760
+ const itemRow = contentRow - 2;
761
+ if (itemRow < 0)
762
+ return null;
763
+ const linesPerItem = 3;
764
+ const itemIndex = Math.floor(itemRow / linesPerItem);
765
+ const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
766
+ if (!menu || itemIndex >= menu.items.length || itemIndex < 0)
767
+ return null;
768
+ return itemIndex;
769
+ }
770
+ async activateCurrentMenuItem() {
771
+ const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
772
+ if (!menu)
773
+ return;
774
+ const item = menu.items[this.menuState.selectedIndex];
775
+ if (!item || item.enabled === false)
776
+ return;
777
+ await this.runMenuAction(item.id);
778
+ }
399
779
  async handleKeypress(text, key) {
400
780
  if ((key.ctrl && key.name === 'g') || key.name === 'f10') {
401
781
  this.toggleMenuMode();
@@ -428,6 +808,19 @@ class FullscreenTui {
428
808
  this.close();
429
809
  return;
430
810
  }
811
+ if (key.ctrl && key.name === 't' && this.busy) {
812
+ if (this.toolAbortController && !this.toolAbortController.signal.aborted) {
813
+ this.toolAbortController.abort('Aborted by user');
814
+ this.toolAbortController = null;
815
+ this.toolAbortSignalRef.signal = undefined;
816
+ this.statusLine = 'Tool execution cancelled.';
817
+ }
818
+ else {
819
+ this.statusLine = 'No active tool to cancel.';
820
+ }
821
+ this.requestRender();
822
+ return;
823
+ }
431
824
  if (this.uiMode === 'menu') {
432
825
  await this.handleMenuKeypress(text, key);
433
826
  return;
@@ -460,7 +853,7 @@ class FullscreenTui {
460
853
  return;
461
854
  }
462
855
  if (this.cursor > 0) {
463
- this.cursor -= 1;
856
+ this.cursor = this.graphemeLeftIndex(this.inputValue, this.cursor);
464
857
  }
465
858
  break;
466
859
  case 'right':
@@ -469,7 +862,7 @@ class FullscreenTui {
469
862
  return;
470
863
  }
471
864
  if (this.cursor < this.inputValue.length) {
472
- this.cursor += 1;
865
+ this.cursor = this.graphemeRightIndex(this.inputValue, this.cursor);
473
866
  }
474
867
  break;
475
868
  case 'home':
@@ -642,13 +1035,13 @@ class FullscreenTui {
642
1035
  return;
643
1036
  case 'left':
644
1037
  if (this.cursor > 0) {
645
- this.cursor -= 1;
1038
+ this.cursor = this.graphemeLeftIndex(this.inputValue, this.cursor);
646
1039
  this.requestRender();
647
1040
  }
648
1041
  return;
649
1042
  case 'right':
650
1043
  if (this.cursor < this.inputValue.length) {
651
- this.cursor += 1;
1044
+ this.cursor = this.graphemeRightIndex(this.inputValue, this.cursor);
652
1045
  this.requestRender();
653
1046
  }
654
1047
  return;
@@ -875,6 +1268,8 @@ class FullscreenTui {
875
1268
  activity.pushLine(`task ${promptText}`);
876
1269
  this.busy = true;
877
1270
  this.currentAbortController = new AbortController();
1271
+ this.toolAbortController = new AbortController();
1272
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
878
1273
  this.startAnimation();
879
1274
  this.statusLine = 'Running conductor...';
880
1275
  this.requestRender();
@@ -886,8 +1281,10 @@ class FullscreenTui {
886
1281
  headless: true,
887
1282
  sandbox: this.options.sandbox,
888
1283
  signal: this.currentAbortController.signal,
1284
+ toolAbortSignalRef: this.toolAbortSignalRef,
889
1285
  onUserInput: (question) => this.requestClarification(question),
890
1286
  onEvent: (event) => {
1287
+ this.manageToolAbortSignal(event.event);
891
1288
  activity.handle(event);
892
1289
  this.updatePreviewFromEvent(event);
893
1290
  this.requestRender();
@@ -912,6 +1309,8 @@ class FullscreenTui {
912
1309
  finally {
913
1310
  this.busy = false;
914
1311
  this.currentAbortController = null;
1312
+ this.toolAbortController = null;
1313
+ this.toolAbortSignalRef.signal = undefined;
915
1314
  this.stopAnimation();
916
1315
  if (this.exitRequested) {
917
1316
  this.close();
@@ -933,6 +1332,8 @@ class FullscreenTui {
933
1332
  activity.pushLine(`skill /${skillName}${args.length > 0 ? ` ${args.join(' ')}` : ''}`);
934
1333
  this.busy = true;
935
1334
  this.currentAbortController = new AbortController();
1335
+ this.toolAbortController = new AbortController();
1336
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
936
1337
  this.startAnimation();
937
1338
  this.statusLine = `Running /${skillName}...`;
938
1339
  this.requestRender();
@@ -941,14 +1342,17 @@ class FullscreenTui {
941
1342
  sessionId: this.selectedSessionId || undefined,
942
1343
  sandbox: this.options.sandbox,
943
1344
  signal: this.currentAbortController.signal,
1345
+ toolAbortSignalRef: this.toolAbortSignalRef,
944
1346
  onUserInput: (question) => this.requestClarification(question),
945
1347
  onEvent: (event) => {
1348
+ this.manageToolAbortSignal(event.event);
946
1349
  activity.handle(event);
947
1350
  this.updatePreviewFromEvent(event);
948
1351
  this.requestRender();
949
1352
  },
950
1353
  });
951
1354
  activity.finish();
1355
+ await this.refreshSessions({ selectedSessionId: this.selectedSessionId });
952
1356
  await this.refreshCommands();
953
1357
  this.finishExecutionPreview({
954
1358
  persist: true,
@@ -967,6 +1371,8 @@ class FullscreenTui {
967
1371
  finally {
968
1372
  this.busy = false;
969
1373
  this.currentAbortController = null;
1374
+ this.toolAbortController = null;
1375
+ this.toolAbortSignalRef.signal = undefined;
970
1376
  this.stopAnimation();
971
1377
  if (this.exitRequested) {
972
1378
  this.close();
@@ -995,6 +1401,8 @@ class FullscreenTui {
995
1401
  activity.pushLine(`run ${commandText}`);
996
1402
  this.busy = true;
997
1403
  this.currentAbortController = new AbortController();
1404
+ this.toolAbortController = new AbortController();
1405
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
998
1406
  this.startAnimation();
999
1407
  this.statusLine = `Running ${commandText}...`;
1000
1408
  this.requestRender();
@@ -1003,14 +1411,17 @@ class FullscreenTui {
1003
1411
  sessionId: this.selectedSessionId || undefined,
1004
1412
  sandbox: this.options.sandbox,
1005
1413
  signal: this.currentAbortController.signal,
1414
+ toolAbortSignalRef: this.toolAbortSignalRef,
1006
1415
  onUserInput: (question) => this.requestClarification(question),
1007
1416
  onEvent: (event) => {
1417
+ this.manageToolAbortSignal(event.event);
1008
1418
  activity.handle(event);
1009
1419
  this.updatePreviewFromEvent(event);
1010
1420
  this.requestRender();
1011
1421
  },
1012
1422
  });
1013
1423
  activity.finish();
1424
+ await this.refreshSessions({ selectedSessionId: this.selectedSessionId });
1014
1425
  await this.refreshCommands();
1015
1426
  this.finishExecutionPreview({
1016
1427
  persist: true,
@@ -1029,6 +1440,8 @@ class FullscreenTui {
1029
1440
  finally {
1030
1441
  this.busy = false;
1031
1442
  this.currentAbortController = null;
1443
+ this.toolAbortController = null;
1444
+ this.toolAbortSignalRef.signal = undefined;
1032
1445
  this.stopAnimation();
1033
1446
  if (this.exitRequested) {
1034
1447
  this.close();
@@ -1056,6 +1469,8 @@ class FullscreenTui {
1056
1469
  activity.pushLine(`skill ${commandText}`);
1057
1470
  this.busy = true;
1058
1471
  this.currentAbortController = new AbortController();
1472
+ this.toolAbortController = new AbortController();
1473
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
1059
1474
  this.startAnimation();
1060
1475
  this.statusLine = `Running /${commandName}...`;
1061
1476
  this.requestRender();
@@ -1065,12 +1480,14 @@ class FullscreenTui {
1065
1480
  signal: this.currentAbortController.signal,
1066
1481
  onUserInput: (question) => this.requestClarification(question),
1067
1482
  onEvent: (event) => {
1483
+ this.manageToolAbortSignal(event.event);
1068
1484
  activity.handle(event);
1069
1485
  this.updatePreviewFromEvent(event);
1070
1486
  this.requestRender();
1071
1487
  },
1072
1488
  });
1073
1489
  activity.finish();
1490
+ await this.refreshSessions({ selectedSessionId: this.selectedSessionId });
1074
1491
  await this.refreshCommands();
1075
1492
  this.finishExecutionPreview({
1076
1493
  persist: true,
@@ -1088,6 +1505,8 @@ class FullscreenTui {
1088
1505
  finally {
1089
1506
  this.busy = false;
1090
1507
  this.currentAbortController = null;
1508
+ this.toolAbortController = null;
1509
+ this.toolAbortSignalRef.signal = undefined;
1091
1510
  this.stopAnimation();
1092
1511
  if (this.exitRequested) {
1093
1512
  this.close();
@@ -1119,6 +1538,8 @@ class FullscreenTui {
1119
1538
  activity.pushLine(`shell ${persistOutput ? '!!' : '!'}${trimmedCommand}`);
1120
1539
  this.busy = true;
1121
1540
  this.currentAbortController = new AbortController();
1541
+ this.toolAbortController = this.currentAbortController;
1542
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
1122
1543
  this.startAnimation();
1123
1544
  this.statusLine = `Running ${persistOutput ? '!!' : '!'}${trimmedCommand}...`;
1124
1545
  this.requestRender();
@@ -1163,6 +1584,8 @@ class FullscreenTui {
1163
1584
  finally {
1164
1585
  this.busy = false;
1165
1586
  this.currentAbortController = null;
1587
+ this.toolAbortController = null;
1588
+ this.toolAbortSignalRef.signal = undefined;
1166
1589
  this.stopAnimation();
1167
1590
  if (this.exitRequested) {
1168
1591
  this.close();
@@ -2355,6 +2778,36 @@ class FullscreenTui {
2355
2778
  currentProcessActivity() {
2356
2779
  return this.activityFor(this.selectedSessionId || 'global');
2357
2780
  }
2781
+ graphemeLeftIndex(text, index) {
2782
+ if (index <= 0)
2783
+ return 0;
2784
+ if (!graphemeSegmenter)
2785
+ return index - 1;
2786
+ const graphemes = Array.from(graphemeSegmenter.segment(text), (g) => g.segment);
2787
+ let pos = 0;
2788
+ for (const g of graphemes) {
2789
+ if (pos + g.length >= index)
2790
+ break;
2791
+ pos += g.length;
2792
+ }
2793
+ return pos;
2794
+ }
2795
+ graphemeRightIndex(text, index) {
2796
+ if (index >= text.length)
2797
+ return text.length;
2798
+ if (!graphemeSegmenter)
2799
+ return index + 1;
2800
+ const graphemes = Array.from(graphemeSegmenter.segment(text), (g) => g.segment);
2801
+ let pos = 0;
2802
+ for (const g of graphemes) {
2803
+ if (pos >= index) {
2804
+ pos += g.length;
2805
+ break;
2806
+ }
2807
+ pos += g.length;
2808
+ }
2809
+ return Math.min(pos, text.length);
2810
+ }
2358
2811
  insertText(text) {
2359
2812
  const previousInput = this.inputValue;
2360
2813
  this.inputValue = `${this.inputValue.slice(0, this.cursor)}${text}${this.inputValue.slice(this.cursor)}`;
@@ -2366,8 +2819,9 @@ class FullscreenTui {
2366
2819
  return;
2367
2820
  }
2368
2821
  const previousInput = this.inputValue;
2369
- this.inputValue = `${this.inputValue.slice(0, this.cursor - 1)}${this.inputValue.slice(this.cursor)}`;
2370
- this.cursor -= 1;
2822
+ const deleteFrom = this.graphemeLeftIndex(this.inputValue, this.cursor);
2823
+ this.inputValue = `${this.inputValue.slice(0, deleteFrom)}${this.inputValue.slice(this.cursor)}`;
2824
+ this.cursor = deleteFrom;
2371
2825
  this.refreshCommandBarStatus(previousInput);
2372
2826
  }
2373
2827
  deleteForward() {
@@ -2509,23 +2963,28 @@ class FullscreenTui {
2509
2963
  }
2510
2964
  lines.push(status);
2511
2965
  lines.push(inputLine);
2966
+ if (this.selection) {
2967
+ this.applySelectionHighlight(lines, widths, rightHeights, contentHeight);
2968
+ }
2512
2969
  this.paintFrame(lines, { columns, rows });
2513
2970
  if (workspaceView?.cursor && !this.pendingPrompt) {
2514
2971
  output.write(`\u001b[${workspaceView.cursor.row};${workspaceView.cursor.column}H`);
2515
2972
  return;
2516
2973
  }
2517
- const cursorColumn = Math.min(columns, inputPrefix.length + this.cursor + 1);
2974
+ const textBeforeCursor = this.inputValue.slice(0, this.cursor);
2975
+ const cursorColumn = Math.min(columns, inputPrefix.length + measureDisplayWidth(textBeforeCursor) + 1);
2518
2976
  output.write(`\u001b[${rows};${cursorColumn}H`);
2519
2977
  }
2520
2978
  paintFrame(lines, size) {
2521
- const patch = computeFramePatch(this.previousFrameLines, lines, this.previousFrameSize, size);
2979
+ const paddedLines = lines.map((line) => padRight(line, size.columns));
2980
+ const patch = computeFramePatch(this.previousFrameLines, paddedLines, this.previousFrameSize, size);
2522
2981
  let buffer = '\u001b[0m';
2523
2982
  if (patch.fullRender) {
2524
- buffer += `\u001b[H\u001b[2J${lines.join('\n')}\u001b[?25h`;
2983
+ buffer += `\u001b[H${paddedLines.join('\n')}\u001b[?25h`;
2525
2984
  }
2526
2985
  else if (patch.changedRows.length > 0) {
2527
2986
  for (const change of patch.changedRows) {
2528
- buffer += `\u001b[${change.row};1H\u001b[2K${change.line}`;
2987
+ buffer += `\u001b[${change.row};1H${change.line}`;
2529
2988
  }
2530
2989
  buffer += '\u001b[?25h';
2531
2990
  }
@@ -2533,7 +2992,7 @@ class FullscreenTui {
2533
2992
  buffer += '\u001b[?25h';
2534
2993
  }
2535
2994
  output.write(buffer);
2536
- this.previousFrameLines = [...lines];
2995
+ this.previousFrameLines = [...paddedLines];
2537
2996
  this.previousFrameSize = size;
2538
2997
  }
2539
2998
  buildHeaderLine() {
@@ -2547,32 +3006,35 @@ class FullscreenTui {
2547
3006
  return `${sessionLabel} ${mode} ${skillLabel}`;
2548
3007
  }
2549
3008
  buildStatusLine() {
3009
+ const spinner = this.busy || this.currentPreview?.entries.some((entry) => entry.pending)
3010
+ ? ` ${currentSpinnerFrame()}`
3011
+ : '';
2550
3012
  if (this.uiMode === 'menu') {
2551
3013
  const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
2552
- return `Menu ${menu?.label ?? ''} | ${menu?.items[this.menuState.selectedIndex]?.description ?? 'Choose an action.'}`;
3014
+ return `${spinner} Menu ${menu?.label ?? ''} | ${menu?.items[this.menuState.selectedIndex]?.description ?? 'Choose an action.'}`;
2553
3015
  }
2554
3016
  if ((this.uiMode === 'picker' || this.uiMode === 'palette') && this.pickerState) {
2555
3017
  const selected = this.pickerState.filteredItems[this.pickerState.selectedIndex];
2556
- return `${this.pickerState.title} | ${selected?.description ?? this.pickerState.emptyText}`;
3018
+ return `${spinner} ${this.pickerState.title} | ${selected?.description ?? this.pickerState.emptyText}`;
2557
3019
  }
2558
3020
  if (this.uiMode === 'viewer' && this.viewerState) {
2559
- return `${this.viewerState.title} | ${this.viewerState.path ? path.relative(this.workspaceRoot, this.viewerState.path) : 'read-only view'}`;
3021
+ return `${spinner} ${this.viewerState.title} | ${this.viewerState.path ? path.relative(this.workspaceRoot, this.viewerState.path) : 'read-only view'}`;
2560
3022
  }
2561
3023
  if (this.uiMode === 'editor' && this.editorState) {
2562
3024
  const dirty = this.editorState.dirty ? 'modified' : 'saved';
2563
- return `${this.editorState.title} | ${dirty} | Ln ${this.editorState.cursorLine + 1}, Col ${this.editorState.cursorColumn + 1}`;
3025
+ return `${spinner} ${this.editorState.title} | ${dirty} | Ln ${this.editorState.cursorLine + 1}, Col ${this.editorState.cursorColumn + 1}`;
2564
3026
  }
2565
3027
  const focus = paneDisplayName(this.focusedPane);
2566
3028
  if (!this.pendingPrompt && this.focusedPane !== 'sessions') {
2567
3029
  const follow = this.paneScroll[this.focusedPane] === 0 ? 'follow' : `scroll ${this.paneScroll[this.focusedPane]}`;
2568
- return `${this.statusLine} | Focus ${focus} | ${follow}`;
3030
+ return `${spinner} ${this.statusLine} | Focus ${focus} | ${follow}`;
2569
3031
  }
2570
3032
  if (this.pendingPrompt) {
2571
3033
  return this.pendingPrompt.kind === 'clarify'
2572
- ? `Clarify: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`
2573
- : `Input: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`;
3034
+ ? `${spinner} Clarify: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`
3035
+ : `${spinner} Input: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`;
2574
3036
  }
2575
- return `${this.statusLine} | Focus ${focus}`;
3037
+ return `${spinner} ${this.statusLine} | Focus ${focus}`;
2576
3038
  }
2577
3039
  buildInputPrefix() {
2578
3040
  if (this.uiMode === 'menu') {
@@ -2865,6 +3327,8 @@ class FullscreenTui {
2865
3327
  ['<- ->', 'Pane'],
2866
3328
  [this.focusedPane === 'sessions' ? 'Up/Down' : 'PgUp/PgDn', this.focusedPane === 'sessions' ? 'Select' : 'Scroll'],
2867
3329
  ['End', this.focusedPane === 'sessions' ? 'Bottom' : 'Follow'],
3330
+ ['Drag', 'Select'],
3331
+ ...(this.busy ? [['Ctrl+T', 'Cancel']] : []),
2868
3332
  ['/cancel', 'Stop'],
2869
3333
  ];
2870
3334
  }
@@ -2879,6 +3343,16 @@ class FullscreenTui {
2879
3343
  ? `${title}${this.focusedPane === kind ? ' [focus]' : ''} [follow]`
2880
3344
  : `${title}${this.focusedPane === kind ? ' [focus]' : ''} [-${this.paneScroll[kind]}]`;
2881
3345
  }
3346
+ cachedCollectTailWrappedLines(cacheKey, body, innerWidth, limit) {
3347
+ const cached = this.wrapCache.get(cacheKey);
3348
+ const bodyKey = `${body.length}:${body.length > 0 ? body[body.length - 1] : ''}:${limit}`;
3349
+ if (cached && cached.bodyRef === body && cached.bodyKey === bodyKey && cached.innerWidth === innerWidth) {
3350
+ return { lines: cached.result, truncated: body.length > 0 && cached.result.length < body.length };
3351
+ }
3352
+ const result = collectTailWrappedLines(body, innerWidth, limit);
3353
+ this.wrapCache.set(cacheKey, { bodyRef: body, bodyKey, innerWidth, result: result.lines });
3354
+ return result;
3355
+ }
2882
3356
  getRenderPaneMetrics(kind, width = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).width, height = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).height) {
2883
3357
  if ((kind === 'process' || kind === 'messages') && this.paneScroll[kind] === 0) {
2884
3358
  const innerWidth = Math.max(1, width - 2);
@@ -2886,7 +3360,7 @@ class FullscreenTui {
2886
3360
  const rawBody = kind === 'messages'
2887
3361
  ? this.buildMessagesPaneBody({ recentEntryLimit: Math.max(8, viewportLines * 2) })
2888
3362
  : this.buildProcessPaneBody({ tailLineLimit: Math.max(16, viewportLines * 4) });
2889
- const tail = collectTailWrappedLines(rawBody, innerWidth, viewportLines);
3363
+ const tail = this.cachedCollectTailWrappedLines(`${kind}:tail`, rawBody, innerWidth, viewportLines);
2890
3364
  return {
2891
3365
  lines: tail.lines,
2892
3366
  start: 0,
@@ -2904,7 +3378,7 @@ class FullscreenTui {
2904
3378
  getPaneMetrics(kind, width = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).width, height = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).height) {
2905
3379
  const innerWidth = Math.max(1, width - 2);
2906
3380
  const rawBody = this.getPaneBody(kind);
2907
- const lines = wrapBodyLines(rawBody, innerWidth);
3381
+ const lines = this.cachedWrapBodyLines(kind, rawBody, innerWidth);
2908
3382
  const viewportLines = Math.max(0, height - 2);
2909
3383
  const maxStart = Math.max(0, lines.length - viewportLines);
2910
3384
  if (kind === 'sessions') {
@@ -2926,6 +3400,16 @@ class FullscreenTui {
2926
3400
  maxStartIsExact: true,
2927
3401
  };
2928
3402
  }
3403
+ cachedWrapBodyLines(cacheKey, body, innerWidth) {
3404
+ const cached = this.wrapCache.get(cacheKey);
3405
+ const bodyKey = `${body.length}:${body.length > 0 ? body[body.length - 1] : ''}`;
3406
+ if (cached && cached.bodyRef === body && cached.bodyKey === bodyKey && cached.innerWidth === innerWidth) {
3407
+ return cached.result;
3408
+ }
3409
+ const result = wrapBodyLines(body, innerWidth);
3410
+ this.wrapCache.set(cacheKey, { bodyRef: body, bodyKey, innerWidth, result });
3411
+ return result;
3412
+ }
2929
3413
  getPaneBody(kind) {
2930
3414
  switch (kind) {
2931
3415
  case 'sessions':
@@ -2933,7 +3417,7 @@ class FullscreenTui {
2933
3417
  case 'messages':
2934
3418
  return this.buildMessagesPaneBody();
2935
3419
  case 'process':
2936
- return this.buildProcessPaneBody();
3420
+ return this.buildProcessPaneBody({ tailLineLimit: MAX_ACTIVITY_LINES });
2937
3421
  case 'context':
2938
3422
  return this.buildContextPaneBody();
2939
3423
  }
@@ -2951,8 +3435,7 @@ class FullscreenTui {
2951
3435
  }
2952
3436
  }
2953
3437
  formatMessageHeader(entry) {
2954
- const spinner = entry.pending ? ` ${currentSpinnerFrame()}` : '';
2955
- return formatDisplayMessageHeader(entry, spinner);
3438
+ return formatDisplayMessageHeader(entry, '');
2956
3439
  }
2957
3440
  close() {
2958
3441
  if (!this.closeResolver) {
@@ -2962,6 +3445,21 @@ class FullscreenTui {
2962
3445
  this.closeResolver = null;
2963
3446
  resolve();
2964
3447
  }
3448
+ manageToolAbortSignal(event) {
3449
+ if (event.type !== 'tool_call' || !event.toolState) {
3450
+ return;
3451
+ }
3452
+ if (event.toolState === 'starting' || event.toolState === 'running') {
3453
+ if (!this.toolAbortController || this.toolAbortController.signal.aborted) {
3454
+ this.toolAbortController = new AbortController();
3455
+ }
3456
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
3457
+ }
3458
+ else {
3459
+ this.toolAbortController = null;
3460
+ this.toolAbortSignalRef.signal = undefined;
3461
+ }
3462
+ }
2965
3463
  isEditingInput() {
2966
3464
  return this.uiMode !== 'editor' && this.inputValue.length > 0;
2967
3465
  }
@@ -3254,6 +3752,7 @@ export async function runFileCommand(workspaceRoot, fileSpec, args, options = {}
3254
3752
  }, args, options);
3255
3753
  }
3256
3754
  async function executeRunnableCommand(workspaceRoot, target, args, options = {}) {
3755
+ const usageByModel = {};
3257
3756
  const executionLog = options.sessionId
3258
3757
  ? createSessionExecutionLogWriter({
3259
3758
  workspaceRoot,
@@ -3265,6 +3764,9 @@ async function executeRunnableCommand(workspaceRoot, target, args, options = {})
3265
3764
  })
3266
3765
  : null;
3267
3766
  const emitLogEvent = (event) => {
3767
+ if (event.event.type === 'usage') {
3768
+ recordTokenUsage(usageByModel, event.modelId, event.event.usage);
3769
+ }
3268
3770
  executionLog?.recordEvent(event);
3269
3771
  options.onEvent?.(event);
3270
3772
  };
@@ -3275,12 +3777,14 @@ async function executeRunnableCommand(workspaceRoot, target, args, options = {})
3275
3777
  stream: true,
3276
3778
  sandbox: options.sandbox,
3277
3779
  signal: options.signal,
3780
+ toolAbortSignalRef: options.toolAbortSignalRef,
3278
3781
  onUserInput: options.onUserInput,
3279
3782
  onEvent: (event) => emitLogEvent({ scope: 'child', childSlug: target.childSlug, event }),
3280
3783
  onChildEvent: (childSlug, event) => emitLogEvent({ scope: 'child', childSlug, event }),
3281
3784
  });
3282
3785
  if (options.sessionId) {
3283
3786
  await appendConductorSessionMessages(workspaceRoot, options.sessionId, buildPersistedRunnableTranscript(target.inputText, result));
3787
+ await mergeSessionUsage(workspaceRoot, options.sessionId, usageByModel);
3284
3788
  }
3285
3789
  await executionLog?.finish({
3286
3790
  status: result.error ? 'error' : 'success',
@@ -3296,6 +3800,7 @@ async function executeRunnableCommand(workspaceRoot, target, args, options = {})
3296
3800
  output: [],
3297
3801
  error: error.message,
3298
3802
  }));
3803
+ await mergeSessionUsage(workspaceRoot, options.sessionId, usageByModel);
3299
3804
  }
3300
3805
  await executionLog?.finish({
3301
3806
  status: 'error',
@@ -4091,6 +4596,37 @@ function expandTabs(text, tabWidth) {
4091
4596
  export function measureDisplayWidth(text) {
4092
4597
  return splitGraphemes(text).reduce((total, grapheme) => total + graphemeDisplayWidth(grapheme), 0);
4093
4598
  }
4599
+ function displayColumnToStringIndex(text, displayCol) {
4600
+ let col = 0;
4601
+ let i = 0;
4602
+ const len = text.length;
4603
+ while (i < len) {
4604
+ if (text.charCodeAt(i) === 0x1b && i + 1 < len && text[i + 1] === '[') {
4605
+ const end = text.indexOf('m', i + 2);
4606
+ if (end !== -1) {
4607
+ i = end + 1;
4608
+ continue;
4609
+ }
4610
+ }
4611
+ if (col >= displayCol)
4612
+ return i;
4613
+ const code = text.codePointAt(i);
4614
+ const charWidth = code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a ||
4615
+ (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
4616
+ (code >= 0xac00 && code <= 0xd7a3) ||
4617
+ (code >= 0xf900 && code <= 0xfaff) ||
4618
+ (code >= 0xfe10 && code <= 0xfe19) ||
4619
+ (code >= 0xfe30 && code <= 0xfe6f) ||
4620
+ (code >= 0xff01 && code <= 0xff60) ||
4621
+ (code >= 0xffe0 && code <= 0xffe6) ||
4622
+ (code >= 0x1f300 && code <= 0x1f9ff) ||
4623
+ (code >= 0x20000 && code <= 0x2fffd) ||
4624
+ (code >= 0x30000 && code <= 0x3fffd)) ? 2 : 1;
4625
+ col += charWidth;
4626
+ i += code > 0xffff ? 2 : 1;
4627
+ }
4628
+ return i;
4629
+ }
4094
4630
  export function padRight(text, width) {
4095
4631
  const visibleWidth = measureDisplayWidth(text);
4096
4632
  return visibleWidth >= width ? text : `${text}${' '.repeat(width - visibleWidth)}`;