@aragon-agent/cli 0.5.6 → 0.5.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 (156) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/README.md +1429 -1105
  3. package/dist/agent/coalesce.d.ts +13 -0
  4. package/dist/agent/coalesce.js +50 -11
  5. package/dist/agent/controller.d.ts +141 -2
  6. package/dist/agent/controller.js +396 -4
  7. package/dist/agent/entry-limits.d.ts +65 -0
  8. package/dist/agent/entry-limits.js +133 -0
  9. package/dist/agent/headless.d.ts +8 -0
  10. package/dist/agent/headless.js +40 -0
  11. package/dist/agent/reducer.d.ts +253 -3
  12. package/dist/agent/reducer.js +553 -28
  13. package/dist/agent/retry-view.d.ts +68 -0
  14. package/dist/agent/retry-view.js +95 -0
  15. package/dist/agent/system-prompt.d.ts +11 -0
  16. package/dist/agent/system-prompt.js +2 -0
  17. package/dist/cli.js +122 -4
  18. package/dist/commands/builtins.d.ts +1 -1
  19. package/dist/commands/builtins.js +313 -2
  20. package/dist/commands/perf.d.ts +45 -0
  21. package/dist/commands/perf.js +78 -0
  22. package/dist/config/cli-commands.d.ts +53 -2
  23. package/dist/config/cli-commands.js +136 -3
  24. package/dist/config/env.d.ts +6 -1
  25. package/dist/config/env.js +131 -1
  26. package/dist/config/load.d.ts +63 -0
  27. package/dist/config/load.js +125 -2
  28. package/dist/config/schema.d.ts +273 -10
  29. package/dist/config/schema.js +250 -7
  30. package/dist/config/store.js +20 -1
  31. package/dist/fast/critique.d.ts +43 -0
  32. package/dist/fast/critique.js +62 -0
  33. package/dist/fast/digest.d.ts +42 -0
  34. package/dist/fast/digest.js +85 -0
  35. package/dist/fast/frames.d.ts +99 -0
  36. package/dist/fast/frames.js +180 -0
  37. package/dist/fast/limits.d.ts +98 -0
  38. package/dist/fast/limits.js +98 -0
  39. package/dist/fast/prompt.d.ts +72 -0
  40. package/dist/fast/prompt.js +94 -0
  41. package/dist/fast/resolve.d.ts +43 -0
  42. package/dist/fast/resolve.js +105 -0
  43. package/dist/fast/review-call.d.ts +99 -0
  44. package/dist/fast/review-call.js +130 -0
  45. package/dist/fast/reviewer.d.ts +248 -0
  46. package/dist/fast/reviewer.js +664 -0
  47. package/dist/fast/types.d.ts +128 -0
  48. package/dist/fast/types.js +14 -0
  49. package/dist/fast/wiring.d.ts +170 -0
  50. package/dist/fast/wiring.js +316 -0
  51. package/dist/logging/install.d.ts +15 -1
  52. package/dist/logging/install.js +37 -4
  53. package/dist/logging/logger.d.ts +1 -1
  54. package/dist/session/persist.js +57 -1
  55. package/dist/team/normalize.d.ts +26 -1
  56. package/dist/team/normalize.js +23 -3
  57. package/dist/team/prompt.d.ts +11 -0
  58. package/dist/team/prompt.js +10 -0
  59. package/dist/team/report.d.ts +18 -0
  60. package/dist/team/report.js +27 -1
  61. package/dist/team/runtime.d.ts +13 -2
  62. package/dist/team/runtime.js +33 -6
  63. package/dist/team/subagent.d.ts +19 -1
  64. package/dist/team/subagent.js +72 -10
  65. package/dist/team/task-tool.d.ts +26 -0
  66. package/dist/team/task-tool.js +48 -4
  67. package/dist/team/types.d.ts +80 -1
  68. package/dist/tools/bash-tool.js +19 -3
  69. package/dist/tools/diff.d.ts +25 -5
  70. package/dist/tools/diff.js +72 -41
  71. package/dist/tools/file-change-store.d.ts +34 -0
  72. package/dist/tools/file-change-store.js +57 -0
  73. package/dist/tools/fs-tools.d.ts +26 -0
  74. package/dist/tools/fs-tools.js +62 -5
  75. package/dist/tools/index.d.ts +31 -0
  76. package/dist/tools/index.js +7 -1
  77. package/dist/tools/patch.d.ts +115 -0
  78. package/dist/tools/patch.js +453 -0
  79. package/dist/tools/terminal-output.d.ts +107 -0
  80. package/dist/tools/terminal-output.js +198 -0
  81. package/dist/tools/tool-output-store.d.ts +68 -0
  82. package/dist/tools/tool-output-store.js +102 -0
  83. package/dist/ui/ActivityLine.d.ts +48 -0
  84. package/dist/ui/ActivityLine.js +22 -0
  85. package/dist/ui/App.d.ts +1 -4
  86. package/dist/ui/App.js +357 -44
  87. package/dist/ui/BottomStatusRow.d.ts +43 -0
  88. package/dist/ui/BottomStatusRow.js +14 -0
  89. package/dist/ui/Composer.d.ts +1 -6
  90. package/dist/ui/Composer.js +2 -2
  91. package/dist/ui/Markdown.d.ts +7 -1
  92. package/dist/ui/Markdown.js +15 -48
  93. package/dist/ui/PromptInput.d.ts +1 -15
  94. package/dist/ui/PromptInput.js +1 -38
  95. package/dist/ui/StatusBar.d.ts +50 -0
  96. package/dist/ui/StatusBar.js +7 -3
  97. package/dist/ui/TeamPanel.d.ts +5 -4
  98. package/dist/ui/TeamPanel.js +1 -1
  99. package/dist/ui/TodoPanel.d.ts +5 -4
  100. package/dist/ui/Transcript.d.ts +80 -5
  101. package/dist/ui/Transcript.js +179 -11
  102. package/dist/ui/activity-phrases.d.ts +33 -0
  103. package/dist/ui/activity-phrases.js +50 -0
  104. package/dist/ui/entries/AssistantEntry.d.ts +38 -1
  105. package/dist/ui/entries/AssistantEntry.js +34 -3
  106. package/dist/ui/entries/DiffView.d.ts +67 -0
  107. package/dist/ui/entries/DiffView.js +103 -0
  108. package/dist/ui/entries/FastCard.d.ts +53 -0
  109. package/dist/ui/entries/FastCard.js +87 -0
  110. package/dist/ui/entries/RetryCard.d.ts +56 -0
  111. package/dist/ui/entries/RetryCard.js +60 -0
  112. package/dist/ui/entries/TeamCard.d.ts +9 -1
  113. package/dist/ui/entries/TeamCard.js +28 -2
  114. package/dist/ui/entries/TodoCard.d.ts +9 -1
  115. package/dist/ui/entries/TodoCard.js +23 -1
  116. package/dist/ui/entries/ToolCard.d.ts +50 -1
  117. package/dist/ui/entries/ToolCard.js +104 -7
  118. package/dist/ui/entries/ToolPreview.d.ts +2 -2
  119. package/dist/ui/entries/UserEntry.d.ts +9 -1
  120. package/dist/ui/entries/UserEntry.js +17 -1
  121. package/dist/ui/exit-snapshot.d.ts +7 -0
  122. package/dist/ui/glyphs.d.ts +9 -0
  123. package/dist/ui/glyphs.js +2 -0
  124. package/dist/ui/layout/AppShell.d.ts +13 -22
  125. package/dist/ui/layout/AppShell.js +3 -38
  126. package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
  127. package/dist/ui/layout/MeasuredEntry.js +30 -0
  128. package/dist/ui/layout/ScrollViewport.js +8 -2
  129. package/dist/ui/layout/viewport-geometry.d.ts +28 -0
  130. package/dist/ui/layout/viewport-geometry.js +27 -0
  131. package/dist/ui/layout/virtual-window.d.ts +100 -0
  132. package/dist/ui/layout/virtual-window.js +308 -0
  133. package/dist/ui/live-clamp.d.ts +42 -0
  134. package/dist/ui/live-clamp.js +74 -0
  135. package/dist/ui/markdown-blocks.d.ts +25 -1
  136. package/dist/ui/markdown-blocks.js +43 -0
  137. package/dist/ui/overlays/HelpOverlay.js +16 -2
  138. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  139. package/dist/ui/overlays/SettingsScreen.js +147 -1
  140. package/dist/ui/render-cache.d.ts +81 -0
  141. package/dist/ui/render-cache.js +195 -0
  142. package/dist/ui/render-governor.d.ts +54 -0
  143. package/dist/ui/render-governor.js +78 -0
  144. package/dist/ui/transcript-text.d.ts +11 -0
  145. package/dist/ui/transcript-text.js +32 -0
  146. package/dist/ui/use-height-store.d.ts +31 -0
  147. package/dist/ui/use-height-store.js +76 -0
  148. package/dist/ui/use-render-governor.d.ts +39 -0
  149. package/dist/ui/use-render-governor.js +57 -0
  150. package/dist/ui/use-startup-notices.d.ts +9 -1
  151. package/dist/ui/use-startup-notices.js +35 -1
  152. package/dist/ui/use-wheel-routing.d.ts +16 -32
  153. package/dist/ui/use-wheel-routing.js +21 -38
  154. package/package.json +2 -2
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. package/dist/ui/layout/regions.js +0 -52
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The bottom status row — the one already-budgeted row above the composer, now
3
+ * shared by two occupants (agent-activity-presentation §3.2.3 / D-17 / P0-1).
4
+ *
5
+ * THE ACTIVITY LINE IS NOT A NEW `AppShell` SLOT, AND THIS FILE IS WHY.
6
+ *
7
+ * `viewportRows()` (`layout/budget.ts`) is the ONE function that says how many
8
+ * rows the viewport gets, and it enumerates exactly `header + toast + composer +
9
+ * status`. In full-screen `AppShell`'s root is `height={frameHeight(rows)}` with
10
+ * `overflow="hidden"`, the bottom chrome box is `flexShrink={0}` and the viewport
11
+ * box is its only `flexShrink={1}` child — so an extra chrome row does not make
12
+ * the frame taller, Yoga takes the row out of the TRANSCRIPT. Meanwhile
13
+ * `viewportRows()` keeps returning the old number to `ScrollViewport`,
14
+ * `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`.
15
+ * The transcript would draw one row shorter than every consumer believes, ONLY
16
+ * WHILE A RUN IS IN FLIGHT, so the layout would shift on submit and shift back on
17
+ * `agent_end`.
18
+ *
19
+ * `ToastStack.tsx` already legislates this for itself: *"in `fullscreen` this
20
+ * ALWAYS occupies exactly one row — an empty row when there is nothing to say.
21
+ * Rendering conditionally would make the viewport height (and therefore the whole
22
+ * transcript) jump every time a toast appears or expires."* A run starts and ends
23
+ * far more often than a toast appears and expires, so the activity line is the
24
+ * STRONGER case for that rule, not an exception to it.
25
+ *
26
+ * Sharing the row means `budget.ts` and `AppShell.tsx` come out of this round
27
+ * UNMODIFIED — and `budget.test.ts` asserts that they did (AC-8a, DoD #8).
28
+ *
29
+ * PRECEDENCE: a toast wins. A transient ack is a RESPONSE TO THE USER, and the
30
+ * row nearest the input belongs to it.
31
+ */
32
+ import React from 'react';
33
+ import type { Theme } from './theme.js';
34
+ import type { Toast } from '../agent/reducer.js';
35
+ import type { RenderMode } from './layout/frame.js';
36
+ export interface BottomStatusRowProps {
37
+ mode: RenderMode;
38
+ toasts: Toast[];
39
+ /** `<ActivityLine>` while a run owns the screen, else `null`. */
40
+ activity: React.ReactNode;
41
+ theme: Theme;
42
+ }
43
+ export declare function BottomStatusRow({ mode, toasts, activity, theme, }: BottomStatusRowProps): React.ReactElement | null;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ import { ToastStack } from './ToastStack.js';
4
+ export function BottomStatusRow({ mode, toasts, activity, theme, }) {
5
+ // A toast wins the row; so does an empty toast stack in full-screen, whose
6
+ // blank `<Text> </Text>` IS the budgeted row and must keep being emitted.
7
+ if (toasts.length > 0 || !activity) {
8
+ return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
9
+ }
10
+ // Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
11
+ // branch is already a conditional 0-3 row stack, and this is simply one more
12
+ // conditional row above it.
13
+ return _jsx(Box, { flexShrink: 0, children: activity });
14
+ }
@@ -35,14 +35,9 @@ export interface ComposerProps {
35
35
  /** Mirror of the controller's effective mode; drives the chip and the border. */
36
36
  agentMode: AgentMode;
37
37
  popupMaxRows?: number;
38
- /** Pure pass-through to `PromptInput` (mouse-wheel-region-routing §5.6). */
39
- historyIntent?: {
40
- dir: 'up' | 'down';
41
- nonce: number;
42
- };
43
38
  theme: Theme;
44
39
  caps: TermCapabilities;
45
40
  onSubmit: (text: string) => void;
46
41
  onHelp?: () => void;
47
42
  }
48
- export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
43
+ export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
@@ -56,7 +56,7 @@ function hintText(running, submitCount, glyphs, mode) {
56
56
  '? help',
57
57
  ].join(dot);
58
58
  }
59
- export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }) {
59
+ export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
60
60
  const [hasDraft, setHasDraft] = useState(false);
61
61
  const onDraftChange = useCallback((next) => setHasDraft(next), []);
62
62
  // Plan mode inserts ONE branch, above `draft` and below `running`. The
@@ -73,7 +73,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
73
73
  : theme.idleBorder ?? theme.border;
74
74
  const glyphs = pickGlyphs(caps);
75
75
  const visible = showHint && hintsEnabled;
76
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, historyIntent: historyIntent }), visible && (
76
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
77
77
  // The chip rides the EXISTING hint row rather than adding one, so it
78
78
  // costs zero rows on a 24-row terminal.
79
79
  _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(running, submitCount, glyphs, agentMode) })] }))] }));
@@ -7,7 +7,13 @@
7
7
  * *italic* / `code` / ~~strike~~. Anything else renders as plain text. Still
8
8
  * intentionally small — not a CommonMark implementation.
9
9
  *
10
- * The grammar lives in `markdown-blocks.ts`; this file only renders.
10
+ * The grammar lives in `markdown-blocks.ts`; this file only renders. That was
11
+ * true of the file header long before it was true of the file
12
+ * (tui-render-performance R2): the block grammar used to run here, in the render
13
+ * body, on every frame. Both expensive halves are now memoised on the input
14
+ * string — `parseMarkdownCached` for the AST, `highlightCached` for the ANSI —
15
+ * while theme and glyphs stay render-time, so `/theme` still takes effect
16
+ * immediately (K-5).
11
17
  */
12
18
  import React from 'react';
13
19
  import type { Theme } from './theme.js';
@@ -1,54 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- import { highlight } from 'cli-highlight';
4
3
  import { pickGlyphs } from './glyphs.js';
5
4
  import { railBorderProps } from './layout/Gutter.js';
6
- import { formatTable, isRule, isTableDivider, parseHeading, parseInline, parseTableRow, tableAlignments, } from './markdown-blocks.js';
7
- const FENCE = /^```(\w*)\s*$/;
5
+ import { highlightCached, parseMarkdownCached } from './render-cache.js';
6
+ import { formatTable, isRule, parseHeading, parseInline, } from './markdown-blocks.js';
8
7
  /** Width of the `---` rule. Fixed so it cannot force a wrap on narrow frames. */
9
8
  const RULE_WIDTH = 24;
10
9
  export function Markdown({ text, theme, caps }) {
11
10
  const glyphs = pickGlyphs(caps);
12
- const lines = text.split('\n');
13
- const blocks = [];
14
- let i = 0;
15
- let key = 0;
16
- while (i < lines.length) {
17
- const line = lines[i] ?? '';
18
- const fence = FENCE.exec(line.trim());
19
- if (fence) {
20
- const lang = fence[1] || '';
21
- const codeLines = [];
22
- i += 1;
23
- while (i < lines.length && !FENCE.test((lines[i] ?? '').trim())) {
24
- codeLines.push(lines[i] ?? '');
25
- i += 1;
26
- }
27
- i += 1; // skip closing fence
28
- blocks.push(_jsx(CodeBlock, { code: codeLines.join('\n'), lang: lang, theme: theme, glyphs: glyphs }, `b${key++}`));
29
- continue;
30
- }
31
- // A pipe row followed by a divider row starts a table; consume the run.
32
- const headerCells = parseTableRow(line);
33
- if (headerCells && i + 1 < lines.length && isTableDivider(lines[i + 1] ?? '')) {
34
- const align = tableAlignments(lines[i + 1] ?? '');
35
- const rows = [headerCells];
36
- i += 2;
37
- while (i < lines.length) {
38
- const cells = parseTableRow(lines[i] ?? '');
39
- if (!cells)
40
- break;
41
- rows.push(cells);
42
- i += 1;
43
- }
44
- const formatted = formatTable(rows, align);
45
- blocks.push(_jsx(Box, { flexDirection: "column", children: formatted.map((row, r) => (_jsx(Text, { wrap: "truncate", bold: r === 0, color: r === 0 ? theme.accent : undefined, children: row }, r))) }, `b${key++}`));
46
- continue;
11
+ const blocks = parseMarkdownCached(text);
12
+ return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, i) => renderBlock(block, i, theme, glyphs)) }));
13
+ }
14
+ function renderBlock(block, key, theme, glyphs) {
15
+ switch (block.kind) {
16
+ case 'code':
17
+ return (_jsx(CodeBlock, { code: block.code, lang: block.lang, theme: theme, glyphs: glyphs }, `b${key}`));
18
+ case 'table': {
19
+ const formatted = formatTable(block.rows, block.align);
20
+ return (_jsx(Box, { flexDirection: "column", children: formatted.map((row, r) => (_jsx(Text, { wrap: "truncate", bold: r === 0, color: r === 0 ? theme.accent : undefined, children: row }, r))) }, `b${key}`));
47
21
  }
48
- blocks.push(_jsx(InlineLine, { line: line, theme: theme, glyphs: glyphs }, `b${key++}`));
49
- i += 1;
22
+ default:
23
+ return _jsx(InlineLine, { line: block.text, theme: theme, glyphs: glyphs }, `b${key}`);
50
24
  }
51
- return _jsx(Box, { flexDirection: "column", children: blocks });
52
25
  }
53
26
  /**
54
27
  * Fenced code. The round border is gone (§4.3): nested inside a tool card it
@@ -57,13 +30,7 @@ export function Markdown({ text, theme, caps }) {
57
30
  * code" boundary for 1 column and 0 rows.
58
31
  */
59
32
  function CodeBlock({ code, lang, theme, glyphs, }) {
60
- let rendered = code;
61
- try {
62
- rendered = highlight(code, { language: lang || undefined, ignoreIllegals: true });
63
- }
64
- catch {
65
- rendered = code;
66
- }
33
+ const rendered = highlightCached(code, lang);
67
34
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [lang.length > 0 && (_jsx(Text, { color: theme.chip.fg, backgroundColor: theme.chip.bg, children: ` ${lang} ` })), _jsx(Text, { children: rendered })] }));
68
35
  }
69
36
  /** Six heading levels used to render identically; now they carry three weights. */
@@ -55,20 +55,6 @@ interface PromptInputProps {
55
55
  };
56
56
  /** Fires only when the draft flips empty ⇄ non-empty, for the border color. */
57
57
  onDraftChange?: (hasDraft: boolean) => void;
58
- /**
59
- * One prompt-history step, pushed by the wheel router when a notch lands on
60
- * the composer band (mouse-wheel-region-routing §5.6). `nonce` is monotonic
61
- * so two notches in the same direction are two steps.
62
- *
63
- * Applied through the SAME `verticalOrHistory` / popup-selection branches the
64
- * keyboard uses, deliberately: it means the wheel and `↑`/`↓` cannot drift
65
- * apart, and the "never clobber an in-progress draft" guard protects the
66
- * wheel for free.
67
- */
68
- historyIntent?: {
69
- dir: 'up' | 'down';
70
- nonce: number;
71
- };
72
58
  }
73
59
  /** Apply an edit intent as a pure buffer/cursor transform. */
74
60
  export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
@@ -98,5 +84,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
98
84
  end: number;
99
85
  query: string;
100
86
  } | null;
101
- export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, historyIntent, }: PromptInputProps): React.ReactElement;
87
+ export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }: PromptInputProps): React.ReactElement;
102
88
  export {};
@@ -127,7 +127,7 @@ function isControlSeq(input) {
127
127
  // ---------------------------------------------------------------------------
128
128
  // Component
129
129
  // ---------------------------------------------------------------------------
130
- export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, historyIntent, }) {
130
+ export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }) {
131
131
  const [buffer, setBuffer] = useState('');
132
132
  const [cursor, setCursor] = useState(0);
133
133
  const [historyIndex, setHistoryIndex] = useState(null);
@@ -261,43 +261,6 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
261
261
  recallDown();
262
262
  }
263
263
  };
264
- // --- Wheel-driven history step (mouse-wheel-region-routing §5.6). ---------
265
- // The applier closes over the buffer, the cursor and the popup state, so it
266
- // is re-created on every render and reached through a ref: depending on it
267
- // directly would re-fire the effect on every keystroke.
268
- const applyHistoryIntent = useRef(() => { });
269
- applyHistoryIntent.current = (dir) => {
270
- // Mirrors the keyboard branch exactly — with the completion popup open,
271
- // Up/Down move the selection rather than recalling history.
272
- if (popupItems.length > 0) {
273
- setSel(dir === 'up'
274
- ? Math.max(0, clampedSel - 1)
275
- : Math.min(popupItems.length - 1, clampedSel + 1));
276
- return;
277
- }
278
- verticalOrHistory(dir);
279
- };
280
- const historyIntentRef = useRef(historyIntent);
281
- historyIntentRef.current = historyIntent;
282
- // I-9: seeded from the incoming nonce during the FIRST render, so a mount
283
- // that inherits a nonce cannot replay it. This component really does remount
284
- // — `App` returns the terminal-too-small placeholder before `AppShell` —
285
- // so without the guard, shrinking the window mid-wheel and growing it back
286
- // recalls a history entry with no gesture behind it.
287
- const lastHistoryNonce = useRef(historyIntent?.nonce);
288
- const historyNonce = historyIntent?.nonce ?? 0;
289
- useEffect(() => {
290
- const current = historyIntentRef.current;
291
- if (!current || !isActive)
292
- return;
293
- if (current.nonce === lastHistoryNonce.current)
294
- return;
295
- lastHistoryNonce.current = current.nonce;
296
- applyHistoryIntent.current(current.dir);
297
- // Keyed on the nonce alone, same reasoning as `ScrollViewport`'s intent
298
- // effect: `historyIntent` is a fresh object on every parent render.
299
- // eslint-disable-next-line react-hooks/exhaustive-deps
300
- }, [historyNonce]);
301
264
  // --- Input handling. -----------------------------------------------------
302
265
  useInput((input, key) => {
303
266
  // FIRST STATEMENT, BEFORE THE POPUP BRANCH, AND IT HAS TO STAY THERE.
@@ -88,6 +88,56 @@ interface StatusBarProps {
88
88
  done: number;
89
89
  total: number;
90
90
  };
91
+ /**
92
+ * Live retry state (llm-api-retry-backoff §6.6), or absent when nothing is
93
+ * retrying — an ordinary session's bar is then unchanged.
94
+ *
95
+ * LEFT CLUSTER, after `teamActive`, and it DEGRADES. It is a run-state fact like
96
+ * `agents 3/5`, and the left cluster is `flexShrink={0}` so every column it takes
97
+ * comes out of the context gauge; below `RETRY_UI.statusCompactCols` it renders
98
+ * as `[r3]`.
99
+ *
100
+ * This does NOT contradict the "no `cols >= N` breakpoint" rule on `agentMode`
101
+ * above: that rule exists because the MODE must never be unreportable. The retry
102
+ * has a second, fuller home in the transcript card, which is exactly why it is
103
+ * the kind of readout that can afford to shrink.
104
+ */
105
+ retryActive?: {
106
+ attempt: number;
107
+ max: number;
108
+ secondsLeft: number;
109
+ };
110
+ /**
111
+ * Render-governor rung (tui-render-performance L4). `> 0` renders a muted
112
+ * `eco` chip.
113
+ *
114
+ * I-L4-2 — THE GOVERNOR IS USER-VISIBLE, and this is where. A user who cannot
115
+ * see why the stream got chunkier will conclude the model got slower, and
116
+ * degrading silently is the one thing a claim of robustness cannot afford.
117
+ * It sits in the RIGHT cluster next to the other derived numbers and is
118
+ * suppressed on a narrow terminal, which is the `teamActive` treatment rather
119
+ * than the `agentMode` one: the mode must never be unreportable, whereas an
120
+ * eco chip on a 50-column terminal costs the context gauge more than it says.
121
+ */
122
+ ecoRung?: number;
123
+ /**
124
+ * The live fast tier (fast-model-tier §6), or absent when it is off for this
125
+ * session — in which case an ordinary session's bar is byte-identical.
126
+ *
127
+ * RIGHT CLUSTER, next to `eco`, and it DEGRADES below
128
+ * `FAST_LIMITS.statusCompactCols`. This is the `teamActive` / `ecoRung`
129
+ * treatment rather than the `agentMode` one, and the distinction is the note
130
+ * on `agentMode` above: the mode must never be unreportable, whereas the fast
131
+ * tier has a fuller home in `/fast status` — which is the GUARANTEED reporting
132
+ * surface — and a chip on a 60-column terminal costs the context gauge more
133
+ * than it says.
134
+ *
135
+ * `inFlight` renders `fast*`, so a review that is taking a while is visible
136
+ * without a spinner competing with the run's own.
137
+ */
138
+ fastActive?: {
139
+ inFlight: boolean;
140
+ };
91
141
  }
92
142
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
93
143
  export {};
@@ -4,8 +4,12 @@ import { pickGlyphs } from './glyphs.js';
4
4
  import { formatCost, formatTokens, formatDuration } from '../agent/usage.js';
5
5
  import { buildGauge } from './gauge.js';
6
6
  import { MODE_LABEL } from '../agent/agent-mode.js';
7
+ import { formatRetryChip } from '../agent/retry-view.js';
8
+ import { FAST_LIMITS } from '../fast/limits.js';
7
9
  import { TEAM_LIMITS } from '../team/limits.js';
8
10
  import { TODO_LIMITS } from '../todo/limits.js';
11
+ /** Below this the `eco` chip costs the context gauge more than it is worth. */
12
+ const ECO_MIN_COLS = 72;
9
13
  /**
10
14
  * I-5 — DO NOT "CLEAN THIS UP". Ink short-circuits identical output at TWO
11
15
  * gates (`ink.js:132` and `log-update.js:13`), so a fixed frame whose React
@@ -25,7 +29,7 @@ function redrawChar(nonce) {
25
29
  return nonce % 2 === 0 ? ' ' : NBSP;
26
30
  }
27
31
  export function StatusBar(props) {
28
- const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, } = props;
32
+ const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, retryActive, ecoRung = 0, fastActive, } = props;
29
33
  const { stdout } = useStdout();
30
34
  const cols = stdout?.columns ?? 80;
31
35
  const running = status === 'running';
@@ -51,7 +55,7 @@ export function StatusBar(props) {
51
55
  // land in one predictable place.
52
56
  _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
53
57
  ? `agents ${teamActive.running}/${teamActive.total}`
54
- : `[${teamActive.running}]`] })), todoActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= TODO_LIMITS.statusCompactCols
58
+ : `[${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
55
59
  ? `todo ${todoActive.done}/${todoActive.total}`
56
- : `[${todoActive.done}/${todoActive.total}]`] })), 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: [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 >= 72 && _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)] }))] })] }));
60
+ : `[${todoActive.done}/${todoActive.total}]`] })), 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: [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 >= 72 && _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)] }))] })] }));
57
61
  }
@@ -10,10 +10,11 @@
10
10
  * not mounted at all, so a session that never delegates has zero rows and zero
11
11
  * layout shift.
12
12
  *
13
- * IT MUST BE MOUNTED INSIDE `AppShell`'s MEASURED BOTTOM BOX (I-10). The wheel
14
- * router reads the composer band boundary from `measureElement(bottomRef)`; a
15
- * panel rendered outside it makes that boundary stale the moment a dispatch
16
- * starts, and the wheel begins scrolling prompt history over the transcript.
13
+ * IT MUST BE MOUNTED IN `AppShell`'s BOTTOM CHROME (I-10), not in the middle
14
+ * band. The roster is transient status about the run in progress, so it belongs
15
+ * with the other transient rows — toast, composer, status bar — rather than
16
+ * inside the scrolling transcript, where it would be interleaved with history
17
+ * and scroll out of sight exactly when the user wants to watch it.
17
18
  */
18
19
  import React from 'react';
19
20
  import type { Theme } from './theme.js';
@@ -120,7 +120,7 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
120
120
  // `reducedMotion` and a non-Unicode terminal fall back to the same
121
121
  // static marker, exactly as `AssistantEntry` and `ToolCard` already do.
122
122
  const animate = isRunning(run) && !reducedMotion && caps.unicode;
123
- return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
123
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
124
124
  }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject] }))] }));
125
125
  }
126
126
  function marker(run, glyphs) {
@@ -8,10 +8,11 @@
8
8
  * this component is not rendered at all, so a session that never plans has zero
9
9
  * columns and zero layout shift (R-g's "no furniture when idle" / AC-24).
10
10
  *
11
- * IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER INSIDE `bottomRef`
12
- * (C-6 / I-4). That box is MEASURED and the wheel router reads the composer band
13
- * boundary from it; a column added there would inflate the measurement and start
14
- * routing transcript scrolls into prompt history.
11
+ * IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER IN THE BOTTOM CHROME
12
+ * (C-6 / I-4). This is a COLUMN BESIDE the viewport, and the middle band is the
13
+ * only row-wrapped region there is; moved down into the bottom chrome it would
14
+ * stop being a rail and start eating viewport height, growing and shrinking the
15
+ * transcript every time the list changed length.
15
16
  *
16
17
  * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 18-36
17
18
  * columns wide and the system prompt's last line is "Respond in the language the
@@ -15,12 +15,23 @@
15
15
  * parallel product. What inline still guarantees is its GEOMETRY contract — no
16
16
  * fixed frame, `<Static>` keeps the settled history, no private ANSI — not its
17
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).
18
27
  */
19
28
  import React from 'react';
20
29
  import { type Theme } from './theme.js';
21
30
  import type { TermCapabilities } from './capabilities.js';
22
31
  import { type DensityMode } from './density.js';
23
32
  import type { Entry } from '../agent/reducer.js';
33
+ import type { RenderMode } from './layout/frame.js';
34
+ import type { HeightStore } from './use-height-store.js';
24
35
  /**
25
36
  * Largest prefix length P such that `entries[0..P)` are all terminal, none is
26
37
  * within the last `LIVE_TAIL` entries, and none is currently expanded. Pure and
@@ -34,9 +45,60 @@ interface TranscriptProps {
34
45
  thinkingVisible: boolean;
35
46
  reducedMotion: boolean;
36
47
  density: DensityMode;
48
+ /**
49
+ * The render mode, threaded from `App` as a plain scalar prop.
50
+ *
51
+ * It decides whether the collapsed thinking marker may offer `ctrl+t` (D-16):
52
+ * inline prints settled entries into `<Static>`, which cannot re-print them.
53
+ * NOT read from a module-level singleton — `render-memo.test.tsx` asserts prop
54
+ * stability across frames, and a hidden read would be invisible to it.
55
+ */
56
+ mode: RenderMode;
57
+ /**
58
+ * Wall-clock seconds, threaded from `App`'s existing 200 ms ticker, for the
59
+ * live tool card's stall row (agent-activity-presentation-live §3.3.4 / D-36).
60
+ *
61
+ * BOTH MAP CALLBACKS BELOW DECIDE PER ENTRY WHETHER TO PASS IT ON, and neither
62
+ * may pass it unconditionally: `ToolCard` is `React.memo` with the default
63
+ * comparator, so a prop that changes every second on every card would defeat
64
+ * that boundary for the whole transcript.
65
+ */
66
+ nowSec?: number;
37
67
  theme: Theme;
38
68
  caps: TermCapabilities;
39
69
  }
70
+ export interface EntryViewProps {
71
+ entry: Entry;
72
+ /** Previous entry in the list; `undefined` for the first item. */
73
+ prev: Entry | undefined;
74
+ expanded: boolean;
75
+ thinkingVisible: boolean;
76
+ reducedMotion: boolean;
77
+ density: DensityMode;
78
+ mode: RenderMode;
79
+ theme: Theme;
80
+ caps: TermCapabilities;
81
+ /** Inline mode only (L5); `undefined` in full-screen, which means no clamp. */
82
+ liveClampRows?: number;
83
+ /** Wall-clock seconds; set ONLY on a running tool entry (§3.3.4 / P1-3). */
84
+ nowSec?: number;
85
+ }
86
+ declare function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, mode, theme, caps, liveClampRows, nowSec, }: EntryViewProps): React.ReactElement | null;
87
+ /**
88
+ * THE memo boundary that makes a long transcript cheap (L2 / R3).
89
+ *
90
+ * The comparator is written out rather than defaulted because `prev` is the one
91
+ * prop whose identity matters for a reason the type does not show: it feeds
92
+ * `separationRows`, so an entry whose PREDECESSOR changed kind must re-render
93
+ * even though the entry itself did not.
94
+ *
95
+ * I-L2-1 — `theme` and `caps` are `useMemo`d in `App.tsx` and are therefore
96
+ * referentially stable across frames. If a future change makes either a fresh
97
+ * object per render, EVERY boundary below becomes a no-op AND NOTHING FAILS;
98
+ * `render-memo.test.tsx` asserts that stability directly so the trap is a red
99
+ * test rather than a silent regression.
100
+ */
101
+ export declare const EntryView: React.MemoExoticComponent<typeof EntryViewImpl>;
40
102
  /**
41
103
  * Full-screen transcript body — the same entry renderers with NO `<Static>`.
42
104
  *
@@ -51,12 +113,25 @@ interface TranscriptProps {
51
113
  * mark feeds nothing else — `/save` serializes `entries` in full — so leaving it
52
114
  * un-advanced has no side effect.
53
115
  *
54
- * Rendering budget (I-3): yoga lays out every child even when `overflow: hidden`
55
- * clips it, so cost grows linearly with the entry count. Only the last
56
- * `windowSize` entries are rendered; the reducer and `/save` keep everything.
116
+ * VIRTUALISED (L3 / R1). `windowSize` is now the SCROLL HORIZON — how far back
117
+ * the user can scroll — and no longer the rendering budget, because entries
118
+ * outside the viewport are not mounted at all. What bounds the frame is
119
+ * `selectWindow`, and the two spacers are what keep the scroll maths honest
120
+ * while they are absent.
57
121
  */
58
- export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, windowSize, }: TranscriptProps & {
122
+ export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, }: TranscriptProps & {
59
123
  windowSize: number;
124
+ /** The viewport's own width — narrower than the terminal when the rail is up. */
125
+ cols: number;
126
+ heights: HeightStore;
127
+ /**
128
+ * Written during render with the number of entries actually mounted, for
129
+ * `/perf` (AC-9). A ref rather than a callback: a `setState` here would make
130
+ * observing the render budget cost a render.
131
+ */
132
+ mountedSink?: {
133
+ current: number;
134
+ };
60
135
  }): React.ReactElement;
61
- export declare function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, }: TranscriptProps): React.ReactElement;
136
+ export declare function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, }: TranscriptProps): React.ReactElement;
62
137
  export {};