@aragon-agent/cli 0.5.8 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/README.md +1429 -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/cli.js +86 -2
  16. package/dist/commands/builtins.d.ts +1 -1
  17. package/dist/commands/builtins.js +308 -2
  18. package/dist/config/cli-commands.d.ts +53 -2
  19. package/dist/config/cli-commands.js +136 -3
  20. package/dist/config/env.d.ts +6 -3
  21. package/dist/config/env.js +103 -3
  22. package/dist/config/load.d.ts +49 -0
  23. package/dist/config/load.js +83 -1
  24. package/dist/config/schema.d.ts +212 -2
  25. package/dist/config/schema.js +207 -1
  26. package/dist/config/store.js +20 -1
  27. package/dist/fast/critique.d.ts +43 -0
  28. package/dist/fast/critique.js +62 -0
  29. package/dist/fast/digest.d.ts +42 -0
  30. package/dist/fast/digest.js +85 -0
  31. package/dist/fast/frames.d.ts +99 -0
  32. package/dist/fast/frames.js +180 -0
  33. package/dist/fast/limits.d.ts +98 -0
  34. package/dist/fast/limits.js +98 -0
  35. package/dist/fast/prompt.d.ts +72 -0
  36. package/dist/fast/prompt.js +94 -0
  37. package/dist/fast/resolve.d.ts +43 -0
  38. package/dist/fast/resolve.js +105 -0
  39. package/dist/fast/review-call.d.ts +99 -0
  40. package/dist/fast/review-call.js +130 -0
  41. package/dist/fast/reviewer.d.ts +248 -0
  42. package/dist/fast/reviewer.js +664 -0
  43. package/dist/fast/types.d.ts +128 -0
  44. package/dist/fast/types.js +14 -0
  45. package/dist/fast/wiring.d.ts +170 -0
  46. package/dist/fast/wiring.js +316 -0
  47. package/dist/logging/install.d.ts +15 -1
  48. package/dist/logging/install.js +37 -4
  49. package/dist/logging/logger.d.ts +1 -1
  50. package/dist/session/persist.js +57 -1
  51. package/dist/team/normalize.d.ts +26 -1
  52. package/dist/team/normalize.js +23 -3
  53. package/dist/team/prompt.d.ts +11 -0
  54. package/dist/team/prompt.js +10 -0
  55. package/dist/team/report.d.ts +18 -0
  56. package/dist/team/report.js +27 -1
  57. package/dist/team/runtime.d.ts +13 -2
  58. package/dist/team/runtime.js +33 -6
  59. package/dist/team/subagent.d.ts +19 -1
  60. package/dist/team/subagent.js +72 -10
  61. package/dist/team/task-tool.d.ts +26 -0
  62. package/dist/team/task-tool.js +48 -4
  63. package/dist/team/types.d.ts +80 -1
  64. package/dist/tools/bash-tool.js +19 -3
  65. package/dist/tools/diff.d.ts +25 -5
  66. package/dist/tools/diff.js +72 -41
  67. package/dist/tools/file-change-store.d.ts +34 -0
  68. package/dist/tools/file-change-store.js +57 -0
  69. package/dist/tools/fs-tools.d.ts +26 -0
  70. package/dist/tools/fs-tools.js +62 -5
  71. package/dist/tools/index.d.ts +31 -0
  72. package/dist/tools/index.js +7 -1
  73. package/dist/tools/patch.d.ts +115 -0
  74. package/dist/tools/patch.js +453 -0
  75. package/dist/tools/terminal-output.d.ts +107 -0
  76. package/dist/tools/terminal-output.js +198 -0
  77. package/dist/tools/tool-output-store.d.ts +68 -0
  78. package/dist/tools/tool-output-store.js +102 -0
  79. package/dist/ui/ActivityLine.d.ts +48 -0
  80. package/dist/ui/ActivityLine.js +22 -0
  81. package/dist/ui/App.d.ts +1 -4
  82. package/dist/ui/App.js +273 -37
  83. package/dist/ui/BottomStatusRow.d.ts +43 -0
  84. package/dist/ui/BottomStatusRow.js +14 -0
  85. package/dist/ui/Composer.d.ts +1 -6
  86. package/dist/ui/Composer.js +2 -2
  87. package/dist/ui/PromptInput.d.ts +1 -15
  88. package/dist/ui/PromptInput.js +1 -38
  89. package/dist/ui/StatusBar.d.ts +37 -0
  90. package/dist/ui/StatusBar.js +5 -3
  91. package/dist/ui/TeamPanel.d.ts +5 -4
  92. package/dist/ui/TeamPanel.js +1 -1
  93. package/dist/ui/TodoPanel.d.ts +5 -4
  94. package/dist/ui/Transcript.d.ts +26 -3
  95. package/dist/ui/Transcript.js +76 -10
  96. package/dist/ui/activity-phrases.d.ts +33 -0
  97. package/dist/ui/activity-phrases.js +50 -0
  98. package/dist/ui/entries/AssistantEntry.d.ts +20 -0
  99. package/dist/ui/entries/AssistantEntry.js +3 -2
  100. package/dist/ui/entries/DiffView.d.ts +67 -0
  101. package/dist/ui/entries/DiffView.js +103 -0
  102. package/dist/ui/entries/FastCard.d.ts +53 -0
  103. package/dist/ui/entries/FastCard.js +87 -0
  104. package/dist/ui/entries/RetryCard.d.ts +56 -0
  105. package/dist/ui/entries/RetryCard.js +60 -0
  106. package/dist/ui/entries/TeamCard.js +6 -1
  107. package/dist/ui/entries/ToolCard.d.ts +38 -1
  108. package/dist/ui/entries/ToolCard.js +68 -5
  109. package/dist/ui/glyphs.d.ts +9 -0
  110. package/dist/ui/glyphs.js +2 -0
  111. package/dist/ui/layout/AppShell.d.ts +13 -22
  112. package/dist/ui/layout/AppShell.js +3 -38
  113. package/dist/ui/layout/virtual-window.d.ts +1 -1
  114. package/dist/ui/layout/virtual-window.js +109 -5
  115. package/dist/ui/overlays/HelpOverlay.js +16 -2
  116. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  117. package/dist/ui/overlays/SettingsScreen.js +147 -1
  118. package/dist/ui/transcript-text.js +27 -0
  119. package/dist/ui/use-wheel-routing.d.ts +16 -32
  120. package/dist/ui/use-wheel-routing.js +21 -38
  121. package/package.json +2 -2
  122. package/dist/ui/layout/regions.d.ts +0 -55
  123. package/dist/ui/layout/regions.js +0 -52
package/dist/ui/App.js CHANGED
@@ -28,10 +28,12 @@ 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';
35
37
  import { AppShell } from './layout/AppShell.js';
36
38
  import { ScrollViewport } from './layout/ScrollViewport.js';
37
39
  import { todoRailRows, todoRailWidth } from './layout/rail.js';
@@ -43,14 +45,13 @@ import { useRenderGovernor } from './use-render-governor.js';
43
45
  import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
44
46
  import { frameHeight, MIN_FULLSCREEN_ROWS } from './layout/frame.js';
45
47
  import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
46
- import { UNMEASURED_GEOMETRY } from './layout/regions.js';
47
48
  import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
48
49
  import { installConsoleBridge } from './console-bridge.js';
49
50
  import { publishExitSnapshot } from './exit-snapshot.js';
50
51
  import { OverlayFrame } from './layout/OverlayFrame.js';
51
52
  import { helpRows } from './overlays/HelpOverlay.js';
52
53
  import { ModelPicker } from './overlays/ModelPicker.js';
53
- import { SettingsScreen } from './overlays/SettingsScreen.js';
54
+ import { SettingsScreen, fastSettingsFrom, readFastSettings, } from './overlays/SettingsScreen.js';
54
55
  import { ConfirmDialog } from './overlays/ConfirmDialog.js';
55
56
  import { QuestionOverlay } from './overlays/QuestionOverlay.js';
56
57
  import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
@@ -93,7 +94,14 @@ export function resolveSettingsMaxTokens(raw, previous) {
93
94
  toast: ['warn', 'Max tokens must be a number or "auto".'],
94
95
  };
95
96
  }
96
- export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }) {
97
+ /**
98
+ * The lazy `useReducer` initializer. Module scope, so the reference is stable
99
+ * across renders and React never re-runs it.
100
+ */
101
+ function seedViewState(thinkingVisible) {
102
+ return initialViewState({ thinkingVisible });
103
+ }
104
+ export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, }) {
97
105
  const { exit } = useApp();
98
106
  const { stdout } = useStdout();
99
107
  const fullscreen = mode === 'fullscreen';
@@ -110,7 +118,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
110
118
  });
111
119
  /** The transcript height cache, owned here and read by `TranscriptList` (L3). */
112
120
  const heights = useHeightStore();
113
- const [state, dispatch] = useReducer(viewReducer, undefined, initialViewState);
121
+ // SEEDED FROM CONFIG, not dispatched after mount (§3.1.1): a post-mount
122
+ // `toggleThinking` would let `/resume` flash a restored thinking block on
123
+ // frame 1. `seedViewState` is module-scope so the lazy initializer is a stable
124
+ // function reference.
125
+ const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
114
126
  const [elapsedMs, setElapsedMs] = useState(0);
115
127
  const [confirmState, setConfirmState] = useState(null);
116
128
  /** The request the question / plan overlay is currently rendering. */
@@ -121,8 +133,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
121
133
  const [promptHistory, setPromptHistory] = useState(() => loadPromptHistory());
122
134
  // Scroll plumbing: intent goes down, one derived display number comes back.
123
135
  const [scrollIntent, setScrollIntent] = useState();
124
- /** One prompt-history step per wheel notch over the composer band (§4.7). */
125
- const [historyIntent, setHistoryIntent] = useState();
126
136
  const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
127
137
  const [scrolledLines, setScrolledLines] = useState(0);
128
138
  // I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
@@ -155,12 +165,21 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
155
165
  const ctrlCArmed = useRef(false);
156
166
  const ctrlCTimer = useRef(null);
157
167
  const runBaselineOut = useRef(0);
168
+ /**
169
+ * Epoch ms the current run began — the activity phrase's seed.
170
+ *
171
+ * ASSIGNED IN RENDER SCOPE, NOT IN THE ELAPSED EFFECT (P1-7). That effect runs
172
+ * AFTER the render that first sees `status === 'running'`, so a ref it wrote
173
+ * would be one frame late — and the first frame of a run is exactly when the
174
+ * phrase is chosen. `Transcript.tsx` updates `highWater` / `prevLen` during
175
+ * render for the same reason: a value derived from a transition is needed by
176
+ * the very render that observes the transition.
177
+ */
178
+ const runStartedAt = useRef(0);
158
179
  const toastTimers = useRef(new Map());
159
180
  /** Every outstanding `gate.request()`. Held in a ref, never in state: it is
160
181
  * bookkeeping for promises, and re-rendering on it would be noise. */
161
182
  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
183
  /**
165
184
  * What `/perf` reports. Assigned during render, exactly like `stateRef` above,
166
185
  * because a slash command runs outside the render pass and a value captured in
@@ -289,10 +308,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
289
308
  for (const action of merged)
290
309
  dispatch(action);
291
310
  }, []);
311
+ /**
312
+ * The CLI-local diff side channel (§3.3.7). Memoized once, beside the `cost`
313
+ * lookup below, because `reduceEvent` TAKES a patch rather than reading one:
314
+ * the store is consume-once, so this object must not be rebuilt per event.
315
+ */
316
+ const patchSource = useMemo(() => ({ take: (id) => controller.takeFilePatch(id) }), [controller]);
292
317
  useEffect(() => {
293
318
  const unsubscribe = controller.subscribe((event) => {
294
319
  const cost = controller.getModelInfo().cost;
295
- for (const action of reduceEvent(event, cost)) {
320
+ for (const action of reduceEvent(event, cost, patchSource)) {
296
321
  // MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
297
322
  // `ViewState.errorNoticed`, rather than read back from the reducer one
298
323
  // commit later (C-11). This is the whole of the batching-independence
@@ -406,7 +431,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
406
431
  cancelFollowThrough();
407
432
  unsubscribe();
408
433
  };
409
- }, [controller, flushPending, cancelPendingHuman, cancelFollowThrough]);
434
+ }, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough]);
410
435
  // --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
411
436
  //
412
437
  // A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
@@ -440,16 +465,32 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
440
465
  dispatch({ type: 'teamUpdate', snapshot });
441
466
  break;
442
467
  }
443
- case 'usage':
468
+ case 'usage': {
444
469
  // Child spend is REAL SPEND and must reach the status bar, or the
445
470
  // session cost readout under-reports by however much the team consumed
446
471
  // — 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
- });
472
+ //
473
+ // THE COST TABLE IS SELECTED BY TIER (fast-model-tier §3.6 / R-7).
474
+ // Always using the lead's would over-report a Haiku child under a
475
+ // Sonnet lead by roughly an order of magnitude, which is a different
476
+ // way of being wrong about the same number.
477
+ // FROM THE CONTROLLER, NOT FROM `cfg` (the rule the `agent_end`
478
+ // handler above records): this effect's dependency list never changes,
479
+ // so it closes over the `cfg` object of the render it mounted in, and
480
+ // `setModel` builds a NEW one.
481
+ const live = controller.getConfig();
482
+ const fastTier = event.tier === 'fast' ? controller.getFastStatus().tier : null;
483
+ const ref = fastTier && fastTier.ok
484
+ ? fastTier.ref
485
+ : { providerId: live.provider, modelId: live.model };
486
+ // Unknown pricing contributes ZERO rather than a fabricated figure
487
+ // (C-11 / RV-4); `/fast status` and the dispatch report say `unknown`.
488
+ const costDelta = controller.isPricedModel(ref)
489
+ ? computeCost(event.usage, controller.getModelInfoFor(ref).cost)
490
+ : 0;
491
+ dispatch({ type: 'teamUsage', usage: event.usage, costDelta });
452
492
  break;
493
+ }
453
494
  case 'dispatch_end':
454
495
  dispatch({ type: 'teamEnd', outcome: event.outcome });
455
496
  break;
@@ -491,6 +532,83 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
491
532
  }
492
533
  });
493
534
  }, [controller]);
535
+ // --- Fast-tier event stream (fast-model-tier §5.3). --------------------
536
+ //
537
+ // A FOURTH SUBSCRIPTION, for the reason the team and todo ones record:
538
+ // `FastEvent` is deliberately not a member of core's `AgentEvent` union
539
+ // (D-10), so it cannot arrive through `controller.subscribe`.
540
+ //
541
+ // `subscribeFast` returns a no-op unsubscribe when the tier was off at
542
+ // construction, so this can subscribe unconditionally and an ordinary session
543
+ // pays one closure for the whole run.
544
+ useEffect(() => {
545
+ // Seed the chip from the tier resolved at construction, so a fast-enabled
546
+ // session shows it on the first frame rather than after the first review.
547
+ const initial = controller.getFastStatus();
548
+ if (initial.registered)
549
+ dispatch({ type: 'fastTier', snapshot: initial.snapshot });
550
+ return controller.subscribeFast((event) => {
551
+ switch (event.type) {
552
+ case 'review_start':
553
+ dispatch({
554
+ type: 'fastStart',
555
+ index: event.index,
556
+ turn: event.turn,
557
+ model: controller.getFastStatus().snapshot.model,
558
+ });
559
+ break;
560
+ case 'review_end':
561
+ dispatch({ type: 'fastEnd', review: event.review });
562
+ // The card settled, so the chip's `inFlight` and the session totals
563
+ // both moved. One dispatch rather than two: `fastTier` carries them.
564
+ dispatch({ type: 'fastTier', snapshot: controller.getFastStatus().snapshot });
565
+ break;
566
+ case 'usage': {
567
+ // Review spend is REAL SPEND at the FAST model's price (§3.6 / AC-18).
568
+ const ref = controller.getFastStatus().tier;
569
+ const costDelta = ref.ok && controller.isPricedModel(ref.ref)
570
+ ? computeCost(event.usage, controller.getModelInfoFor(ref.ref).cost)
571
+ : 0;
572
+ dispatch({ type: 'fastUsage', usage: event.usage, costDelta });
573
+ break;
574
+ }
575
+ case 'tier_changed':
576
+ dispatch({ type: 'fastTier', snapshot: event.snapshot });
577
+ break;
578
+ }
579
+ });
580
+ }, [controller]);
581
+ // --- Live tool output (agent-activity-presentation-live §3.3.2). --------
582
+ //
583
+ // A FIFTH SUBSCRIPTION, and THE ONE THAT DOES GO THROUGH THE STREAMING
584
+ // COALESCER (D-34 / P0-2). The three above each carry a discrete act at human
585
+ // frequency — a roster change, a `todo_write`, a review verdict — and each has
586
+ // a comment refusing the buffer for that reason. This one carries a STREAM at
587
+ // pipe speed: the producer is a child process's stdout, so dispatching per
588
+ // chunk would be one React commit per `read()`.
589
+ //
590
+ // THE EFFECT PUSHES INTO `pending.current` AND ARMS `flushTimer` ITSELF.
591
+ // There is no "buffered path for every other action" to fall into —
592
+ // `App.tsx:436-447` buffers exactly `textDelta` / `thinkingDelta` and
593
+ // dispatches everything else immediately — so writing this the way its three
594
+ // siblings are written leaves `mergeDeltas`'s new clause dead code and R-1
595
+ // with one bound instead of two. AC-38 is the pin, mutation-checked against
596
+ // the direct-dispatch form.
597
+ //
598
+ // `Date.now()` IS READ HERE, in the listener, not in the reducer, which is
599
+ // what keeps `viewReducer` pure.
600
+ useEffect(() => {
601
+ return controller.subscribeToolOutput(({ toolCallId, rows }) => {
602
+ pending.current.push({ type: 'toolOutputDelta', toolCallId, rows, at: Date.now() });
603
+ if (!flushTimer.current) {
604
+ // READ FROM THE REF AT ARM TIME, for the reason `App.tsx:439-441`
605
+ // records: this effect's dependency list never changes, so a captured
606
+ // number would be pinned to the mount render and the governor's ladder
607
+ // would move nothing.
608
+ flushTimer.current = setTimeout(() => flushPending(), governorInterval.current);
609
+ }
610
+ });
611
+ }, [controller, flushPending, governorInterval]);
494
612
  // --- Elapsed timer + tokens/sec baseline while running. ----------------
495
613
  useEffect(() => {
496
614
  if (state.status !== 'running') {
@@ -503,6 +621,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
503
621
  const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 200);
504
622
  return () => clearInterval(id);
505
623
  }, [state.status]);
624
+ // --- Retry countdown tick (llm-api-retry-backoff §6.5). ----------------
625
+ //
626
+ // ONLY WHILE A BACKOFF IS ACTUALLY WAITING, and the effect's teardown clears it —
627
+ // so an idle session, and even a session that is retrying but has already fired
628
+ // its next attempt, has NO timer at all. 1 Hz rather than the elapsed timer's
629
+ // 200 ms because the card shows whole seconds and a five-times-finer tick would
630
+ // buy four identical frames.
631
+ useEffect(() => {
632
+ if (state.retry?.phase !== 'waiting')
633
+ return undefined;
634
+ const id = setInterval(() => dispatch({ type: 'retryTick' }), 1000);
635
+ return () => clearInterval(id);
636
+ }, [state.retry?.phase]);
506
637
  // --- Toast auto-dismiss (single effect keyed on the toast list). -------
507
638
  useEffect(() => {
508
639
  for (const t of state.toasts) {
@@ -803,6 +934,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
803
934
  controller.setModel(values.provider, values.model, values.baseUrl || undefined);
804
935
  controller.setThinkingLevel(values.thinkingLevel);
805
936
  controller.setMaxTokens(cap.applied);
937
+ // THE SETTER AND THE PERSIST ARE BOTH REQUIRED, the pair `/todo panel`
938
+ // documents: persisting alone reports success and changes nothing until the
939
+ // next launch. `thinkingVisible` lives in `ViewState`, not in the config the
940
+ // controller holds, so the live half is a dispatch rather than a setter —
941
+ // and it is a no-op when the row already agrees with the current state.
942
+ if ((values.showThinking === 'on') !== stateRef.current.thinkingVisible) {
943
+ dispatch({ type: 'toggleThinking' });
944
+ }
806
945
  const key = values.apiKey.trim();
807
946
  if (key.length > 0) {
808
947
  // Registration site 4 (§4.4.3), and the one that matters most: this is how
@@ -817,6 +956,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
817
956
  model: values.model,
818
957
  baseUrl: values.baseUrl.trim() ? values.baseUrl.trim() : null,
819
958
  thinkingLevel: values.thinkingLevel,
959
+ showThinking: values.showThinking === 'on',
960
+ // PERSIST ONLY, with no paired live setter — the store is allocated in
961
+ // `AgentController`'s constructor, so this row takes effect on the next
962
+ // launch. That is what manual row 8 asks for, and it is why the seed above
963
+ // reads `cfg` rather than `ViewState`.
964
+ liveToolOutput: values.liveToolOutput === 'on',
820
965
  // Omitted entirely when the field was unusable, so the previous value
821
966
  // survives instead of being overwritten with a guess.
822
967
  ...(cap.persist !== 'skip' ? { maxTokens: cap.persist } : {}),
@@ -827,6 +972,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
827
972
  // merges it — a shallow merge here would reset `redactSecrets` every time
828
973
  // someone touched the level.
829
974
  patch.log = { level: values.logLevel };
975
+ // --- Fast tier (fast-model-tier §4.5) ---------------------------------
976
+ //
977
+ // THE SETTER AND THE PERSIST ARE BOTH REQUIRED, and this is the pair `/todo
978
+ // panel` documents (P1-2): `App` reads `controller.getConfig()` at render
979
+ // time, so persisting alone reports success and changes nothing until the
980
+ // next launch, while the setter alone forgets by morning.
981
+ //
982
+ // `setFastConfig` also re-resolves the tier, which matters HERE more than
983
+ // anywhere: this same Enter press may have changed the main provider, the
984
+ // main model or the API key, and `fast.provider: ''` inherits from all
985
+ // three (§3.2 rule 8 / RV-3).
986
+ const fastPatch = readFastSettings(values);
987
+ controller.setFastConfig(fastPatch);
988
+ patch.fast = fastPatch;
830
989
  persistConfig(patch);
831
990
  getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
832
991
  dispatch({ type: 'setOverlay', overlay: null });
@@ -864,26 +1023,24 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
864
1023
  }
865
1024
  resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
866
1025
  };
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.
1026
+ // --- Mouse wheel routing (wheel-scrolls-transcript-only §4.1). Table and
1027
+ // coalescer live in the hook (D-13); only the wiring it cannot own stays here.
1028
+ // Inline arrows are fine — the hook re-reads its options through a ref every
1029
+ // render. The `Math.max(0, …)` mirrors the keyboard branch below, and for the
1030
+ // same reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
872
1031
  const { clearCoalescer } = useWheelRouting({
873
1032
  mouseSource,
874
- geometryRef,
875
- geometryOverride,
876
1033
  getOverlay: () => stateRef.current.overlay,
877
1034
  onScroll: scrollBy,
878
1035
  onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
879
- onHistoryStep: (dir) => setHistoryIntent((prev) => ({ dir, nonce: (prev?.nonce ?? 0) + 1 })),
880
1036
  });
881
1037
  // A fresh overlay always opens at the top; carrying the previous overlay's
882
1038
  // offset over would open Help mid-list for no reason the user can see.
883
1039
  //
884
1040
  // `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.
1041
+ // to the same transition (wheel-scrolls-transcript-only §4.3): an
1042
+ // accumulated-but-unflushed transcript scroll must not survive an overlay
1043
+ // opening.
887
1044
  useEffect(() => {
888
1045
  setOverlayScroll(0);
889
1046
  clearCoalescer();
@@ -991,7 +1148,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
991
1148
  }
992
1149
  }
993
1150
  if (key.ctrl && (input === 't' || input === 'T')) {
1151
+ const revealing = !stateRef.current.thinkingVisible;
994
1152
  dispatch({ type: 'toggleThinking' });
1153
+ // THE ACK IS MODE-AWARE FOR THE SAME REASON THE MARKER IS (D-16 / P0-2).
1154
+ // Inline mode cannot repaint what `<Static>` has already printed, so the
1155
+ // toast says what it DID rather than what the user hoped. The hide
1156
+ // direction needs no qualifier: hiding applies to everything drawn from
1157
+ // here on, and nothing is claimed about scrollback either way.
1158
+ if (!revealing)
1159
+ toast('info', 'Thinking hidden.');
1160
+ else if (fullscreen)
1161
+ toast('info', 'Thinking shown.');
1162
+ else
1163
+ toast('info', 'Thinking shown for new output.');
995
1164
  return;
996
1165
  }
997
1166
  if (key.ctrl && (input === 'o' || input === 'O')) {
@@ -1056,6 +1225,40 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1056
1225
  const tokPerSec = state.status === 'running' && elapsedMs > 500
1057
1226
  ? Math.max(0, Math.round((state.usageTotal.outputTokens - runBaselineOut.current) / (elapsedMs / 1000)))
1058
1227
  : 0;
1228
+ // --- The activity line's seed (§3.2.3 / P1-7). --------------------------
1229
+ //
1230
+ // RENDER SCOPE. Without this the ref holds `0` on the first run (an arbitrary
1231
+ // word) and the PREVIOUS run's start on every run after, so every run opens on
1232
+ // a wrong phrase for one frame and then jumps — a flicker at the start of
1233
+ // every single turn, which is the one moment this feature exists to make feel
1234
+ // calm.
1235
+ const running = state.status === 'running';
1236
+ if (running && runStartedAt.current === 0)
1237
+ runStartedAt.current = Date.now();
1238
+ if (!running && runStartedAt.current !== 0)
1239
+ runStartedAt.current = 0;
1240
+ // --- The running tool (agent-activity-presentation-live §3.4 / L4). -----
1241
+ //
1242
+ // A REVERSE LOOP WITH AN EARLY EXIT, NOT `.filter(...).pop()` (P2-3).
1243
+ // `state.entries` is bounded by `transcriptRetain`, which DEFAULTS TO 1000,
1244
+ // and this runs in render scope at up to 30 fps — an allocating full scan
1245
+ // there is the exact cost `tui-render-performance` exists to remove. The
1246
+ // running tool, when there is one, is within a few entries of the end.
1247
+ let runningTool;
1248
+ if (running) {
1249
+ for (let i = state.entries.length - 1; i >= 0; i -= 1) {
1250
+ const e = state.entries[i];
1251
+ if (e.kind === 'tool' && e.status === 'running') {
1252
+ runningTool = e.name;
1253
+ break;
1254
+ }
1255
+ }
1256
+ }
1257
+ // WALL-CLOCK SECONDS, and the 200 ms ticker above is what causes the render
1258
+ // that re-reads it — so no timer is added (D-36 / E-15). Seconds rather than
1259
+ // milliseconds because `ToolCard` is `React.memo`'d with the default
1260
+ // comparator; `Transcript` passes it on only to running tool entries.
1261
+ const nowSec = running ? Math.floor(Date.now() / 1000) : undefined;
1059
1262
  // Row budget for the viewport and everything sized against it (§4.2a). The
1060
1263
  // inline path has no fixed frame, hence no height to fit into: `Infinity`
1061
1264
  // tells `OverlayFrame` to render everything and show no position indicator.
@@ -1100,18 +1303,33 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1100
1303
  model: cfg.model,
1101
1304
  baseUrl: cfg.baseUrl ?? '',
1102
1305
  thinkingLevel: cfg.thinkingLevel,
1306
+ // SEEDED AND READ BACK, both halves (§4.3): omitting either is the
1307
+ // documented silent half of adding a row — the field renders empty and
1308
+ // an untouched save writes a value the user never chose.
1309
+ //
1310
+ // FROM `ViewState`, NOT FROM `cfg`. `thinkingVisible` is session state
1311
+ // that `Ctrl+T` also writes, and there is no controller setter to keep
1312
+ // `cfg` in step — so seeding from the config would show the LAUNCH
1313
+ // value under a transcript that has been toggled since, and an
1314
+ // untouched save would then quietly toggle it back.
1315
+ showThinking: state.thinkingVisible ? 'on' : 'off',
1316
+ // FROM `cfg`, unlike its neighbour, and the difference is real: the
1317
+ // live tail is decided at construction (the store is allocated there
1318
+ // and there is no setter), so the config IS the current value and
1319
+ // nothing can have toggled it since launch.
1320
+ liveToolOutput: cfg.liveToolOutput ? 'on' : 'off',
1103
1321
  // `auto`, not an empty string: an empty field reads as "unset", which
1104
1322
  // is indistinguishable from AUTO and is not a state this can be in.
1105
1323
  maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : 'auto',
1106
1324
  apiKey: '',
1107
1325
  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;
1326
+ // The four fast rows, seeded through the pure mapper so the screen and
1327
+ // the save handler cannot disagree about what `(inherit)` means.
1328
+ ...fastSettingsFrom(cfg.fast),
1329
+ }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave,
1330
+ // Read from `cfg` (render scope, re-read every frame), so `/retry off`
1331
+ // while the screen is open shows the new value on the next paint.
1332
+ 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
1333
  // A resize can drop below the usable floor. Do NOT leave the alt-screen —
1116
1334
  // entering and leaving it on every drag is worse than a placeholder — and take
1117
1335
  // the height from `frameHeight()` like everything else, because this is the
@@ -1148,13 +1366,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1148
1366
  viewportRows: fullscreen ? viewportBudget : rows,
1149
1367
  offset: scrolledLines,
1150
1368
  };
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] }));
1369
+ 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
1370
  // The completion popup lives inside the bottom chrome and can add up to 9
1153
1371
  // rows; unbounded, it pushes the status bar past the frame height where
1154
1372
  // `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
1155
1373
  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
1374
+ 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' }) }) }));
1375
+ 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,
1376
+ // `!overlayNode` suppresses the line while an overlay owns the screen:
1377
+ // the full-screen branch replaces the whole viewport with the overlay,
1378
+ // and a working line under a settings screen is noise attached to a
1379
+ // surface the run is not visible on. (Inline overlays are part of the
1380
+ // document flow, so there the suppression is conservative rather than
1381
+ // necessary — P2-5.)
1382
+ activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, 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
1383
  ? {
1159
1384
  teamActive: {
1160
1385
  running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
@@ -1164,5 +1389,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1164
1389
  }
1165
1390
  : {}), ...(state.todos && !showRail
1166
1391
  ? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
1392
+ : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.retry
1393
+ ? {
1394
+ retryActive: {
1395
+ attempt: state.retry.attempt,
1396
+ max: state.retry.maxRetries,
1397
+ // Recomputed at RENDER from the absolute `resumeAt`, which is
1398
+ // what keeps the chip and the transcript card showing the same
1399
+ // number without either of them owning a second clock.
1400
+ secondsLeft: secondsLeft(state.retry.resumeAt, Date.now()),
1401
+ },
1402
+ }
1167
1403
  : {}) }) }));
1168
1404
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The bottom status row — the one already-budgeted row above the composer, now
3
+ * shared by two occupants (agent-activity-presentation §3.2.3 / D-17 / P0-1).
4
+ *
5
+ * THE ACTIVITY LINE IS NOT A NEW `AppShell` SLOT, AND THIS FILE IS WHY.
6
+ *
7
+ * `viewportRows()` (`layout/budget.ts`) is the ONE function that says how many
8
+ * rows the viewport gets, and it enumerates exactly `header + toast + composer +
9
+ * status`. In full-screen `AppShell`'s root is `height={frameHeight(rows)}` with
10
+ * `overflow="hidden"`, the bottom chrome box is `flexShrink={0}` and the viewport
11
+ * box is its only `flexShrink={1}` child — so an extra chrome row does not make
12
+ * the frame taller, Yoga takes the row out of the TRANSCRIPT. Meanwhile
13
+ * `viewportRows()` keeps returning the old number to `ScrollViewport`,
14
+ * `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`.
15
+ * The transcript would draw one row shorter than every consumer believes, ONLY
16
+ * WHILE A RUN IS IN FLIGHT, so the layout would shift on submit and shift back on
17
+ * `agent_end`.
18
+ *
19
+ * `ToastStack.tsx` already legislates this for itself: *"in `fullscreen` this
20
+ * ALWAYS occupies exactly one row — an empty row when there is nothing to say.
21
+ * Rendering conditionally would make the viewport height (and therefore the whole
22
+ * transcript) jump every time a toast appears or expires."* A run starts and ends
23
+ * far more often than a toast appears and expires, so the activity line is the
24
+ * STRONGER case for that rule, not an exception to it.
25
+ *
26
+ * Sharing the row means `budget.ts` and `AppShell.tsx` come out of this round
27
+ * UNMODIFIED — and `budget.test.ts` asserts that they did (AC-8a, DoD #8).
28
+ *
29
+ * PRECEDENCE: a toast wins. A transient ack is a RESPONSE TO THE USER, and the
30
+ * row nearest the input belongs to it.
31
+ */
32
+ import React from 'react';
33
+ import type { Theme } from './theme.js';
34
+ import type { Toast } from '../agent/reducer.js';
35
+ import type { RenderMode } from './layout/frame.js';
36
+ export interface BottomStatusRowProps {
37
+ mode: RenderMode;
38
+ toasts: Toast[];
39
+ /** `<ActivityLine>` while a run owns the screen, else `null`. */
40
+ activity: React.ReactNode;
41
+ theme: Theme;
42
+ }
43
+ export declare function BottomStatusRow({ mode, toasts, activity, theme, }: BottomStatusRowProps): React.ReactElement | null;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ import { ToastStack } from './ToastStack.js';
4
+ export function BottomStatusRow({ mode, toasts, activity, theme, }) {
5
+ // A toast wins the row; so does an empty toast stack in full-screen, whose
6
+ // blank `<Text> </Text>` IS the budgeted row and must keep being emitted.
7
+ if (toasts.length > 0 || !activity) {
8
+ return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
9
+ }
10
+ // Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
11
+ // branch is already a conditional 0-3 row stack, and this is simply one more
12
+ // conditional row above it.
13
+ return _jsx(Box, { flexShrink: 0, children: activity });
14
+ }
@@ -35,14 +35,9 @@ export interface ComposerProps {
35
35
  /** Mirror of the controller's effective mode; drives the chip and the border. */
36
36
  agentMode: AgentMode;
37
37
  popupMaxRows?: number;
38
- /** Pure pass-through to `PromptInput` (mouse-wheel-region-routing §5.6). */
39
- historyIntent?: {
40
- dir: 'up' | 'down';
41
- nonce: number;
42
- };
43
38
  theme: Theme;
44
39
  caps: TermCapabilities;
45
40
  onSubmit: (text: string) => void;
46
41
  onHelp?: () => void;
47
42
  }
48
- export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
43
+ export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
@@ -56,7 +56,7 @@ function hintText(running, submitCount, glyphs, mode) {
56
56
  '? help',
57
57
  ].join(dot);
58
58
  }
59
- export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }) {
59
+ export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
60
60
  const [hasDraft, setHasDraft] = useState(false);
61
61
  const onDraftChange = useCallback((next) => setHasDraft(next), []);
62
62
  // Plan mode inserts ONE branch, above `draft` and below `running`. The
@@ -73,7 +73,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
73
73
  : theme.idleBorder ?? theme.border;
74
74
  const glyphs = pickGlyphs(caps);
75
75
  const visible = showHint && hintsEnabled;
76
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, historyIntent: historyIntent }), visible && (
76
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
77
77
  // The chip rides the EXISTING hint row rather than adding one, so it
78
78
  // costs zero rows on a 24-row terminal.
79
79
  _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(running, submitCount, glyphs, agentMode) })] }))] }));
@@ -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 {};