@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
package/dist/ui/App.js CHANGED
@@ -28,10 +28,21 @@ import { Transcript, TranscriptList } from './Transcript.js';
28
28
  import { PromptInput } from './PromptInput.js';
29
29
  import { Composer } from './Composer.js';
30
30
  import { StatusBar } from './StatusBar.js';
31
+ import { secondsLeft } from '../agent/retry-view.js';
31
32
  import { TeamPanel } from './TeamPanel.js';
32
33
  import { TodoPanel } from './TodoPanel.js';
33
34
  import { TodoStrip } from './TodoStrip.js';
34
- import { ToastStack } from './ToastStack.js';
35
+ import { ActivityLine } from './ActivityLine.js';
36
+ import { BottomStatusRow } from './BottomStatusRow.js';
37
+ import { UpdateLine } from './UpdateLine.js';
38
+ // The updater's two PURE modules. `shouldRenderUpdateLine` decides the row's
39
+ // presence AT THE CALL SITE (C-15), and `UPDATE_LIMITS` owns the width
40
+ // threshold - both are values, and both are resolved here rather than inside
41
+ // `UpdateLine` so that component's only edge into `update/` stays erasable by
42
+ // tsc (cli-auto-update section 3.1 rule 1 / IF-1). Neither module opens a
43
+ // socket, spawns a process or creates a timer.
44
+ import { shouldRenderUpdateLine } from '../update/types.js';
45
+ import { UPDATE_LIMITS } from '../update/limits.js';
35
46
  import { AppShell } from './layout/AppShell.js';
36
47
  import { ScrollViewport } from './layout/ScrollViewport.js';
37
48
  import { todoRailRows, todoRailWidth } from './layout/rail.js';
@@ -43,14 +54,13 @@ import { useRenderGovernor } from './use-render-governor.js';
43
54
  import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
44
55
  import { frameHeight, MIN_FULLSCREEN_ROWS } from './layout/frame.js';
45
56
  import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
46
- import { UNMEASURED_GEOMETRY } from './layout/regions.js';
47
57
  import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
48
58
  import { installConsoleBridge } from './console-bridge.js';
49
59
  import { publishExitSnapshot } from './exit-snapshot.js';
50
60
  import { OverlayFrame } from './layout/OverlayFrame.js';
51
61
  import { helpRows } from './overlays/HelpOverlay.js';
52
62
  import { ModelPicker } from './overlays/ModelPicker.js';
53
- import { SettingsScreen } from './overlays/SettingsScreen.js';
63
+ import { SettingsScreen, fastSettingsFrom, readFastSettings, } from './overlays/SettingsScreen.js';
54
64
  import { ConfirmDialog } from './overlays/ConfirmDialog.js';
55
65
  import { QuestionOverlay } from './overlays/QuestionOverlay.js';
56
66
  import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
@@ -93,7 +103,14 @@ export function resolveSettingsMaxTokens(raw, previous) {
93
103
  toast: ['warn', 'Max tokens must be a number or "auto".'],
94
104
  };
95
105
  }
96
- export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }) {
106
+ /**
107
+ * The lazy `useReducer` initializer. Module scope, so the reference is stable
108
+ * across renders and React never re-runs it.
109
+ */
110
+ function seedViewState(thinkingVisible) {
111
+ return initialViewState({ thinkingVisible });
112
+ }
113
+ export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, updateBridge, }) {
97
114
  const { exit } = useApp();
98
115
  const { stdout } = useStdout();
99
116
  const fullscreen = mode === 'fullscreen';
@@ -110,9 +127,21 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
110
127
  });
111
128
  /** The transcript height cache, owned here and read by `TranscriptList` (L3). */
112
129
  const heights = useHeightStore();
113
- const [state, dispatch] = useReducer(viewReducer, undefined, initialViewState);
130
+ // SEEDED FROM CONFIG, not dispatched after mount (§3.1.1): a post-mount
131
+ // `toggleThinking` would let `/resume` flash a restored thinking block on
132
+ // frame 1. `seedViewState` is module-scope so the lazy initializer is a stable
133
+ // function reference.
134
+ const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
114
135
  const [elapsedMs, setElapsedMs] = useState(0);
115
136
  const [confirmState, setConfirmState] = useState(null);
137
+ /**
138
+ * The updater's latest snapshot, or `null` while there is no service
139
+ * (cli-auto-update section 6.1).
140
+ *
141
+ * `UpdateSnapshot` is a VALUE OBJECT the service replaces wholesale on every
142
+ * transition, so this state can use referential equality and never diff fields.
143
+ */
144
+ const [updateSnapshot, setUpdateSnapshot] = useState(null);
116
145
  /** The request the question / plan overlay is currently rendering. */
117
146
  const [humanRequest, setHumanRequest] = useState(null);
118
147
  // Seeded from the history file, not from `CliConfig`: the list is no longer
@@ -121,8 +150,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
121
150
  const [promptHistory, setPromptHistory] = useState(() => loadPromptHistory());
122
151
  // Scroll plumbing: intent goes down, one derived display number comes back.
123
152
  const [scrollIntent, setScrollIntent] = useState();
124
- /** One prompt-history step per wheel notch over the composer band (§4.7). */
125
- const [historyIntent, setHistoryIntent] = useState();
126
153
  const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
127
154
  const [scrolledLines, setScrolledLines] = useState(0);
128
155
  // I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
@@ -155,12 +182,21 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
155
182
  const ctrlCArmed = useRef(false);
156
183
  const ctrlCTimer = useRef(null);
157
184
  const runBaselineOut = useRef(0);
185
+ /**
186
+ * Epoch ms the current run began — the activity phrase's seed.
187
+ *
188
+ * ASSIGNED IN RENDER SCOPE, NOT IN THE ELAPSED EFFECT (P1-7). That effect runs
189
+ * AFTER the render that first sees `status === 'running'`, so a ref it wrote
190
+ * would be one frame late — and the first frame of a run is exactly when the
191
+ * phrase is chosen. `Transcript.tsx` updates `highWater` / `prevLen` during
192
+ * render for the same reason: a value derived from a transition is needed by
193
+ * the very render that observes the transition.
194
+ */
195
+ const runStartedAt = useRef(0);
158
196
  const toastTimers = useRef(new Map());
159
197
  /** Every outstanding `gate.request()`. Held in a ref, never in state: it is
160
198
  * bookkeeping for promises, and re-rendering on it would be noise. */
161
199
  const pendingHuman = useRef(new Set());
162
- /** Written by `AppShell`'s layout effect, read at wheel-event time (§4.5). */
163
- const geometryRef = useRef(UNMEASURED_GEOMETRY);
164
200
  /**
165
201
  * What `/perf` reports. Assigned during render, exactly like `stateRef` above,
166
202
  * because a slash command runs outside the render pass and a value captured in
@@ -181,6 +217,15 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
181
217
  // `ViewState` is a field `restoreEntries`, `/clear`, `/reset` and the session
182
218
  // round-trip each have to have an opinion about, and none of them has an
183
219
  // opinion about a 3-second timer.
220
+ /**
221
+ * Whether the one `installing -> ready` toast has already been pushed
222
+ * (cli-auto-update section 6.3).
223
+ *
224
+ * A REF AND NOT STATE: this must survive every re-render without causing one,
225
+ * and it is the whole of AC-19 - ten re-renders while the phase sits at
226
+ * `ready` must still produce exactly one toast.
227
+ */
228
+ const updateToastPushed = useRef(false);
184
229
  const budgetRef = useRef(emptyBudget());
185
230
  const followTimer = useRef(null);
186
231
  /** True while the run in flight was started by the grace timer, not the user. */
@@ -289,10 +334,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
289
334
  for (const action of merged)
290
335
  dispatch(action);
291
336
  }, []);
337
+ /**
338
+ * The CLI-local diff side channel (§3.3.7). Memoized once, beside the `cost`
339
+ * lookup below, because `reduceEvent` TAKES a patch rather than reading one:
340
+ * the store is consume-once, so this object must not be rebuilt per event.
341
+ */
342
+ const patchSource = useMemo(() => ({ take: (id) => controller.takeFilePatch(id) }), [controller]);
292
343
  useEffect(() => {
293
344
  const unsubscribe = controller.subscribe((event) => {
294
345
  const cost = controller.getModelInfo().cost;
295
- for (const action of reduceEvent(event, cost)) {
346
+ for (const action of reduceEvent(event, cost, patchSource)) {
296
347
  // MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
297
348
  // `ViewState.errorNoticed`, rather than read back from the reducer one
298
349
  // commit later (C-11). This is the whole of the batching-independence
@@ -406,7 +457,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
406
457
  cancelFollowThrough();
407
458
  unsubscribe();
408
459
  };
409
- }, [controller, flushPending, cancelPendingHuman, cancelFollowThrough]);
460
+ }, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough]);
410
461
  // --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
411
462
  //
412
463
  // A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
@@ -440,16 +491,32 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
440
491
  dispatch({ type: 'teamUpdate', snapshot });
441
492
  break;
442
493
  }
443
- case 'usage':
494
+ case 'usage': {
444
495
  // Child spend is REAL SPEND and must reach the status bar, or the
445
496
  // session cost readout under-reports by however much the team consumed
446
497
  // — the single most misleading possible failure of this feature (R-5).
447
- dispatch({
448
- type: 'teamUsage',
449
- usage: event.usage,
450
- costDelta: computeCost(event.usage, controller.getModelInfo().cost),
451
- });
498
+ //
499
+ // THE COST TABLE IS SELECTED BY TIER (fast-model-tier §3.6 / R-7).
500
+ // Always using the lead's would over-report a Haiku child under a
501
+ // Sonnet lead by roughly an order of magnitude, which is a different
502
+ // way of being wrong about the same number.
503
+ // FROM THE CONTROLLER, NOT FROM `cfg` (the rule the `agent_end`
504
+ // handler above records): this effect's dependency list never changes,
505
+ // so it closes over the `cfg` object of the render it mounted in, and
506
+ // `setModel` builds a NEW one.
507
+ const live = controller.getConfig();
508
+ const fastTier = event.tier === 'fast' ? controller.getFastStatus().tier : null;
509
+ const ref = fastTier && fastTier.ok
510
+ ? fastTier.ref
511
+ : { providerId: live.provider, modelId: live.model };
512
+ // Unknown pricing contributes ZERO rather than a fabricated figure
513
+ // (C-11 / RV-4); `/fast status` and the dispatch report say `unknown`.
514
+ const costDelta = controller.isPricedModel(ref)
515
+ ? computeCost(event.usage, controller.getModelInfoFor(ref).cost)
516
+ : 0;
517
+ dispatch({ type: 'teamUsage', usage: event.usage, costDelta });
452
518
  break;
519
+ }
453
520
  case 'dispatch_end':
454
521
  dispatch({ type: 'teamEnd', outcome: event.outcome });
455
522
  break;
@@ -491,6 +558,83 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
491
558
  }
492
559
  });
493
560
  }, [controller]);
561
+ // --- Fast-tier event stream (fast-model-tier §5.3). --------------------
562
+ //
563
+ // A FOURTH SUBSCRIPTION, for the reason the team and todo ones record:
564
+ // `FastEvent` is deliberately not a member of core's `AgentEvent` union
565
+ // (D-10), so it cannot arrive through `controller.subscribe`.
566
+ //
567
+ // `subscribeFast` returns a no-op unsubscribe when the tier was off at
568
+ // construction, so this can subscribe unconditionally and an ordinary session
569
+ // pays one closure for the whole run.
570
+ useEffect(() => {
571
+ // Seed the chip from the tier resolved at construction, so a fast-enabled
572
+ // session shows it on the first frame rather than after the first review.
573
+ const initial = controller.getFastStatus();
574
+ if (initial.registered)
575
+ dispatch({ type: 'fastTier', snapshot: initial.snapshot });
576
+ return controller.subscribeFast((event) => {
577
+ switch (event.type) {
578
+ case 'review_start':
579
+ dispatch({
580
+ type: 'fastStart',
581
+ index: event.index,
582
+ turn: event.turn,
583
+ model: controller.getFastStatus().snapshot.model,
584
+ });
585
+ break;
586
+ case 'review_end':
587
+ dispatch({ type: 'fastEnd', review: event.review });
588
+ // The card settled, so the chip's `inFlight` and the session totals
589
+ // both moved. One dispatch rather than two: `fastTier` carries them.
590
+ dispatch({ type: 'fastTier', snapshot: controller.getFastStatus().snapshot });
591
+ break;
592
+ case 'usage': {
593
+ // Review spend is REAL SPEND at the FAST model's price (§3.6 / AC-18).
594
+ const ref = controller.getFastStatus().tier;
595
+ const costDelta = ref.ok && controller.isPricedModel(ref.ref)
596
+ ? computeCost(event.usage, controller.getModelInfoFor(ref.ref).cost)
597
+ : 0;
598
+ dispatch({ type: 'fastUsage', usage: event.usage, costDelta });
599
+ break;
600
+ }
601
+ case 'tier_changed':
602
+ dispatch({ type: 'fastTier', snapshot: event.snapshot });
603
+ break;
604
+ }
605
+ });
606
+ }, [controller]);
607
+ // --- Live tool output (agent-activity-presentation-live §3.3.2). --------
608
+ //
609
+ // A FIFTH SUBSCRIPTION, and THE ONE THAT DOES GO THROUGH THE STREAMING
610
+ // COALESCER (D-34 / P0-2). The three above each carry a discrete act at human
611
+ // frequency — a roster change, a `todo_write`, a review verdict — and each has
612
+ // a comment refusing the buffer for that reason. This one carries a STREAM at
613
+ // pipe speed: the producer is a child process's stdout, so dispatching per
614
+ // chunk would be one React commit per `read()`.
615
+ //
616
+ // THE EFFECT PUSHES INTO `pending.current` AND ARMS `flushTimer` ITSELF.
617
+ // There is no "buffered path for every other action" to fall into —
618
+ // `App.tsx:436-447` buffers exactly `textDelta` / `thinkingDelta` and
619
+ // dispatches everything else immediately — so writing this the way its three
620
+ // siblings are written leaves `mergeDeltas`'s new clause dead code and R-1
621
+ // with one bound instead of two. AC-38 is the pin, mutation-checked against
622
+ // the direct-dispatch form.
623
+ //
624
+ // `Date.now()` IS READ HERE, in the listener, not in the reducer, which is
625
+ // what keeps `viewReducer` pure.
626
+ useEffect(() => {
627
+ return controller.subscribeToolOutput(({ toolCallId, rows }) => {
628
+ pending.current.push({ type: 'toolOutputDelta', toolCallId, rows, at: Date.now() });
629
+ if (!flushTimer.current) {
630
+ // READ FROM THE REF AT ARM TIME, for the reason `App.tsx:439-441`
631
+ // records: this effect's dependency list never changes, so a captured
632
+ // number would be pinned to the mount render and the governor's ladder
633
+ // would move nothing.
634
+ flushTimer.current = setTimeout(() => flushPending(), governorInterval.current);
635
+ }
636
+ });
637
+ }, [controller, flushPending, governorInterval]);
494
638
  // --- Elapsed timer + tokens/sec baseline while running. ----------------
495
639
  useEffect(() => {
496
640
  if (state.status !== 'running') {
@@ -503,6 +647,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
503
647
  const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 200);
504
648
  return () => clearInterval(id);
505
649
  }, [state.status]);
650
+ // --- Retry countdown tick (llm-api-retry-backoff §6.5). ----------------
651
+ //
652
+ // ONLY WHILE A BACKOFF IS ACTUALLY WAITING, and the effect's teardown clears it —
653
+ // so an idle session, and even a session that is retrying but has already fired
654
+ // its next attempt, has NO timer at all. 1 Hz rather than the elapsed timer's
655
+ // 200 ms because the card shows whole seconds and a five-times-finer tick would
656
+ // buy four identical frames.
657
+ useEffect(() => {
658
+ if (state.retry?.phase !== 'waiting')
659
+ return undefined;
660
+ const id = setInterval(() => dispatch({ type: 'retryTick' }), 1000);
661
+ return () => clearInterval(id);
662
+ }, [state.retry?.phase]);
506
663
  // --- Toast auto-dismiss (single effect keyed on the toast list). -------
507
664
  useEffect(() => {
508
665
  for (const t of state.toasts) {
@@ -670,6 +827,48 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
670
827
  // --- Helpers -----------------------------------------------------------
671
828
  const notify = (level, text) => dispatch({ type: 'notice', level, text });
672
829
  const toast = (level, text) => dispatch({ type: 'pushToast', level, text });
830
+ // --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
831
+ //
832
+ // TWO ARRIVAL ORDERS, AND BOTH HAPPEN. The service is built by a dynamic
833
+ // `import()` fired after `render()`, so on a warm module cache it can be
834
+ // attached BEFORE this effect runs, and on a cold one long after. Reading
835
+ // `bridge.service` first and installing `onAttach` second covers both without
836
+ // a race - the same shape `confirmBridge` uses one screen up.
837
+ useEffect(() => {
838
+ if (!updateBridge)
839
+ return undefined;
840
+ const bridge = updateBridge;
841
+ let unsubscribe = null;
842
+ const attach = (service) => {
843
+ setUpdateSnapshot(service.snapshot());
844
+ unsubscribe = service.subscribe(setUpdateSnapshot);
845
+ };
846
+ if (bridge.service)
847
+ attach(bridge.service);
848
+ else
849
+ bridge.onAttach = attach;
850
+ return () => {
851
+ bridge.onAttach = null;
852
+ unsubscribe?.();
853
+ };
854
+ }, [updateBridge]);
855
+ // ONE TOAST, ON THE `installing -> ready` EDGE ONLY (section 6.3 / AC-19).
856
+ //
857
+ // It exists because the moment of completion may arrive while the user is
858
+ // reading the transcript with the row occupied by a run: the toast wins the row
859
+ // for its TTL and then hands it back to the persistent line. The ref is what
860
+ // makes ten re-renders at `ready` push exactly one.
861
+ useEffect(() => {
862
+ if (updateSnapshot?.phase !== 'ready')
863
+ return;
864
+ if (updateToastPushed.current)
865
+ return;
866
+ updateToastPushed.current = true;
867
+ toast('info', `${updateSnapshot.latestVersion ?? 'Update'} installed - restart aragon to apply.`);
868
+ // `toast` is a stable dispatch wrapper; depending on it would re-run this on
869
+ // every render and defeat the ref guard's purpose rather than its mechanism.
870
+ // eslint-disable-next-line react-hooks/exhaustive-deps
871
+ }, [updateSnapshot?.phase, updateSnapshot?.latestVersion]);
673
872
  const persistConfig = (patch) => {
674
873
  try {
675
874
  updatePersistedConfig(patch);
@@ -727,6 +926,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
727
926
  // Read at command time, so `/todo status` reports the live count rather than
728
927
  // whatever it was when this context was built.
729
928
  followBudget: budgetRef.current,
929
+ // Read at command time for the same reason, and additionally because the
930
+ // service ATTACHES LATE (section 3.8): a value captured when this closure was
931
+ // first created would be `undefined` for the whole session on a cold module
932
+ // cache, and `/update` would report "disabled" on a session where it is on.
933
+ // `?? undefined` because the bridge holds `null` and the port is optional.
934
+ update: updateBridge?.service ?? undefined,
730
935
  });
731
936
  /**
732
937
  * Submitting a prompt no longer touches `config.json` at all — it appends one
@@ -803,6 +1008,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
803
1008
  controller.setModel(values.provider, values.model, values.baseUrl || undefined);
804
1009
  controller.setThinkingLevel(values.thinkingLevel);
805
1010
  controller.setMaxTokens(cap.applied);
1011
+ // THE SETTER AND THE PERSIST ARE BOTH REQUIRED, the pair `/todo panel`
1012
+ // documents: persisting alone reports success and changes nothing until the
1013
+ // next launch. `thinkingVisible` lives in `ViewState`, not in the config the
1014
+ // controller holds, so the live half is a dispatch rather than a setter —
1015
+ // and it is a no-op when the row already agrees with the current state.
1016
+ if ((values.showThinking === 'on') !== stateRef.current.thinkingVisible) {
1017
+ dispatch({ type: 'toggleThinking' });
1018
+ }
806
1019
  const key = values.apiKey.trim();
807
1020
  if (key.length > 0) {
808
1021
  // Registration site 4 (§4.4.3), and the one that matters most: this is how
@@ -817,6 +1030,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
817
1030
  model: values.model,
818
1031
  baseUrl: values.baseUrl.trim() ? values.baseUrl.trim() : null,
819
1032
  thinkingLevel: values.thinkingLevel,
1033
+ showThinking: values.showThinking === 'on',
1034
+ // PERSIST ONLY, with no paired live setter — the store is allocated in
1035
+ // `AgentController`'s constructor, so this row takes effect on the next
1036
+ // launch. That is what manual row 8 asks for, and it is why the seed above
1037
+ // reads `cfg` rather than `ViewState`.
1038
+ liveToolOutput: values.liveToolOutput === 'on',
820
1039
  // Omitted entirely when the field was unusable, so the previous value
821
1040
  // survives instead of being overwritten with a guess.
822
1041
  ...(cap.persist !== 'skip' ? { maxTokens: cap.persist } : {}),
@@ -827,6 +1046,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
827
1046
  // merges it — a shallow merge here would reset `redactSecrets` every time
828
1047
  // someone touched the level.
829
1048
  patch.log = { level: values.logLevel };
1049
+ // --- Fast tier (fast-model-tier §4.5) ---------------------------------
1050
+ //
1051
+ // THE SETTER AND THE PERSIST ARE BOTH REQUIRED, and this is the pair `/todo
1052
+ // panel` documents (P1-2): `App` reads `controller.getConfig()` at render
1053
+ // time, so persisting alone reports success and changes nothing until the
1054
+ // next launch, while the setter alone forgets by morning.
1055
+ //
1056
+ // `setFastConfig` also re-resolves the tier, which matters HERE more than
1057
+ // anywhere: this same Enter press may have changed the main provider, the
1058
+ // main model or the API key, and `fast.provider: ''` inherits from all
1059
+ // three (§3.2 rule 8 / RV-3).
1060
+ const fastPatch = readFastSettings(values);
1061
+ controller.setFastConfig(fastPatch);
1062
+ patch.fast = fastPatch;
830
1063
  persistConfig(patch);
831
1064
  getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
832
1065
  dispatch({ type: 'setOverlay', overlay: null });
@@ -864,26 +1097,24 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
864
1097
  }
865
1098
  resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
866
1099
  };
867
- // --- Mouse wheel region routing (§4.6). Table and coalescer live in the
868
- // hook (D-13); only the wiring it cannot own stays here. Inline arrows are
869
- // fine — the hook re-reads its options through a ref every render. The
870
- // `Math.max(0, …)` mirrors the keyboard branch below, and for the same
871
- // reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
1100
+ // --- Mouse wheel routing (wheel-scrolls-transcript-only §4.1). Table and
1101
+ // coalescer live in the hook (D-13); only the wiring it cannot own stays here.
1102
+ // Inline arrows are fine — the hook re-reads its options through a ref every
1103
+ // render. The `Math.max(0, …)` mirrors the keyboard branch below, and for the
1104
+ // same reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
872
1105
  const { clearCoalescer } = useWheelRouting({
873
1106
  mouseSource,
874
- geometryRef,
875
- geometryOverride,
876
1107
  getOverlay: () => stateRef.current.overlay,
877
1108
  onScroll: scrollBy,
878
1109
  onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
879
- onHistoryStep: (dir) => setHistoryIntent((prev) => ({ dir, nonce: (prev?.nonce ?? 0) + 1 })),
880
1110
  });
881
1111
  // A fresh overlay always opens at the top; carrying the previous overlay's
882
1112
  // offset over would open Help mid-list for no reason the user can see.
883
1113
  //
884
1114
  // `clearCoalescer()` rides along here rather than adding a second subscriber
885
- // to the same transition (§4.7 rule 2): an accumulated-but-unflushed
886
- // transcript scroll must not survive an overlay opening.
1115
+ // to the same transition (wheel-scrolls-transcript-only §4.3): an
1116
+ // accumulated-but-unflushed transcript scroll must not survive an overlay
1117
+ // opening.
887
1118
  useEffect(() => {
888
1119
  setOverlayScroll(0);
889
1120
  clearCoalescer();
@@ -991,7 +1222,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
991
1222
  }
992
1223
  }
993
1224
  if (key.ctrl && (input === 't' || input === 'T')) {
1225
+ const revealing = !stateRef.current.thinkingVisible;
994
1226
  dispatch({ type: 'toggleThinking' });
1227
+ // THE ACK IS MODE-AWARE FOR THE SAME REASON THE MARKER IS (D-16 / P0-2).
1228
+ // Inline mode cannot repaint what `<Static>` has already printed, so the
1229
+ // toast says what it DID rather than what the user hoped. The hide
1230
+ // direction needs no qualifier: hiding applies to everything drawn from
1231
+ // here on, and nothing is claimed about scrollback either way.
1232
+ if (!revealing)
1233
+ toast('info', 'Thinking hidden.');
1234
+ else if (fullscreen)
1235
+ toast('info', 'Thinking shown.');
1236
+ else
1237
+ toast('info', 'Thinking shown for new output.');
995
1238
  return;
996
1239
  }
997
1240
  if (key.ctrl && (input === 'o' || input === 'O')) {
@@ -1056,6 +1299,40 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1056
1299
  const tokPerSec = state.status === 'running' && elapsedMs > 500
1057
1300
  ? Math.max(0, Math.round((state.usageTotal.outputTokens - runBaselineOut.current) / (elapsedMs / 1000)))
1058
1301
  : 0;
1302
+ // --- The activity line's seed (§3.2.3 / P1-7). --------------------------
1303
+ //
1304
+ // RENDER SCOPE. Without this the ref holds `0` on the first run (an arbitrary
1305
+ // word) and the PREVIOUS run's start on every run after, so every run opens on
1306
+ // a wrong phrase for one frame and then jumps — a flicker at the start of
1307
+ // every single turn, which is the one moment this feature exists to make feel
1308
+ // calm.
1309
+ const running = state.status === 'running';
1310
+ if (running && runStartedAt.current === 0)
1311
+ runStartedAt.current = Date.now();
1312
+ if (!running && runStartedAt.current !== 0)
1313
+ runStartedAt.current = 0;
1314
+ // --- The running tool (agent-activity-presentation-live §3.4 / L4). -----
1315
+ //
1316
+ // A REVERSE LOOP WITH AN EARLY EXIT, NOT `.filter(...).pop()` (P2-3).
1317
+ // `state.entries` is bounded by `transcriptRetain`, which DEFAULTS TO 1000,
1318
+ // and this runs in render scope at up to 30 fps — an allocating full scan
1319
+ // there is the exact cost `tui-render-performance` exists to remove. The
1320
+ // running tool, when there is one, is within a few entries of the end.
1321
+ let runningTool;
1322
+ if (running) {
1323
+ for (let i = state.entries.length - 1; i >= 0; i -= 1) {
1324
+ const e = state.entries[i];
1325
+ if (e.kind === 'tool' && e.status === 'running') {
1326
+ runningTool = e.name;
1327
+ break;
1328
+ }
1329
+ }
1330
+ }
1331
+ // WALL-CLOCK SECONDS, and the 200 ms ticker above is what causes the render
1332
+ // that re-reads it — so no timer is added (D-36 / E-15). Seconds rather than
1333
+ // milliseconds because `ToolCard` is `React.memo`'d with the default
1334
+ // comparator; `Transcript` passes it on only to running tool entries.
1335
+ const nowSec = running ? Math.floor(Date.now() / 1000) : undefined;
1059
1336
  // Row budget for the viewport and everything sized against it (§4.2a). The
1060
1337
  // inline path has no fixed frame, hence no height to fit into: `Infinity`
1061
1338
  // tells `OverlayFrame` to render everything and show no position indicator.
@@ -1100,18 +1377,33 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1100
1377
  model: cfg.model,
1101
1378
  baseUrl: cfg.baseUrl ?? '',
1102
1379
  thinkingLevel: cfg.thinkingLevel,
1380
+ // SEEDED AND READ BACK, both halves (§4.3): omitting either is the
1381
+ // documented silent half of adding a row — the field renders empty and
1382
+ // an untouched save writes a value the user never chose.
1383
+ //
1384
+ // FROM `ViewState`, NOT FROM `cfg`. `thinkingVisible` is session state
1385
+ // that `Ctrl+T` also writes, and there is no controller setter to keep
1386
+ // `cfg` in step — so seeding from the config would show the LAUNCH
1387
+ // value under a transcript that has been toggled since, and an
1388
+ // untouched save would then quietly toggle it back.
1389
+ showThinking: state.thinkingVisible ? 'on' : 'off',
1390
+ // FROM `cfg`, unlike its neighbour, and the difference is real: the
1391
+ // live tail is decided at construction (the store is allocated there
1392
+ // and there is no setter), so the config IS the current value and
1393
+ // nothing can have toggled it since launch.
1394
+ liveToolOutput: cfg.liveToolOutput ? 'on' : 'off',
1103
1395
  // `auto`, not an empty string: an empty field reads as "unset", which
1104
1396
  // is indistinguishable from AUTO and is not a state this can be in.
1105
1397
  maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : 'auto',
1106
1398
  apiKey: '',
1107
1399
  logLevel: cfg.log.level,
1108
- }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
1109
- // A MEASUREMENT MUST NEVER OUTLIVE THE FRAME IT DESCRIBED (P1-5): neither
1110
- // path below renders the full-screen `AppShell`, so nothing else would
1111
- // overwrite a stale `bottom`. See `UNMEASURED_GEOMETRY` for why there is no
1112
- // arithmetic fallback.
1113
- if (!fullscreen || rows < MIN_FULLSCREEN_ROWS)
1114
- geometryRef.current = UNMEASURED_GEOMETRY;
1400
+ // The four fast rows, seeded through the pure mapper so the screen and
1401
+ // the save handler cannot disagree about what `(inherit)` means.
1402
+ ...fastSettingsFrom(cfg.fast),
1403
+ }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave,
1404
+ // Read from `cfg` (render scope, re-read every frame), so `/retry off`
1405
+ // while the screen is open shows the new value on the next paint.
1406
+ retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
1115
1407
  // A resize can drop below the usable floor. Do NOT leave the alt-screen —
1116
1408
  // entering and leaving it on every drag is worse than a placeholder — and take
1117
1409
  // the height from `frameHeight()` like everything else, because this is the
@@ -1148,13 +1440,32 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1148
1440
  viewportRows: fullscreen ? viewportBudget : rows,
1149
1441
  offset: scrolledLines,
1150
1442
  };
1151
- const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }), overlayNode] }));
1443
+ const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
1152
1444
  // The completion popup lives inside the bottom chrome and can add up to 9
1153
1445
  // rows; unbounded, it pushes the status bar past the frame height where
1154
1446
  // `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
1155
1447
  const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
1156
- const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, historyIntent: historyIntent, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
1157
- return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, geometryRef: geometryRef, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: state.status === 'running', reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(ToastStack, { toasts: state.toasts, theme: theme, mode: mode }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
1448
+ const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
1449
+ return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: state.status === 'running', reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(BottomStatusRow, { mode: mode, toasts: state.toasts,
1450
+ // `!overlayNode` suppresses the line while an overlay owns the screen:
1451
+ // the full-screen branch replaces the whole viewport with the overlay,
1452
+ // and a working line under a settings screen is noise attached to a
1453
+ // surface the run is not visible on. (Inline overlays are part of the
1454
+ // document flow, so there the suppression is conservative rather than
1455
+ // necessary — P2-5.)
1456
+ activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null,
1457
+ // PRESENCE IS DECIDED HERE, NOT BY `UpdateLine` RETURNING `null`
1458
+ // (C-15 / cli-auto-update P0-1 / D-19). `BottomStatusRow` tests this
1459
+ // prop for truthiness, and a React element is truthy however it
1460
+ // renders - so a component that rendered nothing would give the row
1461
+ // ZERO rows and silently unbalance the frame budget. Same rule the
1462
+ // activity line above follows with `running && !overlayNode`.
1463
+ //
1464
+ // `!overlayNode` for the same reason it appears there: the full-screen
1465
+ // branch replaces the whole viewport with the overlay, and an update
1466
+ // notice under a settings screen is noise attached to a surface it is
1467
+ // not visible on.
1468
+ update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
1158
1469
  ? {
1159
1470
  teamActive: {
1160
1471
  running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
@@ -1164,5 +1475,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1164
1475
  }
1165
1476
  : {}), ...(state.todos && !showRail
1166
1477
  ? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
1478
+ : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.retry
1479
+ ? {
1480
+ retryActive: {
1481
+ attempt: state.retry.attempt,
1482
+ max: state.retry.maxRetries,
1483
+ // Recomputed at RENDER from the absolute `resumeAt`, which is
1484
+ // what keeps the chip and the transcript card showing the same
1485
+ // number without either of them owning a second clock.
1486
+ secondsLeft: secondsLeft(state.retry.resumeAt, Date.now()),
1487
+ },
1488
+ }
1167
1489
  : {}) }) }));
1168
1490
  }