deepclause-sdk 0.0.73 → 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.
Files changed (47) hide show
  1. package/dist/cli/run.d.ts +3 -0
  2. package/dist/cli/run.d.ts.map +1 -1
  3. package/dist/cli/run.js +1 -0
  4. package/dist/cli/run.js.map +1 -1
  5. package/dist/cli/search.d.ts +4 -11
  6. package/dist/cli/search.d.ts.map +1 -1
  7. package/dist/cli/search.js +100 -42
  8. package/dist/cli/search.js.map +1 -1
  9. package/dist/cli/tools.js +2 -2
  10. package/dist/cli/tools.js.map +1 -1
  11. package/dist/cli/tui.d.ts +6 -0
  12. package/dist/cli/tui.d.ts.map +1 -1
  13. package/dist/cli/tui.js +610 -36
  14. package/dist/cli/tui.js.map +1 -1
  15. package/dist/compaction.d.ts +25 -2
  16. package/dist/compaction.d.ts.map +1 -1
  17. package/dist/compaction.js +167 -8
  18. package/dist/compaction.js.map +1 -1
  19. package/dist/prolog/bridge.d.ts +16 -0
  20. package/dist/prolog/bridge.d.ts.map +1 -1
  21. package/dist/prolog/bridge.js +12 -0
  22. package/dist/prolog/bridge.js.map +1 -1
  23. package/dist/prolog-src/deepclause_mi.pl +69 -5
  24. package/dist/runner.d.ts +10 -5
  25. package/dist/runner.d.ts.map +1 -1
  26. package/dist/runner.js +89 -23
  27. package/dist/runner.js.map +1 -1
  28. package/dist/system/assets/compactors/default-loop-compactor.dml +15 -5
  29. package/dist/system/assets/compactors/default-session-compactor.dml +15 -5
  30. package/dist/system/runtime/conductor.d.ts +4 -0
  31. package/dist/system/runtime/conductor.d.ts.map +1 -1
  32. package/dist/system/runtime/conductor.js +15 -29
  33. package/dist/system/runtime/conductor.js.map +1 -1
  34. package/dist/system/runtime/dml-executor.d.ts +5 -5
  35. package/dist/system/runtime/dml-executor.d.ts.map +1 -1
  36. package/dist/system/runtime/dml-executor.js +2 -0
  37. package/dist/system/runtime/dml-executor.js.map +1 -1
  38. package/dist/system/runtime/runtime-tools.d.ts +3 -0
  39. package/dist/system/runtime/runtime-tools.d.ts.map +1 -1
  40. package/dist/system/runtime/runtime-tools.js +12 -5
  41. package/dist/system/runtime/runtime-tools.js.map +1 -1
  42. package/dist/tools.js +2 -2
  43. package/dist/tools.js.map +1 -1
  44. package/dist/types.d.ts +3 -6
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +2 -1
  47. package/src/prolog-src/deepclause_mi.pl +69 -5
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;
@@ -329,8 +391,21 @@ class FullscreenTui {
329
391
  process: 0,
330
392
  context: 0,
331
393
  };
394
+ lastPaneMetrics = {
395
+ sessions: { maxStart: 0, viewportLines: 0, maxStartIsExact: true },
396
+ messages: { maxStart: 0, viewportLines: 0, maxStartIsExact: true },
397
+ process: { maxStart: 0, viewportLines: 0, maxStartIsExact: true },
398
+ context: { maxStart: 0, viewportLines: 0, maxStartIsExact: true },
399
+ };
332
400
  currentPreview = null;
333
401
  currentAbortController = null;
402
+ toolAbortController = null;
403
+ toolAbortSignalRef = {};
404
+ mouseParser = null;
405
+ mouseDataHandler = null;
406
+ mouseEscapeState = 'idle';
407
+ selection = null;
408
+ isDraggingSelection = false;
334
409
  inputValue = '';
335
410
  cursor = 0;
336
411
  busy = false;
@@ -350,6 +425,8 @@ class FullscreenTui {
350
425
  lastRenderAt = 0;
351
426
  previousFrameLines = null;
352
427
  previousFrameSize = null;
428
+ resizeHandler = null;
429
+ wrapCache = new Map();
353
430
  constructor(workspaceRoot, options = {}) {
354
431
  this.workspaceRoot = workspaceRoot;
355
432
  this.options = options;
@@ -360,12 +437,22 @@ class FullscreenTui {
360
437
  await this.refreshCommands();
361
438
  await this.refreshSessions({ createIfMissing: true });
362
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.`;
363
- emitKeypressEvents(input);
364
440
  if (input.isTTY) {
365
441
  input.setRawMode(true);
366
442
  }
443
+ emitKeypressEvents(input);
367
444
  input.on('keypress', this.onKeypress);
368
- 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');
369
456
  this.requestRender();
370
457
  await new Promise((resolve) => {
371
458
  this.closeResolver = resolve;
@@ -380,16 +467,315 @@ class FullscreenTui {
380
467
  clearInterval(this.animationTimer);
381
468
  this.animationTimer = null;
382
469
  }
383
- 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
+ }
384
480
  releaseTuiInputStream(input);
385
481
  this.previousFrameLines = null;
386
482
  this.previousFrameSize = null;
387
- output.write('\u001b[?25h\u001b[?1049l');
483
+ output.write('\u001b[?25h\u001b[?1006l\u001b[?1002l\u001b[?1049l');
388
484
  }
389
485
  }
390
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
+ }
391
497
  void this.handleKeypress(text, key);
392
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
+ }
393
779
  async handleKeypress(text, key) {
394
780
  if ((key.ctrl && key.name === 'g') || key.name === 'f10') {
395
781
  this.toggleMenuMode();
@@ -422,6 +808,19 @@ class FullscreenTui {
422
808
  this.close();
423
809
  return;
424
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
+ }
425
824
  if (this.uiMode === 'menu') {
426
825
  await this.handleMenuKeypress(text, key);
427
826
  return;
@@ -454,7 +853,7 @@ class FullscreenTui {
454
853
  return;
455
854
  }
456
855
  if (this.cursor > 0) {
457
- this.cursor -= 1;
856
+ this.cursor = this.graphemeLeftIndex(this.inputValue, this.cursor);
458
857
  }
459
858
  break;
460
859
  case 'right':
@@ -463,7 +862,7 @@ class FullscreenTui {
463
862
  return;
464
863
  }
465
864
  if (this.cursor < this.inputValue.length) {
466
- this.cursor += 1;
865
+ this.cursor = this.graphemeRightIndex(this.inputValue, this.cursor);
467
866
  }
468
867
  break;
469
868
  case 'home':
@@ -636,13 +1035,13 @@ class FullscreenTui {
636
1035
  return;
637
1036
  case 'left':
638
1037
  if (this.cursor > 0) {
639
- this.cursor -= 1;
1038
+ this.cursor = this.graphemeLeftIndex(this.inputValue, this.cursor);
640
1039
  this.requestRender();
641
1040
  }
642
1041
  return;
643
1042
  case 'right':
644
1043
  if (this.cursor < this.inputValue.length) {
645
- this.cursor += 1;
1044
+ this.cursor = this.graphemeRightIndex(this.inputValue, this.cursor);
646
1045
  this.requestRender();
647
1046
  }
648
1047
  return;
@@ -869,6 +1268,8 @@ class FullscreenTui {
869
1268
  activity.pushLine(`task ${promptText}`);
870
1269
  this.busy = true;
871
1270
  this.currentAbortController = new AbortController();
1271
+ this.toolAbortController = new AbortController();
1272
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
872
1273
  this.startAnimation();
873
1274
  this.statusLine = 'Running conductor...';
874
1275
  this.requestRender();
@@ -880,8 +1281,10 @@ class FullscreenTui {
880
1281
  headless: true,
881
1282
  sandbox: this.options.sandbox,
882
1283
  signal: this.currentAbortController.signal,
1284
+ toolAbortSignalRef: this.toolAbortSignalRef,
883
1285
  onUserInput: (question) => this.requestClarification(question),
884
1286
  onEvent: (event) => {
1287
+ this.manageToolAbortSignal(event.event);
885
1288
  activity.handle(event);
886
1289
  this.updatePreviewFromEvent(event);
887
1290
  this.requestRender();
@@ -906,6 +1309,8 @@ class FullscreenTui {
906
1309
  finally {
907
1310
  this.busy = false;
908
1311
  this.currentAbortController = null;
1312
+ this.toolAbortController = null;
1313
+ this.toolAbortSignalRef.signal = undefined;
909
1314
  this.stopAnimation();
910
1315
  if (this.exitRequested) {
911
1316
  this.close();
@@ -927,6 +1332,8 @@ class FullscreenTui {
927
1332
  activity.pushLine(`skill /${skillName}${args.length > 0 ? ` ${args.join(' ')}` : ''}`);
928
1333
  this.busy = true;
929
1334
  this.currentAbortController = new AbortController();
1335
+ this.toolAbortController = new AbortController();
1336
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
930
1337
  this.startAnimation();
931
1338
  this.statusLine = `Running /${skillName}...`;
932
1339
  this.requestRender();
@@ -935,14 +1342,17 @@ class FullscreenTui {
935
1342
  sessionId: this.selectedSessionId || undefined,
936
1343
  sandbox: this.options.sandbox,
937
1344
  signal: this.currentAbortController.signal,
1345
+ toolAbortSignalRef: this.toolAbortSignalRef,
938
1346
  onUserInput: (question) => this.requestClarification(question),
939
1347
  onEvent: (event) => {
1348
+ this.manageToolAbortSignal(event.event);
940
1349
  activity.handle(event);
941
1350
  this.updatePreviewFromEvent(event);
942
1351
  this.requestRender();
943
1352
  },
944
1353
  });
945
1354
  activity.finish();
1355
+ await this.refreshSessions({ selectedSessionId: this.selectedSessionId });
946
1356
  await this.refreshCommands();
947
1357
  this.finishExecutionPreview({
948
1358
  persist: true,
@@ -961,6 +1371,8 @@ class FullscreenTui {
961
1371
  finally {
962
1372
  this.busy = false;
963
1373
  this.currentAbortController = null;
1374
+ this.toolAbortController = null;
1375
+ this.toolAbortSignalRef.signal = undefined;
964
1376
  this.stopAnimation();
965
1377
  if (this.exitRequested) {
966
1378
  this.close();
@@ -989,6 +1401,8 @@ class FullscreenTui {
989
1401
  activity.pushLine(`run ${commandText}`);
990
1402
  this.busy = true;
991
1403
  this.currentAbortController = new AbortController();
1404
+ this.toolAbortController = new AbortController();
1405
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
992
1406
  this.startAnimation();
993
1407
  this.statusLine = `Running ${commandText}...`;
994
1408
  this.requestRender();
@@ -997,14 +1411,17 @@ class FullscreenTui {
997
1411
  sessionId: this.selectedSessionId || undefined,
998
1412
  sandbox: this.options.sandbox,
999
1413
  signal: this.currentAbortController.signal,
1414
+ toolAbortSignalRef: this.toolAbortSignalRef,
1000
1415
  onUserInput: (question) => this.requestClarification(question),
1001
1416
  onEvent: (event) => {
1417
+ this.manageToolAbortSignal(event.event);
1002
1418
  activity.handle(event);
1003
1419
  this.updatePreviewFromEvent(event);
1004
1420
  this.requestRender();
1005
1421
  },
1006
1422
  });
1007
1423
  activity.finish();
1424
+ await this.refreshSessions({ selectedSessionId: this.selectedSessionId });
1008
1425
  await this.refreshCommands();
1009
1426
  this.finishExecutionPreview({
1010
1427
  persist: true,
@@ -1023,6 +1440,8 @@ class FullscreenTui {
1023
1440
  finally {
1024
1441
  this.busy = false;
1025
1442
  this.currentAbortController = null;
1443
+ this.toolAbortController = null;
1444
+ this.toolAbortSignalRef.signal = undefined;
1026
1445
  this.stopAnimation();
1027
1446
  if (this.exitRequested) {
1028
1447
  this.close();
@@ -1050,6 +1469,8 @@ class FullscreenTui {
1050
1469
  activity.pushLine(`skill ${commandText}`);
1051
1470
  this.busy = true;
1052
1471
  this.currentAbortController = new AbortController();
1472
+ this.toolAbortController = new AbortController();
1473
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
1053
1474
  this.startAnimation();
1054
1475
  this.statusLine = `Running /${commandName}...`;
1055
1476
  this.requestRender();
@@ -1059,12 +1480,14 @@ class FullscreenTui {
1059
1480
  signal: this.currentAbortController.signal,
1060
1481
  onUserInput: (question) => this.requestClarification(question),
1061
1482
  onEvent: (event) => {
1483
+ this.manageToolAbortSignal(event.event);
1062
1484
  activity.handle(event);
1063
1485
  this.updatePreviewFromEvent(event);
1064
1486
  this.requestRender();
1065
1487
  },
1066
1488
  });
1067
1489
  activity.finish();
1490
+ await this.refreshSessions({ selectedSessionId: this.selectedSessionId });
1068
1491
  await this.refreshCommands();
1069
1492
  this.finishExecutionPreview({
1070
1493
  persist: true,
@@ -1082,6 +1505,8 @@ class FullscreenTui {
1082
1505
  finally {
1083
1506
  this.busy = false;
1084
1507
  this.currentAbortController = null;
1508
+ this.toolAbortController = null;
1509
+ this.toolAbortSignalRef.signal = undefined;
1085
1510
  this.stopAnimation();
1086
1511
  if (this.exitRequested) {
1087
1512
  this.close();
@@ -1113,6 +1538,8 @@ class FullscreenTui {
1113
1538
  activity.pushLine(`shell ${persistOutput ? '!!' : '!'}${trimmedCommand}`);
1114
1539
  this.busy = true;
1115
1540
  this.currentAbortController = new AbortController();
1541
+ this.toolAbortController = this.currentAbortController;
1542
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
1116
1543
  this.startAnimation();
1117
1544
  this.statusLine = `Running ${persistOutput ? '!!' : '!'}${trimmedCommand}...`;
1118
1545
  this.requestRender();
@@ -1157,6 +1584,8 @@ class FullscreenTui {
1157
1584
  finally {
1158
1585
  this.busy = false;
1159
1586
  this.currentAbortController = null;
1587
+ this.toolAbortController = null;
1588
+ this.toolAbortSignalRef.signal = undefined;
1160
1589
  this.stopAnimation();
1161
1590
  if (this.exitRequested) {
1162
1591
  this.close();
@@ -2278,13 +2707,13 @@ class FullscreenTui {
2278
2707
  this.animationTimer = null;
2279
2708
  }
2280
2709
  scrollFocusedPane(direction, amount) {
2281
- const metrics = this.getPaneMetrics(this.focusedPane);
2282
- if (metrics.maxStart === 0) {
2283
- this.statusLine = `${paneDisplayName(this.focusedPane)} fits on screen.`;
2284
- this.requestRender();
2285
- return;
2286
- }
2287
2710
  if (this.focusedPane === 'sessions') {
2711
+ const metrics = this.getLastPaneMetrics(this.focusedPane) ?? this.getPaneMetrics(this.focusedPane);
2712
+ if (metrics.maxStart === 0) {
2713
+ this.statusLine = `${paneDisplayName(this.focusedPane)} fits on screen.`;
2714
+ this.requestRender();
2715
+ return;
2716
+ }
2288
2717
  const delta = direction === 'down' ? amount : -amount;
2289
2718
  this.paneScroll.sessions = clamp(this.paneScroll.sessions + delta, 0, metrics.maxStart);
2290
2719
  this.statusLine = `Sessions scroll ${this.paneScroll.sessions + 1}/${metrics.maxStart + 1}.`;
@@ -2292,8 +2721,19 @@ class FullscreenTui {
2292
2721
  return;
2293
2722
  }
2294
2723
  const pane = this.focusedPane;
2724
+ const metrics = this.getLastPaneMetrics(pane)
2725
+ ?? (this.paneScroll[pane] === 0 ? this.getRenderPaneMetrics(pane) : this.getPaneMetrics(pane));
2726
+ if (metrics.maxStart === 0) {
2727
+ this.paneScroll[pane] = 0;
2728
+ this.statusLine = `${paneDisplayName(pane)} fits on screen.`;
2729
+ this.requestRender();
2730
+ return;
2731
+ }
2295
2732
  const delta = direction === 'up' ? amount : -amount;
2296
- this.paneScroll[pane] = clamp(this.paneScroll[pane] + delta, 0, metrics.maxStart);
2733
+ const nextScroll = this.paneScroll[pane] + delta;
2734
+ this.paneScroll[pane] = metrics.maxStartIsExact
2735
+ ? clamp(nextScroll, 0, metrics.maxStart)
2736
+ : Math.max(0, nextScroll);
2297
2737
  this.statusLine = this.paneScroll[pane] === 0
2298
2738
  ? `${paneDisplayName(pane)} following newest content.`
2299
2739
  : `${paneDisplayName(pane)} scrolled ${this.paneScroll[pane]} line${this.paneScroll[pane] === 1 ? '' : 's'} above latest.`;
@@ -2338,6 +2778,36 @@ class FullscreenTui {
2338
2778
  currentProcessActivity() {
2339
2779
  return this.activityFor(this.selectedSessionId || 'global');
2340
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
+ }
2341
2811
  insertText(text) {
2342
2812
  const previousInput = this.inputValue;
2343
2813
  this.inputValue = `${this.inputValue.slice(0, this.cursor)}${text}${this.inputValue.slice(this.cursor)}`;
@@ -2349,8 +2819,9 @@ class FullscreenTui {
2349
2819
  return;
2350
2820
  }
2351
2821
  const previousInput = this.inputValue;
2352
- this.inputValue = `${this.inputValue.slice(0, this.cursor - 1)}${this.inputValue.slice(this.cursor)}`;
2353
- 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;
2354
2825
  this.refreshCommandBarStatus(previousInput);
2355
2826
  }
2356
2827
  deleteForward() {
@@ -2492,23 +2963,28 @@ class FullscreenTui {
2492
2963
  }
2493
2964
  lines.push(status);
2494
2965
  lines.push(inputLine);
2966
+ if (this.selection) {
2967
+ this.applySelectionHighlight(lines, widths, rightHeights, contentHeight);
2968
+ }
2495
2969
  this.paintFrame(lines, { columns, rows });
2496
2970
  if (workspaceView?.cursor && !this.pendingPrompt) {
2497
2971
  output.write(`\u001b[${workspaceView.cursor.row};${workspaceView.cursor.column}H`);
2498
2972
  return;
2499
2973
  }
2500
- 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);
2501
2976
  output.write(`\u001b[${rows};${cursorColumn}H`);
2502
2977
  }
2503
2978
  paintFrame(lines, size) {
2504
- 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);
2505
2981
  let buffer = '\u001b[0m';
2506
2982
  if (patch.fullRender) {
2507
- buffer += `\u001b[H\u001b[2J${lines.join('\n')}\u001b[?25h`;
2983
+ buffer += `\u001b[H${paddedLines.join('\n')}\u001b[?25h`;
2508
2984
  }
2509
2985
  else if (patch.changedRows.length > 0) {
2510
2986
  for (const change of patch.changedRows) {
2511
- buffer += `\u001b[${change.row};1H\u001b[2K${change.line}`;
2987
+ buffer += `\u001b[${change.row};1H${change.line}`;
2512
2988
  }
2513
2989
  buffer += '\u001b[?25h';
2514
2990
  }
@@ -2516,7 +2992,7 @@ class FullscreenTui {
2516
2992
  buffer += '\u001b[?25h';
2517
2993
  }
2518
2994
  output.write(buffer);
2519
- this.previousFrameLines = [...lines];
2995
+ this.previousFrameLines = [...paddedLines];
2520
2996
  this.previousFrameSize = size;
2521
2997
  }
2522
2998
  buildHeaderLine() {
@@ -2530,32 +3006,35 @@ class FullscreenTui {
2530
3006
  return `${sessionLabel} ${mode} ${skillLabel}`;
2531
3007
  }
2532
3008
  buildStatusLine() {
3009
+ const spinner = this.busy || this.currentPreview?.entries.some((entry) => entry.pending)
3010
+ ? ` ${currentSpinnerFrame()}`
3011
+ : '';
2533
3012
  if (this.uiMode === 'menu') {
2534
3013
  const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
2535
- 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.'}`;
2536
3015
  }
2537
3016
  if ((this.uiMode === 'picker' || this.uiMode === 'palette') && this.pickerState) {
2538
3017
  const selected = this.pickerState.filteredItems[this.pickerState.selectedIndex];
2539
- return `${this.pickerState.title} | ${selected?.description ?? this.pickerState.emptyText}`;
3018
+ return `${spinner} ${this.pickerState.title} | ${selected?.description ?? this.pickerState.emptyText}`;
2540
3019
  }
2541
3020
  if (this.uiMode === 'viewer' && this.viewerState) {
2542
- 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'}`;
2543
3022
  }
2544
3023
  if (this.uiMode === 'editor' && this.editorState) {
2545
3024
  const dirty = this.editorState.dirty ? 'modified' : 'saved';
2546
- 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}`;
2547
3026
  }
2548
3027
  const focus = paneDisplayName(this.focusedPane);
2549
3028
  if (!this.pendingPrompt && this.focusedPane !== 'sessions') {
2550
3029
  const follow = this.paneScroll[this.focusedPane] === 0 ? 'follow' : `scroll ${this.paneScroll[this.focusedPane]}`;
2551
- return `${this.statusLine} | Focus ${focus} | ${follow}`;
3030
+ return `${spinner} ${this.statusLine} | Focus ${focus} | ${follow}`;
2552
3031
  }
2553
3032
  if (this.pendingPrompt) {
2554
3033
  return this.pendingPrompt.kind === 'clarify'
2555
- ? `Clarify: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`
2556
- : `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}`;
2557
3036
  }
2558
- return `${this.statusLine} | Focus ${focus}`;
3037
+ return `${spinner} ${this.statusLine} | Focus ${focus}`;
2559
3038
  }
2560
3039
  buildInputPrefix() {
2561
3040
  if (this.uiMode === 'menu') {
@@ -2708,6 +3187,11 @@ class FullscreenTui {
2708
3187
  }
2709
3188
  buildPaneView(kind, title, width, height) {
2710
3189
  const metrics = this.getRenderPaneMetrics(kind, width, height);
3190
+ this.lastPaneMetrics[kind] = {
3191
+ maxStart: metrics.maxStart,
3192
+ viewportLines: metrics.viewportLines,
3193
+ maxStartIsExact: metrics.maxStartIsExact,
3194
+ };
2711
3195
  const paneTitle = this.buildPaneTitle(title, kind, metrics);
2712
3196
  return buildPane(paneTitle, metrics.lines.slice(metrics.start, metrics.start + metrics.viewportLines), width, height, kind, this.focusedPane === kind);
2713
3197
  }
@@ -2843,6 +3327,8 @@ class FullscreenTui {
2843
3327
  ['<- ->', 'Pane'],
2844
3328
  [this.focusedPane === 'sessions' ? 'Up/Down' : 'PgUp/PgDn', this.focusedPane === 'sessions' ? 'Select' : 'Scroll'],
2845
3329
  ['End', this.focusedPane === 'sessions' ? 'Bottom' : 'Follow'],
3330
+ ['Drag', 'Select'],
3331
+ ...(this.busy ? [['Ctrl+T', 'Cancel']] : []),
2846
3332
  ['/cancel', 'Stop'],
2847
3333
  ];
2848
3334
  }
@@ -2857,6 +3343,16 @@ class FullscreenTui {
2857
3343
  ? `${title}${this.focusedPane === kind ? ' [focus]' : ''} [follow]`
2858
3344
  : `${title}${this.focusedPane === kind ? ' [focus]' : ''} [-${this.paneScroll[kind]}]`;
2859
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
+ }
2860
3356
  getRenderPaneMetrics(kind, width = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).width, height = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).height) {
2861
3357
  if ((kind === 'process' || kind === 'messages') && this.paneScroll[kind] === 0) {
2862
3358
  const innerWidth = Math.max(1, width - 2);
@@ -2864,20 +3360,25 @@ class FullscreenTui {
2864
3360
  const rawBody = kind === 'messages'
2865
3361
  ? this.buildMessagesPaneBody({ recentEntryLimit: Math.max(8, viewportLines * 2) })
2866
3362
  : this.buildProcessPaneBody({ tailLineLimit: Math.max(16, viewportLines * 4) });
2867
- const tail = collectTailWrappedLines(rawBody, innerWidth, viewportLines);
3363
+ const tail = this.cachedCollectTailWrappedLines(`${kind}:tail`, rawBody, innerWidth, viewportLines);
2868
3364
  return {
2869
3365
  lines: tail.lines,
2870
3366
  start: 0,
2871
3367
  maxStart: tail.truncated ? 1 : 0,
2872
3368
  viewportLines,
3369
+ maxStartIsExact: !tail.truncated,
2873
3370
  };
2874
3371
  }
2875
3372
  return this.getPaneMetrics(kind, width, height);
2876
3373
  }
3374
+ getLastPaneMetrics(kind) {
3375
+ const metrics = this.lastPaneMetrics[kind];
3376
+ return metrics.viewportLines > 0 ? metrics : null;
3377
+ }
2877
3378
  getPaneMetrics(kind, width = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).width, height = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).height) {
2878
3379
  const innerWidth = Math.max(1, width - 2);
2879
3380
  const rawBody = this.getPaneBody(kind);
2880
- const lines = wrapBodyLines(rawBody, innerWidth);
3381
+ const lines = this.cachedWrapBodyLines(kind, rawBody, innerWidth);
2881
3382
  const viewportLines = Math.max(0, height - 2);
2882
3383
  const maxStart = Math.max(0, lines.length - viewportLines);
2883
3384
  if (kind === 'sessions') {
@@ -2887,6 +3388,7 @@ class FullscreenTui {
2887
3388
  start: this.paneScroll.sessions,
2888
3389
  maxStart,
2889
3390
  viewportLines,
3391
+ maxStartIsExact: true,
2890
3392
  };
2891
3393
  }
2892
3394
  this.paneScroll[kind] = clamp(this.paneScroll[kind], 0, maxStart);
@@ -2895,8 +3397,19 @@ class FullscreenTui {
2895
3397
  start: Math.max(0, maxStart - this.paneScroll[kind]),
2896
3398
  maxStart,
2897
3399
  viewportLines,
3400
+ maxStartIsExact: true,
2898
3401
  };
2899
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
+ }
2900
3413
  getPaneBody(kind) {
2901
3414
  switch (kind) {
2902
3415
  case 'sessions':
@@ -2904,7 +3417,7 @@ class FullscreenTui {
2904
3417
  case 'messages':
2905
3418
  return this.buildMessagesPaneBody();
2906
3419
  case 'process':
2907
- return this.buildProcessPaneBody();
3420
+ return this.buildProcessPaneBody({ tailLineLimit: MAX_ACTIVITY_LINES });
2908
3421
  case 'context':
2909
3422
  return this.buildContextPaneBody();
2910
3423
  }
@@ -2922,8 +3435,7 @@ class FullscreenTui {
2922
3435
  }
2923
3436
  }
2924
3437
  formatMessageHeader(entry) {
2925
- const spinner = entry.pending ? ` ${currentSpinnerFrame()}` : '';
2926
- return formatDisplayMessageHeader(entry, spinner);
3438
+ return formatDisplayMessageHeader(entry, '');
2927
3439
  }
2928
3440
  close() {
2929
3441
  if (!this.closeResolver) {
@@ -2933,6 +3445,21 @@ class FullscreenTui {
2933
3445
  this.closeResolver = null;
2934
3446
  resolve();
2935
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
+ }
2936
3463
  isEditingInput() {
2937
3464
  return this.uiMode !== 'editor' && this.inputValue.length > 0;
2938
3465
  }
@@ -3225,6 +3752,7 @@ export async function runFileCommand(workspaceRoot, fileSpec, args, options = {}
3225
3752
  }, args, options);
3226
3753
  }
3227
3754
  async function executeRunnableCommand(workspaceRoot, target, args, options = {}) {
3755
+ const usageByModel = {};
3228
3756
  const executionLog = options.sessionId
3229
3757
  ? createSessionExecutionLogWriter({
3230
3758
  workspaceRoot,
@@ -3236,6 +3764,9 @@ async function executeRunnableCommand(workspaceRoot, target, args, options = {})
3236
3764
  })
3237
3765
  : null;
3238
3766
  const emitLogEvent = (event) => {
3767
+ if (event.event.type === 'usage') {
3768
+ recordTokenUsage(usageByModel, event.modelId, event.event.usage);
3769
+ }
3239
3770
  executionLog?.recordEvent(event);
3240
3771
  options.onEvent?.(event);
3241
3772
  };
@@ -3246,12 +3777,14 @@ async function executeRunnableCommand(workspaceRoot, target, args, options = {})
3246
3777
  stream: true,
3247
3778
  sandbox: options.sandbox,
3248
3779
  signal: options.signal,
3780
+ toolAbortSignalRef: options.toolAbortSignalRef,
3249
3781
  onUserInput: options.onUserInput,
3250
3782
  onEvent: (event) => emitLogEvent({ scope: 'child', childSlug: target.childSlug, event }),
3251
3783
  onChildEvent: (childSlug, event) => emitLogEvent({ scope: 'child', childSlug, event }),
3252
3784
  });
3253
3785
  if (options.sessionId) {
3254
3786
  await appendConductorSessionMessages(workspaceRoot, options.sessionId, buildPersistedRunnableTranscript(target.inputText, result));
3787
+ await mergeSessionUsage(workspaceRoot, options.sessionId, usageByModel);
3255
3788
  }
3256
3789
  await executionLog?.finish({
3257
3790
  status: result.error ? 'error' : 'success',
@@ -3267,6 +3800,7 @@ async function executeRunnableCommand(workspaceRoot, target, args, options = {})
3267
3800
  output: [],
3268
3801
  error: error.message,
3269
3802
  }));
3803
+ await mergeSessionUsage(workspaceRoot, options.sessionId, usageByModel);
3270
3804
  }
3271
3805
  await executionLog?.finish({
3272
3806
  status: 'error',
@@ -3494,6 +4028,15 @@ export function previewMessageFromEvent(logEvent) {
3494
4028
  return null;
3495
4029
  }
3496
4030
  return previewQuestionMessage(logEvent.event.prompt, logEvent.scope === 'child' ? logEvent.childSlug : undefined);
4031
+ case 'memory_compaction':
4032
+ return {
4033
+ role: 'system',
4034
+ kind: 'output',
4035
+ tag: logEvent.scope === 'child' ? logEvent.childSlug : undefined,
4036
+ content: logEvent.event.content
4037
+ ? `Memory compaction: ${logEvent.event.content}`
4038
+ : 'Memory compaction triggered.',
4039
+ };
3497
4040
  default:
3498
4041
  return null;
3499
4042
  }
@@ -4053,6 +4596,37 @@ function expandTabs(text, tabWidth) {
4053
4596
  export function measureDisplayWidth(text) {
4054
4597
  return splitGraphemes(text).reduce((total, grapheme) => total + graphemeDisplayWidth(grapheme), 0);
4055
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
+ }
4056
4630
  export function padRight(text, width) {
4057
4631
  const visibleWidth = measureDisplayWidth(text);
4058
4632
  return visibleWidth >= width ? text : `${text}${' '.repeat(width - visibleWidth)}`;