@aragon-agent/cli 0.5.8 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +1157 -747
  2. package/README.md +1552 -1163
  3. package/dist/agent/coalesce.d.ts +13 -0
  4. package/dist/agent/coalesce.js +50 -11
  5. package/dist/agent/controller.d.ts +141 -2
  6. package/dist/agent/controller.js +396 -4
  7. package/dist/agent/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +40 -0
  9. package/dist/agent/reducer.d.ts +245 -3
  10. package/dist/agent/reducer.js +490 -17
  11. package/dist/agent/retry-view.d.ts +68 -0
  12. package/dist/agent/retry-view.js +95 -0
  13. package/dist/agent/system-prompt.d.ts +11 -0
  14. package/dist/agent/system-prompt.js +2 -0
  15. package/dist/boot/guard.d.ts +135 -0
  16. package/dist/boot/guard.js +218 -0
  17. package/dist/boot/rollback.d.ts +86 -0
  18. package/dist/boot/rollback.js +185 -0
  19. package/dist/cli.js +368 -3
  20. package/dist/commands/builtins.d.ts +1 -1
  21. package/dist/commands/builtins.js +426 -2
  22. package/dist/commands/registry.d.ts +17 -0
  23. package/dist/config/app-paths.d.ts +31 -0
  24. package/dist/config/app-paths.js +37 -0
  25. package/dist/config/cli-commands.d.ts +79 -2
  26. package/dist/config/cli-commands.js +185 -3
  27. package/dist/config/env.d.ts +15 -3
  28. package/dist/config/env.js +143 -3
  29. package/dist/config/load.d.ts +66 -0
  30. package/dist/config/load.js +107 -1
  31. package/dist/config/schema.d.ts +268 -2
  32. package/dist/config/schema.js +288 -1
  33. package/dist/config/store.js +32 -1
  34. package/dist/fast/critique.d.ts +43 -0
  35. package/dist/fast/critique.js +62 -0
  36. package/dist/fast/digest.d.ts +42 -0
  37. package/dist/fast/digest.js +85 -0
  38. package/dist/fast/frames.d.ts +99 -0
  39. package/dist/fast/frames.js +180 -0
  40. package/dist/fast/limits.d.ts +98 -0
  41. package/dist/fast/limits.js +98 -0
  42. package/dist/fast/prompt.d.ts +72 -0
  43. package/dist/fast/prompt.js +94 -0
  44. package/dist/fast/resolve.d.ts +43 -0
  45. package/dist/fast/resolve.js +105 -0
  46. package/dist/fast/review-call.d.ts +99 -0
  47. package/dist/fast/review-call.js +130 -0
  48. package/dist/fast/reviewer.d.ts +248 -0
  49. package/dist/fast/reviewer.js +664 -0
  50. package/dist/fast/types.d.ts +128 -0
  51. package/dist/fast/types.js +14 -0
  52. package/dist/fast/wiring.d.ts +170 -0
  53. package/dist/fast/wiring.js +316 -0
  54. package/dist/launcher.d.ts +14 -0
  55. package/dist/launcher.js +43 -0
  56. package/dist/logging/install.d.ts +15 -1
  57. package/dist/logging/install.js +37 -4
  58. package/dist/logging/logger.d.ts +1 -1
  59. package/dist/session/persist.js +57 -1
  60. package/dist/team/normalize.d.ts +26 -1
  61. package/dist/team/normalize.js +23 -3
  62. package/dist/team/prompt.d.ts +11 -0
  63. package/dist/team/prompt.js +10 -0
  64. package/dist/team/report.d.ts +18 -0
  65. package/dist/team/report.js +27 -1
  66. package/dist/team/runtime.d.ts +13 -2
  67. package/dist/team/runtime.js +33 -6
  68. package/dist/team/subagent.d.ts +19 -1
  69. package/dist/team/subagent.js +72 -10
  70. package/dist/team/task-tool.d.ts +26 -0
  71. package/dist/team/task-tool.js +48 -4
  72. package/dist/team/types.d.ts +80 -1
  73. package/dist/tools/bash-tool.js +19 -3
  74. package/dist/tools/diff.d.ts +25 -5
  75. package/dist/tools/diff.js +72 -41
  76. package/dist/tools/file-change-store.d.ts +34 -0
  77. package/dist/tools/file-change-store.js +57 -0
  78. package/dist/tools/fs-tools.d.ts +26 -0
  79. package/dist/tools/fs-tools.js +62 -5
  80. package/dist/tools/index.d.ts +31 -0
  81. package/dist/tools/index.js +7 -1
  82. package/dist/tools/patch.d.ts +115 -0
  83. package/dist/tools/patch.js +453 -0
  84. package/dist/tools/terminal-output.d.ts +107 -0
  85. package/dist/tools/terminal-output.js +198 -0
  86. package/dist/tools/tool-output-store.d.ts +68 -0
  87. package/dist/tools/tool-output-store.js +102 -0
  88. package/dist/ui/ActivityLine.d.ts +48 -0
  89. package/dist/ui/ActivityLine.js +22 -0
  90. package/dist/ui/App.d.ts +11 -4
  91. package/dist/ui/App.js +359 -37
  92. package/dist/ui/BottomStatusRow.d.ts +65 -0
  93. package/dist/ui/BottomStatusRow.js +17 -0
  94. package/dist/ui/Composer.d.ts +1 -6
  95. package/dist/ui/Composer.js +2 -2
  96. package/dist/ui/PromptInput.d.ts +1 -15
  97. package/dist/ui/PromptInput.js +1 -38
  98. package/dist/ui/StatusBar.d.ts +37 -0
  99. package/dist/ui/StatusBar.js +5 -3
  100. package/dist/ui/TeamPanel.d.ts +5 -4
  101. package/dist/ui/TeamPanel.js +1 -1
  102. package/dist/ui/TodoPanel.d.ts +5 -4
  103. package/dist/ui/Transcript.d.ts +26 -3
  104. package/dist/ui/Transcript.js +76 -10
  105. package/dist/ui/UpdateLine.d.ts +44 -0
  106. package/dist/ui/UpdateLine.js +129 -0
  107. package/dist/ui/activity-phrases.d.ts +33 -0
  108. package/dist/ui/activity-phrases.js +50 -0
  109. package/dist/ui/entries/AssistantEntry.d.ts +20 -0
  110. package/dist/ui/entries/AssistantEntry.js +3 -2
  111. package/dist/ui/entries/DiffView.d.ts +67 -0
  112. package/dist/ui/entries/DiffView.js +103 -0
  113. package/dist/ui/entries/FastCard.d.ts +53 -0
  114. package/dist/ui/entries/FastCard.js +87 -0
  115. package/dist/ui/entries/RetryCard.d.ts +56 -0
  116. package/dist/ui/entries/RetryCard.js +60 -0
  117. package/dist/ui/entries/TeamCard.js +6 -1
  118. package/dist/ui/entries/ToolCard.d.ts +38 -1
  119. package/dist/ui/entries/ToolCard.js +68 -5
  120. package/dist/ui/glyphs.d.ts +9 -0
  121. package/dist/ui/glyphs.js +2 -0
  122. package/dist/ui/layout/AppShell.d.ts +13 -22
  123. package/dist/ui/layout/AppShell.js +3 -38
  124. package/dist/ui/layout/virtual-window.d.ts +1 -1
  125. package/dist/ui/layout/virtual-window.js +109 -5
  126. package/dist/ui/overlays/HelpOverlay.js +16 -2
  127. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  128. package/dist/ui/overlays/SettingsScreen.js +147 -1
  129. package/dist/ui/transcript-text.js +27 -0
  130. package/dist/ui/use-wheel-routing.d.ts +16 -32
  131. package/dist/ui/use-wheel-routing.js +21 -38
  132. package/dist/update/classify-failure.d.ts +32 -0
  133. package/dist/update/classify-failure.js +60 -0
  134. package/dist/update/install-lock.d.ts +55 -0
  135. package/dist/update/install-lock.js +184 -0
  136. package/dist/update/install-source.d.ts +97 -0
  137. package/dist/update/install-source.js +218 -0
  138. package/dist/update/installer.d.ts +108 -0
  139. package/dist/update/installer.js +242 -0
  140. package/dist/update/limits.d.ts +111 -0
  141. package/dist/update/limits.js +111 -0
  142. package/dist/update/npm-view.d.ts +62 -0
  143. package/dist/update/npm-view.js +133 -0
  144. package/dist/update/registry.d.ts +52 -0
  145. package/dist/update/registry.js +161 -0
  146. package/dist/update/semver.d.ts +89 -0
  147. package/dist/update/semver.js +295 -0
  148. package/dist/update/service.d.ts +193 -0
  149. package/dist/update/service.js +636 -0
  150. package/dist/update/state.d.ts +103 -0
  151. package/dist/update/state.js +142 -0
  152. package/dist/update/types.d.ts +169 -0
  153. package/dist/update/types.js +68 -0
  154. package/package.json +3 -3
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. package/dist/ui/layout/regions.js +0 -52
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { pickGlyphs } from './glyphs.js';
4
+ export function UpdateLine({ snapshot, compact, theme, caps }) {
5
+ const glyphs = pickGlyphs(caps);
6
+ const version = snapshot.latestVersion ?? snapshot.currentVersion;
7
+ const advice = snapshot.advice ?? '';
8
+ const dot = glyphs.midDot;
9
+ const line = (() => {
10
+ if (snapshot.phase === 'installing') {
11
+ return {
12
+ glyph: glyphs.arrowUp,
13
+ text: compact
14
+ ? `${version}${glyphs.ellipsis}`
15
+ : `updating to ${version}${glyphs.ellipsis}`,
16
+ color: theme.muted,
17
+ };
18
+ }
19
+ if (snapshot.phase === 'ready') {
20
+ return {
21
+ glyph: glyphs.check,
22
+ text: compact ? `${version} ready` : `${version} installed ${dot} restart aragon to apply`,
23
+ color: theme.toast.success,
24
+ };
25
+ }
26
+ if (snapshot.phase === 'failed') {
27
+ // H2's three classified failures (cli-auto-update-hardening section 8).
28
+ // Each names the REAL obstacle rather than the generic one, because each
29
+ // is allowed to break the three-strike silence and a notice that breaks
30
+ // silence must be worth the interruption.
31
+ if (snapshot.reason === 'blocked-by-os') {
32
+ // `npm i -g` FROM THE SAME SHELL HITS THE SAME HANDLE. On Windows the
33
+ // shim that launched us is being read by the `cmd.exe` running it for
34
+ // as long as our process lives, and npm's install rewrites that shim -
35
+ // so the generic advice is not merely unhelpful here, it is a wrong
36
+ // instruction (R-3 / section 5.3.1).
37
+ return {
38
+ glyph: glyphs.warn,
39
+ text: compact || !advice
40
+ ? 'update blocked'
41
+ : `update blocked ${dot} close other aragon windows, then: ${advice}`,
42
+ color: theme.noticeWarn,
43
+ };
44
+ }
45
+ if (snapshot.reason === 'no-space') {
46
+ return {
47
+ glyph: glyphs.warn,
48
+ text: compact ? 'no disk space' : `update failed ${dot} no disk space`,
49
+ color: theme.noticeWarn,
50
+ };
51
+ }
52
+ if (snapshot.reason === 'not-writable') {
53
+ return {
54
+ glyph: glyphs.warn,
55
+ text: compact || !advice
56
+ ? 'update blocked'
57
+ : `update needs write access ${dot} ${advice}`,
58
+ color: theme.noticeWarn,
59
+ };
60
+ }
61
+ return {
62
+ glyph: glyphs.warn,
63
+ text: compact || !advice ? 'update failed' : `update failed ${dot} ${advice}`,
64
+ color: theme.noticeWarn,
65
+ };
66
+ }
67
+ if (snapshot.phase === 'available') {
68
+ if (snapshot.reason === 'node-too-old') {
69
+ // Both numbers or neither: "needs a newer Node" without saying which is
70
+ // a notice the user cannot act on.
71
+ const detail = snapshot.requiredNode && snapshot.runningNode
72
+ ? `${version} needs Node ${snapshot.requiredNode} (running ${snapshot.runningNode})`
73
+ : `${version} needs a newer Node`;
74
+ return {
75
+ glyph: glyphs.warn,
76
+ text: compact ? 'needs newer Node' : detail,
77
+ color: theme.noticeWarn,
78
+ };
79
+ }
80
+ if (snapshot.reason === 'install-ineffective') {
81
+ return {
82
+ glyph: glyphs.warn,
83
+ text: compact
84
+ ? 'check npm prefix'
85
+ : `${version} installed elsewhere${advice ? ` ${dot} ${advice}` : ''}`,
86
+ color: theme.noticeWarn,
87
+ };
88
+ }
89
+ return {
90
+ glyph: glyphs.arrowUp,
91
+ text: compact
92
+ ? version
93
+ : `${version} available${advice ? ` ${dot} ${advice}` : ''}`,
94
+ color: theme.muted,
95
+ };
96
+ }
97
+ // H1's rollback notice, and it goes HERE rather than in a phase branch
98
+ // (cli-auto-update-hardening section 8 / P0-1). It is true for a whole
99
+ // SESSION rather than for a phase, so `shouldRenderUpdateLine` decides its
100
+ // presence from `snapshot.rolledBackFrom` outside `phase` entirely - and the
101
+ // phases that leaves are exactly `idle` / `checking`, which is this
102
+ // fallthrough. An earlier draft put it in the `failed` / `available`
103
+ // branches and keyed it on phase `idle`, a phase neither branch handles;
104
+ // three independent suppressions, so the one line telling a user their CLI
105
+ // had been silently downgraded could never have appeared.
106
+ //
107
+ // NOT `version` ABOVE: that is `latestVersion ?? currentVersion`, and after
108
+ // a rollback `latestVersion` may well be the BAD release we just escaped.
109
+ if (snapshot.rolledBackFrom) {
110
+ const to = snapshot.rolledBackTo ?? snapshot.currentVersion;
111
+ return {
112
+ glyph: glyphs.warn,
113
+ text: compact
114
+ ? 'rolled back'
115
+ : `rolled back to ${to} after ${snapshot.rolledBackFrom} failed to start`,
116
+ color: theme.noticeWarn,
117
+ };
118
+ }
119
+ // `idle` / `checking`, and a `failed` below the notice threshold. The CALLER
120
+ // never mounts us here (`shouldRenderUpdateLine` is false), but the row must
121
+ // still be exactly one row if it ever is - the same blank-row discipline
122
+ // `ToastStack` applies to its own budgeted row.
123
+ return null;
124
+ })();
125
+ if (!line) {
126
+ return (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: " " }) }));
127
+ }
128
+ return (_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", color: line.color, children: [' ', line.glyph, " ", line.text] }) }));
129
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The working line's vocabulary (agent-activity-presentation §3.2.1).
3
+ *
4
+ * PURE: no React, no Ink, no clock. Everything here is a function of the two
5
+ * numbers the caller already has, which is what makes the rotation testable
6
+ * without a terminal and without fake timers.
7
+ */
8
+ /** Rotation cadence. Long enough to read, short enough to prove liveness. */
9
+ export declare const PHRASE_ROTATE_MS = 4000;
10
+ /**
11
+ * ASCII ONLY, by the rule this tree is under (`glyphs.ts` header, enforced by
12
+ * `glyphs.test.ts`'s static scan). Present participles, so the line reads as an
13
+ * action in progress; twelve characters or fewer so the row never fights the
14
+ * terminal's width.
15
+ */
16
+ export declare const ACTIVITY_PHRASES: readonly string[];
17
+ /**
18
+ * The phrase for a run at a moment.
19
+ *
20
+ * PURE AND TOTAL: the same inputs give the same word, for any finite numbers,
21
+ * including a `now` before `startedAt`.
22
+ *
23
+ * The run's start seeds the sequence, so two consecutive runs do not open on the
24
+ * same word — which is what makes the line read as alive rather than as a fixed
25
+ * label. (The seed's granularity is one second, so two runs starting inside the
26
+ * same second do share an opening word — P2-6.)
27
+ *
28
+ * `rotate: false` is the reduced-motion path: the word is chosen once for the run
29
+ * and then holds. Rotating text IS motion, and a user who asked for
30
+ * `reducedMotion` asked for the screen to stop changing under them; degrading
31
+ * only the spinner would honor the letter of the setting and not its point.
32
+ */
33
+ export declare function pickActivityPhrase(startedAt: number, now: number, rotate?: boolean): string;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The working line's vocabulary (agent-activity-presentation §3.2.1).
3
+ *
4
+ * PURE: no React, no Ink, no clock. Everything here is a function of the two
5
+ * numbers the caller already has, which is what makes the rotation testable
6
+ * without a terminal and without fake timers.
7
+ */
8
+ /** Rotation cadence. Long enough to read, short enough to prove liveness. */
9
+ export const PHRASE_ROTATE_MS = 4_000;
10
+ /**
11
+ * ASCII ONLY, by the rule this tree is under (`glyphs.ts` header, enforced by
12
+ * `glyphs.test.ts`'s static scan). Present participles, so the line reads as an
13
+ * action in progress; twelve characters or fewer so the row never fights the
14
+ * terminal's width.
15
+ */
16
+ export const ACTIVITY_PHRASES = [
17
+ 'Assembling', 'Brewing', 'Calibrating', 'Cogitating', 'Composing',
18
+ 'Computing', 'Concocting', 'Conjuring', 'Considering', 'Crunching',
19
+ 'Distilling', 'Divining', 'Drafting', 'Excavating', 'Fathoming',
20
+ 'Fermenting', 'Finessing', 'Foraging', 'Germinating', 'Herding',
21
+ 'Incubating', 'Inferring', 'Marinating', 'Meandering', 'Mulling',
22
+ 'Musing', 'Noodling', 'Percolating', 'Pondering', 'Puzzling',
23
+ 'Ruminating', 'Scheming', 'Sculpting', 'Simmering', 'Sketching',
24
+ 'Spelunking', 'Steeping', 'Stitching', 'Summoning', 'Tinkering',
25
+ 'Unfurling', 'Untangling', 'Weaving', 'Whittling', 'Wrangling',
26
+ ];
27
+ /**
28
+ * The phrase for a run at a moment.
29
+ *
30
+ * PURE AND TOTAL: the same inputs give the same word, for any finite numbers,
31
+ * including a `now` before `startedAt`.
32
+ *
33
+ * The run's start seeds the sequence, so two consecutive runs do not open on the
34
+ * same word — which is what makes the line read as alive rather than as a fixed
35
+ * label. (The seed's granularity is one second, so two runs starting inside the
36
+ * same second do share an opening word — P2-6.)
37
+ *
38
+ * `rotate: false` is the reduced-motion path: the word is chosen once for the run
39
+ * and then holds. Rotating text IS motion, and a user who asked for
40
+ * `reducedMotion` asked for the screen to stop changing under them; degrading
41
+ * only the spinner would honor the letter of the setting and not its point.
42
+ */
43
+ export function pickActivityPhrase(startedAt, now, rotate = true) {
44
+ const n = ACTIVITY_PHRASES.length;
45
+ if (!Number.isFinite(startedAt) || !Number.isFinite(now))
46
+ return ACTIVITY_PHRASES[0];
47
+ const seed = Math.floor(startedAt / 1000);
48
+ const step = rotate ? Math.floor(Math.max(0, now - startedAt) / PHRASE_ROTATE_MS) : 0;
49
+ return ACTIVITY_PHRASES[(((seed + step) % n) + n) % n];
50
+ }
@@ -23,6 +23,26 @@ interface AssistantEntryProps {
23
23
  thinkingVisible: boolean;
24
24
  streaming: boolean;
25
25
  aborted?: boolean;
26
+ /**
27
+ * Sealed reasoning time (§3.1.2). OMITTED WHEN UNKNOWN rather than printed as
28
+ * `0s`: a session restored from a file written by an older build has none, and
29
+ * `thought for 0s` is a lie where a bare `thought` is merely terse.
30
+ */
31
+ thinkingMs?: number;
32
+ /**
33
+ * Whether pressing `Ctrl+T` would actually reveal THIS entry's body — i.e.
34
+ * `mode === 'fullscreen'` (D-16 / P0-2).
35
+ *
36
+ * In inline mode a settled entry has already been printed into Ink's
37
+ * `<Static>`, which cannot un-print or re-print (`Transcript.tsx:9-10`), and
38
+ * the settled boundary is held monotonic precisely so an entry never flows back
39
+ * out. With thinking hidden by default the body is never printed there AT ALL,
40
+ * so a row reading `ctrl+t to show` would be an instruction that provably does
41
+ * nothing. The row degrades to `* thought for 12s` instead — still honest that
42
+ * reasoning happened and how long it took, and silent about a key that cannot
43
+ * deliver.
44
+ */
45
+ revealable?: boolean;
26
46
  /**
27
47
  * Inline mode only (L5): keep at most this many rows of a LIVE entry, so the
28
48
  * non-`<Static>` region cannot reach `stdout.rows`. `undefined` means no
@@ -20,8 +20,9 @@ import { pickGlyphs } from '../glyphs.js';
20
20
  import { Markdown } from '../Markdown.js';
21
21
  import { splitLinesCached } from '../render-cache.js';
22
22
  import { clampLiveText } from '../live-clamp.js';
23
+ import { formatDuration } from '../../agent/usage.js';
23
24
  function AssistantEntryImpl(props) {
24
- const { text, thinking, thinkingOpen, thinkingVisible, streaming, aborted, liveClampRows, theme, caps, } = props;
25
+ const { text, thinking, thinkingOpen, thinkingVisible, streaming, aborted, thinkingMs, revealable, liveClampRows, theme, caps, } = props;
25
26
  const glyphs = pickGlyphs(caps);
26
27
  const hasThinking = !!thinking && thinking.trim().length > 0;
27
28
  const hasText = text.trim().length > 0;
@@ -36,7 +37,7 @@ function AssistantEntryImpl(props) {
36
37
  ? clampLiveText(thinking, thinkingRows)
37
38
  : { text: thinking ?? '', hiddenRows: 0 };
38
39
  const body = liveClampRows === undefined ? { text, hiddenRows: 0 } : clampLiveText(text, liveClampRows);
39
- return (_jsxs(Box, { flexDirection: "column", children: [hasThinking && thinkingVisible && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [glyphs.thinking, " thinking", thinkingOpen ? glyphs.ellipsis : ''] }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [clampedThinking.hiddenRows > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.ellipsis, " ", clampedThinking.hiddenRows, " earlier", ' ', clampedThinking.hiddenRows === 1 ? 'line' : 'lines'] })), splitLinesCached(clampedThinking.text).map((line, i) => (_jsx(Text, { color: theme.thinking, dimColor: true, children: line }, i)))] })] })), (hasText || streaming) && (_jsxs(Box, { flexDirection: "column", children: [body.hiddenRows > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.ellipsis, " ", body.hiddenRows, " earlier", ' ', body.hiddenRows === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), hasText ? (_jsx(Markdown, { text: body.text, theme: theme, caps: caps })) : (_jsx(Text, { color: theme.muted, children: glyphs.ellipsis })), aborted && _jsx(Text, { color: theme.noticeWarn, children: "[aborted]" })] }))] }));
40
+ return (_jsxs(Box, { flexDirection: "column", children: [hasThinking && thinkingVisible && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [glyphs.thinking, " thinking", thinkingOpen ? glyphs.ellipsis : ''] }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [clampedThinking.hiddenRows > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.ellipsis, " ", clampedThinking.hiddenRows, " earlier", ' ', clampedThinking.hiddenRows === 1 ? 'line' : 'lines'] })), splitLinesCached(clampedThinking.text).map((line, i) => (_jsx(Text, { color: theme.thinking, dimColor: true, children: line }, i)))] })] })), hasThinking && !thinkingVisible && !streaming && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.thinking, " thought", thinkingMs !== undefined ? ` for ${formatDuration(thinkingMs)}` : '', revealable ? ` ${glyphs.midDot} ctrl+t to show` : ''] })), (hasText || streaming) && (_jsxs(Box, { flexDirection: "column", children: [body.hiddenRows > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.ellipsis, " ", body.hiddenRows, " earlier", ' ', body.hiddenRows === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), hasText ? (_jsx(Markdown, { text: body.text, theme: theme, caps: caps })) : (_jsx(Text, { color: theme.muted, children: glyphs.ellipsis })), aborted && _jsx(Text, { color: theme.noticeWarn, children: "[aborted]" })] }))] }));
40
41
  }
41
42
  /** Rows of live thinking kept on screen while an answer streams. */
42
43
  const STREAMING_THINKING_ROWS = 24;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * A `FilePatch` as a diff (agent-activity-presentation §3.4): a `+6 -5` summary,
3
+ * then hunk-separated rows with a right-aligned line-number gutter, a `+`/`-`/
4
+ * space sign column and `theme.diff` colors.
5
+ *
6
+ * `wrap="truncate"` ON EVERY ROW IS A CORRECTNESS REQUIREMENT, NOT A TASTE ONE.
7
+ * `estimateEntryRows` computes a diff card's height by COUNTING rows, so a
8
+ * wrapped patch line would occupy two rows while the estimate charged one — an
9
+ * under-estimate, which `virtual-window.ts` names as the unsafe direction. One
10
+ * patch line is one terminal row, always.
11
+ *
12
+ * No new keybinding and no new state: collapse rides on `expandedToolIds` and
13
+ * the existing `Ctrl+O` handler, which targets the MOST RECENT tool card (P2-4).
14
+ *
15
+ * `caps` is deliberately NOT a prop. Every character this component emits is
16
+ * ASCII — `+`, `-`, `@@`, digits — because `+` and `-` are the diff's own
17
+ * vocabulary and `renderUnifiedDiff` already spells them literally. A glyph
18
+ * lookup here would buy nothing and add a memo dependency.
19
+ */
20
+ import React from 'react';
21
+ import type { Theme } from '../theme.js';
22
+ import type { FilePatch, PatchLine } from '../../tools/patch.js';
23
+ /**
24
+ * Diff rows shown before collapse.
25
+ *
26
+ * Higher than the generic preview's 8 (D-11): a diff's rows are half as tall in
27
+ * information terms, and 8 rows of a 3-context hunk is barely one change.
28
+ * DUPLICATED AS A NUMBER in `virtual-window.ts`, matching what
29
+ * `TOOL_COLLAPSED_LINES` already does and the comment that justifies it.
30
+ */
31
+ export declare const DIFF_COLLAPSED_LINES = 12;
32
+ export interface DiffViewProps {
33
+ patch: FilePatch;
34
+ expanded?: boolean;
35
+ /**
36
+ * Inline mode only (L5): ceiling on the rows a LIVE card contributes to the
37
+ * non-`<Static>` region. `undefined` — what the full-screen branch passes —
38
+ * means no clamp.
39
+ */
40
+ liveClampRows?: number;
41
+ theme: Theme;
42
+ }
43
+ /** One rendered row: a patch line, or the `@@` separator between two hunks. */
44
+ interface DiffRow {
45
+ kind: PatchLine['kind'] | 'meta';
46
+ /** The gutter number, or `undefined` for a separator and the newline note. */
47
+ num?: number;
48
+ text: string;
49
+ }
50
+ /**
51
+ * Flatten the hunks into rows, inserting one `@@` separator BETWEEN hunks and
52
+ * none before the first.
53
+ *
54
+ * Exported for `diff-view.test.tsx`, and because the row count it produces is
55
+ * the number `estimateEntryRows` reproduces arithmetically — the two must not
56
+ * disagree.
57
+ */
58
+ export declare function diffRows(patch: FilePatch): DiffRow[];
59
+ declare function DiffViewImpl({ patch, expanded, liveClampRows, theme, }: DiffViewProps): React.ReactElement;
60
+ /**
61
+ * `React.memo` with the DEFAULT comparator, matching `ToolCard`'s boundary.
62
+ * `patch` qualifies as a prop only because the reducer writes it ONCE at
63
+ * `toolExecEnd` and never rebuilds it (P2-9); `theme` is `useMemo`d in `App.tsx`
64
+ * (I-L2-1).
65
+ */
66
+ export declare const DiffView: React.MemoExoticComponent<typeof DiffViewImpl>;
67
+ export {};
@@ -0,0 +1,103 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * A `FilePatch` as a diff (agent-activity-presentation §3.4): a `+6 -5` summary,
4
+ * then hunk-separated rows with a right-aligned line-number gutter, a `+`/`-`/
5
+ * space sign column and `theme.diff` colors.
6
+ *
7
+ * `wrap="truncate"` ON EVERY ROW IS A CORRECTNESS REQUIREMENT, NOT A TASTE ONE.
8
+ * `estimateEntryRows` computes a diff card's height by COUNTING rows, so a
9
+ * wrapped patch line would occupy two rows while the estimate charged one — an
10
+ * under-estimate, which `virtual-window.ts` names as the unsafe direction. One
11
+ * patch line is one terminal row, always.
12
+ *
13
+ * No new keybinding and no new state: collapse rides on `expandedToolIds` and
14
+ * the existing `Ctrl+O` handler, which targets the MOST RECENT tool card (P2-4).
15
+ *
16
+ * `caps` is deliberately NOT a prop. Every character this component emits is
17
+ * ASCII — `+`, `-`, `@@`, digits — because `+` and `-` are the diff's own
18
+ * vocabulary and `renderUnifiedDiff` already spells them literally. A glyph
19
+ * lookup here would buy nothing and add a memo dependency.
20
+ */
21
+ import React from 'react';
22
+ import { Box, Text } from 'ink';
23
+ /**
24
+ * Diff rows shown before collapse.
25
+ *
26
+ * Higher than the generic preview's 8 (D-11): a diff's rows are half as tall in
27
+ * information terms, and 8 rows of a 3-context hunk is barely one change.
28
+ * DUPLICATED AS A NUMBER in `virtual-window.ts`, matching what
29
+ * `TOOL_COLLAPSED_LINES` already does and the comment that justifies it.
30
+ */
31
+ export const DIFF_COLLAPSED_LINES = 12;
32
+ /** Gutter width bounds; three digits is the floor so narrow files still align. */
33
+ const GUTTER_MIN = 3;
34
+ const GUTTER_MAX = 6;
35
+ /**
36
+ * Flatten the hunks into rows, inserting one `@@` separator BETWEEN hunks and
37
+ * none before the first.
38
+ *
39
+ * Exported for `diff-view.test.tsx`, and because the row count it produces is
40
+ * the number `estimateEntryRows` reproduces arithmetically — the two must not
41
+ * disagree.
42
+ */
43
+ export function diffRows(patch) {
44
+ const rows = [];
45
+ patch.hunks.forEach((hunk, index) => {
46
+ if (index > 0) {
47
+ rows.push({
48
+ kind: 'meta',
49
+ text: `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`,
50
+ });
51
+ }
52
+ for (const line of hunk.lines) {
53
+ rows.push({
54
+ kind: line.kind,
55
+ // An `add` shows its new number, a `del` its old, a `ctx` its new.
56
+ ...(pickNumber(line) !== undefined ? { num: pickNumber(line) } : {}),
57
+ text: line.text,
58
+ });
59
+ }
60
+ });
61
+ return rows;
62
+ }
63
+ function pickNumber(line) {
64
+ return line.kind === 'del' ? line.oldLine : line.newLine ?? line.oldLine;
65
+ }
66
+ function signOf(kind) {
67
+ if (kind === 'add')
68
+ return '+';
69
+ if (kind === 'del')
70
+ return '-';
71
+ return ' ';
72
+ }
73
+ function colorOf(kind, theme) {
74
+ if (kind === 'add')
75
+ return theme.diff.add;
76
+ if (kind === 'del')
77
+ return theme.diff.remove;
78
+ if (kind === 'meta')
79
+ return theme.diff.meta;
80
+ return theme.diff.context;
81
+ }
82
+ function gutterWidth(rows) {
83
+ let max = 0;
84
+ for (const row of rows)
85
+ if (row.num !== undefined && row.num > max)
86
+ max = row.num;
87
+ return Math.min(GUTTER_MAX, Math.max(GUTTER_MIN, String(max).length));
88
+ }
89
+ function DiffViewImpl({ patch, expanded, liveClampRows, theme, }) {
90
+ const rows = diffRows(patch);
91
+ const ceiling = Math.min(expanded ? rows.length : DIFF_COLLAPSED_LINES, liveClampRows ?? Number.POSITIVE_INFINITY);
92
+ const visible = ceiling >= rows.length ? rows : rows.slice(0, Math.max(0, ceiling));
93
+ const hidden = rows.length - visible.length;
94
+ const width = gutterWidth(rows);
95
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: theme.diff.add, children: ["+", patch.added] }), _jsx(Text, { color: theme.muted, children: " " }), _jsxs(Text, { color: theme.diff.remove, children: ["-", patch.removed] }), patch.truncated && _jsx(Text, { color: theme.muted, children: " (truncated)" }), patch.degraded && (_jsxs(Text, { color: theme.muted, children: [" old side not read: ", patch.degraded] }))] }), visible.map((row, i) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.muted, children: String(row.num ?? '').padStart(width) }), _jsxs(Text, { color: colorOf(row.kind, theme), children: [' ', signOf(row.kind), " ", row.text] })] }, i))), hidden > 0 ? (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })) : (rows.length > DIFF_COLLAPSED_LINES && _jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] }));
96
+ }
97
+ /**
98
+ * `React.memo` with the DEFAULT comparator, matching `ToolCard`'s boundary.
99
+ * `patch` qualifies as a prop only because the reducer writes it ONCE at
100
+ * `toolExecEnd` and never rebuilds it (P2-9); `theme` is `useMemo`d in `App.tsx`
101
+ * (I-L2-1).
102
+ */
103
+ export const DiffView = React.memo(DiffViewImpl);
@@ -0,0 +1,53 @@
1
+ /**
2
+ * FastCard — one fast-model review in the transcript (fast-model-tier §6).
3
+ *
4
+ * ASCII ONLY: every glyph comes from `pickGlyphs`.
5
+ *
6
+ * SINGLE AND QUIET, NEVER MORE THAN SIX ROWS. It renders through the shared
7
+ * `EntryFrame` supplied by `EntryView`, so a review reads as one more step in
8
+ * the transcript rather than a second kind of object.
9
+ *
10
+ * COLLAPSED BY CONSTRUCTION rather than by an expand toggle: the critique is
11
+ * already clamped to `fast.reviewMaxChars` before it ever reaches here, so there
12
+ * is nothing hidden for a `Ctrl+O` to reveal.
13
+ *
14
+ * `ok`, `failed` and `dropped` each render ONE muted line. `ok` is the common
15
+ * case and must stay visually cheap — a full card every few turns saying
16
+ * "nothing to report" would be the feature's most annoying possible surface —
17
+ * while `failed` and `dropped` exist so that "it did nothing" is never the
18
+ * observable outcome (R-8).
19
+ */
20
+ import React from 'react';
21
+ import type { Theme } from '../theme.js';
22
+ import type { TermCapabilities } from '../capabilities.js';
23
+ export type FastCardStatus = 'running' | 'ok' | 'advice' | 'empty' | 'failed' | 'dropped';
24
+ export interface FastCardProps {
25
+ reviewIndex: number;
26
+ model: string;
27
+ status: FastCardStatus;
28
+ text?: string;
29
+ detail?: string;
30
+ turn: number;
31
+ durationMs?: number;
32
+ live: boolean;
33
+ reducedMotion?: boolean;
34
+ theme: Theme;
35
+ caps: TermCapabilities;
36
+ }
37
+ /**
38
+ * The card's colour.
39
+ *
40
+ * `advice` is the ACCENT rather than a warning colour: a second opinion is not
41
+ * an error, and colouring it like one would teach the user to read every review
42
+ * as something having gone wrong.
43
+ */
44
+ export declare function fastCardColor(status: FastCardStatus, theme: Theme): string | undefined;
45
+ declare function FastCardImpl(props: FastCardProps): React.ReactElement;
46
+ /**
47
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
48
+ * `mapEntry` already preserves object identity for untouched entries, so the
49
+ * props below are stable references on a settled card, and `theme` / `caps` are
50
+ * `useMemo`d in `App.tsx` (I-L2-1).
51
+ */
52
+ export declare const FastCard: React.MemoExoticComponent<typeof FastCardImpl>;
53
+ export {};
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * FastCard — one fast-model review in the transcript (fast-model-tier §6).
4
+ *
5
+ * ASCII ONLY: every glyph comes from `pickGlyphs`.
6
+ *
7
+ * SINGLE AND QUIET, NEVER MORE THAN SIX ROWS. It renders through the shared
8
+ * `EntryFrame` supplied by `EntryView`, so a review reads as one more step in
9
+ * the transcript rather than a second kind of object.
10
+ *
11
+ * COLLAPSED BY CONSTRUCTION rather than by an expand toggle: the critique is
12
+ * already clamped to `fast.reviewMaxChars` before it ever reaches here, so there
13
+ * is nothing hidden for a `Ctrl+O` to reveal.
14
+ *
15
+ * `ok`, `failed` and `dropped` each render ONE muted line. `ok` is the common
16
+ * case and must stay visually cheap — a full card every few turns saying
17
+ * "nothing to report" would be the feature's most annoying possible surface —
18
+ * while `failed` and `dropped` exist so that "it did nothing" is never the
19
+ * observable outcome (R-8).
20
+ */
21
+ import React from 'react';
22
+ import { Box, Text } from 'ink';
23
+ import Spinner from 'ink-spinner';
24
+ import { pickGlyphs } from '../glyphs.js';
25
+ import { railBorderProps } from '../layout/Gutter.js';
26
+ import { formatDuration } from '../../agent/usage.js';
27
+ import { FAST_LIMITS } from '../../fast/limits.js';
28
+ /**
29
+ * The card's colour.
30
+ *
31
+ * `advice` is the ACCENT rather than a warning colour: a second opinion is not
32
+ * an error, and colouring it like one would teach the user to read every review
33
+ * as something having gone wrong.
34
+ */
35
+ export function fastCardColor(status, theme) {
36
+ if (status === 'running')
37
+ return theme.toolRunning;
38
+ if (status === 'advice')
39
+ return theme.accent;
40
+ if (status === 'failed')
41
+ return theme.toolError;
42
+ return theme.muted;
43
+ }
44
+ /** The one-line summary that follows the header on every non-advice status. */
45
+ function statusLine(props) {
46
+ switch (props.status) {
47
+ case 'running':
48
+ return 'reviewing...';
49
+ case 'ok':
50
+ return 'on track';
51
+ case 'empty':
52
+ return 'no answer';
53
+ case 'failed':
54
+ return props.detail ? `failed: ${props.detail}` : 'failed';
55
+ case 'dropped':
56
+ return props.detail ? `dropped: ${props.detail}` : 'dropped';
57
+ default:
58
+ return '';
59
+ }
60
+ }
61
+ /**
62
+ * Split the critique into at most `FAST_LIMITS.cardTextRows` lines.
63
+ *
64
+ * The clamp is a ROW budget, not a character one: `reviewMaxChars` already
65
+ * bounded the text, and this only stops a critique with no whitespace at all
66
+ * from turning into a wall on a narrow terminal. Ink wraps the rest.
67
+ */
68
+ function textRows(text) {
69
+ const lines = text.split('\n').filter((l) => l.trim().length > 0);
70
+ return lines.slice(0, FAST_LIMITS.cardTextRows);
71
+ }
72
+ function FastCardImpl(props) {
73
+ const { reviewIndex, model, status, text, turn, durationMs, live, reducedMotion, theme, caps } = props;
74
+ const glyphs = pickGlyphs(caps);
75
+ const color = fastCardColor(status, theme);
76
+ const duration = durationMs === undefined ? '' : ` ${glyphs.midDot} ${formatDuration(durationMs)}`;
77
+ const head = `fast review #${reviewIndex} ${glyphs.midDot} ${model} ${glyphs.midDot} turn ${turn}${duration}`;
78
+ const showBody = status === 'advice' && text !== undefined && text.trim().length > 0;
79
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "fast" }), live && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [' ', _jsx(Spinner, { type: "dots" }), " ", head] })) : (_jsxs(Text, { color: theme.muted, wrap: "truncate", children: [' ', live ? `${glyphs.spinnerStill} ` : '', head] }))] }), showBody ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: textRows(text).map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))) })) : (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: "truncate", children: statusLine(props) }) }))] }));
80
+ }
81
+ /**
82
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
83
+ * `mapEntry` already preserves object identity for untouched entries, so the
84
+ * props below are stable references on a settled card, and `theme` / `caps` are
85
+ * `useMemo`d in `App.tsx` (I-L2-1).
86
+ */
87
+ export const FastCard = React.memo(FastCardImpl);
@@ -0,0 +1,56 @@
1
+ /**
2
+ * RetryCard — the one-line retry entry (llm-api-retry-backoff §6.5).
3
+ *
4
+ * ASCII ONLY: every glyph comes from `pickGlyphs`, and the line itself is built by
5
+ * the pure `formatRetryLine` in `agent/retry-view.ts` so it can be asserted
6
+ * without a terminal.
7
+ *
8
+ * ONE LINE. ALWAYS ONE LINE. Ten retries rendered as ten notices would bury the
9
+ * transcript in the one moment the user most needs it legible, so this card is
10
+ * rewritten in place for the whole turn and settles into a single row of history.
11
+ * Amber while in flight, muted once recovered, red once the ladder is exhausted.
12
+ */
13
+ import React from 'react';
14
+ import type { Theme } from '../theme.js';
15
+ import type { TermCapabilities } from '../capabilities.js';
16
+ import { type RetryPhase } from '../../agent/retry-view.js';
17
+ export interface RetryCardProps {
18
+ attempt: number;
19
+ maxRetries: number;
20
+ errorType: string;
21
+ phase: RetryPhase;
22
+ resumeAt?: number;
23
+ totalRetries?: number;
24
+ elapsedMs?: number;
25
+ /**
26
+ * `Date.now()` at render, injected so the component stays testable.
27
+ *
28
+ * The countdown is recomputed from `resumeAt - now` on every frame; `App` runs a
29
+ * 1 Hz tick ONLY while the phase is `waiting`, so an idle session has no timer.
30
+ */
31
+ now: number;
32
+ /**
33
+ * Whether Esc would actually be felt. `App` passes `true` only for an
34
+ * interactive session, because the hint has to be the truth: the wait is
35
+ * signal-aware in `withRetry`, so Esc lands within a frame — but there is no Esc
36
+ * in headless mode to promise.
37
+ */
38
+ interactive?: boolean;
39
+ reducedMotion?: boolean;
40
+ theme: Theme;
41
+ caps: TermCapabilities;
42
+ }
43
+ /**
44
+ * The card's tone. `exhausted` is the only failure: an `interrupted` card records
45
+ * a user decision, and colouring that red would blame them for it.
46
+ */
47
+ export declare function retryCardColor(phase: RetryPhase, theme: Theme): string | undefined;
48
+ declare function RetryCardImpl(props: RetryCardProps): React.ReactElement;
49
+ /**
50
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
51
+ * `mapEntry` preserves object identity for untouched entries, and `theme` / `caps`
52
+ * are `useMemo`d in `App.tsx` (I-L2-1). `now` changes once a second while the card
53
+ * is waiting, which is exactly when it must re-render.
54
+ */
55
+ export declare const RetryCard: React.MemoExoticComponent<typeof RetryCardImpl>;
56
+ export {};