@aragon-agent/cli 0.5.6 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/README.md +1429 -1105
  3. package/dist/agent/coalesce.d.ts +13 -0
  4. package/dist/agent/coalesce.js +50 -11
  5. package/dist/agent/controller.d.ts +141 -2
  6. package/dist/agent/controller.js +396 -4
  7. package/dist/agent/entry-limits.d.ts +65 -0
  8. package/dist/agent/entry-limits.js +133 -0
  9. package/dist/agent/headless.d.ts +8 -0
  10. package/dist/agent/headless.js +40 -0
  11. package/dist/agent/reducer.d.ts +253 -3
  12. package/dist/agent/reducer.js +553 -28
  13. package/dist/agent/retry-view.d.ts +68 -0
  14. package/dist/agent/retry-view.js +95 -0
  15. package/dist/agent/system-prompt.d.ts +11 -0
  16. package/dist/agent/system-prompt.js +2 -0
  17. package/dist/cli.js +122 -4
  18. package/dist/commands/builtins.d.ts +1 -1
  19. package/dist/commands/builtins.js +313 -2
  20. package/dist/commands/perf.d.ts +45 -0
  21. package/dist/commands/perf.js +78 -0
  22. package/dist/config/cli-commands.d.ts +53 -2
  23. package/dist/config/cli-commands.js +136 -3
  24. package/dist/config/env.d.ts +6 -1
  25. package/dist/config/env.js +131 -1
  26. package/dist/config/load.d.ts +63 -0
  27. package/dist/config/load.js +125 -2
  28. package/dist/config/schema.d.ts +273 -10
  29. package/dist/config/schema.js +250 -7
  30. package/dist/config/store.js +20 -1
  31. package/dist/fast/critique.d.ts +43 -0
  32. package/dist/fast/critique.js +62 -0
  33. package/dist/fast/digest.d.ts +42 -0
  34. package/dist/fast/digest.js +85 -0
  35. package/dist/fast/frames.d.ts +99 -0
  36. package/dist/fast/frames.js +180 -0
  37. package/dist/fast/limits.d.ts +98 -0
  38. package/dist/fast/limits.js +98 -0
  39. package/dist/fast/prompt.d.ts +72 -0
  40. package/dist/fast/prompt.js +94 -0
  41. package/dist/fast/resolve.d.ts +43 -0
  42. package/dist/fast/resolve.js +105 -0
  43. package/dist/fast/review-call.d.ts +99 -0
  44. package/dist/fast/review-call.js +130 -0
  45. package/dist/fast/reviewer.d.ts +248 -0
  46. package/dist/fast/reviewer.js +664 -0
  47. package/dist/fast/types.d.ts +128 -0
  48. package/dist/fast/types.js +14 -0
  49. package/dist/fast/wiring.d.ts +170 -0
  50. package/dist/fast/wiring.js +316 -0
  51. package/dist/logging/install.d.ts +15 -1
  52. package/dist/logging/install.js +37 -4
  53. package/dist/logging/logger.d.ts +1 -1
  54. package/dist/session/persist.js +57 -1
  55. package/dist/team/normalize.d.ts +26 -1
  56. package/dist/team/normalize.js +23 -3
  57. package/dist/team/prompt.d.ts +11 -0
  58. package/dist/team/prompt.js +10 -0
  59. package/dist/team/report.d.ts +18 -0
  60. package/dist/team/report.js +27 -1
  61. package/dist/team/runtime.d.ts +13 -2
  62. package/dist/team/runtime.js +33 -6
  63. package/dist/team/subagent.d.ts +19 -1
  64. package/dist/team/subagent.js +72 -10
  65. package/dist/team/task-tool.d.ts +26 -0
  66. package/dist/team/task-tool.js +48 -4
  67. package/dist/team/types.d.ts +80 -1
  68. package/dist/tools/bash-tool.js +19 -3
  69. package/dist/tools/diff.d.ts +25 -5
  70. package/dist/tools/diff.js +72 -41
  71. package/dist/tools/file-change-store.d.ts +34 -0
  72. package/dist/tools/file-change-store.js +57 -0
  73. package/dist/tools/fs-tools.d.ts +26 -0
  74. package/dist/tools/fs-tools.js +62 -5
  75. package/dist/tools/index.d.ts +31 -0
  76. package/dist/tools/index.js +7 -1
  77. package/dist/tools/patch.d.ts +115 -0
  78. package/dist/tools/patch.js +453 -0
  79. package/dist/tools/terminal-output.d.ts +107 -0
  80. package/dist/tools/terminal-output.js +198 -0
  81. package/dist/tools/tool-output-store.d.ts +68 -0
  82. package/dist/tools/tool-output-store.js +102 -0
  83. package/dist/ui/ActivityLine.d.ts +48 -0
  84. package/dist/ui/ActivityLine.js +22 -0
  85. package/dist/ui/App.d.ts +1 -4
  86. package/dist/ui/App.js +357 -44
  87. package/dist/ui/BottomStatusRow.d.ts +43 -0
  88. package/dist/ui/BottomStatusRow.js +14 -0
  89. package/dist/ui/Composer.d.ts +1 -6
  90. package/dist/ui/Composer.js +2 -2
  91. package/dist/ui/Markdown.d.ts +7 -1
  92. package/dist/ui/Markdown.js +15 -48
  93. package/dist/ui/PromptInput.d.ts +1 -15
  94. package/dist/ui/PromptInput.js +1 -38
  95. package/dist/ui/StatusBar.d.ts +50 -0
  96. package/dist/ui/StatusBar.js +7 -3
  97. package/dist/ui/TeamPanel.d.ts +5 -4
  98. package/dist/ui/TeamPanel.js +1 -1
  99. package/dist/ui/TodoPanel.d.ts +5 -4
  100. package/dist/ui/Transcript.d.ts +80 -5
  101. package/dist/ui/Transcript.js +179 -11
  102. package/dist/ui/activity-phrases.d.ts +33 -0
  103. package/dist/ui/activity-phrases.js +50 -0
  104. package/dist/ui/entries/AssistantEntry.d.ts +38 -1
  105. package/dist/ui/entries/AssistantEntry.js +34 -3
  106. package/dist/ui/entries/DiffView.d.ts +67 -0
  107. package/dist/ui/entries/DiffView.js +103 -0
  108. package/dist/ui/entries/FastCard.d.ts +53 -0
  109. package/dist/ui/entries/FastCard.js +87 -0
  110. package/dist/ui/entries/RetryCard.d.ts +56 -0
  111. package/dist/ui/entries/RetryCard.js +60 -0
  112. package/dist/ui/entries/TeamCard.d.ts +9 -1
  113. package/dist/ui/entries/TeamCard.js +28 -2
  114. package/dist/ui/entries/TodoCard.d.ts +9 -1
  115. package/dist/ui/entries/TodoCard.js +23 -1
  116. package/dist/ui/entries/ToolCard.d.ts +50 -1
  117. package/dist/ui/entries/ToolCard.js +104 -7
  118. package/dist/ui/entries/ToolPreview.d.ts +2 -2
  119. package/dist/ui/entries/UserEntry.d.ts +9 -1
  120. package/dist/ui/entries/UserEntry.js +17 -1
  121. package/dist/ui/exit-snapshot.d.ts +7 -0
  122. package/dist/ui/glyphs.d.ts +9 -0
  123. package/dist/ui/glyphs.js +2 -0
  124. package/dist/ui/layout/AppShell.d.ts +13 -22
  125. package/dist/ui/layout/AppShell.js +3 -38
  126. package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
  127. package/dist/ui/layout/MeasuredEntry.js +30 -0
  128. package/dist/ui/layout/ScrollViewport.js +8 -2
  129. package/dist/ui/layout/viewport-geometry.d.ts +28 -0
  130. package/dist/ui/layout/viewport-geometry.js +27 -0
  131. package/dist/ui/layout/virtual-window.d.ts +100 -0
  132. package/dist/ui/layout/virtual-window.js +308 -0
  133. package/dist/ui/live-clamp.d.ts +42 -0
  134. package/dist/ui/live-clamp.js +74 -0
  135. package/dist/ui/markdown-blocks.d.ts +25 -1
  136. package/dist/ui/markdown-blocks.js +43 -0
  137. package/dist/ui/overlays/HelpOverlay.js +16 -2
  138. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  139. package/dist/ui/overlays/SettingsScreen.js +147 -1
  140. package/dist/ui/render-cache.d.ts +81 -0
  141. package/dist/ui/render-cache.js +195 -0
  142. package/dist/ui/render-governor.d.ts +54 -0
  143. package/dist/ui/render-governor.js +78 -0
  144. package/dist/ui/transcript-text.d.ts +11 -0
  145. package/dist/ui/transcript-text.js +32 -0
  146. package/dist/ui/use-height-store.d.ts +31 -0
  147. package/dist/ui/use-height-store.js +76 -0
  148. package/dist/ui/use-render-governor.d.ts +39 -0
  149. package/dist/ui/use-render-governor.js +57 -0
  150. package/dist/ui/use-startup-notices.d.ts +9 -1
  151. package/dist/ui/use-startup-notices.js +35 -1
  152. package/dist/ui/use-wheel-routing.d.ts +16 -32
  153. package/dist/ui/use-wheel-routing.js +21 -38
  154. package/package.json +2 -2
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. package/dist/ui/layout/regions.js +0 -52
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Bounded view state (tui-render-performance L1 / R5).
3
+ *
4
+ * `ViewState.entries` used to grow without any ceiling: `argsRaw`, assistant
5
+ * `text` and `thinking` all concatenated forever, and the entry array was never
6
+ * trimmed. That makes the per-frame cost of the renderer proportional to the
7
+ * total number of characters the session has ever produced, because Ink's
8
+ * `renderNodeToOutput` walks and measures every mounted text node whether or not
9
+ * `overflow: hidden` clips it.
10
+ *
11
+ * Two bounds live here, both pure and React-free:
12
+ * - `appendBounded` caps one field by eliding its MIDDLE, and
13
+ * - `trimEntries` ring-trims the entry array itself.
14
+ *
15
+ * ASCII ONLY. This module is produced outside `src/ui/**`, where no terminal
16
+ * capabilities are available, so the elision marker has to be safe on a legacy
17
+ * `cmd.exe` -- the same rule `PREVIEW_TRUNCATION_MARK` in `reducer.ts` records.
18
+ */
19
+ import { DEFAULT_TRANSCRIPT_RETAIN } from '../config/schema.js';
20
+ /**
21
+ * The per-field ceilings.
22
+ *
23
+ * `text` is the generous one on purpose: a 256 KiB answer is already far past
24
+ * anything a terminal can usefully show, and the middle is what gets elided, so
25
+ * both the opening and the live cursor survive. `argsRaw` is the tight one --
26
+ * it is a streamed JSON fragment rendered as a 60-character card summary, so
27
+ * anything past a few KiB is invisible by construction.
28
+ */
29
+ export const ENTRY_LIMITS = {
30
+ text: 262_144,
31
+ thinking: 65_536,
32
+ argsRaw: 16_384,
33
+ };
34
+ /** Marker inserted at the elision point. ASCII -- produced outside `src/ui/**`. */
35
+ export const ELISION_MARK_PREFIX = '\n[... ';
36
+ export const ELISION_MARK_SUFFIX = ' characters elided ...]\n';
37
+ /** Matches a marker this module wrote, so re-elision collapses rather than nests. */
38
+ const ELISION_RE = /\n\[\.\.\. (\d+) characters elided \.\.\.\]\n/;
39
+ /** Fixed cost of the marker, before the digits of the count. */
40
+ const MARKER_FIXED = ELISION_MARK_PREFIX.length + ELISION_MARK_SUFFIX.length;
41
+ /**
42
+ * Fraction of the cap reserved for the HEAD. The tail gets the rest because that
43
+ * is where the cursor is; the head exists so the answer still starts where the
44
+ * model started it.
45
+ */
46
+ const HEAD_FRACTION = 4;
47
+ /** Undo one elision, so a second one merges into it rather than nesting. */
48
+ function splitElided(prev) {
49
+ const m = ELISION_RE.exec(prev);
50
+ if (!m)
51
+ return { head: '', elided: 0, tail: prev };
52
+ const index = m.index;
53
+ return {
54
+ head: prev.slice(0, index),
55
+ elided: Number.parseInt(m[1], 10) || 0,
56
+ tail: prev.slice(index + m[0].length),
57
+ };
58
+ }
59
+ /**
60
+ * Append `delta` to `prev`, keeping the result at or below `cap` by eliding the
61
+ * MIDDLE. Head and tail are both preserved: the head is the answer's opening,
62
+ * the tail is where the cursor is. Idempotent -- re-eliding an already elided
63
+ * string collapses the two markers into one.
64
+ *
65
+ * The returned string is NEVER longer than `cap` (AC-7). The marker's own digit
66
+ * count is bounded before the tail budget is computed, so the arithmetic cannot
67
+ * overshoot by a digit the way a naive `cap - marker.length` would.
68
+ */
69
+ export function appendBounded(prev, delta, cap) {
70
+ if (!Number.isFinite(cap) || cap <= 0)
71
+ return '';
72
+ const split = splitElided(prev);
73
+ let head = split.head;
74
+ let tail = split.tail + delta;
75
+ let elided = split.elided;
76
+ // Fast path: nothing has ever been elided and it still fits. This is the path
77
+ // every ordinary answer takes, so it must allocate nothing beyond the concat.
78
+ if (elided === 0 && tail.length <= cap)
79
+ return tail;
80
+ const headBudget = Math.max(0, Math.min(Math.floor(cap / HEAD_FRACTION), cap - MARKER_FIXED - 1));
81
+ if (elided === 0) {
82
+ // First elision: the head has to be carved out of the accumulated text.
83
+ head = tail.slice(0, headBudget);
84
+ tail = tail.slice(headBudget);
85
+ }
86
+ else if (head.length > headBudget) {
87
+ // A later `cap` reduction can leave the stored head oversized; charge the
88
+ // difference to the elision count rather than silently keeping it.
89
+ elided += head.length - headBudget;
90
+ head = head.slice(0, headBudget);
91
+ }
92
+ if (head.length + MARKER_FIXED >= cap) {
93
+ // Degenerate cap (only reachable from a hand-written test): keep the tail.
94
+ return tail.slice(Math.max(0, tail.length - cap));
95
+ }
96
+ // `elided + tail.length` is an UPPER BOUND on the final count, so its digit
97
+ // count is an upper bound on the marker's width. Budgeting against the bound
98
+ // is what makes the `<= cap` guarantee hold without a second pass.
99
+ const digitBound = String(elided + tail.length).length;
100
+ const markerBound = MARKER_FIXED + digitBound;
101
+ const tailKeep = Math.max(0, Math.min(tail.length, cap - head.length - markerBound));
102
+ const total = elided + (tail.length - tailKeep);
103
+ return `${head}${ELISION_MARK_PREFIX}${total}${ELISION_MARK_SUFFIX}${tail.slice(tail.length - tailKeep)}`;
104
+ }
105
+ /**
106
+ * Ring-trim to `retain` entries; returns the SAME array reference when no-op.
107
+ *
108
+ * The identity return is load-bearing: the reducer spreads this result into a
109
+ * new `ViewState` on every append, and a fresh array on every action would
110
+ * defeat the `React.memo` boundaries L2 installs on the transcript.
111
+ */
112
+ export function trimEntries(entries, retain) {
113
+ const keep = Math.max(1, Math.floor(retain));
114
+ if (entries.length <= keep)
115
+ return { entries, dropped: 0 };
116
+ return { entries: entries.slice(entries.length - keep), dropped: entries.length - keep };
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // The resolved retain ceiling
120
+ // ---------------------------------------------------------------------------
121
+ /**
122
+ * `viewReducer` is pure and takes no config, so the resolved `transcriptRetain`
123
+ * reaches it through a module-level singleton -- the shape `prompt-history.ts`
124
+ * already uses for `setHistoryEnabled`. `load.ts` is the ONLY writer.
125
+ */
126
+ let resolvedRetain = DEFAULT_TRANSCRIPT_RETAIN;
127
+ export function setEntryRetain(value) {
128
+ if (Number.isFinite(value) && value > 0)
129
+ resolvedRetain = Math.floor(value);
130
+ }
131
+ export function entryRetain() {
132
+ return resolvedRetain;
133
+ }
@@ -8,6 +8,7 @@ import type { AgentEvent, ModelInfo } from '@aragon-agent/core';
8
8
  import type { PreflightResult } from './controller.js';
9
9
  import type { TeamEvent } from '../team/types.js';
10
10
  import type { TodoEvent, TodoSnapshot } from '../todo/types.js';
11
+ import type { FastEvent } from '../fast/types.js';
11
12
  import { type FollowThroughMode } from '../todo/follow-through.js';
12
13
  /** The minimal controller surface headless mode needs (real or stubbed). */
13
14
  export interface HeadlessController {
@@ -35,6 +36,13 @@ export interface HeadlessController {
35
36
  * keeps a `--no-todo` headless run byte-identical to today's.
36
37
  */
37
38
  subscribeTodos?(listener: (event: TodoEvent) => void): () => void;
39
+ /**
40
+ * The CLI-local fast-tier stream — OPTIONAL, for the identical reason the two
41
+ * above are (fast-model-tier §3.9). A controller with no tier has no fast
42
+ * events, and `runHeadless` calls it with `?.`, which keeps a default `-p` run
43
+ * byte-identical to today's.
44
+ */
45
+ subscribeFast?(listener: (event: FastEvent) => void): () => void;
38
46
  /**
39
47
  * The current plan — OPTIONAL, for the identical reason the two subscriptions
40
48
  * above are (todo-plan-followthrough §3.6 rule 2). ABSENT MEANS THE
@@ -21,6 +21,8 @@ export async function runHeadless(controller, prompt, options = {}) {
21
21
  let sawTurnEnd = false;
22
22
  let wroteText = false;
23
23
  const total = { inputTokens: 0, outputTokens: 0 };
24
+ /** Retries seen this run, so the recovery line can name a count. */
25
+ let retryCount = 0;
24
26
  const unsubscribe = controller.subscribe((event) => {
25
27
  switch (event.type) {
26
28
  case 'message_update': {
@@ -33,6 +35,25 @@ export async function runHeadless(controller, prompt, options = {}) {
33
35
  errored = true;
34
36
  err.write(`\n${formatStreamError(se.error)}\n`);
35
37
  }
38
+ else if (se.type === 'retry_scheduled') {
39
+ // STDERR, NEVER STDOUT (llm-api-retry-backoff §6.7). stdout is the
40
+ // machine-readable answer and has to stay parseable by
41
+ // `aragon -p | jq`; a retry line on it would corrupt every consumer.
42
+ // Not `--quiet`-gated, unlike the tool lines: a three-minute pause with
43
+ // no explanation anywhere is not a "compact" run, it is a hang.
44
+ retryCount = se.attempt;
45
+ // The em dash is a literal because `agent/headless.ts` is the CLI's one
46
+ // documented glyph-scanner exemption: `aragon -p` does no capability
47
+ // probe, so there is no `Glyphs` set to read here.
48
+ err.write(`\n[retry ${se.attempt}/${se.maxRetries}] ${se.errorType} — ` +
49
+ `waiting ${Math.round(se.delayMs / 1000)}s\n`);
50
+ }
51
+ else if (se.type === 'stream_restart') {
52
+ // The partial answer already on stdout is about to be replayed. Saying so
53
+ // is the only way a piped consumer can tell a duplicated paragraph from
54
+ // a model that repeated itself.
55
+ err.write(`[retry] discarding a partial response and restarting\n`);
56
+ }
36
57
  break;
37
58
  }
38
59
  case 'tool_execution_start':
@@ -46,6 +67,10 @@ export async function runHeadless(controller, prompt, options = {}) {
46
67
  break;
47
68
  case 'turn_end':
48
69
  sawTurnEnd = true;
70
+ if (retryCount > 0) {
71
+ err.write(`[retry] recovered after ${retryCount} ${retryCount === 1 ? 'retry' : 'retries'}\n`);
72
+ retryCount = 0;
73
+ }
49
74
  total.inputTokens += event.usage.inputTokens;
50
75
  total.outputTokens += event.usage.outputTokens;
51
76
  break;
@@ -128,6 +153,20 @@ export async function runHeadless(controller, prompt, options = {}) {
128
153
  lastActive = activeIndex;
129
154
  err.write(`[todo] ${activeIndex + 1}/${total} ${items[activeIndex]?.content ?? ''}\n`);
130
155
  });
156
+ // Fast-tier review spend must reach the `[usage]` footer, or `-p` under-reports
157
+ // by however much the reviewer consumed (§3.6).
158
+ //
159
+ // NOTHING IS WRITTEN TO STDOUT, AND NOTHING TO STDERR EITHER (D-12). stdout is
160
+ // the answer channel and is piped into other tools; stderr in `-p` carries the
161
+ // tool trace, and an unrequested second opinion is not a tool the user asked
162
+ // for. The reviews are in the log records either way, which is where a `-p`
163
+ // user goes to ask what happened.
164
+ const unsubscribeFast = controller.subscribeFast?.((event) => {
165
+ if (event.type !== 'usage')
166
+ return;
167
+ total.inputTokens += event.usage.inputTokens;
168
+ total.outputTokens += event.usage.outputTokens;
169
+ });
131
170
  // --- The bounded continuation loop (todo-plan-followthrough §3.6). --------
132
171
  //
133
172
  // THE THREE `unsubscribe` CALLS STAY OUTSIDE IT (rule 4). The loop wraps the
@@ -187,6 +226,7 @@ export async function runHeadless(controller, prompt, options = {}) {
187
226
  unsubscribe();
188
227
  unsubscribeTeam?.();
189
228
  unsubscribeTodos?.();
229
+ unsubscribeFast?.();
190
230
  }
191
231
  // Ensure a trailing newline so piped output is well-formed.
192
232
  if (wroteText)
@@ -13,7 +13,10 @@
13
13
  import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agent/core';
14
14
  import type { AgentMode } from './agent-mode.js';
15
15
  import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
16
+ import type { FastReview, FastSnapshot } from '../fast/types.js';
16
17
  import type { TodoItem, TodoSnapshot } from '../todo/types.js';
18
+ import type { RetryPhase } from './retry-view.js';
19
+ import type { FilePatch } from '../tools/patch.js';
17
20
  export type NoticeLevel = 'info' | 'warn' | 'error';
18
21
  export type ToastLevel = NoticeLevel | 'success';
19
22
  export type Overlay = null | 'settings' | 'model' | 'help' | 'confirm' | 'question' | 'plan';
@@ -40,6 +43,21 @@ export type Entry = {
40
43
  streaming: boolean;
41
44
  aborted?: boolean;
42
45
  usage?: TokenUsage;
46
+ /**
47
+ * Epoch ms the turn started reasoning (agent-activity-presentation §3.1.2).
48
+ * Set by `thinkingStart`, cleared by `streamRestart`.
49
+ */
50
+ thinkingStartedAt?: number;
51
+ /**
52
+ * Wall-clock reasoning time, SEALED ONCE — at the first text delta, or at
53
+ * turn/run end for a turn that thought and then called a tool without
54
+ * emitting any text.
55
+ *
56
+ * With thinking hidden this is the only thing the collapsed marker can say
57
+ * that the user could not otherwise know, which is the whole argument for
58
+ * having a marker at all (§1.2).
59
+ */
60
+ thinkingMs?: number;
43
61
  } | {
44
62
  id: string;
45
63
  kind: 'tool';
@@ -52,6 +70,44 @@ export type Entry = {
52
70
  preview?: string;
53
71
  durationMs?: number;
54
72
  isError?: boolean;
73
+ /**
74
+ * The structured file diff (agent-activity-presentation §3.3), or absent.
75
+ *
76
+ * IMMUTABLE AND WRITTEN ONCE, by `toolExecEnd`. `ToolCard` is `React.memo`'d
77
+ * with the DEFAULT comparator, so this object is only free while its
78
+ * reference is stable — which it is, because the reducer never rebuilds it
79
+ * and `mapEntry` preserves identity for untouched entries. A future action
80
+ * that recomputed or normalized a patch would turn that memo boundary into
81
+ * a no-op WITH NOTHING FAILING (P2-9).
82
+ *
83
+ * Needs no clause in `normalizeLoadedEntries` (D-9): the four that exist
84
+ * are all about a LIVE flag that would never settle, and a patch is settled
85
+ * by construction.
86
+ */
87
+ patch?: FilePatch;
88
+ /**
89
+ * The sanitised live tail while the tool runs
90
+ * (agent-activity-presentation-live §3.3.1). Cleared at settle (D-23), and
91
+ * cleared again by EVERY path that reaches `status: 'idle'` (D-35).
92
+ *
93
+ * REDUCER STATE RATHER THAN A STORE READ DURING RENDER, and that is the
94
+ * load-bearing decision of the round (D-22): `entryRevision` is what tells
95
+ * the height cache an entry's rendered output changed, and an entry mutated
96
+ * outside `ViewState` keeps its revision and freezes at a stale height AND
97
+ * a stale subtree with nothing anywhere reporting it (I-L3-1).
98
+ */
99
+ live?: readonly string[];
100
+ /**
101
+ * Monotonic counter -- the revision term for a NON-APPEND mutation (D-22).
102
+ *
103
+ * A COUNTER AND NOT `live.length`: a fixed-size tail evicting its oldest
104
+ * row while appending a new one of the same width leaves the joined length
105
+ * unchanged, which is exactly the lossy case `virtual-window.ts` says the
106
+ * reducer cannot produce today and that this round introduces.
107
+ */
108
+ liveSeq?: number;
109
+ /** Epoch ms of the last output row, for the stall row (§3.3.4). */
110
+ lastOutputAt?: number;
55
111
  } | {
56
112
  id: string;
57
113
  kind: 'notice';
@@ -107,12 +163,85 @@ export type Entry = {
107
163
  * card this session produced.
108
164
  */
109
165
  interrupted?: boolean;
166
+ }
167
+ /**
168
+ * The turn's retry story (llm-api-retry-backoff §6.4). ONE per turn, REWRITTEN
169
+ * IN PLACE — ten retries rendered as ten notices would bury the transcript in
170
+ * the one moment the user most needs it legible.
171
+ *
172
+ * JSON-SERIALIZABLE by construction, so `/save` needs no format change.
173
+ *
174
+ * `phase: 'waiting' | 'retrying'` is normalized away ON LOAD, in
175
+ * `session/persist.ts`, exactly as the team entry's `active` and the todo
176
+ * card's `live` are (R-10): `Transcript`'s settled boundary is MONOTONIC, so a
177
+ * card that never settles never reaches `<Static>` and is re-rendered on every
178
+ * frame for the rest of the session.
179
+ */
180
+ | {
181
+ id: string;
182
+ kind: 'retry';
183
+ /** 1-based retry index of the LATEST attempt this card has seen. */
184
+ attempt: number;
185
+ maxRetries: number;
186
+ errorType: string;
187
+ message: string;
188
+ delayMs: number;
189
+ /** Epoch ms the next attempt fires; the UI ticks against this. */
190
+ resumeAt?: number;
191
+ phase: RetryPhase;
192
+ startedAt: number;
193
+ /** Filled on settle. */
194
+ totalRetries?: number;
195
+ elapsedMs?: number;
196
+ }
197
+ /**
198
+ * One fast-model review (fast-model-tier §5.4). ONE CARD PER REVIEW, never
199
+ * rewritten across reviews — a critique is a single event, unlike the todo
200
+ * card's per-turn checklist.
201
+ *
202
+ * JSON-SERIALIZABLE by construction, so `/save` needs no format change.
203
+ *
204
+ * `live: true` is normalized away ON LOAD, in `session/persist.ts`, next to
205
+ * `team.active`, `todo.live` and the retry card's phase, and for the identical
206
+ * reason (C-5): `Transcript`'s settled boundary is MONOTONIC, so an entry that
207
+ * never settles never reaches `<Static>` and is re-rendered on every frame for
208
+ * the rest of the session. The reducer never legitimately sees a stale-live
209
+ * entry — only the file does.
210
+ */
211
+ | {
212
+ id: string;
213
+ kind: 'fast';
214
+ /** 1-based within the session, so the card can name itself. */
215
+ reviewIndex: number;
216
+ model: string;
217
+ status: 'running' | 'ok' | 'advice' | 'empty' | 'failed' | 'dropped';
218
+ /** Present only for `advice`. */
219
+ text?: string;
220
+ /** Failure / drop reason, shown as one muted line. */
221
+ detail?: string;
222
+ turn: number;
223
+ durationMs?: number;
224
+ live: boolean;
110
225
  };
111
226
  export interface UsageTotal {
112
227
  inputTokens: number;
113
228
  outputTokens: number;
114
229
  costUsd: number;
115
230
  }
231
+ /**
232
+ * The ephemeral retry projection held in `ViewState.retry` (§9).
233
+ *
234
+ * Only the two IN-FLIGHT phases exist here: once a card settles the projection is
235
+ * `null`, because the settled card IS the history and a chip that reported a
236
+ * finished retry would never go away.
237
+ */
238
+ export interface RetrySnapshot {
239
+ attempt: number;
240
+ maxRetries: number;
241
+ resumeAt?: number;
242
+ phase: 'waiting' | 'retrying';
243
+ errorType: string;
244
+ }
116
245
  export interface ViewState {
117
246
  entries: Entry[];
118
247
  status: 'idle' | 'running';
@@ -150,18 +279,64 @@ export interface ViewState {
150
279
  * build (AC-24).
151
280
  */
152
281
  todos: TodoSnapshot | null;
282
+ /**
283
+ * The live retry projection, or `null` when nothing is retrying.
284
+ *
285
+ * NEVER PERSISTED — `entries` is the history and the settled card is the
286
+ * record. It drives the status-bar chip and the 1 Hz tick, and it is nulled on
287
+ * every settle path. Same contract as `team`.
288
+ */
289
+ retry: RetrySnapshot | null;
290
+ /**
291
+ * The live fast-tier projection, or `null` when the tier is off for this
292
+ * session (fast-model-tier §5.2).
293
+ *
294
+ * NEVER PERSISTED — `entries` is the history and the settled cards are the
295
+ * record. It drives the status chip and `/fast status`'s totals. `null` is
296
+ * what leaves an ordinary session's status bar byte-identical (AC-4).
297
+ */
298
+ fast: FastSnapshot | null;
153
299
  seq: number;
154
300
  /** The `kind: 'team'` entry the live dispatch is writing into. */
155
301
  teamEntryId?: string;
156
302
  /** The `kind: 'todo'` entry this turn is writing into. */
157
303
  todoEntryId?: string;
304
+ /** The `kind: 'retry'` entry this turn is writing into. */
305
+ retryEntryId?: string;
306
+ /** The `kind: 'fast'` entry the in-flight review is writing into. */
307
+ fastEntryId?: string;
158
308
  streamingId?: string;
159
309
  turnProduced: boolean;
160
310
  errorNoticed: boolean;
161
311
  /** The current run was aborted by the user (suppresses the failure guard). */
162
312
  aborted: boolean;
313
+ /**
314
+ * Entries removed by `transcriptRetain` (tui-render-performance L1).
315
+ *
316
+ * Rendered by `transcript-text` and reported by `/perf`; NEVER PERSISTED —
317
+ * `SavedSession` keeps `{ model, messages, entries, todos }` exactly as it is,
318
+ * so `/save` and `/resume` need no format change and old files load unchanged.
319
+ */
320
+ droppedEntries: number;
321
+ }
322
+ export interface ViewStateSeed {
323
+ /** Start with thinking blocks expanded. Defaults to FALSE, the product default. */
324
+ thinkingVisible?: boolean;
163
325
  }
164
- export declare function initialViewState(): ViewState;
326
+ /**
327
+ * The initial view state, optionally seeded from config.
328
+ *
329
+ * THE DEFAULT IS `false`, NOT `true`, AND THAT IS THE WHOLE SAFETY ARGUMENT
330
+ * (D-2). There is exactly one production call site — `App`'s `useReducer`, which
331
+ * passes `cfg.showThinking`. Every other caller is a test. A future call site
332
+ * that forgets the seed fails CLOSED, toward the product default, rather than
333
+ * silently restoring the reasoning firehose for one code path with nothing
334
+ * reporting it.
335
+ *
336
+ * The seed is a PARAMETER rather than a post-mount dispatch so that `/resume`
337
+ * cannot flash a restored thinking block on frame 1.
338
+ */
339
+ export declare function initialViewState(seed?: ViewStateSeed): ViewState;
165
340
  export type ViewAction = {
166
341
  type: 'submit';
167
342
  text: string;
@@ -199,6 +374,22 @@ export type ViewAction = {
199
374
  isError: boolean;
200
375
  duration: number;
201
376
  preview: string;
377
+ /** A fully-built, immutable diff, so the reducer stays pure (§3.3.7). */
378
+ patch?: FilePatch;
379
+ }
380
+ /**
381
+ * A running tool's live tail (agent-activity-presentation-live §3.3.1).
382
+ *
383
+ * `rows` IS THE WHOLE TAIL, not an increment: the store replaces it on every
384
+ * chunk, which is what lets `mergeDeltas` keep only the last action per
385
+ * `toolCallId` (D-30). `at` is read in `App`'s listener rather than here, so
386
+ * this reducer stays pure.
387
+ */
388
+ | {
389
+ type: 'toolOutputDelta';
390
+ toolCallId: string;
391
+ rows: readonly string[];
392
+ at: number;
202
393
  } | {
203
394
  type: 'turnEnd';
204
395
  usage: TokenUsage;
@@ -258,6 +449,54 @@ export type ViewAction = {
258
449
  snapshot: TodoSnapshot;
259
450
  } | {
260
451
  type: 'todoCleared';
452
+ } | {
453
+ type: 'retryScheduled';
454
+ attempt: number;
455
+ maxRetries: number;
456
+ delayMs: number;
457
+ resumeAt: number;
458
+ errorType: string;
459
+ message: string;
460
+ } | {
461
+ type: 'retryAttempt';
462
+ attempt: number;
463
+ maxRetries: number;
464
+ } | {
465
+ type: 'streamRestart';
466
+ discardedToolCallIds: string[];
467
+ }
468
+ /**
469
+ * The 1 Hz countdown tick. NO STATE OF ITS OWN — it bumps `seq` so the frame
470
+ * re-renders and the card recomputes `resumeAt - Date.now()`. `App` runs the
471
+ * interval ONLY while `state.retry?.phase === 'waiting'`, so an idle session has
472
+ * no timer at all.
473
+ */
474
+ | {
475
+ type: 'retryTick';
476
+ } | {
477
+ type: 'fastStart';
478
+ index: number;
479
+ turn: number;
480
+ model: string;
481
+ } | {
482
+ type: 'fastEnd';
483
+ review: FastReview;
484
+ }
485
+ /**
486
+ * Review spend.
487
+ *
488
+ * `costDelta` IS PRECOMPUTED BY THE CALLER, exactly as `teamUsage`'s is: the
489
+ * reducer stays pure and cost-table-free, which is the property that makes it
490
+ * testable — and the fast tier has a DIFFERENT cost table from the lead, so a
491
+ * reducer that computed its own would have to know about both (§3.6 / R-7).
492
+ */
493
+ | {
494
+ type: 'fastUsage';
495
+ usage: TokenUsage;
496
+ costDelta: number;
497
+ } | {
498
+ type: 'fastTier';
499
+ snapshot: FastSnapshot;
261
500
  };
262
501
  export declare const STORED_PREVIEW_CHARS = 8000;
263
502
  export declare const STORED_PREVIEW_LINES = 200;
@@ -276,12 +515,23 @@ export declare const PREVIEW_TRUNCATION_MARK = "...";
276
515
  * over-promises how much more is expandable.
277
516
  */
278
517
  export declare function buildToolPreview(result: ToolResult): string;
518
+ /**
519
+ * Where a `FilePatch` is collected from on `tool_execution_end`.
520
+ *
521
+ * Supplying one makes `reduceEvent` CONSUME-ONCE rather than pure, which is safe
522
+ * because `App.tsx` calls it exactly once per event and `mergeDeltas` only ever
523
+ * merges `textDelta` / `thinkingDelta`, never `toolExecEnd`.
524
+ */
525
+ export interface PatchSource {
526
+ take(toolCallId: string): FilePatch | undefined;
527
+ }
279
528
  /**
280
529
  * Map one core `AgentEvent` to view actions. `cost` (the active model's cost
281
530
  * table) lets `turn_end` accumulate USD cost; omit it in tests that only assert
282
- * token totals.
531
+ * token totals. `patches` is the CLI-local diff side channel (§3.3.7); omitting
532
+ * it — every existing test, and headless mode — produces byte-identical actions.
283
533
  */
284
- export declare function reduceEvent(event: AgentEvent, cost?: ModelCost): ViewAction[];
534
+ export declare function reduceEvent(event: AgentEvent, cost?: ModelCost, patches?: PatchSource): ViewAction[];
285
535
  /** Turn a provider error into a friendly banner, mapping LLMError.errorType. */
286
536
  export declare function formatStreamError(error: Error): string;
287
537
  export declare function viewReducer(state: ViewState, action: ViewAction): ViewState;