@aragon-agent/cli 0.5.8 → 0.5.10

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 +1157 -747
  2. package/README.md +1552 -1163
  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/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +40 -0
  9. package/dist/agent/reducer.d.ts +245 -3
  10. package/dist/agent/reducer.js +490 -17
  11. package/dist/agent/retry-view.d.ts +68 -0
  12. package/dist/agent/retry-view.js +95 -0
  13. package/dist/agent/system-prompt.d.ts +11 -0
  14. package/dist/agent/system-prompt.js +2 -0
  15. package/dist/boot/guard.d.ts +135 -0
  16. package/dist/boot/guard.js +218 -0
  17. package/dist/boot/rollback.d.ts +86 -0
  18. package/dist/boot/rollback.js +185 -0
  19. package/dist/cli.js +368 -3
  20. package/dist/commands/builtins.d.ts +1 -1
  21. package/dist/commands/builtins.js +426 -2
  22. package/dist/commands/registry.d.ts +17 -0
  23. package/dist/config/app-paths.d.ts +31 -0
  24. package/dist/config/app-paths.js +37 -0
  25. package/dist/config/cli-commands.d.ts +79 -2
  26. package/dist/config/cli-commands.js +185 -3
  27. package/dist/config/env.d.ts +15 -3
  28. package/dist/config/env.js +143 -3
  29. package/dist/config/load.d.ts +66 -0
  30. package/dist/config/load.js +107 -1
  31. package/dist/config/schema.d.ts +268 -2
  32. package/dist/config/schema.js +288 -1
  33. package/dist/config/store.js +32 -1
  34. package/dist/fast/critique.d.ts +43 -0
  35. package/dist/fast/critique.js +62 -0
  36. package/dist/fast/digest.d.ts +42 -0
  37. package/dist/fast/digest.js +85 -0
  38. package/dist/fast/frames.d.ts +99 -0
  39. package/dist/fast/frames.js +180 -0
  40. package/dist/fast/limits.d.ts +98 -0
  41. package/dist/fast/limits.js +98 -0
  42. package/dist/fast/prompt.d.ts +72 -0
  43. package/dist/fast/prompt.js +94 -0
  44. package/dist/fast/resolve.d.ts +43 -0
  45. package/dist/fast/resolve.js +105 -0
  46. package/dist/fast/review-call.d.ts +99 -0
  47. package/dist/fast/review-call.js +130 -0
  48. package/dist/fast/reviewer.d.ts +248 -0
  49. package/dist/fast/reviewer.js +664 -0
  50. package/dist/fast/types.d.ts +128 -0
  51. package/dist/fast/types.js +14 -0
  52. package/dist/fast/wiring.d.ts +170 -0
  53. package/dist/fast/wiring.js +316 -0
  54. package/dist/launcher.d.ts +14 -0
  55. package/dist/launcher.js +43 -0
  56. package/dist/logging/install.d.ts +15 -1
  57. package/dist/logging/install.js +37 -4
  58. package/dist/logging/logger.d.ts +1 -1
  59. package/dist/session/persist.js +57 -1
  60. package/dist/team/normalize.d.ts +26 -1
  61. package/dist/team/normalize.js +23 -3
  62. package/dist/team/prompt.d.ts +11 -0
  63. package/dist/team/prompt.js +10 -0
  64. package/dist/team/report.d.ts +18 -0
  65. package/dist/team/report.js +27 -1
  66. package/dist/team/runtime.d.ts +13 -2
  67. package/dist/team/runtime.js +33 -6
  68. package/dist/team/subagent.d.ts +19 -1
  69. package/dist/team/subagent.js +72 -10
  70. package/dist/team/task-tool.d.ts +26 -0
  71. package/dist/team/task-tool.js +48 -4
  72. package/dist/team/types.d.ts +80 -1
  73. package/dist/tools/bash-tool.js +19 -3
  74. package/dist/tools/diff.d.ts +25 -5
  75. package/dist/tools/diff.js +72 -41
  76. package/dist/tools/file-change-store.d.ts +34 -0
  77. package/dist/tools/file-change-store.js +57 -0
  78. package/dist/tools/fs-tools.d.ts +26 -0
  79. package/dist/tools/fs-tools.js +62 -5
  80. package/dist/tools/index.d.ts +31 -0
  81. package/dist/tools/index.js +7 -1
  82. package/dist/tools/patch.d.ts +115 -0
  83. package/dist/tools/patch.js +453 -0
  84. package/dist/tools/terminal-output.d.ts +107 -0
  85. package/dist/tools/terminal-output.js +198 -0
  86. package/dist/tools/tool-output-store.d.ts +68 -0
  87. package/dist/tools/tool-output-store.js +102 -0
  88. package/dist/ui/ActivityLine.d.ts +48 -0
  89. package/dist/ui/ActivityLine.js +22 -0
  90. package/dist/ui/App.d.ts +11 -4
  91. package/dist/ui/App.js +359 -37
  92. package/dist/ui/BottomStatusRow.d.ts +65 -0
  93. package/dist/ui/BottomStatusRow.js +17 -0
  94. package/dist/ui/Composer.d.ts +1 -6
  95. package/dist/ui/Composer.js +2 -2
  96. package/dist/ui/PromptInput.d.ts +1 -15
  97. package/dist/ui/PromptInput.js +1 -38
  98. package/dist/ui/StatusBar.d.ts +37 -0
  99. package/dist/ui/StatusBar.js +5 -3
  100. package/dist/ui/TeamPanel.d.ts +5 -4
  101. package/dist/ui/TeamPanel.js +1 -1
  102. package/dist/ui/TodoPanel.d.ts +5 -4
  103. package/dist/ui/Transcript.d.ts +26 -3
  104. package/dist/ui/Transcript.js +76 -10
  105. package/dist/ui/UpdateLine.d.ts +44 -0
  106. package/dist/ui/UpdateLine.js +129 -0
  107. package/dist/ui/activity-phrases.d.ts +33 -0
  108. package/dist/ui/activity-phrases.js +50 -0
  109. package/dist/ui/entries/AssistantEntry.d.ts +20 -0
  110. package/dist/ui/entries/AssistantEntry.js +3 -2
  111. package/dist/ui/entries/DiffView.d.ts +67 -0
  112. package/dist/ui/entries/DiffView.js +103 -0
  113. package/dist/ui/entries/FastCard.d.ts +53 -0
  114. package/dist/ui/entries/FastCard.js +87 -0
  115. package/dist/ui/entries/RetryCard.d.ts +56 -0
  116. package/dist/ui/entries/RetryCard.js +60 -0
  117. package/dist/ui/entries/TeamCard.js +6 -1
  118. package/dist/ui/entries/ToolCard.d.ts +38 -1
  119. package/dist/ui/entries/ToolCard.js +68 -5
  120. package/dist/ui/glyphs.d.ts +9 -0
  121. package/dist/ui/glyphs.js +2 -0
  122. package/dist/ui/layout/AppShell.d.ts +13 -22
  123. package/dist/ui/layout/AppShell.js +3 -38
  124. package/dist/ui/layout/virtual-window.d.ts +1 -1
  125. package/dist/ui/layout/virtual-window.js +109 -5
  126. package/dist/ui/overlays/HelpOverlay.js +16 -2
  127. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  128. package/dist/ui/overlays/SettingsScreen.js +147 -1
  129. package/dist/ui/transcript-text.js +27 -0
  130. package/dist/ui/use-wheel-routing.d.ts +16 -32
  131. package/dist/ui/use-wheel-routing.js +21 -38
  132. package/dist/update/classify-failure.d.ts +32 -0
  133. package/dist/update/classify-failure.js +60 -0
  134. package/dist/update/install-lock.d.ts +55 -0
  135. package/dist/update/install-lock.js +184 -0
  136. package/dist/update/install-source.d.ts +97 -0
  137. package/dist/update/install-source.js +218 -0
  138. package/dist/update/installer.d.ts +108 -0
  139. package/dist/update/installer.js +242 -0
  140. package/dist/update/limits.d.ts +111 -0
  141. package/dist/update/limits.js +111 -0
  142. package/dist/update/npm-view.d.ts +62 -0
  143. package/dist/update/npm-view.js +133 -0
  144. package/dist/update/registry.d.ts +52 -0
  145. package/dist/update/registry.js +161 -0
  146. package/dist/update/semver.d.ts +89 -0
  147. package/dist/update/semver.js +295 -0
  148. package/dist/update/service.d.ts +193 -0
  149. package/dist/update/service.js +636 -0
  150. package/dist/update/state.d.ts +103 -0
  151. package/dist/update/state.js +142 -0
  152. package/dist/update/types.d.ts +169 -0
  153. package/dist/update/types.js +68 -0
  154. package/package.json +3 -3
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. package/dist/ui/layout/regions.js +0 -52
@@ -0,0 +1,65 @@
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: toast > activity > update > blank. A toast wins because a
30
+ * transient ack is a RESPONSE TO THE USER, and the row nearest the input belongs
31
+ * to it. The update line is LAST because it is the only one of the three that is
32
+ * PERSISTENT (cli-auto-update D-2): it loses nothing by waiting for the run to
33
+ * end, whereas an activity line deferred is an activity line that never renders.
34
+ * That also means a user who is actively working never sees the updater at all
35
+ * until they stop, which is the whole of the silence requirement.
36
+ */
37
+ import React from 'react';
38
+ import type { Theme } from './theme.js';
39
+ import type { Toast } from '../agent/reducer.js';
40
+ import type { RenderMode } from './layout/frame.js';
41
+ export interface BottomStatusRowProps {
42
+ mode: RenderMode;
43
+ toasts: Toast[];
44
+ /** `<ActivityLine>` while a run owns the screen, else `null`. */
45
+ activity: React.ReactNode;
46
+ /**
47
+ * `<UpdateLine>` when the updater has something to say, else `null` — AND THE
48
+ * CALLER DECIDES WHICH (C-15 / cli-auto-update P0-1).
49
+ *
50
+ * `if (update)` below tests a REACT ELEMENT, and an element whose own render
51
+ * returns `null` is still truthy: that branch would emit a box of ZERO rows,
52
+ * the frame would keep its fixed height, Yoga would hand the row to the
53
+ * transcript, and `viewportRows()` would go on reporting the old number to
54
+ * five consumers. So `UpdateLine` is typed `React.ReactElement` and `App`
55
+ * gates it with `shouldRenderUpdateLine(snapshot)`.
56
+ *
57
+ * OPTIONAL, because `bottom-status-row.test.tsx` and the `App.tsx` fixtures
58
+ * construct this component without it and the test tree is inside
59
+ * `tsconfig.test.json` (`typecheck-scope.test.ts`); a required prop turns
60
+ * `npm run typecheck` red for a reason unrelated to this feature (P2-1).
61
+ */
62
+ update?: React.ReactNode;
63
+ theme: Theme;
64
+ }
65
+ export declare function BottomStatusRow({ mode, toasts, activity, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
@@ -0,0 +1,17 @@
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, update, theme, }) {
5
+ // A toast wins the row outright.
6
+ if (toasts.length > 0)
7
+ return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
8
+ // Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
9
+ // branch is already a conditional 0-3 row stack, and these are simply one more
10
+ // conditional row above it.
11
+ if (activity)
12
+ return _jsx(Box, { flexShrink: 0, children: activity });
13
+ if (update)
14
+ return _jsx(Box, { flexShrink: 0, children: update });
15
+ // The blank full-screen row that IS the budget must keep being emitted.
16
+ return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
17
+ }
@@ -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) })] }))] }));
@@ -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,25 @@ 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
+ };
91
110
  /**
92
111
  * Render-governor rung (tui-render-performance L4). `> 0` renders a muted
93
112
  * `eco` chip.
@@ -101,6 +120,24 @@ interface StatusBarProps {
101
120
  * eco chip on a 50-column terminal costs the context gauge more than it says.
102
121
  */
103
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
+ };
104
141
  }
105
142
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
106
143
  export {};
@@ -4,6 +4,8 @@ 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';
9
11
  /** Below this the `eco` chip costs the context gauge more than it is worth. */
@@ -27,7 +29,7 @@ function redrawChar(nonce) {
27
29
  return nonce % 2 === 0 ? ' ' : NBSP;
28
30
  }
29
31
  export function StatusBar(props) {
30
- const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, ecoRung = 0, } = 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;
31
33
  const { stdout } = useStdout();
32
34
  const cols = stdout?.columns ?? 80;
33
35
  const running = status === 'running';
@@ -53,7 +55,7 @@ export function StatusBar(props) {
53
55
  // land in one predictable place.
54
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
55
57
  ? `agents ${teamActive.running}/${teamActive.total}`
56
- : `[${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
57
59
  ? `todo ${todoActive.done}/${todoActive.total}`
58
- : `[${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: [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)] }))] })] }));
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)] }))] })] }));
59
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
@@ -30,6 +30,7 @@ import { type Theme } from './theme.js';
30
30
  import type { TermCapabilities } from './capabilities.js';
31
31
  import { type DensityMode } from './density.js';
32
32
  import type { Entry } from '../agent/reducer.js';
33
+ import type { RenderMode } from './layout/frame.js';
33
34
  import type { HeightStore } from './use-height-store.js';
34
35
  /**
35
36
  * Largest prefix length P such that `entries[0..P)` are all terminal, none is
@@ -44,6 +45,25 @@ interface TranscriptProps {
44
45
  thinkingVisible: boolean;
45
46
  reducedMotion: boolean;
46
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;
47
67
  theme: Theme;
48
68
  caps: TermCapabilities;
49
69
  }
@@ -55,12 +75,15 @@ export interface EntryViewProps {
55
75
  thinkingVisible: boolean;
56
76
  reducedMotion: boolean;
57
77
  density: DensityMode;
78
+ mode: RenderMode;
58
79
  theme: Theme;
59
80
  caps: TermCapabilities;
60
81
  /** Inline mode only (L5); `undefined` in full-screen, which means no clamp. */
61
82
  liveClampRows?: number;
83
+ /** Wall-clock seconds; set ONLY on a running tool entry (§3.3.4 / P1-3). */
84
+ nowSec?: number;
62
85
  }
63
- declare function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, theme, caps, liveClampRows, }: EntryViewProps): React.ReactElement | null;
86
+ declare function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, mode, theme, caps, liveClampRows, nowSec, }: EntryViewProps): React.ReactElement | null;
64
87
  /**
65
88
  * THE memo boundary that makes a long transcript cheap (L2 / R3).
66
89
  *
@@ -96,7 +119,7 @@ export declare const EntryView: React.MemoExoticComponent<typeof EntryViewImpl>;
96
119
  * `selectWindow`, and the two spacers are what keep the scroll maths honest
97
120
  * while they are absent.
98
121
  */
99
- export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, windowSize, cols, heights, mountedSink, }: TranscriptProps & {
122
+ export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, }: TranscriptProps & {
100
123
  windowSize: number;
101
124
  /** The viewport's own width — narrower than the terminal when the rail is up. */
102
125
  cols: number;
@@ -110,5 +133,5 @@ export declare function TranscriptList({ entries, expandedToolIds, thinkingVisib
110
133
  current: number;
111
134
  };
112
135
  }): React.ReactElement;
113
- 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;
114
137
  export {};
@@ -37,6 +37,8 @@ import { AssistantEntry } from './entries/AssistantEntry.js';
37
37
  import { ToolCard, statusColor, toolEntryGlyph } from './entries/ToolCard.js';
38
38
  import { TeamCard, teamCardColor } from './entries/TeamCard.js';
39
39
  import { TodoCard, todoCardColor } from './entries/TodoCard.js';
40
+ import { RetryCard, retryCardColor } from './entries/RetryCard.js';
41
+ import { FastCard, fastCardColor } from './entries/FastCard.js';
40
42
  import { MeasuredEntry } from './layout/MeasuredEntry.js';
41
43
  import { useViewportGeometry } from './layout/viewport-geometry.js';
42
44
  import { estimateEntryRows, heightKey, selectWindow, VIRTUAL_LIMITS, } from './layout/virtual-window.js';
@@ -83,10 +85,34 @@ export function computeSettledCount(entries, expandedToolIds) {
83
85
  // on every frame for the rest of the session.
84
86
  if (e.kind === 'todo' && e.live)
85
87
  break;
88
+ // Nor is a retry card that is still counting down or still in flight: it is
89
+ // rewritten on every `retryScheduled` / `retryAttempt` and once a second by
90
+ // `retryTick`, and `<Static>` cannot un-print what it has already drawn.
91
+ // `session/persist.ts` forces a loaded card to `interrupted`, which is the
92
+ // other half — a card that never settles is re-rendered on every frame for
93
+ // the rest of the session (R-10).
94
+ if (e.kind === 'retry' && (e.phase === 'waiting' || e.phase === 'retrying'))
95
+ break;
96
+ // A live review card is not settled either: it is rewritten once, when the
97
+ // call comes back, and `<Static>` cannot un-print what it has already drawn
98
+ // (C-5). `session/persist.ts` forces `live: false` on load, which is the
99
+ // other half.
100
+ if (e.kind === 'fast' && e.live)
101
+ break;
86
102
  count = i + 1;
87
103
  }
88
104
  return count;
89
105
  }
106
+ /**
107
+ * The value `EntryView` should receive for `nowSec` on THIS entry.
108
+ *
109
+ * `undefined` for everything that is not a running tool, which is what keeps
110
+ * every other card's memo boundary intact — `undefined === undefined` in the
111
+ * comparator, on every frame, forever.
112
+ */
113
+ function nowSecFor(entry, nowSec) {
114
+ return entry.kind === 'tool' && entry.status === 'running' ? nowSec : undefined;
115
+ }
90
116
  function noticeColor(level, theme) {
91
117
  if (level === 'error')
92
118
  return theme.noticeError;
@@ -101,7 +127,7 @@ function noticeSymbol(level, theme) {
101
127
  return theme.symbols.warn;
102
128
  return theme.symbols.info;
103
129
  }
104
- function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, theme, caps, liveClampRows, }) {
130
+ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, mode, theme, caps, liveClampRows, nowSec, }) {
105
131
  const glyphs = pickGlyphs(caps);
106
132
  const separation = separationRows(prev, entry, density);
107
133
  const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
@@ -113,14 +139,37 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
113
139
  // both an animation and a Unicode-only glyph, so reduced motion and an
114
140
  // ASCII terminal fall back to the same static marker (P2-11 / A-10).
115
141
  const animate = entry.streaming && !reducedMotion && caps.unicode;
116
- return frame(animate ? _jsx(Spinner, { type: "dots" }) : glyphs.assistant, theme.primary, _jsx(AssistantEntry, { text: entry.text, thinking: entry.thinking, thinkingOpen: entry.thinkingOpen, thinkingVisible: thinkingVisible, streaming: entry.streaming, aborted: entry.aborted, liveClampRows: liveClampRows, theme: theme, caps: caps }));
142
+ return frame(animate ? _jsx(Spinner, { type: "dots" }) : glyphs.assistant, theme.primary, _jsx(AssistantEntry, { text: entry.text, thinking: entry.thinking, thinkingOpen: entry.thinkingOpen, thinkingVisible: thinkingVisible, streaming: entry.streaming, aborted: entry.aborted,
143
+ // `EntryViewImpl` hands each renderer EXPLICIT SCALAR PROPS, so
144
+ // nothing on the entry reaches a component by itself (P1-6b).
145
+ thinkingMs: entry.thinkingMs, revealable: mode === 'fullscreen', liveClampRows: liveClampRows, theme: theme, caps: caps }));
117
146
  }
118
147
  case 'tool':
119
- return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion, liveClampRows: liveClampRows, theme: theme, caps: caps }));
148
+ return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, patch: entry.patch, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion, liveClampRows: liveClampRows,
149
+ // `entry.live` AND NOT `entry.live ?? []` (P2-4): `ToolCard`'s default
150
+ // comparator sees a fresh `[]` as a changed prop, which is why that
151
+ // file already keeps a shared `EMPTY_LINES`.
152
+ live: entry.live, lastOutputAt: entry.lastOutputAt, nowSec: nowSec, theme: theme, caps: caps }));
120
153
  case 'team':
121
154
  return frame(glyphs.teamAgent, teamCardColor(entry.runs, entry.active, entry.aborted, theme), _jsx(TeamCard, { requested: entry.requested, runs: entry.runs, aborted: entry.aborted, durationMs: entry.durationMs, active: entry.active, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
122
155
  case 'todo':
123
156
  return frame(toolGlyph(glyphs, 'todo_write'), todoCardColor(entry.doneCount, entry.total, entry.live, theme), _jsx(TodoCard, { items: entry.items, doneCount: entry.doneCount, total: entry.total, live: entry.live, interrupted: entry.interrupted, theme: theme, caps: caps }));
157
+ case 'retry':
158
+ return frame(entry.phase === 'exhausted' ? glyphs.error : glyphs.retry, retryCardColor(entry.phase, theme), _jsx(RetryCard, { attempt: entry.attempt, maxRetries: entry.maxRetries, errorType: entry.errorType, phase: entry.phase, resumeAt: entry.resumeAt, totalRetries: entry.totalRetries, elapsedMs: entry.elapsedMs,
159
+ // READ HERE, not threaded from `App`, and that is what keeps the memo
160
+ // boundary intact: a `now` prop on every `EntryView` would change on
161
+ // every frame and re-render the whole transcript. The `retryTick` action
162
+ // refreshes THIS entry's identity once a second, which re-renders this
163
+ // one card and gives it a fresh clock; nothing else moves.
164
+ now: Date.now(),
165
+ // NOT THREADED FROM `App`: reaching this renderer means a TUI is
166
+ // mounted, and the TUI always binds Esc to abort. Headless mode writes
167
+ // its retry lines to stderr and never mounts an entry component, and the
168
+ // exit replay goes through `transcript-text.ts`. `RetryCard` keeps the
169
+ // prop so the render tests can assert the suppressed form.
170
+ reducedMotion: reducedMotion, theme: theme, caps: caps }));
171
+ case 'fast':
172
+ return frame(entry.status === 'failed' ? glyphs.toolError : glyphs.teamAgent, fastCardColor(entry.status, theme), _jsx(FastCard, { reviewIndex: entry.reviewIndex, model: entry.model, status: entry.status, text: entry.text, detail: entry.detail, turn: entry.turn, durationMs: entry.durationMs, live: entry.live, reducedMotion: reducedMotion, theme: theme, caps: caps }));
124
173
  case 'notice':
125
174
  return frame(noticeSymbol(entry.level, theme), noticeColor(entry.level, theme), _jsx(Text, { color: noticeColor(entry.level, theme), children: entry.text }));
126
175
  default:
@@ -148,9 +197,21 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
148
197
  a.thinkingVisible === b.thinkingVisible &&
149
198
  a.reducedMotion === b.reducedMotion &&
150
199
  a.density === b.density &&
200
+ a.mode === b.mode &&
151
201
  a.theme === b.theme &&
152
202
  a.caps === b.caps &&
153
- a.liveClampRows === b.liveClampRows);
203
+ a.liveClampRows === b.liveClampRows &&
204
+ /*
205
+ * ELEVENTH TERM, AND OMITTING IT IS THE TRAP I-L2-1 DESCRIBES ONE COMMENT UP
206
+ * (P1-3). This comparator is a CLOSED LIST: a prop absent from it changes
207
+ * without invalidating the boundary, so `ToolCard` is never re-rendered and
208
+ * the stall row freezes at whatever second it first drew — AC-26's "and the
209
+ * seconds advance" failing with nothing reporting it.
210
+ *
211
+ * It is free: `nowSecFor` returns `undefined` for every entry that is not a
212
+ * running tool, and `undefined === undefined` on every frame.
213
+ */
214
+ a.nowSec === b.nowSec);
154
215
  });
155
216
  /**
156
217
  * Full-screen transcript body — the same entry renderers with NO `<Static>`.
@@ -172,7 +233,7 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
172
233
  * `selectWindow`, and the two spacers are what keep the scroll maths honest
173
234
  * while they are absent.
174
235
  */
175
- export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, windowSize, cols, heights, mountedSink, }) {
236
+ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, }) {
176
237
  const size = Math.max(1, Math.floor(windowSize));
177
238
  const visible = entries.length > size ? entries.slice(-size) : entries;
178
239
  const collapsed = entries.length - visible.length;
@@ -190,7 +251,12 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
190
251
  const { resolve, report } = heights;
191
252
  const heightOf = useCallback((index) => {
192
253
  const entry = visible[index];
193
- return resolve(keyOf(entry), () => estimateEntryRows(entry, cols, density, !!expandedToolIds[entry.id]));
254
+ // `thinkingVisible` IS PASSED, not left to the parameter's conservative
255
+ // default (P1-6a / condition 5). With thinking hidden the default would
256
+ // inflate every assistant entry that thought by its whole wrapped length,
257
+ // and an entry estimated at 400 rows against a real height of 1 is exactly
258
+ // the one `selectWindow` then never mounts and never re-measures.
259
+ return resolve(keyOf(entry), () => estimateEntryRows(entry, cols, density, !!expandedToolIds[entry.id], thinkingVisible));
194
260
  },
195
261
  // Memoised only incidentally: `visible` IS `entries` while the session is
196
262
  // shorter than the horizon and a fresh slice once it is past it, so this
@@ -199,7 +265,7 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
199
265
  // `keyOf` is omitted from the list because every input it closes over is
200
266
  // already in it.
201
267
  // eslint-disable-next-line react-hooks/exhaustive-deps
202
- [visible, expandedToolIds, cols, density, flags, resolve]);
268
+ [visible, expandedToolIds, cols, density, flags, resolve, thinkingVisible]);
203
269
  const selection = selectWindow({
204
270
  entries: visible,
205
271
  heightOf,
@@ -212,10 +278,10 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
212
278
  mountedSink.current = slice.length;
213
279
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [collapsed > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: [glyphs.ellipsis, " ", collapsed, " earlier ", collapsed === 1 ? 'entry' : 'entries', " collapsed (/save exports the full session)"] })), selection.leadingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.leadingRows })), slice.map((entry, i) => {
214
280
  const index = selection.startIndex + i;
215
- return (_jsx(MeasuredEntry, { heightKey: keyOf(entry), onMeasure: report, children: _jsx(EntryView, { entry: entry, prev: index > 0 ? visible[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }) }, entry.id));
281
+ return (_jsx(MeasuredEntry, { heightKey: keyOf(entry), onMeasure: report, children: _jsx(EntryView, { entry: entry, prev: index > 0 ? visible[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSecFor(entry, nowSec), theme: theme, caps: caps }) }, entry.id));
216
282
  }), selection.trailingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.trailingRows }))] }));
217
283
  }
218
- export function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, }) {
284
+ export function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, }) {
219
285
  const highWater = useRef(0);
220
286
  const prevLen = useRef(0);
221
287
  const { rows } = useTerminalSize();
@@ -246,5 +312,5 @@ export function Transcript({ entries, expandedToolIds, thinkingVisible, reducedM
246
312
  // a live entry at all, which is worse than a tall frame.
247
313
  const liveBudget = Math.max(INLINE_LIVE_MIN_ROWS, rows - INLINE_LIVE_MARGIN);
248
314
  const liveClampRows = Math.max(1, Math.floor(liveBudget / Math.max(1, liveEntries.length)) - 1);
249
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: settledEntries, children: (entry, index) => (_jsx(EntryView, { entry: entry, prev: index > 0 ? settledEntries[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }, entry.id)) }), _jsx(Box, { flexDirection: "column", children: liveEntries.map((entry, i) => (_jsx(EntryView, { entry: entry, prev: i > 0 ? liveEntries[i - 1] : entries[settled - 1], expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps, liveClampRows: liveClampRows }, entry.id))) })] }));
315
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: settledEntries, children: (entry, index) => (_jsx(EntryView, { entry: entry, prev: index > 0 ? settledEntries[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, theme: theme, caps: caps }, entry.id)) }), _jsx(Box, { flexDirection: "column", children: liveEntries.map((entry, i) => (_jsx(EntryView, { entry: entry, prev: i > 0 ? liveEntries[i - 1] : entries[settled - 1], expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, theme: theme, caps: caps, liveClampRows: liveClampRows, nowSec: nowSecFor(entry, nowSec) }, entry.id))) })] }));
250
316
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The update line (cli-auto-update section 6.2) - the THIRD occupant of the
3
+ * already-budgeted bottom row, after the toast stack and the activity line.
4
+ *
5
+ * ONE ROW, ALWAYS, AND NEVER `null` (C-15 / P0-1 / D-19). The return type is
6
+ * `React.ReactElement` rather than `React.ReactElement | null`, so the compiler
7
+ * refuses the shortcut: `BottomStatusRow` decides presence with `if (update)`,
8
+ * which tests a REACT ELEMENT, and an element whose render returns `null` is
9
+ * still truthy. Taking that branch would emit a box of ZERO rows - the frame
10
+ * keeps its fixed height, Yoga hands the row to the transcript, and
11
+ * `viewportRows()` goes on reporting the old number to `ScrollViewport`,
12
+ * `selectWindow`, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`. The
13
+ * transcript would gain a row while the updater is idle and lose it the moment
14
+ * it has news. Presence is the CALLER's decision, through
15
+ * `shouldRenderUpdateLine`, exactly as `App.tsx` already decides the activity
16
+ * line's with `running && !overlayNode`.
17
+ *
18
+ * PURE: a function of `snapshot + compact + theme + caps`. No state, no timer,
19
+ * no clock. `<Text wrap="truncate">` so a narrow terminal degrades by truncation
20
+ * rather than by wrapping into a second row the fixed frame cannot afford.
21
+ *
22
+ * ASCII ONLY, and every user-visible character comes from `pickGlyphs(caps)`
23
+ * (C-3). No literal `...`, `+` or `~` may be spelled inline - `ActivityLine`'s
24
+ * routing of even the ellipsis through `pickGlyphs` is the precedent.
25
+ *
26
+ * `compact` RATHER THAN `cols` (IF-1). Section 6.2 describes the width tiers in
27
+ * terms of `cols`, but resolving them here would mean importing
28
+ * `UPDATE_LIMITS.statusCompactCols` - a VALUE - from `update/`, and section 3.1
29
+ * rule 1 requires this module's only edge into that tree to be erasable by tsc.
30
+ * The caller owns `UPDATE_LIMITS` already (it computes presence from it), so it
31
+ * resolves the threshold too. Same rule as presence: the call site decides.
32
+ */
33
+ import React from 'react';
34
+ import type { Theme } from './theme.js';
35
+ import type { TermCapabilities } from './capabilities.js';
36
+ import type { UpdateSnapshot } from '../update/types.js';
37
+ export interface UpdateLineProps {
38
+ snapshot: UpdateSnapshot;
39
+ /** `cols < UPDATE_LIMITS.statusCompactCols`, resolved by the caller. */
40
+ compact: boolean;
41
+ theme: Theme;
42
+ caps: TermCapabilities;
43
+ }
44
+ export declare function UpdateLine({ snapshot, compact, theme, caps }: UpdateLineProps): React.ReactElement;