@aragon-agent/cli 0.6.7 → 0.6.9

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 (117) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +107 -77
  3. package/dist/agent/controller.d.ts +11 -7
  4. package/dist/agent/controller.js +35 -48
  5. package/dist/agent/headless.d.ts +2 -1
  6. package/dist/agent/prompt-options.d.ts +15 -0
  7. package/dist/agent/prompt-options.js +1 -0
  8. package/dist/agent/reducer.d.ts +7 -113
  9. package/dist/agent/reducer.js +0 -62
  10. package/dist/cli.js +111 -182
  11. package/dist/commands/builtins.js +3 -1
  12. package/dist/commands/perf.d.ts +32 -2
  13. package/dist/commands/perf.js +32 -2
  14. package/dist/commands/registry.d.ts +4 -5
  15. package/dist/config/env.js +0 -130
  16. package/dist/config/load.d.ts +0 -2
  17. package/dist/config/load.js +2 -31
  18. package/dist/config/schema.d.ts +1 -14
  19. package/dist/config/schema.js +0 -1
  20. package/dist/config/store.js +2 -0
  21. package/dist/input/interrupt-gesture.d.ts +18 -0
  22. package/dist/input/interrupt-gesture.js +19 -0
  23. package/dist/input/pointer-router.d.ts +11 -0
  24. package/dist/input/pointer-router.js +14 -0
  25. package/dist/logging/install.d.ts +2 -2
  26. package/dist/logging/install.js +2 -2
  27. package/dist/todo/follow-through.d.ts +0 -4
  28. package/dist/todo/follow-through.js +0 -4
  29. package/dist/todo/limits.d.ts +0 -10
  30. package/dist/todo/limits.js +0 -10
  31. package/dist/todo/prompt.d.ts +0 -15
  32. package/dist/todo/store.d.ts +8 -13
  33. package/dist/todo/store.js +13 -14
  34. package/dist/todo/types.d.ts +2 -1
  35. package/dist/ui/ActivityLine.d.ts +26 -34
  36. package/dist/ui/ActivityLine.js +30 -19
  37. package/dist/ui/App.d.ts +3 -41
  38. package/dist/ui/App.js +223 -247
  39. package/dist/ui/BottomStatusRow.d.ts +1 -3
  40. package/dist/ui/BottomStatusRow.js +4 -8
  41. package/dist/ui/Composer.d.ts +38 -5
  42. package/dist/ui/Composer.js +45 -23
  43. package/dist/ui/Header.d.ts +2 -16
  44. package/dist/ui/Header.js +3 -18
  45. package/dist/ui/Logo.d.ts +1 -17
  46. package/dist/ui/Logo.js +0 -16
  47. package/dist/ui/ModeChip.d.ts +7 -18
  48. package/dist/ui/ModeChip.js +9 -0
  49. package/dist/ui/PromptCaret.d.ts +11 -0
  50. package/dist/ui/PromptCaret.js +31 -0
  51. package/dist/ui/PromptInput.d.ts +10 -33
  52. package/dist/ui/PromptInput.js +37 -20
  53. package/dist/ui/StatusBar.d.ts +1 -11
  54. package/dist/ui/StatusBar.js +14 -8
  55. package/dist/ui/ToastStack.d.ts +2 -18
  56. package/dist/ui/ToastStack.js +4 -11
  57. package/dist/ui/Transcript.d.ts +5 -71
  58. package/dist/ui/Transcript.js +10 -199
  59. package/dist/ui/composer-rows.js +50 -9
  60. package/dist/ui/entries/AssistantEntry.d.ts +0 -23
  61. package/dist/ui/entries/AssistantEntry.js +3 -4
  62. package/dist/ui/entries/DiffView.d.ts +1 -7
  63. package/dist/ui/entries/DiffView.js +2 -2
  64. package/dist/ui/entries/ToolCard.d.ts +0 -6
  65. package/dist/ui/entries/ToolCard.js +4 -13
  66. package/dist/ui/frame-differ.d.ts +13 -0
  67. package/dist/ui/frame-differ.js +37 -24
  68. package/dist/ui/frame-observer.d.ts +17 -0
  69. package/dist/ui/frame-observer.js +81 -0
  70. package/dist/ui/frame-parser.d.ts +7 -0
  71. package/dist/ui/frame-parser.js +18 -0
  72. package/dist/ui/ink-runtime.d.ts +1 -0
  73. package/dist/ui/ink-runtime.js +21 -0
  74. package/dist/ui/layout/AppShell.d.ts +4 -58
  75. package/dist/ui/layout/AppShell.js +3 -6
  76. package/dist/ui/layout/OverlayFrame.d.ts +1 -25
  77. package/dist/ui/layout/OverlayFrame.js +0 -24
  78. package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
  79. package/dist/ui/layout/ScrollIndicator.js +4 -4
  80. package/dist/ui/layout/ScrollViewport.d.ts +16 -66
  81. package/dist/ui/layout/ScrollViewport.js +195 -185
  82. package/dist/ui/layout/budget.d.ts +3 -51
  83. package/dist/ui/layout/budget.js +4 -53
  84. package/dist/ui/layout/document-layout.d.ts +13 -0
  85. package/dist/ui/layout/document-layout.js +11 -0
  86. package/dist/ui/layout/follow-state.d.ts +1 -0
  87. package/dist/ui/layout/follow-state.js +1 -3
  88. package/dist/ui/layout/frame.d.ts +0 -22
  89. package/dist/ui/layout/frame.js +0 -36
  90. package/dist/ui/layout/scroll-indicator.d.ts +1 -1
  91. package/dist/ui/layout/scroll-indicator.js +3 -2
  92. package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
  93. package/dist/ui/layout/scrollbar-geometry.js +13 -0
  94. package/dist/ui/layout/todo-layout.d.ts +2 -3
  95. package/dist/ui/layout/todo-layout.js +9 -9
  96. package/dist/ui/layout/viewport-geometry.d.ts +2 -1
  97. package/dist/ui/layout/viewport-geometry.js +2 -1
  98. package/dist/ui/layout/virtual-window.d.ts +1 -0
  99. package/dist/ui/layout/virtual-window.js +1 -1
  100. package/dist/ui/live-clamp.d.ts +1 -29
  101. package/dist/ui/live-clamp.js +1 -29
  102. package/dist/ui/overlays/HelpOverlay.js +6 -2
  103. package/dist/ui/run-status-row.d.ts +61 -0
  104. package/dist/ui/run-status-row.js +70 -0
  105. package/dist/ui/screen.d.ts +1 -1
  106. package/dist/ui/screen.js +4 -6
  107. package/dist/ui/scrollbar-controller.d.ts +30 -0
  108. package/dist/ui/scrollbar-controller.js +124 -0
  109. package/dist/ui/transcript-text.d.ts +0 -8
  110. package/dist/ui/transcript-text.js +0 -8
  111. package/dist/ui/use-startup-notices.d.ts +1 -57
  112. package/dist/ui/use-startup-notices.js +3 -54
  113. package/dist/ui/use-wheel-routing.d.ts +3 -1
  114. package/dist/ui/use-wheel-routing.js +6 -0
  115. package/package.json +2 -2
  116. package/dist/ui/TodoStrip.d.ts +0 -36
  117. package/dist/ui/TodoStrip.js +0 -32
@@ -19,10 +19,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
19
19
  * stays a plain `useState` because it is written from the debounced glob effect
20
20
  * rather than from a key.
21
21
  */
22
- import { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
22
+ import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
23
23
  import { Box, Text, useInput, useStdout } from 'ink';
24
24
  import { glob as tinyGlob } from 'tinyglobby';
25
25
  import { pickGlyphs } from './glyphs.js';
26
+ import { PromptCaret } from './PromptCaret.js';
26
27
  import { recognize } from '../input/keymap.js';
27
28
  import { AutocompletePopup } from './AutocompletePopup.js';
28
29
  import { buildAutocompleteLayout } from './layout/autocomplete.js';
@@ -43,8 +44,8 @@ const CHIP_FULL_MIN_COLS = 64;
43
44
  * reads `999+`.
44
45
  */
45
46
  const CHIP_MAX_N = 999;
46
- /** `↓ 999+ new lines · PgDn` — the widest form the full branch can produce. */
47
- const CHIP_FULL_CELLS = 24;
47
+ /** `↓ 999+ lines below · PgDn` — the widest form the full branch can produce. */
48
+ const CHIP_FULL_CELLS = 27;
48
49
  /** `↓999+` — the widest form the terse branch can produce. */
49
50
  const CHIP_TERSE_CELLS = 6;
50
51
  /**
@@ -64,7 +65,7 @@ export function scrollChip(scrolledLines, cols, glyphs) {
64
65
  }
65
66
  const noun = n === 1 ? 'line' : 'lines';
66
67
  return {
67
- text: `${glyphs.arrowDown} ${shown} new ${noun} ${glyphs.midDot} PgDn`,
68
+ text: `${glyphs.arrowDown} ${shown} ${noun} below ${glyphs.midDot} PgDn`,
68
69
  cells: CHIP_FULL_CELLS,
69
70
  };
70
71
  }
@@ -247,7 +248,7 @@ function isControlSeq(input) {
247
248
  // ---------------------------------------------------------------------------
248
249
  // Component
249
250
  // ---------------------------------------------------------------------------
250
- export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines = 0, }) {
251
+ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, borderColor, onDraftChange, onNotice, scrolledLines = 0, }) {
251
252
  const { stdout } = useStdout();
252
253
  const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
253
254
  const { buffer, cursor, historyIndex, dismissed, sel } = editor;
@@ -314,23 +315,20 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
314
315
  // Deriving it twice — once for the frame and once for the budget — is exactly
315
316
  // the trap `BottomStatusRow.tsx:5-27` documents.
316
317
  const glyphs = pickGlyphs(caps);
317
- const cols = stdout?.columns ?? 80;
318
+ const cols = colsProp ?? stdout?.columns ?? 80;
318
319
  const terminalRows = stdout?.rows ?? 24;
319
- // Full-screen only: inline mode has no self-drawn viewport, so `scrolledLines`
320
- // is always 0 there and the branch is unreachable rather than merely unused.
321
- // The guard is explicit so it stays that way.
322
- const chip = bordered ? scrollChip(scrolledLines, cols, glyphs) : null;
320
+ const chip = scrollChip(scrolledLines, cols, glyphs);
323
321
  // Err NARROW. Wrapping wider than the box Ink gives us would make Ink wrap a
324
322
  // second time, and then `layout.rows.length` is a lie — which is the one thing
325
323
  // this module may not be.
326
- const chromeCols = bordered ? 2 /* border */ + 2 /* paddingX */ + 2 /* marker */ : 2;
324
+ const chromeCols = 2 /* border */ + 2 /* paddingX */ + 2 /* marker */;
327
325
  const baseCols = Math.max(8, cols - chromeCols - (chip ? chip.cells + 1 : 0));
328
326
  const measure = (wrapCols) => layoutComposer({
329
327
  buffer,
330
328
  cursor,
331
329
  cols: wrapCols,
332
330
  maxRows: draftMaxRows(terminalRows),
333
- active: isActive,
331
+ active: isActive && cursorVisible,
334
332
  });
335
333
  // TWO PASSES, AND THE SECOND ONE IS LOAD-BEARING (I-8). The overflow indicator
336
334
  // rides the last rendered row INSIDE the box the draft wraps in, so its cells
@@ -348,6 +346,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
348
346
  const usableCols = overflows ? Math.max(8, baseCols - OVERFLOW_CELLS - 1) : baseCols;
349
347
  const layout = overflows ? measure(usableCols) : wide;
350
348
  const draftRowCount = Math.max(1, layout.rows.length);
349
+ const caretResetKey = useMemo(() => ({}), [buffer, cursor, isActive, usableCols]);
351
350
  // --- Draft presence and height, reported only on a transition. -----------
352
351
  // Notifying on every keystroke would re-render the whole App tree per key. The
353
352
  // row count changes on a line break or a wrap boundary, not per character
@@ -368,6 +367,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
368
367
  // by line. Two dispatches from one key would put the legacy root back to two
369
368
  // commits and undo the whole of F3 for that branch.
370
369
  const insert = (text) => {
370
+ onInteraction?.();
371
371
  dispatch({ type: 'insert', text });
372
372
  };
373
373
  /**
@@ -379,6 +379,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
379
379
  * needs. `clear` releases every payload in the same dispatch.
380
380
  */
381
381
  const submit = (text) => {
382
+ onInteraction?.();
382
383
  onSubmit(expandPastes(text, editor.pastes));
383
384
  dispatch({ type: 'clear' });
384
385
  };
@@ -404,6 +405,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
404
405
  dispatch({ type: 'recall', buffer: value, cursor: value.length, historyIndex: idx });
405
406
  };
406
407
  const completeSelection = () => {
408
+ onInteraction?.();
407
409
  const chosen = popupItems[clampedSel];
408
410
  if (!chosen)
409
411
  return;
@@ -417,6 +419,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
417
419
  }
418
420
  };
419
421
  const verticalOrHistory = (dir) => {
422
+ onInteraction?.();
420
423
  const moved = moveVertical(buffer, cursor, dir);
421
424
  if (moved) {
422
425
  dispatch({ type: 'moveCursor', cursor: moved.cursor });
@@ -441,13 +444,17 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
441
444
  // appears with a popup open and is therefore easy to ship (AC-P3 / R-P2).
442
445
  if (key.tab && key.shift)
443
446
  return;
447
+ if (key.pageUp || key.pageDown || (key.shift && (key.upArrow || key.downArrow)))
448
+ return;
444
449
  // Popup navigation owns Up/Down/Tab/→/Enter/Esc while it is open.
445
450
  if (popupVisible) {
446
451
  if (key.upArrow) {
452
+ onInteraction?.();
447
453
  dispatch({ type: 'select', sel: Math.max(0, clampedSel - 1) });
448
454
  return;
449
455
  }
450
456
  if (key.downArrow) {
457
+ onInteraction?.();
451
458
  dispatch({ type: 'select', sel: Math.min(popupItems.length - 1, clampedSel + 1) });
452
459
  return;
453
460
  }
@@ -463,15 +470,19 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
463
470
  }
464
471
  if (key.escape) {
465
472
  dispatch({ type: 'dismiss' });
473
+ onEscapeDismiss?.();
466
474
  return;
467
475
  }
468
476
  // Any other key falls through so typing keeps filtering the popup.
469
477
  }
470
- if (key.escape)
471
- return; // App owns abort / close-overlay.
478
+ if (key.escape) {
479
+ onEscape?.();
480
+ return;
481
+ }
472
482
  // Line-editing intents (Home/End/word/kill) recognized from raw keys.
473
483
  const intent = recognize(input, key);
474
484
  if (intent) {
485
+ onInteraction?.();
475
486
  const next = applyEdit(buffer, cursor, intent);
476
487
  dispatch({ type: 'replace', buffer: next.buffer, cursor: next.cursor });
477
488
  return;
@@ -489,6 +500,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
489
500
  return;
490
501
  }
491
502
  if (key.backspace || key.delete) {
503
+ onInteraction?.();
492
504
  // The `cursor > 0` guard lives in the reducer now, so this branch is one
493
505
  // unconditional dispatch and a no-op at the left edge returns the same
494
506
  // state object — which React bails out on, exactly as the old
@@ -497,11 +509,15 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
497
509
  return;
498
510
  }
499
511
  if (key.leftArrow) {
500
- dispatch({ type: 'moveCursor', cursor: Math.max(0, cursor - 1) });
512
+ onInteraction?.();
513
+ const step = cursor >= 2 && (buffer.codePointAt(cursor - 2) ?? 0) > 0xffff ? 2 : 1;
514
+ dispatch({ type: 'moveCursor', cursor: Math.max(0, cursor - step) });
501
515
  return;
502
516
  }
503
517
  if (key.rightArrow) {
504
- dispatch({ type: 'moveCursor', cursor: Math.min(buffer.length, cursor + 1) });
518
+ onInteraction?.();
519
+ const step = (buffer.codePointAt(cursor) ?? 0) > 0xffff ? 2 : 1;
520
+ dispatch({ type: 'moveCursor', cursor: Math.min(buffer.length, cursor + step) });
505
521
  return;
506
522
  }
507
523
  // `!key.shift` is load-bearing: Shift+↑/↓ is the viewport's line-scroll
@@ -539,6 +555,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
539
555
  onNotice?.('warn', refusal);
540
556
  return;
541
557
  }
558
+ onInteraction?.();
542
559
  dispatch({ type: 'input', segments });
543
560
  return;
544
561
  }
@@ -549,7 +566,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
549
566
  const marker = running ? glyphs.steer : glyphs.caret;
550
567
  const markerColor = running ? theme.toolRunning : theme.primary;
551
568
  const placeholder = running
552
- ? `Type to steer the run, Esc to abort${glyphs.ellipsis}`
569
+ ? `Type to steer the run, Esc twice to interrupt${glyphs.ellipsis}`
553
570
  : agentMode === 'plan'
554
571
  ? `Describe what you want to build; I'll research and plan it first${glyphs.ellipsis}`
555
572
  : `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
@@ -562,9 +579,9 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
562
579
  // The caret is drawn as one inverse cell, split at the column
563
580
  // `layoutComposer` reported — the same width table it wrapped with (I-9).
564
581
  const split = splitRowAtColumn(row, layout.cursorCol);
565
- return (_jsxs(_Fragment, { children: [renderSegments(split.before, `b${i}.`), _jsx(Text, { inverse: true, children: split.at ? split.at.text : ' ' }), renderSegments(split.after, `a${i}.`)] }));
582
+ return (_jsxs(_Fragment, { children: [renderSegments(split.before, `b${i}.`), _jsx(PromptCaret, { text: split.at ? split.at.text : ' ', active: isActive && cursorVisible, reducedMotion: reducedMotion, colorLevel: caps.colorLevel, resetKey: caretResetKey, color: split.at?.kind === 'token' ? tokenColor : undefined }), renderSegments(split.after, `a${i}.`)] }));
566
583
  });
567
- const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (_jsx(Text, { wrap: "truncate", color: theme.muted, children: placeholder })) : (rowNodes.map((node, i) => overflow && i === rowNodes.length - 1 ? (
584
+ const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [_jsx(PromptCaret, { text: String.fromCodePoint(placeholder.codePointAt(0)), active: isActive && cursorVisible, reducedMotion: reducedMotion, colorLevel: caps.colorLevel, color: theme.muted, resetKey: caretResetKey }), placeholder.slice(String.fromCodePoint(placeholder.codePointAt(0)).length)] })) : (rowNodes.map((node, i) => overflow && i === rowNodes.length - 1 ? (
568
585
  // The overflow indicator rides the LAST rendered row rather than
569
586
  // taking one of its own: an extra row would put the composer's
570
587
  // real height one above the `draftRows` it just reported (I-8).
@@ -579,5 +596,5 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
579
596
  // the user is typing — `↓ 9` becomes `↓ 10` becomes `↓ 100` — and every
580
597
  // change would otherwise re-wrap a draft that fills the line.
581
598
  _jsx(Box, { flexShrink: 0, alignSelf: "flex-start", marginLeft: 1, width: chip.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: chip.text }) }))] }));
582
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupVisible && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows, layout: popupLayout })), bordered ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: bordered.color, paddingX: 1, children: inputRow })) : (inputRow)] }));
599
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupVisible && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows, layout: popupLayout })), (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: borderColor, paddingX: 1, children: inputRow }))] }));
583
600
  }
@@ -21,6 +21,7 @@ import type { UsageTotal } from '../agent/reducer.js';
21
21
  import type { ContextUsageSnapshot } from '../compaction/types.js';
22
22
  import { type AgentMode } from '../agent/agent-mode.js';
23
23
  interface StatusBarProps {
24
+ interruptHint?: string;
24
25
  model: string;
25
26
  provider: string;
26
27
  usageTotal: UsageTotal;
@@ -51,17 +52,6 @@ interface StatusBarProps {
51
52
  * Ctrl+L redraw carrier — invariant I-5 (spec §4.13). See `redrawChar`.
52
53
  */
53
54
  redrawNonce?: number;
54
- /**
55
- * The session mode. THIS IS THE GUARANTEED MODE INDICATOR (plan-mode §6.4 /
56
- * P1-6): the composer chip disappears on a short terminal, under
57
- * `hints: false`, and in inline render mode, but `StatusBar` renders in both
58
- * `AppShell` branches and its left cluster has no opt-out and no width gate.
59
- *
60
- * Design intent, stated here so it survives a later refactor: the status bar
61
- * must ALWAYS name a non-default mode; the chip may. Do not give this a
62
- * `cols >= N` breakpoint, and do not move it into the right cluster — those
63
- * children are flexible and drop characters under pressure.
64
- */
65
55
  agentMode?: AgentMode;
66
56
  /** Non-null while a deferred `plan -> build` waits for `agent_end`. */
67
57
  pendingAgentMode?: AgentMode | null;
@@ -1,4 +1,5 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import stringWidth from 'string-width';
2
3
  import { Box, Text, useStdout } from 'ink';
3
4
  import { pickGlyphs } from './glyphs.js';
4
5
  import { formatCost, formatTokens, formatDuration, promptTokensOf } from '../agent/usage.js';
@@ -95,6 +96,11 @@ export function StatusBar(props) {
95
96
  : pendingAgentMode
96
97
  ? `${MODE_LABEL[agentMode]} ${glyphs.arrowRight} ${MODE_LABEL[pendingAgentMode]}`
97
98
  : MODE_LABEL[agentMode];
99
+ const actionHints = [running ? (props.interruptHint ?? 'Esc abort') : '',
100
+ servicesActive ? `Ctrl+C stop ${servicesActive.live}` : '',
101
+ scrolledLines > 0 ? 'PgDn down' : ''].filter(Boolean).join(' | ');
102
+ const hintCols = actionHints ? Math.min(Math.max(0, cols - 1), stringWidth(actionHints) + 2) : 0;
103
+ const detailCols = Math.max(0, cols - 1 - hintCols);
98
104
  return (
99
105
  // `flexShrink={0}` on the left cluster is not cosmetic. When the two clusters
100
106
  // over-subscribe the row, yoga shrinks the flexible children and Ink's text
@@ -102,11 +108,11 @@ export function StatusBar(props) {
102
108
  // and "? hel" rather than truncating cleanly at one end. Pinning the left
103
109
  // cluster keeps the run status and model intact and makes the degradation
104
110
  // land in one predictable place.
105
- _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [redrawChar(redrawNonce), statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), teamActive && (_jsxs(Text, { color: theme.toolRunning, bold: true, children: [' ', cols >= TEAM_LIMITS.statusCompactCols
106
- ? `agents ${teamActive.running}/${teamActive.total}`
107
- : `[${teamActive.running}]`] })), retryActive && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', formatRetryChip(retryActive, cols)] })), todoActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= TODO_LIMITS.statusCompactCols
108
- ? `todo ${todoActive.done}/${todoActive.total}`
109
- : `[${todoActive.done}/${todoActive.total}]`] })), servicesActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= PROC_LIMITS.statusCompactCols
110
- ? `svc ${servicesActive.live}`
111
- : `[${servicesActive.live}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [compactionActive && cols >= COMPACTION_LIMITS.statusCompactCols && (_jsxs(Text, { color: compactionActive.inFlight ? theme.accent : theme.muted, children: [compactionActive.inFlight ? 'compacting' : 'compact', ' '] })), fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= ABSOLUTE_PAIR_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: [" ", absolutePair] })), cols >= SESSION_TOTAL_MIN_COLS && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
111
+ _jsxs(Box, { width: cols, flexDirection: "row", justifyContent: "space-between", flexShrink: 0, height: 1, overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(redrawNonce) }) }), hintCols > 0 && (_jsx(Box, { width: hintCols, flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [actionHints, ' '] }) })), _jsxs(Box, { display: detailCols > 0 ? 'flex' : 'none', width: detailCols, flexShrink: 0, flexDirection: "row", justifyContent: "space-between", overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, overflow: "hidden", children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), teamActive && (_jsxs(Text, { color: theme.toolRunning, bold: true, children: [' ', cols >= TEAM_LIMITS.statusCompactCols
112
+ ? `agents ${teamActive.running}/${teamActive.total}`
113
+ : `[${teamActive.running}]`] })), retryActive && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', formatRetryChip(retryActive, cols)] })), todoActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= TODO_LIMITS.statusCompactCols
114
+ ? `todo ${todoActive.done}/${todoActive.total}`
115
+ : `[${todoActive.done}/${todoActive.total}]`] })), servicesActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= PROC_LIMITS.statusCompactCols
116
+ ? `svc ${servicesActive.live}`
117
+ : `[${servicesActive.live}]`] })), cols >= 60 && (cols >= 110 || (!running && !servicesActive && scrolledLines === 0)) && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [compactionActive && cols >= COMPACTION_LIMITS.statusCompactCols && (_jsxs(Text, { color: compactionActive.inFlight ? theme.accent : theme.muted, children: [compactionActive.inFlight ? 'compacting' : 'compact', ' '] })), fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= ABSOLUTE_PAIR_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: [" ", absolutePair] })), cols >= SESSION_TOTAL_MIN_COLS && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] })] }));
112
118
  }
@@ -1,24 +1,8 @@
1
- /**
2
- * Toast strip (spec §4.8). Ephemeral acks created by App/commands and
3
- * auto-dismissed by App's TTL effect; the reducer stays timer-free.
4
- *
5
- * In `fullscreen` this ALWAYS occupies exactly one row — an empty row when there
6
- * is nothing to say. Rendering conditionally would make the viewport height (and
7
- * therefore the whole transcript) jump every time a toast appears or expires.
8
- * There is deliberately no `rows` threshold: jitter is a layout property, not a
9
- * space property, and a short terminal suffers it proportionally MORE, so the
10
- * tier that most needs the fixed row is exactly the one a threshold would skip.
11
- * Only the newest toast is shown; older ones collapse into a `+N` counter.
12
- *
13
- * `inline` keeps the v0.2.0 conditional 0–3 row stack: without a fixed frame,
14
- * growing and shrinking is just normal document flow.
15
- */
1
+ /** Fixed one-row toast strip; only the newest toast is shown. */
16
2
  import React from 'react';
17
3
  import type { Theme } from './theme.js';
18
4
  import type { Toast } from '../agent/reducer.js';
19
- import type { RenderMode } from './layout/frame.js';
20
- export declare function ToastStack({ toasts, theme, mode, }: {
5
+ export declare function ToastStack({ toasts, theme, }: {
21
6
  toasts: Toast[];
22
7
  theme: Theme;
23
- mode?: RenderMode;
24
8
  }): React.ReactElement | null;
@@ -1,6 +1,5 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- const MAX_VISIBLE = 3;
4
3
  function toastColor(level, theme) {
5
4
  switch (level) {
6
5
  case 'success':
@@ -25,14 +24,8 @@ function toastGlyph(level, theme) {
25
24
  return theme.symbols.info;
26
25
  }
27
26
  }
28
- export function ToastStack({ toasts, theme, mode = 'inline', }) {
29
- if (mode === 'fullscreen') {
30
- const latest = toasts[toasts.length - 1];
31
- const extra = Math.max(0, toasts.length - 1);
32
- return (_jsx(Box, { flexShrink: 0, children: latest ? (_jsxs(Text, { wrap: "truncate", color: toastColor(latest.level, theme), children: [' ', toastGlyph(latest.level, theme), " ", latest.text, extra > 0 ? ` +${extra}` : ''] })) : (_jsx(Text, { children: " " })) }));
33
- }
34
- const shown = toasts.slice(-MAX_VISIBLE);
35
- if (shown.length === 0)
36
- return null;
37
- return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: shown.map((t) => (_jsxs(Text, { color: toastColor(t.level, theme), children: [toastGlyph(t.level, theme), " ", t.text] }, t.id))) }));
27
+ export function ToastStack({ toasts, theme, }) {
28
+ const latest = toasts[toasts.length - 1];
29
+ const extra = Math.max(0, toasts.length - 1);
30
+ return (_jsx(Box, { flexShrink: 0, children: latest ? (_jsxs(Text, { wrap: "truncate", color: toastColor(latest.level, theme), children: [' ', toastGlyph(latest.level, theme), " ", latest.text, extra > 0 ? ` +${extra}` : ''] })) : (_jsx(Text, { children: " " })) }));
38
31
  }
@@ -1,65 +1,23 @@
1
- /**
2
- * Transcript (spec §3.8). Splits the entry list into a settled prefix rendered
3
- * once into Ink's `<Static>` (native terminal scrollback, never re-rendered)
4
- * and a live tail rendered normally. This kills per-token full re-renders and
5
- * flicker on long/fast sessions.
6
- *
7
- * `computeSettledCount` is pure and exported for `transcript-static.test.ts`.
8
- * The settled boundary is held MONOTONIC at render time: a previously-settled
9
- * entry never leaves `<Static>` (which cannot un-print it) even if it later
10
- * becomes expanded, so the live tail can never duplicate a scrolled-off entry.
11
- *
12
- * `EntryView` is shared by both render paths ON PURPOSE (§4.3 / §12). Forking it
13
- * to freeze inline's v0.2.0 output would mean writing every future entry-layer
14
- * change twice and watching the two drift; inline is a degradation path, not a
15
- * parallel product. What inline still guarantees is its GEOMETRY contract — no
16
- * fixed frame, `<Static>` keeps the settled history, no private ANSI — not its
17
- * pixel-for-pixel appearance.
18
- *
19
- * Two things changed with tui-render-performance:
20
- * - `EntryView` is `React.memo`'d with an explicit comparator (L2 / R3), which
21
- * is only worth anything because `mapEntry` already preserves object
22
- * identity for untouched entries;
23
- * - `TranscriptList` (the FULL-SCREEN body) mounts only the entries inside the
24
- * viewport and replaces the rest with two spacer boxes (L3 / R1), and the
25
- * inline `Transcript` clamps its live region so `ink.js:121` is unreachable
26
- * (L5 / R6).
27
- */
1
+ /** Virtualized full-screen transcript and entry cards. */
2
+ /** Virtualized full-screen transcript and shared entry cards. */
28
3
  import React from 'react';
29
4
  import { type Theme } from './theme.js';
30
5
  import type { TermCapabilities } from './capabilities.js';
31
6
  import { type DensityMode } from './density.js';
32
7
  import type { Entry } from '../agent/reducer.js';
33
- import type { RenderMode } from './layout/frame.js';
34
8
  import { type TailSink } from './layout/follow-state.js';
35
9
  import type { HeightStore } from './use-height-store.js';
36
- /**
37
- * Largest prefix length P such that `entries[0..P)` are all terminal, none is
38
- * within the last `LIVE_TAIL` entries, and none is currently expanded. Pure and
39
- * non-decreasing as entries terminalize (expanding a card can lower it — the
40
- * component clamps with a monotonic high-water mark to avoid Static duplication).
41
- */
42
- export declare function computeSettledCount(entries: Entry[], expandedToolIds: Record<string, true>): number;
43
10
  interface TranscriptProps {
44
11
  entries: Entry[];
45
12
  expandedToolIds: Record<string, true>;
46
13
  thinkingVisible: boolean;
47
14
  reducedMotion: boolean;
48
15
  density: DensityMode;
49
- /**
50
- * The render mode, threaded from `App` as a plain scalar prop.
51
- *
52
- * It decides whether the collapsed thinking marker may offer `ctrl+t` (D-16):
53
- * inline prints settled entries into `<Static>`, which cannot re-print them.
54
- * NOT read from a module-level singleton — `render-memo.test.tsx` asserts prop
55
- * stability across frames, and a hidden read would be invisible to it.
56
- */
57
- mode: RenderMode;
58
16
  /**
59
17
  * Wall-clock seconds, threaded from `App`'s existing 200 ms ticker, for the
60
18
  * live tool card's stall row (agent-activity-presentation-live §3.3.4 / D-36).
61
19
  *
62
- * BOTH MAP CALLBACKS BELOW DECIDE PER ENTRY WHETHER TO PASS IT ON, and neither
20
+ * The entry map decides per entry whether to pass it on, and it
63
21
  * may pass it unconditionally: `ToolCard` is `React.memo` with the default
64
22
  * comparator, so a prop that changes every second on every card would defeat
65
23
  * that boundary for the whole transcript.
@@ -76,15 +34,12 @@ export interface EntryViewProps {
76
34
  thinkingVisible: boolean;
77
35
  reducedMotion: boolean;
78
36
  density: DensityMode;
79
- mode: RenderMode;
80
37
  theme: Theme;
81
38
  caps: TermCapabilities;
82
- /** Inline mode only (L5); `undefined` in full-screen, which means no clamp. */
83
- liveClampRows?: number;
84
39
  /** Wall-clock seconds; set ONLY on a running tool entry (§3.3.4 / P1-3). */
85
40
  nowSec?: number;
86
41
  }
87
- declare function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, mode, theme, caps, liveClampRows, nowSec, }: EntryViewProps): React.ReactElement | null;
42
+ declare function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, theme, caps, nowSec, }: EntryViewProps): React.ReactElement | null;
88
43
  /**
89
44
  * THE memo boundary that makes a long transcript cheap (L2 / R3).
90
45
  *
@@ -100,27 +55,7 @@ declare function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reduced
100
55
  * test rather than a silent regression.
101
56
  */
102
57
  export declare const EntryView: React.MemoExoticComponent<typeof EntryViewImpl>;
103
- /**
104
- * Full-screen transcript body — the same entry renderers with NO `<Static>`.
105
- *
106
- * `<Static>` prints above the live frame; once the frame is `rows - 1` tall
107
- * there is exactly one visible line up there, so the mechanism stops being a
108
- * history view and starts being a leak. Full-screen therefore owns its history
109
- * inside `ScrollViewport` instead.
110
- *
111
- * `computeSettledCount` is deliberately NOT called here: its only job is to keep
112
- * an entry that already reached `<Static>` from flowing back into the live
113
- * region, and with no Static there is nothing to protect. The settled high-water
114
- * mark feeds nothing else — `/save` serializes `entries` in full — so leaving it
115
- * un-advanced has no side effect.
116
- *
117
- * VIRTUALISED (L3 / R1). `windowSize` is now the SCROLL HORIZON — how far back
118
- * the user can scroll — and no longer the rendering budget, because entries
119
- * outside the viewport are not mounted at all. What bounds the frame is
120
- * `selectWindow`, and the two spacers are what keep the scroll maths honest
121
- * while they are absent.
122
- */
123
- export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }: TranscriptProps & {
58
+ export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }: TranscriptProps & {
124
59
  windowSize: number;
125
60
  /** The viewport's own width — narrower than the terminal when the rail is up. */
126
61
  cols: number;
@@ -153,5 +88,4 @@ export declare function TranscriptList({ entries, expandedToolIds, thinkingVisib
153
88
  */
154
89
  tailSink?: TailSink;
155
90
  }): React.ReactElement;
156
- export declare function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, }: TranscriptProps): React.ReactElement;
157
91
  export {};