deepclause-sdk 0.0.74 → 0.0.76

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'];
@@ -63,7 +64,7 @@ export class LiveExecutionPrinter {
63
64
  }
64
65
  handle(logEvent) {
65
66
  const { event } = logEvent;
66
- if (event.type === 'usage' || event.type === 'finished') {
67
+ if (event.type === 'usage' || event.type === 'finished' || event.type === 'task_activity') {
67
68
  return;
68
69
  }
69
70
  if (event.type === 'stream') {
@@ -147,7 +148,7 @@ export class ActivityBuffer {
147
148
  activeStreamLine = '';
148
149
  handle(logEvent) {
149
150
  const { event } = logEvent;
150
- if (event.type === 'usage' || event.type === 'finished') {
151
+ if (event.type === 'usage' || event.type === 'finished' || event.type === 'task_activity') {
151
152
  return;
152
153
  }
153
154
  if (event.type === 'stream') {
@@ -299,12 +300,141 @@ export class ActivityBuffer {
299
300
  return lines;
300
301
  }
301
302
  }
303
+ function summarizeStepDescription(raw) {
304
+ const firstLine = raw.split(/\r?\n/)[0] ?? '';
305
+ const trimmed = firstLine.replace(/\s+/g, ' ').trim();
306
+ return trimmed.length > 80 ? trimmed.slice(0, 77) + '\u2026' : trimmed;
307
+ }
308
+ export class TaskTracker {
309
+ entries = [];
310
+ activeStack = [];
311
+ maxEntries = 200;
312
+ handle(logEvent) {
313
+ const { event } = logEvent;
314
+ if (event.type !== 'task_activity')
315
+ return;
316
+ if (event.taskState === 'started' && event.taskId) {
317
+ const depth = this.activeStack.length;
318
+ this.activeStack.push(event.taskId);
319
+ this.entries.push({
320
+ id: event.taskId,
321
+ description: summarizeStepDescription(event.taskDescription ?? ''),
322
+ state: 'started',
323
+ depth,
324
+ startedAt: Date.now(),
325
+ });
326
+ this.trim();
327
+ }
328
+ else if ((event.taskState === 'completed' || event.taskState === 'failed') && event.taskId) {
329
+ const idx = this.entries.findIndex(e => e.id === event.taskId);
330
+ if (idx >= 0) {
331
+ this.entries[idx].state = event.taskState;
332
+ this.entries[idx].completedAt = Date.now();
333
+ }
334
+ const stackIdx = this.activeStack.lastIndexOf(event.taskId);
335
+ if (stackIdx >= 0) {
336
+ this.activeStack.splice(stackIdx, 1);
337
+ }
338
+ }
339
+ }
340
+ buildPaneBody(_width) {
341
+ if (this.entries.length === 0) {
342
+ return ['No steps yet.', 'Step activity renders here.'];
343
+ }
344
+ const lines = [];
345
+ for (const entry of this.entries) {
346
+ const indent = ' '.repeat(entry.depth);
347
+ let marker;
348
+ if (entry.state === 'started') {
349
+ marker = '>';
350
+ }
351
+ else if (entry.state === 'completed') {
352
+ marker = '\u2713';
353
+ }
354
+ else {
355
+ marker = '\u2717';
356
+ }
357
+ lines.push(`${indent}${marker} ${entry.description}`);
358
+ }
359
+ return lines;
360
+ }
361
+ clear() {
362
+ this.entries.length = 0;
363
+ this.activeStack.length = 0;
364
+ }
365
+ trim() {
366
+ if (this.entries.length > this.maxEntries) {
367
+ this.entries.splice(0, this.entries.length - this.maxEntries);
368
+ }
369
+ }
370
+ }
302
371
  export function releaseTuiInputStream(stream) {
303
372
  if (stream.isTTY && typeof stream.setRawMode === 'function') {
304
373
  stream.setRawMode(false);
305
374
  }
306
375
  stream.pause();
307
376
  }
377
+ class MouseParser {
378
+ buffer = '';
379
+ feed(data, onEvent) {
380
+ this.buffer += data.toString('utf8');
381
+ while (this.buffer.length > 0) {
382
+ const mouseStart = this.buffer.indexOf('\u001b[<');
383
+ if (mouseStart === -1) {
384
+ if (this.buffer.length > 0) {
385
+ this.buffer = '';
386
+ }
387
+ break;
388
+ }
389
+ this.buffer = this.buffer.slice(mouseStart);
390
+ const parsed = this.tryParseMouse();
391
+ if (parsed === null) {
392
+ if (this.buffer.length > 256) {
393
+ this.buffer = '';
394
+ }
395
+ break;
396
+ }
397
+ onEvent(parsed);
398
+ }
399
+ }
400
+ tryParseMouse() {
401
+ const mIdx = this.buffer.indexOf('M', 3);
402
+ const mIdxSgr = this.buffer.indexOf('m', 3);
403
+ const end = mIdx !== -1 && (mIdxSgr === -1 || mIdx < mIdxSgr) ? mIdx : mIdxSgr;
404
+ if (end === -1)
405
+ return null;
406
+ const payload = this.buffer.slice(3, end);
407
+ const isRelease = this.buffer[end] === 'm';
408
+ this.buffer = this.buffer.slice(end + 1);
409
+ const semicolon = payload.indexOf(';');
410
+ if (semicolon === -1)
411
+ return null;
412
+ const secondSemicolon = payload.indexOf(';', semicolon + 1);
413
+ if (secondSemicolon === -1)
414
+ return null;
415
+ const cb = Number.parseInt(payload.slice(0, semicolon), 10);
416
+ const col = Number.parseInt(payload.slice(semicolon + 1, secondSemicolon), 10);
417
+ const row = Number.parseInt(payload.slice(secondSemicolon + 1), 10);
418
+ if (Number.isNaN(cb) || Number.isNaN(col) || Number.isNaN(row))
419
+ return null;
420
+ const shift = (cb & 4) !== 0;
421
+ const meta = (cb & 8) !== 0;
422
+ const ctrl = (cb & 16) !== 0;
423
+ const buttonCode = cb & 3;
424
+ if (isRelease) {
425
+ return { button: 'release', row, column: col, shift, ctrl, meta };
426
+ }
427
+ if ((cb & 64) !== 0) {
428
+ return { button: buttonCode === 0 ? 'wheel-up' : 'wheel-down', row, column: col, shift, ctrl, meta };
429
+ }
430
+ const isMotion = (cb & 32) !== 0;
431
+ if (isMotion) {
432
+ return { button: 'motion', row, column: col, shift, ctrl, meta };
433
+ }
434
+ const buttonMap = { 0: 'left', 1: 'middle', 2: 'right' };
435
+ return { button: buttonMap[buttonCode] ?? 'left', row, column: col, shift, ctrl, meta };
436
+ }
437
+ }
308
438
  class FullscreenTui {
309
439
  workspaceRoot;
310
440
  options;
@@ -314,6 +444,7 @@ class FullscreenTui {
314
444
  commands = [];
315
445
  activityBySessionId = new Map();
316
446
  ephemeralMessagesBySessionId = new Map();
447
+ taskTrackerBySessionId = new Map();
317
448
  focusedPane = 'messages';
318
449
  uiMode = 'command';
319
450
  menuReturnMode = 'command';
@@ -327,16 +458,25 @@ class FullscreenTui {
327
458
  sessions: 0,
328
459
  messages: 0,
329
460
  process: 0,
461
+ tasks: 0,
330
462
  context: 0,
331
463
  };
332
464
  lastPaneMetrics = {
333
465
  sessions: { maxStart: 0, viewportLines: 0, maxStartIsExact: true },
334
466
  messages: { maxStart: 0, viewportLines: 0, maxStartIsExact: true },
335
467
  process: { maxStart: 0, viewportLines: 0, maxStartIsExact: true },
468
+ tasks: { maxStart: 0, viewportLines: 0, maxStartIsExact: true },
336
469
  context: { maxStart: 0, viewportLines: 0, maxStartIsExact: true },
337
470
  };
338
471
  currentPreview = null;
339
472
  currentAbortController = null;
473
+ toolAbortController = null;
474
+ toolAbortSignalRef = {};
475
+ mouseParser = null;
476
+ mouseDataHandler = null;
477
+ mouseEscapeState = 'idle';
478
+ selection = null;
479
+ isDraggingSelection = false;
340
480
  inputValue = '';
341
481
  cursor = 0;
342
482
  busy = false;
@@ -356,6 +496,8 @@ class FullscreenTui {
356
496
  lastRenderAt = 0;
357
497
  previousFrameLines = null;
358
498
  previousFrameSize = null;
499
+ resizeHandler = null;
500
+ wrapCache = new Map();
359
501
  constructor(workspaceRoot, options = {}) {
360
502
  this.workspaceRoot = workspaceRoot;
361
503
  this.options = options;
@@ -366,12 +508,22 @@ class FullscreenTui {
366
508
  await this.refreshCommands();
367
509
  await this.refreshSessions({ createIfMissing: true });
368
510
  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
511
  if (input.isTTY) {
371
512
  input.setRawMode(true);
372
513
  }
514
+ emitKeypressEvents(input);
373
515
  input.on('keypress', this.onKeypress);
374
- output.write('\u001b[?1049h\u001b[2J\u001b[H');
516
+ this.mouseParser = new MouseParser();
517
+ this.mouseDataHandler = (chunk) => {
518
+ this.mouseParser.feed(chunk, (event) => this.handleMouseEvent(event));
519
+ };
520
+ input.prependListener('data', this.mouseDataHandler);
521
+ this.resizeHandler = () => {
522
+ this.wrapCache.clear();
523
+ this.requestRender();
524
+ };
525
+ output.on('resize', this.resizeHandler);
526
+ output.write('\u001b[?1049h\u001b[?1002h\u001b[?1006h\u001b[2J\u001b[H');
375
527
  this.requestRender();
376
528
  await new Promise((resolve) => {
377
529
  this.closeResolver = resolve;
@@ -386,16 +538,327 @@ class FullscreenTui {
386
538
  clearInterval(this.animationTimer);
387
539
  this.animationTimer = null;
388
540
  }
389
- input.off('keypress', this.onKeypress);
541
+ if (this.mouseDataHandler) {
542
+ input.removeListener('data', this.mouseDataHandler);
543
+ this.mouseDataHandler = null;
544
+ }
545
+ this.mouseParser = null;
546
+ input.removeListener('keypress', this.onKeypress);
547
+ if (this.resizeHandler) {
548
+ output.off('resize', this.resizeHandler);
549
+ this.resizeHandler = null;
550
+ }
390
551
  releaseTuiInputStream(input);
391
552
  this.previousFrameLines = null;
392
553
  this.previousFrameSize = null;
393
- output.write('\u001b[?25h\u001b[?1049l');
554
+ output.write('\u001b[?25h\u001b[?1006l\u001b[?1002l\u001b[?1049l');
394
555
  }
395
556
  }
396
557
  onKeypress = (text, key) => {
558
+ if (key.sequence === '\x1b[<') {
559
+ this.mouseEscapeState = 'consuming';
560
+ return;
561
+ }
562
+ if (this.mouseEscapeState === 'consuming') {
563
+ if (text === 'M' || text === 'm') {
564
+ this.mouseEscapeState = 'idle';
565
+ }
566
+ return;
567
+ }
397
568
  void this.handleKeypress(text, key);
398
569
  };
570
+ handleMouseEvent(event) {
571
+ if (event.button === 'wheel-up' || event.button === 'wheel-down') {
572
+ if (event.shift)
573
+ return;
574
+ const direction = event.button === 'wheel-up' ? 'up' : 'down';
575
+ const paneKind = this.paneKindAtPosition(event.row, event.column);
576
+ if (paneKind) {
577
+ this.focusedPane = paneKind;
578
+ }
579
+ this.scrollFocusedPane(direction, 3);
580
+ return;
581
+ }
582
+ if (event.button === 'left' && !event.shift && !event.ctrl && !event.meta) {
583
+ const menuIndex = this.menuIndexAtColumn(event.column);
584
+ if (menuIndex !== null && event.row === 1) {
585
+ this.clearSelection();
586
+ if (this.uiMode !== 'menu') {
587
+ this.uiMode = 'menu';
588
+ }
589
+ this.menuState.activeIndex = menuIndex;
590
+ this.menuState.selectedIndex = 0;
591
+ this.menuState.typeahead = '';
592
+ this.requestRender();
593
+ return;
594
+ }
595
+ if (this.uiMode === 'menu' && event.row >= 2) {
596
+ const menuItemIndex = this.menuItemAtRow(event.row);
597
+ if (menuItemIndex !== null) {
598
+ this.menuState.selectedIndex = menuItemIndex;
599
+ this.requestRender();
600
+ void this.activateCurrentMenuItem();
601
+ return;
602
+ }
603
+ }
604
+ const paneKind = this.paneKindAtPosition(event.row, event.column);
605
+ if (!paneKind) {
606
+ this.clearSelection();
607
+ this.requestRender();
608
+ return;
609
+ }
610
+ const panePos = this.paneContentPosition(paneKind, event.row, event.column);
611
+ if (!panePos) {
612
+ this.clearSelection();
613
+ this.focusedPane = paneKind;
614
+ this.requestRender();
615
+ return;
616
+ }
617
+ this.selection = { pane: paneKind, startRow: panePos.bodyRow, startCol: panePos.innerCol, endRow: panePos.bodyRow, endCol: panePos.innerCol };
618
+ this.isDraggingSelection = true;
619
+ this.focusedPane = paneKind;
620
+ this.requestRender();
621
+ return;
622
+ }
623
+ if (event.button === 'motion' && !event.shift && this.isDraggingSelection && this.selection) {
624
+ const paneKind = this.paneKindAtPosition(event.row, event.column);
625
+ if (paneKind && paneKind === this.selection.pane) {
626
+ const panePos = this.paneContentPosition(paneKind, event.row, event.column);
627
+ if (panePos) {
628
+ this.selection.endRow = panePos.bodyRow;
629
+ this.selection.endCol = panePos.innerCol;
630
+ this.requestRender();
631
+ }
632
+ }
633
+ return;
634
+ }
635
+ if (event.button === 'release' && !event.shift && this.isDraggingSelection) {
636
+ this.isDraggingSelection = false;
637
+ if (this.selection) {
638
+ const text = this.extractSelectionText();
639
+ if (text) {
640
+ this.copyToClipboard(text);
641
+ this.statusLine = 'Copied selection to clipboard.';
642
+ }
643
+ }
644
+ this.clearSelection();
645
+ this.requestRender();
646
+ return;
647
+ }
648
+ if (event.button === 'release') {
649
+ this.isDraggingSelection = false;
650
+ return;
651
+ }
652
+ }
653
+ clearSelection() {
654
+ if (this.selection) {
655
+ this.selection = null;
656
+ }
657
+ this.isDraggingSelection = false;
658
+ }
659
+ paneContentPosition(paneKind, screenRow, screenCol) {
660
+ const columns = output.columns ?? 120;
661
+ const rows = output.rows ?? 40;
662
+ if (screenRow < 2 || screenRow >= rows - 1)
663
+ return null;
664
+ if (this.uiMode !== 'command')
665
+ return null;
666
+ const contentHeight = rows - 3;
667
+ const widths = computePaneWidths(columns);
668
+ const rightHeights = computeRightPaneHeights(contentHeight);
669
+ const contentRow = screenRow - 2;
670
+ let paneLeft;
671
+ let paneWidth;
672
+ let paneTop;
673
+ switch (paneKind) {
674
+ case 'sessions':
675
+ paneLeft = 0;
676
+ paneWidth = widths.left;
677
+ paneTop = 0;
678
+ break;
679
+ case 'messages':
680
+ paneLeft = widths.left + 1;
681
+ paneWidth = widths.center;
682
+ paneTop = 0;
683
+ break;
684
+ case 'process':
685
+ paneLeft = widths.left + 1 + widths.center + 1;
686
+ paneWidth = widths.right;
687
+ paneTop = 0;
688
+ break;
689
+ case 'tasks':
690
+ paneLeft = widths.left + 1 + widths.center + 1;
691
+ paneWidth = widths.right;
692
+ paneTop = rightHeights.process;
693
+ break;
694
+ case 'context':
695
+ paneLeft = widths.left + 1 + widths.center + 1;
696
+ paneWidth = widths.right;
697
+ paneTop = rightHeights.process + rightHeights.tasks;
698
+ break;
699
+ }
700
+ if (screenCol < paneLeft + 2 || screenCol > paneLeft + paneWidth - 1)
701
+ return null;
702
+ const bodyRow = contentRow - paneTop - 1;
703
+ if (bodyRow < 0)
704
+ return null;
705
+ const innerCol = screenCol - paneLeft - 2;
706
+ return { bodyRow, innerCol };
707
+ }
708
+ extractSelectionText() {
709
+ if (!this.selection)
710
+ return '';
711
+ const sel = this.selection;
712
+ const paneBody = this.getPaneBody(sel.pane);
713
+ const columns = output.columns ?? 120;
714
+ const widths = computePaneWidths(columns);
715
+ const width = sel.pane === 'sessions' ? widths.left : sel.pane === 'messages' ? widths.center : widths.right;
716
+ const innerWidth = Math.max(1, width - 2);
717
+ const wrappedLines = wrapBodyLines(paneBody, innerWidth);
718
+ const startRow = Math.min(sel.startRow, sel.endRow);
719
+ const endRow = Math.max(sel.startRow, sel.endRow);
720
+ const startCol = sel.startRow <= sel.endRow ? sel.startCol : sel.endCol;
721
+ const endCol = sel.startRow <= sel.endRow ? sel.endCol : sel.startCol;
722
+ if (startRow === endRow) {
723
+ const line = wrappedLines[startRow] ?? '';
724
+ return this.stripAnsi(line.slice(startCol, endCol + 1));
725
+ }
726
+ const parts = [];
727
+ const firstLine = wrappedLines[startRow] ?? '';
728
+ parts.push(this.stripAnsi(firstLine.slice(startCol)));
729
+ for (let row = startRow + 1; row < endRow; row++) {
730
+ parts.push(this.stripAnsi(wrappedLines[row] ?? ''));
731
+ }
732
+ const lastLine = wrappedLines[endRow] ?? '';
733
+ parts.push(this.stripAnsi(lastLine.slice(0, endCol + 1)));
734
+ return parts.join('\n');
735
+ }
736
+ stripAnsi(text) {
737
+ return text.replace(/\u001b\[[0-9;]*m/g, '');
738
+ }
739
+ copyToClipboard(text) {
740
+ const osc52 = `\u001b]52;c;${Buffer.from(text).toString('base64')}\u0007`;
741
+ output.write(osc52);
742
+ }
743
+ applySelectionHighlight(lines, widths, rightHeights, _contentHeight) {
744
+ const sel = this.selection;
745
+ if (!sel)
746
+ return;
747
+ const startRow = Math.min(sel.startRow, sel.endRow);
748
+ const endRow = Math.max(sel.startRow, sel.endRow);
749
+ const startCol = sel.startRow <= sel.endRow ? sel.startCol : sel.endCol;
750
+ const endCol = sel.startRow <= sel.endRow ? sel.endCol : sel.startCol;
751
+ let paneLeft;
752
+ let paneWidth;
753
+ let paneTop;
754
+ switch (sel.pane) {
755
+ case 'sessions':
756
+ paneLeft = 0;
757
+ paneWidth = widths.left;
758
+ paneTop = 0;
759
+ break;
760
+ case 'messages':
761
+ paneLeft = widths.left + 1;
762
+ paneWidth = widths.center;
763
+ paneTop = 0;
764
+ break;
765
+ case 'process':
766
+ paneLeft = widths.left + 1 + widths.center + 1;
767
+ paneWidth = widths.right;
768
+ paneTop = 0;
769
+ break;
770
+ case 'tasks':
771
+ paneLeft = widths.left + 1 + widths.center + 1;
772
+ paneWidth = widths.right;
773
+ paneTop = rightHeights.process;
774
+ break;
775
+ case 'context':
776
+ paneLeft = widths.left + 1 + widths.center + 1;
777
+ paneWidth = widths.right;
778
+ paneTop = rightHeights.process + rightHeights.tasks;
779
+ break;
780
+ }
781
+ const innerDisplayStart = paneLeft + 1;
782
+ const innerDisplayEnd = paneLeft + paneWidth - 2;
783
+ for (let bodyRow = startRow; bodyRow <= endRow; bodyRow++) {
784
+ const screenLineIdx = 1 + paneTop + 1 + bodyRow;
785
+ if (screenLineIdx >= lines.length)
786
+ break;
787
+ const line = lines[screenLineIdx];
788
+ const rowStartCol = bodyRow === startRow ? innerDisplayStart + startCol : innerDisplayStart;
789
+ const rowEndCol = bodyRow === endRow ? innerDisplayStart + endCol + 1 : innerDisplayEnd + 1;
790
+ const clampedStartCol = Math.max(innerDisplayStart, rowStartCol);
791
+ const clampedEndCol = Math.min(innerDisplayEnd + 1, rowEndCol);
792
+ if (clampedStartCol >= clampedEndCol)
793
+ continue;
794
+ const strStart = displayColumnToStringIndex(line, clampedStartCol);
795
+ const strEnd = displayColumnToStringIndex(line, clampedEndCol);
796
+ const before = line.slice(0, strStart);
797
+ const segment = line.slice(strStart, strEnd);
798
+ const after = line.slice(strEnd);
799
+ const highlighted = this.invertAnsiColors(segment);
800
+ lines[screenLineIdx] = `${before}${highlighted}${after}`;
801
+ }
802
+ }
803
+ invertAnsiColors(text) {
804
+ return `\u001b[7m${text}\u001b[27m`;
805
+ }
806
+ paneKindAtPosition(row, column) {
807
+ const columns = output.columns ?? 120;
808
+ const rows = output.rows ?? 40;
809
+ if (row < 2 || row >= rows - 1)
810
+ return null;
811
+ if (this.uiMode !== 'command')
812
+ return null;
813
+ const widths = computePaneWidths(columns);
814
+ const rightHeights = computeRightPaneHeights(rows - 3);
815
+ const contentRow = row - 1;
816
+ if (column <= widths.left)
817
+ return 'sessions';
818
+ if (column <= widths.left + 1 + widths.center)
819
+ return 'messages';
820
+ if (contentRow <= rightHeights.process)
821
+ return 'process';
822
+ if (contentRow <= rightHeights.process + rightHeights.tasks)
823
+ return 'tasks';
824
+ return 'context';
825
+ }
826
+ menuIndexAtColumn(column) {
827
+ const menuLabels = ['Session', 'Skills', 'Files', 'Run', 'View', 'Help'];
828
+ let pos = 1 + measureDisplayWidth(' DeepClause ');
829
+ for (let i = 0; i < menuLabels.length; i++) {
830
+ const labelWidth = measureDisplayWidth(` [${i + 1}] ${menuLabels[i]} `);
831
+ if (column >= pos && column < pos + labelWidth)
832
+ return i;
833
+ pos += labelWidth;
834
+ }
835
+ return null;
836
+ }
837
+ menuItemAtRow(row) {
838
+ if (this.uiMode !== 'menu')
839
+ return null;
840
+ const contentRow = row - 2;
841
+ if (contentRow < 2)
842
+ return null;
843
+ const itemRow = contentRow - 2;
844
+ if (itemRow < 0)
845
+ return null;
846
+ const linesPerItem = 3;
847
+ const itemIndex = Math.floor(itemRow / linesPerItem);
848
+ const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
849
+ if (!menu || itemIndex >= menu.items.length || itemIndex < 0)
850
+ return null;
851
+ return itemIndex;
852
+ }
853
+ async activateCurrentMenuItem() {
854
+ const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
855
+ if (!menu)
856
+ return;
857
+ const item = menu.items[this.menuState.selectedIndex];
858
+ if (!item || item.enabled === false)
859
+ return;
860
+ await this.runMenuAction(item.id);
861
+ }
399
862
  async handleKeypress(text, key) {
400
863
  if ((key.ctrl && key.name === 'g') || key.name === 'f10') {
401
864
  this.toggleMenuMode();
@@ -428,6 +891,19 @@ class FullscreenTui {
428
891
  this.close();
429
892
  return;
430
893
  }
894
+ if (key.ctrl && key.name === 't' && this.busy) {
895
+ if (this.toolAbortController && !this.toolAbortController.signal.aborted) {
896
+ this.toolAbortController.abort('Aborted by user');
897
+ this.toolAbortController = null;
898
+ this.toolAbortSignalRef.signal = undefined;
899
+ this.statusLine = 'Tool execution cancelled.';
900
+ }
901
+ else {
902
+ this.statusLine = 'No active tool to cancel.';
903
+ }
904
+ this.requestRender();
905
+ return;
906
+ }
431
907
  if (this.uiMode === 'menu') {
432
908
  await this.handleMenuKeypress(text, key);
433
909
  return;
@@ -460,7 +936,7 @@ class FullscreenTui {
460
936
  return;
461
937
  }
462
938
  if (this.cursor > 0) {
463
- this.cursor -= 1;
939
+ this.cursor = this.graphemeLeftIndex(this.inputValue, this.cursor);
464
940
  }
465
941
  break;
466
942
  case 'right':
@@ -469,7 +945,7 @@ class FullscreenTui {
469
945
  return;
470
946
  }
471
947
  if (this.cursor < this.inputValue.length) {
472
- this.cursor += 1;
948
+ this.cursor = this.graphemeRightIndex(this.inputValue, this.cursor);
473
949
  }
474
950
  break;
475
951
  case 'home':
@@ -642,13 +1118,13 @@ class FullscreenTui {
642
1118
  return;
643
1119
  case 'left':
644
1120
  if (this.cursor > 0) {
645
- this.cursor -= 1;
1121
+ this.cursor = this.graphemeLeftIndex(this.inputValue, this.cursor);
646
1122
  this.requestRender();
647
1123
  }
648
1124
  return;
649
1125
  case 'right':
650
1126
  if (this.cursor < this.inputValue.length) {
651
- this.cursor += 1;
1127
+ this.cursor = this.graphemeRightIndex(this.inputValue, this.cursor);
652
1128
  this.requestRender();
653
1129
  }
654
1130
  return;
@@ -875,6 +1351,8 @@ class FullscreenTui {
875
1351
  activity.pushLine(`task ${promptText}`);
876
1352
  this.busy = true;
877
1353
  this.currentAbortController = new AbortController();
1354
+ this.toolAbortController = new AbortController();
1355
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
878
1356
  this.startAnimation();
879
1357
  this.statusLine = 'Running conductor...';
880
1358
  this.requestRender();
@@ -886,9 +1364,12 @@ class FullscreenTui {
886
1364
  headless: true,
887
1365
  sandbox: this.options.sandbox,
888
1366
  signal: this.currentAbortController.signal,
1367
+ toolAbortSignalRef: this.toolAbortSignalRef,
889
1368
  onUserInput: (question) => this.requestClarification(question),
890
1369
  onEvent: (event) => {
1370
+ this.manageToolAbortSignal(event.event);
891
1371
  activity.handle(event);
1372
+ this.taskTrackerFor(sessionId).handle(event);
892
1373
  this.updatePreviewFromEvent(event);
893
1374
  this.requestRender();
894
1375
  },
@@ -912,6 +1393,8 @@ class FullscreenTui {
912
1393
  finally {
913
1394
  this.busy = false;
914
1395
  this.currentAbortController = null;
1396
+ this.toolAbortController = null;
1397
+ this.toolAbortSignalRef.signal = undefined;
915
1398
  this.stopAnimation();
916
1399
  if (this.exitRequested) {
917
1400
  this.close();
@@ -933,6 +1416,8 @@ class FullscreenTui {
933
1416
  activity.pushLine(`skill /${skillName}${args.length > 0 ? ` ${args.join(' ')}` : ''}`);
934
1417
  this.busy = true;
935
1418
  this.currentAbortController = new AbortController();
1419
+ this.toolAbortController = new AbortController();
1420
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
936
1421
  this.startAnimation();
937
1422
  this.statusLine = `Running /${skillName}...`;
938
1423
  this.requestRender();
@@ -941,14 +1426,18 @@ class FullscreenTui {
941
1426
  sessionId: this.selectedSessionId || undefined,
942
1427
  sandbox: this.options.sandbox,
943
1428
  signal: this.currentAbortController.signal,
1429
+ toolAbortSignalRef: this.toolAbortSignalRef,
944
1430
  onUserInput: (question) => this.requestClarification(question),
945
1431
  onEvent: (event) => {
1432
+ this.manageToolAbortSignal(event.event);
946
1433
  activity.handle(event);
1434
+ this.taskTrackerFor(sessionId).handle(event);
947
1435
  this.updatePreviewFromEvent(event);
948
1436
  this.requestRender();
949
1437
  },
950
1438
  });
951
1439
  activity.finish();
1440
+ await this.refreshSessions({ selectedSessionId: this.selectedSessionId });
952
1441
  await this.refreshCommands();
953
1442
  this.finishExecutionPreview({
954
1443
  persist: true,
@@ -967,6 +1456,8 @@ class FullscreenTui {
967
1456
  finally {
968
1457
  this.busy = false;
969
1458
  this.currentAbortController = null;
1459
+ this.toolAbortController = null;
1460
+ this.toolAbortSignalRef.signal = undefined;
970
1461
  this.stopAnimation();
971
1462
  if (this.exitRequested) {
972
1463
  this.close();
@@ -995,6 +1486,8 @@ class FullscreenTui {
995
1486
  activity.pushLine(`run ${commandText}`);
996
1487
  this.busy = true;
997
1488
  this.currentAbortController = new AbortController();
1489
+ this.toolAbortController = new AbortController();
1490
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
998
1491
  this.startAnimation();
999
1492
  this.statusLine = `Running ${commandText}...`;
1000
1493
  this.requestRender();
@@ -1003,14 +1496,18 @@ class FullscreenTui {
1003
1496
  sessionId: this.selectedSessionId || undefined,
1004
1497
  sandbox: this.options.sandbox,
1005
1498
  signal: this.currentAbortController.signal,
1499
+ toolAbortSignalRef: this.toolAbortSignalRef,
1006
1500
  onUserInput: (question) => this.requestClarification(question),
1007
1501
  onEvent: (event) => {
1502
+ this.manageToolAbortSignal(event.event);
1008
1503
  activity.handle(event);
1504
+ this.taskTrackerFor(sessionId).handle(event);
1009
1505
  this.updatePreviewFromEvent(event);
1010
1506
  this.requestRender();
1011
1507
  },
1012
1508
  });
1013
1509
  activity.finish();
1510
+ await this.refreshSessions({ selectedSessionId: this.selectedSessionId });
1014
1511
  await this.refreshCommands();
1015
1512
  this.finishExecutionPreview({
1016
1513
  persist: true,
@@ -1029,6 +1526,8 @@ class FullscreenTui {
1029
1526
  finally {
1030
1527
  this.busy = false;
1031
1528
  this.currentAbortController = null;
1529
+ this.toolAbortController = null;
1530
+ this.toolAbortSignalRef.signal = undefined;
1032
1531
  this.stopAnimation();
1033
1532
  if (this.exitRequested) {
1034
1533
  this.close();
@@ -1056,6 +1555,8 @@ class FullscreenTui {
1056
1555
  activity.pushLine(`skill ${commandText}`);
1057
1556
  this.busy = true;
1058
1557
  this.currentAbortController = new AbortController();
1558
+ this.toolAbortController = new AbortController();
1559
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
1059
1560
  this.startAnimation();
1060
1561
  this.statusLine = `Running /${commandName}...`;
1061
1562
  this.requestRender();
@@ -1065,12 +1566,15 @@ class FullscreenTui {
1065
1566
  signal: this.currentAbortController.signal,
1066
1567
  onUserInput: (question) => this.requestClarification(question),
1067
1568
  onEvent: (event) => {
1569
+ this.manageToolAbortSignal(event.event);
1068
1570
  activity.handle(event);
1571
+ this.taskTrackerFor(sessionId).handle(event);
1069
1572
  this.updatePreviewFromEvent(event);
1070
1573
  this.requestRender();
1071
1574
  },
1072
1575
  });
1073
1576
  activity.finish();
1577
+ await this.refreshSessions({ selectedSessionId: this.selectedSessionId });
1074
1578
  await this.refreshCommands();
1075
1579
  this.finishExecutionPreview({
1076
1580
  persist: true,
@@ -1088,6 +1592,8 @@ class FullscreenTui {
1088
1592
  finally {
1089
1593
  this.busy = false;
1090
1594
  this.currentAbortController = null;
1595
+ this.toolAbortController = null;
1596
+ this.toolAbortSignalRef.signal = undefined;
1091
1597
  this.stopAnimation();
1092
1598
  if (this.exitRequested) {
1093
1599
  this.close();
@@ -1119,6 +1625,8 @@ class FullscreenTui {
1119
1625
  activity.pushLine(`shell ${persistOutput ? '!!' : '!'}${trimmedCommand}`);
1120
1626
  this.busy = true;
1121
1627
  this.currentAbortController = new AbortController();
1628
+ this.toolAbortController = this.currentAbortController;
1629
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
1122
1630
  this.startAnimation();
1123
1631
  this.statusLine = `Running ${persistOutput ? '!!' : '!'}${trimmedCommand}...`;
1124
1632
  this.requestRender();
@@ -1129,6 +1637,7 @@ class FullscreenTui {
1129
1637
  signal: this.currentAbortController.signal,
1130
1638
  onEvent: (event) => {
1131
1639
  activity.handle(event);
1640
+ this.taskTrackerFor(sessionId).handle(event);
1132
1641
  this.requestRender();
1133
1642
  },
1134
1643
  });
@@ -1163,6 +1672,8 @@ class FullscreenTui {
1163
1672
  finally {
1164
1673
  this.busy = false;
1165
1674
  this.currentAbortController = null;
1675
+ this.toolAbortController = null;
1676
+ this.toolAbortSignalRef.signal = undefined;
1166
1677
  this.stopAnimation();
1167
1678
  if (this.exitRequested) {
1168
1679
  this.close();
@@ -1247,7 +1758,7 @@ class FullscreenTui {
1247
1758
  this.scrollFocusedPane(offset < 0 ? 'up' : 'down', 1);
1248
1759
  }
1249
1760
  cyclePaneFocus(direction) {
1250
- const order = ['sessions', 'messages', 'process', 'context'];
1761
+ const order = ['sessions', 'messages', 'process', 'tasks', 'context'];
1251
1762
  const currentIndex = order.indexOf(this.focusedPane);
1252
1763
  const nextIndex = (currentIndex + direction + order.length) % order.length;
1253
1764
  this.focusedPane = order[nextIndex];
@@ -1369,6 +1880,7 @@ class FullscreenTui {
1369
1880
  { id: 'view.focus.sessions', label: 'Focus Sessions Pane', description: 'Focus the session list pane.' },
1370
1881
  { id: 'view.focus.messages', label: 'Focus Messages Pane', description: 'Focus the conversation history pane.' },
1371
1882
  { id: 'view.focus.process', label: 'Focus Execution Pane', description: 'Focus the live execution log pane.' },
1883
+ { id: 'view.focus.tasks', label: 'Focus Tasks Pane', description: 'Focus the step tracker pane.' },
1372
1884
  { id: 'view.focus.context', label: 'Focus Context Pane', description: 'Focus the session context and token usage pane.' },
1373
1885
  { id: 'view.follow', label: 'Follow Focused Pane', description: 'Jump back to the newest visible content in the active pane.' },
1374
1886
  { id: 'view.palette', label: 'Open Action Palette', description: 'Search all menu actions from one picker.', shortcut: 'Ctrl+P' },
@@ -1486,6 +1998,7 @@ class FullscreenTui {
1486
1998
  case 'view.focus.sessions':
1487
1999
  case 'view.focus.messages':
1488
2000
  case 'view.focus.process':
2001
+ case 'view.focus.tasks':
1489
2002
  case 'view.focus.context': {
1490
2003
  const pane = actionId.slice('view.focus.'.length);
1491
2004
  this.focusedPane = pane;
@@ -2355,6 +2868,36 @@ class FullscreenTui {
2355
2868
  currentProcessActivity() {
2356
2869
  return this.activityFor(this.selectedSessionId || 'global');
2357
2870
  }
2871
+ graphemeLeftIndex(text, index) {
2872
+ if (index <= 0)
2873
+ return 0;
2874
+ if (!graphemeSegmenter)
2875
+ return index - 1;
2876
+ const graphemes = Array.from(graphemeSegmenter.segment(text), (g) => g.segment);
2877
+ let pos = 0;
2878
+ for (const g of graphemes) {
2879
+ if (pos + g.length >= index)
2880
+ break;
2881
+ pos += g.length;
2882
+ }
2883
+ return pos;
2884
+ }
2885
+ graphemeRightIndex(text, index) {
2886
+ if (index >= text.length)
2887
+ return text.length;
2888
+ if (!graphemeSegmenter)
2889
+ return index + 1;
2890
+ const graphemes = Array.from(graphemeSegmenter.segment(text), (g) => g.segment);
2891
+ let pos = 0;
2892
+ for (const g of graphemes) {
2893
+ if (pos >= index) {
2894
+ pos += g.length;
2895
+ break;
2896
+ }
2897
+ pos += g.length;
2898
+ }
2899
+ return Math.min(pos, text.length);
2900
+ }
2358
2901
  insertText(text) {
2359
2902
  const previousInput = this.inputValue;
2360
2903
  this.inputValue = `${this.inputValue.slice(0, this.cursor)}${text}${this.inputValue.slice(this.cursor)}`;
@@ -2366,8 +2909,9 @@ class FullscreenTui {
2366
2909
  return;
2367
2910
  }
2368
2911
  const previousInput = this.inputValue;
2369
- this.inputValue = `${this.inputValue.slice(0, this.cursor - 1)}${this.inputValue.slice(this.cursor)}`;
2370
- this.cursor -= 1;
2912
+ const deleteFrom = this.graphemeLeftIndex(this.inputValue, this.cursor);
2913
+ this.inputValue = `${this.inputValue.slice(0, deleteFrom)}${this.inputValue.slice(this.cursor)}`;
2914
+ this.cursor = deleteFrom;
2371
2915
  this.refreshCommandBarStatus(previousInput);
2372
2916
  }
2373
2917
  deleteForward() {
@@ -2448,6 +2992,14 @@ class FullscreenTui {
2448
2992
  this.activityBySessionId.set(key, created);
2449
2993
  return created;
2450
2994
  }
2995
+ taskTrackerFor(sessionId) {
2996
+ let tracker = this.taskTrackerBySessionId.get(sessionId);
2997
+ if (!tracker) {
2998
+ tracker = new TaskTracker();
2999
+ this.taskTrackerBySessionId.set(sessionId, tracker);
3000
+ }
3001
+ return tracker;
3002
+ }
2451
3003
  async persistShellCommandResult(command, result) {
2452
3004
  let sessionId = this.selectedSessionId;
2453
3005
  if (!sessionId) {
@@ -2485,6 +3037,7 @@ class FullscreenTui {
2485
3037
  const sessionsPane = this.buildPaneView('sessions', 'Sessions', widths.left, contentHeight);
2486
3038
  const messagesPane = this.buildPaneView('messages', 'Messages', widths.center, contentHeight);
2487
3039
  const processPane = this.buildPaneView('process', 'Execution', widths.right, rightHeights.process);
3040
+ const tasksPane = this.buildPaneView('tasks', 'Tasks', widths.right, rightHeights.tasks);
2488
3041
  const contextPane = this.buildPaneView('context', 'Context', widths.right, rightHeights.context);
2489
3042
  const workspaceView = this.uiMode === 'command'
2490
3043
  ? null
@@ -2501,31 +3054,43 @@ class FullscreenTui {
2501
3054
  }
2502
3055
  else {
2503
3056
  for (let index = 0; index < contentHeight; index += 1) {
2504
- const rightLine = index < rightHeights.process
2505
- ? processPane[index]
2506
- : contextPane[index - rightHeights.process];
3057
+ let rightLine;
3058
+ if (index < rightHeights.process) {
3059
+ rightLine = processPane[index];
3060
+ }
3061
+ else if (index < rightHeights.process + rightHeights.tasks) {
3062
+ rightLine = tasksPane[index - rightHeights.process];
3063
+ }
3064
+ else {
3065
+ rightLine = contextPane[index - rightHeights.process - rightHeights.tasks];
3066
+ }
2507
3067
  lines.push(`${sessionsPane[index]}${gap}${messagesPane[index]}${gap}${rightLine}`);
2508
3068
  }
2509
3069
  }
2510
3070
  lines.push(status);
2511
3071
  lines.push(inputLine);
3072
+ if (this.selection) {
3073
+ this.applySelectionHighlight(lines, widths, rightHeights, contentHeight);
3074
+ }
2512
3075
  this.paintFrame(lines, { columns, rows });
2513
3076
  if (workspaceView?.cursor && !this.pendingPrompt) {
2514
3077
  output.write(`\u001b[${workspaceView.cursor.row};${workspaceView.cursor.column}H`);
2515
3078
  return;
2516
3079
  }
2517
- const cursorColumn = Math.min(columns, inputPrefix.length + this.cursor + 1);
3080
+ const textBeforeCursor = this.inputValue.slice(0, this.cursor);
3081
+ const cursorColumn = Math.min(columns, inputPrefix.length + measureDisplayWidth(textBeforeCursor) + 1);
2518
3082
  output.write(`\u001b[${rows};${cursorColumn}H`);
2519
3083
  }
2520
3084
  paintFrame(lines, size) {
2521
- const patch = computeFramePatch(this.previousFrameLines, lines, this.previousFrameSize, size);
3085
+ const paddedLines = lines.map((line) => padRight(line, size.columns));
3086
+ const patch = computeFramePatch(this.previousFrameLines, paddedLines, this.previousFrameSize, size);
2522
3087
  let buffer = '\u001b[0m';
2523
3088
  if (patch.fullRender) {
2524
- buffer += `\u001b[H\u001b[2J${lines.join('\n')}\u001b[?25h`;
3089
+ buffer += `\u001b[H${paddedLines.join('\n')}\u001b[?25h`;
2525
3090
  }
2526
3091
  else if (patch.changedRows.length > 0) {
2527
3092
  for (const change of patch.changedRows) {
2528
- buffer += `\u001b[${change.row};1H\u001b[2K${change.line}`;
3093
+ buffer += `\u001b[${change.row};1H${change.line}`;
2529
3094
  }
2530
3095
  buffer += '\u001b[?25h';
2531
3096
  }
@@ -2533,7 +3098,7 @@ class FullscreenTui {
2533
3098
  buffer += '\u001b[?25h';
2534
3099
  }
2535
3100
  output.write(buffer);
2536
- this.previousFrameLines = [...lines];
3101
+ this.previousFrameLines = [...paddedLines];
2537
3102
  this.previousFrameSize = size;
2538
3103
  }
2539
3104
  buildHeaderLine() {
@@ -2547,32 +3112,35 @@ class FullscreenTui {
2547
3112
  return `${sessionLabel} ${mode} ${skillLabel}`;
2548
3113
  }
2549
3114
  buildStatusLine() {
3115
+ const spinner = this.busy || this.currentPreview?.entries.some((entry) => entry.pending)
3116
+ ? ` ${currentSpinnerFrame()}`
3117
+ : '';
2550
3118
  if (this.uiMode === 'menu') {
2551
3119
  const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
2552
- return `Menu ${menu?.label ?? ''} | ${menu?.items[this.menuState.selectedIndex]?.description ?? 'Choose an action.'}`;
3120
+ return `${spinner} Menu ${menu?.label ?? ''} | ${menu?.items[this.menuState.selectedIndex]?.description ?? 'Choose an action.'}`;
2553
3121
  }
2554
3122
  if ((this.uiMode === 'picker' || this.uiMode === 'palette') && this.pickerState) {
2555
3123
  const selected = this.pickerState.filteredItems[this.pickerState.selectedIndex];
2556
- return `${this.pickerState.title} | ${selected?.description ?? this.pickerState.emptyText}`;
3124
+ return `${spinner} ${this.pickerState.title} | ${selected?.description ?? this.pickerState.emptyText}`;
2557
3125
  }
2558
3126
  if (this.uiMode === 'viewer' && this.viewerState) {
2559
- return `${this.viewerState.title} | ${this.viewerState.path ? path.relative(this.workspaceRoot, this.viewerState.path) : 'read-only view'}`;
3127
+ return `${spinner} ${this.viewerState.title} | ${this.viewerState.path ? path.relative(this.workspaceRoot, this.viewerState.path) : 'read-only view'}`;
2560
3128
  }
2561
3129
  if (this.uiMode === 'editor' && this.editorState) {
2562
3130
  const dirty = this.editorState.dirty ? 'modified' : 'saved';
2563
- return `${this.editorState.title} | ${dirty} | Ln ${this.editorState.cursorLine + 1}, Col ${this.editorState.cursorColumn + 1}`;
3131
+ return `${spinner} ${this.editorState.title} | ${dirty} | Ln ${this.editorState.cursorLine + 1}, Col ${this.editorState.cursorColumn + 1}`;
2564
3132
  }
2565
3133
  const focus = paneDisplayName(this.focusedPane);
2566
3134
  if (!this.pendingPrompt && this.focusedPane !== 'sessions') {
2567
3135
  const follow = this.paneScroll[this.focusedPane] === 0 ? 'follow' : `scroll ${this.paneScroll[this.focusedPane]}`;
2568
- return `${this.statusLine} | Focus ${focus} | ${follow}`;
3136
+ return `${spinner} ${this.statusLine} | Focus ${focus} | ${follow}`;
2569
3137
  }
2570
3138
  if (this.pendingPrompt) {
2571
3139
  return this.pendingPrompt.kind === 'clarify'
2572
- ? `Clarify: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`
2573
- : `Input: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`;
3140
+ ? `${spinner} Clarify: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`
3141
+ : `${spinner} Input: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`;
2574
3142
  }
2575
- return `${this.statusLine} | Focus ${focus}`;
3143
+ return `${spinner} ${this.statusLine} | Focus ${focus}`;
2576
3144
  }
2577
3145
  buildInputPrefix() {
2578
3146
  if (this.uiMode === 'menu') {
@@ -2667,6 +3235,12 @@ class FullscreenTui {
2667
3235
  'Use /cancel to abort the current execution.',
2668
3236
  ];
2669
3237
  }
3238
+ buildTasksPaneBody() {
3239
+ const tracker = this.taskTrackerFor(this.selectedSessionId);
3240
+ const columns = output.columns ?? 120;
3241
+ const widths = computePaneWidths(columns);
3242
+ return tracker.buildPaneBody(widths.right);
3243
+ }
2670
3244
  buildContextPaneBody() {
2671
3245
  if (!this.sessionDetail) {
2672
3246
  return ['No session selected.'];
@@ -2865,6 +3439,8 @@ class FullscreenTui {
2865
3439
  ['<- ->', 'Pane'],
2866
3440
  [this.focusedPane === 'sessions' ? 'Up/Down' : 'PgUp/PgDn', this.focusedPane === 'sessions' ? 'Select' : 'Scroll'],
2867
3441
  ['End', this.focusedPane === 'sessions' ? 'Bottom' : 'Follow'],
3442
+ ['Drag', 'Select'],
3443
+ ...(this.busy ? [['Ctrl+T', 'Cancel']] : []),
2868
3444
  ['/cancel', 'Stop'],
2869
3445
  ];
2870
3446
  }
@@ -2879,6 +3455,16 @@ class FullscreenTui {
2879
3455
  ? `${title}${this.focusedPane === kind ? ' [focus]' : ''} [follow]`
2880
3456
  : `${title}${this.focusedPane === kind ? ' [focus]' : ''} [-${this.paneScroll[kind]}]`;
2881
3457
  }
3458
+ cachedCollectTailWrappedLines(cacheKey, body, innerWidth, limit) {
3459
+ const cached = this.wrapCache.get(cacheKey);
3460
+ const bodyKey = `${body.length}:${body.length > 0 ? body[body.length - 1] : ''}:${limit}`;
3461
+ if (cached && cached.bodyRef === body && cached.bodyKey === bodyKey && cached.innerWidth === innerWidth) {
3462
+ return { lines: cached.result, truncated: body.length > 0 && cached.result.length < body.length };
3463
+ }
3464
+ const result = collectTailWrappedLines(body, innerWidth, limit);
3465
+ this.wrapCache.set(cacheKey, { bodyRef: body, bodyKey, innerWidth, result: result.lines });
3466
+ return result;
3467
+ }
2882
3468
  getRenderPaneMetrics(kind, width = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).width, height = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).height) {
2883
3469
  if ((kind === 'process' || kind === 'messages') && this.paneScroll[kind] === 0) {
2884
3470
  const innerWidth = Math.max(1, width - 2);
@@ -2886,7 +3472,7 @@ class FullscreenTui {
2886
3472
  const rawBody = kind === 'messages'
2887
3473
  ? this.buildMessagesPaneBody({ recentEntryLimit: Math.max(8, viewportLines * 2) })
2888
3474
  : this.buildProcessPaneBody({ tailLineLimit: Math.max(16, viewportLines * 4) });
2889
- const tail = collectTailWrappedLines(rawBody, innerWidth, viewportLines);
3475
+ const tail = this.cachedCollectTailWrappedLines(`${kind}:tail`, rawBody, innerWidth, viewportLines);
2890
3476
  return {
2891
3477
  lines: tail.lines,
2892
3478
  start: 0,
@@ -2904,7 +3490,7 @@ class FullscreenTui {
2904
3490
  getPaneMetrics(kind, width = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).width, height = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).height) {
2905
3491
  const innerWidth = Math.max(1, width - 2);
2906
3492
  const rawBody = this.getPaneBody(kind);
2907
- const lines = wrapBodyLines(rawBody, innerWidth);
3493
+ const lines = this.cachedWrapBodyLines(kind, rawBody, innerWidth);
2908
3494
  const viewportLines = Math.max(0, height - 2);
2909
3495
  const maxStart = Math.max(0, lines.length - viewportLines);
2910
3496
  if (kind === 'sessions') {
@@ -2926,6 +3512,16 @@ class FullscreenTui {
2926
3512
  maxStartIsExact: true,
2927
3513
  };
2928
3514
  }
3515
+ cachedWrapBodyLines(cacheKey, body, innerWidth) {
3516
+ const cached = this.wrapCache.get(cacheKey);
3517
+ const bodyKey = `${body.length}:${body.length > 0 ? body[body.length - 1] : ''}`;
3518
+ if (cached && cached.bodyRef === body && cached.bodyKey === bodyKey && cached.innerWidth === innerWidth) {
3519
+ return cached.result;
3520
+ }
3521
+ const result = wrapBodyLines(body, innerWidth);
3522
+ this.wrapCache.set(cacheKey, { bodyRef: body, bodyKey, innerWidth, result });
3523
+ return result;
3524
+ }
2929
3525
  getPaneBody(kind) {
2930
3526
  switch (kind) {
2931
3527
  case 'sessions':
@@ -2933,7 +3529,9 @@ class FullscreenTui {
2933
3529
  case 'messages':
2934
3530
  return this.buildMessagesPaneBody();
2935
3531
  case 'process':
2936
- return this.buildProcessPaneBody();
3532
+ return this.buildProcessPaneBody({ tailLineLimit: MAX_ACTIVITY_LINES });
3533
+ case 'tasks':
3534
+ return this.buildTasksPaneBody();
2937
3535
  case 'context':
2938
3536
  return this.buildContextPaneBody();
2939
3537
  }
@@ -2951,8 +3549,7 @@ class FullscreenTui {
2951
3549
  }
2952
3550
  }
2953
3551
  formatMessageHeader(entry) {
2954
- const spinner = entry.pending ? ` ${currentSpinnerFrame()}` : '';
2955
- return formatDisplayMessageHeader(entry, spinner);
3552
+ return formatDisplayMessageHeader(entry, '');
2956
3553
  }
2957
3554
  close() {
2958
3555
  if (!this.closeResolver) {
@@ -2962,6 +3559,21 @@ class FullscreenTui {
2962
3559
  this.closeResolver = null;
2963
3560
  resolve();
2964
3561
  }
3562
+ manageToolAbortSignal(event) {
3563
+ if (event.type !== 'tool_call' || !event.toolState) {
3564
+ return;
3565
+ }
3566
+ if (event.toolState === 'starting' || event.toolState === 'running') {
3567
+ if (!this.toolAbortController || this.toolAbortController.signal.aborted) {
3568
+ this.toolAbortController = new AbortController();
3569
+ }
3570
+ this.toolAbortSignalRef.signal = this.toolAbortController.signal;
3571
+ }
3572
+ else {
3573
+ this.toolAbortController = null;
3574
+ this.toolAbortSignalRef.signal = undefined;
3575
+ }
3576
+ }
2965
3577
  isEditingInput() {
2966
3578
  return this.uiMode !== 'editor' && this.inputValue.length > 0;
2967
3579
  }
@@ -3254,6 +3866,7 @@ export async function runFileCommand(workspaceRoot, fileSpec, args, options = {}
3254
3866
  }, args, options);
3255
3867
  }
3256
3868
  async function executeRunnableCommand(workspaceRoot, target, args, options = {}) {
3869
+ const usageByModel = {};
3257
3870
  const executionLog = options.sessionId
3258
3871
  ? createSessionExecutionLogWriter({
3259
3872
  workspaceRoot,
@@ -3265,6 +3878,9 @@ async function executeRunnableCommand(workspaceRoot, target, args, options = {})
3265
3878
  })
3266
3879
  : null;
3267
3880
  const emitLogEvent = (event) => {
3881
+ if (event.event.type === 'usage') {
3882
+ recordTokenUsage(usageByModel, event.modelId, event.event.usage);
3883
+ }
3268
3884
  executionLog?.recordEvent(event);
3269
3885
  options.onEvent?.(event);
3270
3886
  };
@@ -3275,12 +3891,14 @@ async function executeRunnableCommand(workspaceRoot, target, args, options = {})
3275
3891
  stream: true,
3276
3892
  sandbox: options.sandbox,
3277
3893
  signal: options.signal,
3894
+ toolAbortSignalRef: options.toolAbortSignalRef,
3278
3895
  onUserInput: options.onUserInput,
3279
3896
  onEvent: (event) => emitLogEvent({ scope: 'child', childSlug: target.childSlug, event }),
3280
3897
  onChildEvent: (childSlug, event) => emitLogEvent({ scope: 'child', childSlug, event }),
3281
3898
  });
3282
3899
  if (options.sessionId) {
3283
3900
  await appendConductorSessionMessages(workspaceRoot, options.sessionId, buildPersistedRunnableTranscript(target.inputText, result));
3901
+ await mergeSessionUsage(workspaceRoot, options.sessionId, usageByModel);
3284
3902
  }
3285
3903
  await executionLog?.finish({
3286
3904
  status: result.error ? 'error' : 'success',
@@ -3296,6 +3914,7 @@ async function executeRunnableCommand(workspaceRoot, target, args, options = {})
3296
3914
  output: [],
3297
3915
  error: error.message,
3298
3916
  }));
3917
+ await mergeSessionUsage(workspaceRoot, options.sessionId, usageByModel);
3299
3918
  }
3300
3919
  await executionLog?.finish({
3301
3920
  status: 'error',
@@ -4019,17 +4638,23 @@ function computePaneWidths(columns) {
4019
4638
  return { left, center, right };
4020
4639
  }
4021
4640
  function computeRightPaneHeights(totalHeight) {
4022
- if (totalHeight <= 8) {
4023
- return { process: Math.max(1, totalHeight - 1), context: 1 };
4024
- }
4025
- let process = Math.max(8, Math.floor(totalHeight * 0.58));
4026
- let context = totalHeight - process;
4027
- if (context < 6) {
4028
- const deficit = 6 - context;
4029
- process = Math.max(6, process - deficit);
4030
- context = totalHeight - process;
4031
- }
4032
- return { process, context };
4641
+ if (totalHeight <= 12) {
4642
+ return { process: Math.max(1, totalHeight - 2), tasks: 1, context: 1 };
4643
+ }
4644
+ let process = Math.max(6, Math.floor(totalHeight * 0.40));
4645
+ let tasks = Math.max(4, Math.floor(totalHeight * 0.25));
4646
+ let context = totalHeight - process - tasks;
4647
+ if (context < 4) {
4648
+ const deficit = 4 - context;
4649
+ process = Math.max(4, process - deficit);
4650
+ context = totalHeight - process - tasks;
4651
+ }
4652
+ if (context < 4) {
4653
+ const deficit = 4 - context;
4654
+ tasks = Math.max(3, tasks - deficit);
4655
+ context = totalHeight - process - tasks;
4656
+ }
4657
+ return { process, tasks, context };
4033
4658
  }
4034
4659
  function buildPane(title, body, width, height, kind, active = false) {
4035
4660
  if (height <= 0) {
@@ -4091,6 +4716,37 @@ function expandTabs(text, tabWidth) {
4091
4716
  export function measureDisplayWidth(text) {
4092
4717
  return splitGraphemes(text).reduce((total, grapheme) => total + graphemeDisplayWidth(grapheme), 0);
4093
4718
  }
4719
+ function displayColumnToStringIndex(text, displayCol) {
4720
+ let col = 0;
4721
+ let i = 0;
4722
+ const len = text.length;
4723
+ while (i < len) {
4724
+ if (text.charCodeAt(i) === 0x1b && i + 1 < len && text[i + 1] === '[') {
4725
+ const end = text.indexOf('m', i + 2);
4726
+ if (end !== -1) {
4727
+ i = end + 1;
4728
+ continue;
4729
+ }
4730
+ }
4731
+ if (col >= displayCol)
4732
+ return i;
4733
+ const code = text.codePointAt(i);
4734
+ const charWidth = code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a ||
4735
+ (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
4736
+ (code >= 0xac00 && code <= 0xd7a3) ||
4737
+ (code >= 0xf900 && code <= 0xfaff) ||
4738
+ (code >= 0xfe10 && code <= 0xfe19) ||
4739
+ (code >= 0xfe30 && code <= 0xfe6f) ||
4740
+ (code >= 0xff01 && code <= 0xff60) ||
4741
+ (code >= 0xffe0 && code <= 0xffe6) ||
4742
+ (code >= 0x1f300 && code <= 0x1f9ff) ||
4743
+ (code >= 0x20000 && code <= 0x2fffd) ||
4744
+ (code >= 0x30000 && code <= 0x3fffd)) ? 2 : 1;
4745
+ col += charWidth;
4746
+ i += code > 0xffff ? 2 : 1;
4747
+ }
4748
+ return i;
4749
+ }
4094
4750
  export function padRight(text, width) {
4095
4751
  const visibleWidth = measureDisplayWidth(text);
4096
4752
  return visibleWidth >= width ? text : `${text}${' '.repeat(width - visibleWidth)}`;
@@ -4349,8 +5005,20 @@ function renderWindowBody(text, innerWidth, kind) {
4349
5005
  return `${border}${paint(content, ANSI.yellow, ANSI.bgBlue, ANSI.bold)}${border}`;
4350
5006
  }
4351
5007
  if (normalized.startsWith('task') || normalized.startsWith('skill')) {
4352
- return `${border}${paint(content, ANSI.brightWhite, ANSI.bgBlue, ANSI.bold)}${border}`;
5008
+ return `${border}${paint(content, ANSI.brightWhite, ANSI.bgBlue, ANSI.bold)}`;
5009
+ }
5010
+ }
5011
+ if (kind === 'tasks') {
5012
+ if (text.startsWith('>')) {
5013
+ return `${border}${paint(content, ANSI.brightCyan, ANSI.bgBlue, ANSI.bold)}${border}`;
5014
+ }
5015
+ if (text.startsWith('\u2713')) {
5016
+ return `${border}${paint(content, ANSI.green, ANSI.bgBlue)}${border}`;
5017
+ }
5018
+ if (text.startsWith('\u2717')) {
5019
+ return `${border}${paint(content, ANSI.red, ANSI.bgBlue)}${border}`;
4353
5020
  }
5021
+ return `${border}${paint(content, ANSI.white, ANSI.bgBlue)}${border}`;
4354
5022
  }
4355
5023
  if (normalized === 'task memory' ||
4356
5024
  normalized === 'assistant memory' ||
@@ -4379,6 +5047,8 @@ function paneDisplayName(kind) {
4379
5047
  return 'Messages';
4380
5048
  case 'process':
4381
5049
  return 'Execution';
5050
+ case 'tasks':
5051
+ return 'Tasks';
4382
5052
  case 'context':
4383
5053
  return 'Context';
4384
5054
  }
@@ -4393,6 +5063,8 @@ function paneDimensionsForKind(kind, columns, rows) {
4393
5063
  return { width: widths.center, height: Math.max(1, rows - 3) };
4394
5064
  case 'process':
4395
5065
  return { width: widths.right, height: rightHeights.process };
5066
+ case 'tasks':
5067
+ return { width: widths.right, height: rightHeights.tasks };
4396
5068
  case 'context':
4397
5069
  return { width: widths.right, height: rightHeights.context };
4398
5070
  }