@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,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 {};
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * RetryCard — the one-line retry entry (llm-api-retry-backoff §6.5).
4
+ *
5
+ * ASCII ONLY: every glyph comes from `pickGlyphs`, and the line itself is built by
6
+ * the pure `formatRetryLine` in `agent/retry-view.ts` so it can be asserted
7
+ * without a terminal.
8
+ *
9
+ * ONE LINE. ALWAYS ONE LINE. Ten retries rendered as ten notices would bury the
10
+ * transcript in the one moment the user most needs it legible, so this card is
11
+ * rewritten in place for the whole turn and settles into a single row of history.
12
+ * Amber while in flight, muted once recovered, red once the ladder is exhausted.
13
+ */
14
+ import React from 'react';
15
+ import { Box, Text } from 'ink';
16
+ import Spinner from 'ink-spinner';
17
+ import { pickGlyphs } from '../glyphs.js';
18
+ import { formatRetryLine } from '../../agent/retry-view.js';
19
+ /**
20
+ * The card's tone. `exhausted` is the only failure: an `interrupted` card records
21
+ * a user decision, and colouring that red would blame them for it.
22
+ */
23
+ export function retryCardColor(phase, theme) {
24
+ if (phase === 'waiting' || phase === 'retrying')
25
+ return theme.noticeWarn;
26
+ if (phase === 'exhausted')
27
+ return theme.noticeError;
28
+ return theme.muted;
29
+ }
30
+ function RetryCardImpl(props) {
31
+ const { attempt, maxRetries, errorType, phase, resumeAt, totalRetries, elapsedMs, now,
32
+ // DEFAULTS TO TRUE: reaching this component means a TUI is mounted, and the
33
+ // TUI always binds Esc to abort. The prop exists so headless-shaped callers
34
+ // and the render tests can assert the suppressed form.
35
+ interactive = true, reducedMotion, theme, caps, } = props;
36
+ const glyphs = pickGlyphs(caps);
37
+ const color = retryCardColor(phase, theme);
38
+ const inFlight = phase === 'waiting' || phase === 'retrying';
39
+ const line = formatRetryLine({
40
+ attempt,
41
+ maxRetries,
42
+ errorType,
43
+ phase,
44
+ ...(resumeAt !== undefined ? { resumeAt } : {}),
45
+ ...(totalRetries !== undefined ? { totalRetries } : {}),
46
+ ...(elapsedMs !== undefined ? { elapsedMs } : {}),
47
+ }, glyphs, now);
48
+ // REDUCED MOTION SUPPRESSES THE SPINNER AND KEEPS THE COUNTDOWN. A frozen number
49
+ // is not calmer, it is broken: reduced motion is about animation, and a
50
+ // countdown is information.
51
+ const animate = inFlight && !reducedMotion && caps.unicode;
52
+ return (_jsxs(Box, { flexDirection: "row", children: [animate ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), ' '] })) : null, _jsx(Text, { color: color, wrap: "truncate", children: line }), phase === 'waiting' && interactive && (_jsxs(Text, { color: theme.muted, children: [' ', "Esc to cancel"] }))] }));
53
+ }
54
+ /**
55
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
56
+ * `mapEntry` preserves object identity for untouched entries, and `theme` / `caps`
57
+ * are `useMemo`d in `App.tsx` (I-L2-1). `now` changes once a second while the card
58
+ * is waiting, which is exactly when it must re-render.
59
+ */
60
+ export const RetryCard = React.memo(RetryCardImpl);
@@ -28,4 +28,12 @@ export interface TeamCardProps {
28
28
  }
29
29
  /** The card's aggregate colour: any failure dominates. */
30
30
  export declare function teamCardColor(runs: SubagentRun[], active: boolean, aborted: boolean, theme: Theme): string | undefined;
31
- export declare function TeamCard(props: TeamCardProps): React.ReactElement;
31
+ declare function TeamCardImpl(props: TeamCardProps): React.ReactElement;
32
+ /**
33
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
34
+ * `mapEntry` already preserves object identity for untouched entries, so the
35
+ * array props below are stable references on a settled card, and `theme` /
36
+ * `caps` are `useMemo`d in `App.tsx` (I-L2-1).
37
+ */
38
+ export declare const TeamCard: React.MemoExoticComponent<typeof TeamCardImpl>;
39
+ export {};
@@ -1,4 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * TeamCard — the settled-dispatch transcript entry (team-subagents §6.3).
4
+ *
5
+ * ASCII ONLY: every glyph comes from `pickGlyphs`.
6
+ *
7
+ * The panel is EPHEMERAL and this card is the HISTORY. It renders like a
8
+ * `ToolCard` — same `EntryFrame` supplied by `EntryView`, same left rail for the
9
+ * expanded body — so a dispatch reads as one more step in the transcript rather
10
+ * than a second kind of object.
11
+ *
12
+ * `Ctrl+O` expands it to the per-agent summaries, which is why `App.tsx`'s
13
+ * Ctrl+O finder had to widen from `kind === 'tool'` to `tool || team`.
14
+ */
15
+ import React from 'react';
2
16
  import { Box, Text } from 'ink';
3
17
  import Spinner from 'ink-spinner';
4
18
  import { pickGlyphs } from '../glyphs.js';
@@ -15,7 +29,7 @@ export function teamCardColor(runs, active, aborted, theme) {
15
29
  return theme.toolError;
16
30
  return theme.toolDone;
17
31
  }
18
- export function TeamCard(props) {
32
+ function TeamCardImpl(props) {
19
33
  const { requested, runs, aborted, durationMs, active, expanded, reducedMotion, theme, caps } = props;
20
34
  const glyphs = pickGlyphs(caps);
21
35
  const color = teamCardColor(runs, active, aborted, theme);
@@ -34,6 +48,18 @@ export function TeamCard(props) {
34
48
  : `${formatDuration(durationMs ?? 0)} (${ok} ok, ${failed} failed)`;
35
49
  const count = `${runs.length} ${runs.length === 1 ? 'subagent' : 'subagents'}`;
36
50
  const dropped = requested > runs.length ? ` (${runs.length} of ${requested} requested)` : '';
37
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isOk(run) ? theme.toolDone : theme.toolError, children: isOk(run) ? glyphs.toolDone : glyphs.toolError }), _jsxs(Text, { color: theme.primary, children: [" ", run.label.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? 0) - run.startedAt)) : '', ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] })] }, run.label))), expanded &&
51
+ // ABSENT WHEN NO FAST CHILD RAN, so an ordinary dispatch's card is unchanged
52
+ // (fast-model-tier §6). `n fast` rather than a ratio: the total is already one
53
+ // column to the left.
54
+ const fastCount = runs.filter((r) => r.tier === 'fast').length;
55
+ const fastNote = fastCount > 0 ? ` ${fastCount} fast` : '';
56
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, fastNote, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isOk(run) ? theme.toolDone : theme.toolError, children: isOk(run) ? glyphs.toolDone : glyphs.toolError }), _jsxs(Text, { color: theme.primary, children: [' ', `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? 0) - run.startedAt)) : '', ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] }), run.retry && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', "retry ", run.retry.attempt, "/", run.retry.maxRetries] }))] }, run.label))), expanded &&
38
57
  runs.map((run) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: [run.label, " ", glyphs.midDot, " ", run.description] }), _jsx(Text, { color: theme.muted, children: run.summary ?? '(no summary)' })] }, `s-${run.label}`))), !expanded && !active && runs.length > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", runs.length, " ", runs.length === 1 ? 'summary' : 'summaries', " (Ctrl+O)"] })), expanded && _jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" })] })] }));
39
58
  }
59
+ /**
60
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
61
+ * `mapEntry` already preserves object identity for untouched entries, so the
62
+ * array props below are stable references on a settled card, and `theme` /
63
+ * `caps` are `useMemo`d in `App.tsx` (I-L2-1).
64
+ */
65
+ export const TeamCard = React.memo(TeamCardImpl);
@@ -28,4 +28,12 @@ export interface TodoCardProps {
28
28
  }
29
29
  /** The card's aggregate colour: complete is done, anything else is in flight. */
30
30
  export declare function todoCardColor(doneCount: number, total: number, live: boolean, theme: Theme): string | undefined;
31
- export declare function TodoCard(props: TodoCardProps): React.ReactElement;
31
+ declare function TodoCardImpl(props: TodoCardProps): React.ReactElement;
32
+ /**
33
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
34
+ * `mapEntry` already preserves object identity for untouched entries, so the
35
+ * array props below are stable references on a settled card, and `theme` /
36
+ * `caps` are `useMemo`d in `App.tsx` (I-L2-1).
37
+ */
38
+ export declare const TodoCard: React.MemoExoticComponent<typeof TodoCardImpl>;
39
+ export {};
@@ -1,4 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * TodoCard — the transcript checklist entry (todo-plan-execution §6.2).
4
+ *
5
+ * ASCII ONLY: every glyph comes from `pickGlyphs`.
6
+ *
7
+ * ONE CARD PER USER TURN, REWRITTEN IN PLACE. The rail is EPHEMERAL and this is
8
+ * the HISTORY, exactly as `TeamPanel` and `TeamCard` divide the same work. It
9
+ * renders through the shared `EntryFrame` supplied by `EntryView`, so a plan
10
+ * reads as one more step in the transcript rather than a second kind of object.
11
+ *
12
+ * THE FULL LIST, NO WINDOWING (the transcript scrolls; the rail does not), and
13
+ * NEVER ANIMATED even while `live` — a resumed session would otherwise spin
14
+ * forever on a card whose run ended days ago.
15
+ */
16
+ import React from 'react';
2
17
  import { Box, Text } from 'ink';
3
18
  import { pickGlyphs } from '../glyphs.js';
4
19
  import { railBorderProps } from '../layout/Gutter.js';
@@ -24,7 +39,7 @@ function rowColor(item, theme) {
24
39
  return theme.primary;
25
40
  return theme.hintFg ?? theme.muted;
26
41
  }
27
- export function TodoCard(props) {
42
+ function TodoCardImpl(props) {
28
43
  const { items, doneCount, total, live, interrupted, theme, caps } = props;
29
44
  const glyphs = pickGlyphs(caps);
30
45
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "Todos" }), _jsxs(Text, { color: todoCardColor(doneCount, total, live, theme), children: [' ', doneCount, "/", total] }), interrupted && (
@@ -33,3 +48,10 @@ export function TodoCard(props) {
33
48
  // dispatch that never got a duration.
34
49
  _jsxs(Text, { color: theme.muted, children: [' ', "interrupted (session resumed)"] }))] }), _jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: items.map((item, index) => (_jsxs(Text, { wrap: "truncate", color: rowColor(item, theme), children: [marker(item, glyphs).padEnd(3), item.status === 'in_progress' ? item.activeForm : item.content] }, `${index}-${item.content}`))) })] }));
35
50
  }
51
+ /**
52
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
53
+ * `mapEntry` already preserves object identity for untouched entries, so the
54
+ * array props below are stable references on a settled card, and `theme` /
55
+ * `caps` are `useMemo`d in `App.tsx` (I-L2-1).
56
+ */
57
+ export const TodoCard = React.memo(TodoCardImpl);
@@ -17,6 +17,7 @@ import React from 'react';
17
17
  import type { Theme } from '../theme.js';
18
18
  import type { TermCapabilities } from '../capabilities.js';
19
19
  import type { ToolStatus } from '../../agent/reducer.js';
20
+ import type { FilePatch } from '../../tools/patch.js';
20
21
  interface ToolCardProps {
21
22
  name: string;
22
23
  label: string;
@@ -24,15 +25,63 @@ interface ToolCardProps {
24
25
  args?: Record<string, unknown>;
25
26
  status: ToolStatus;
26
27
  preview?: string;
28
+ /**
29
+ * The structured file diff, when the tool produced one (§3.4). Present it and
30
+ * `DiffView` draws INSTEAD of `ToolPreview`; absent — a session saved by an
31
+ * older build, an entry the store's 64-write ring evicted, or a toolset built
32
+ * without a recorder — and the text preview renders exactly what it does
33
+ * today. Nothing ever renders blank.
34
+ */
35
+ patch?: FilePatch;
27
36
  durationMs?: number;
28
37
  isError?: boolean;
29
38
  expanded?: boolean;
30
39
  reducedMotion?: boolean;
40
+ /**
41
+ * Inline mode only (L5): ceiling on the rows a LIVE card contributes to the
42
+ * non-`<Static>` region. `undefined` — what the full-screen branch passes —
43
+ * means no clamp and byte-identical output.
44
+ */
45
+ liveClampRows?: number;
46
+ /**
47
+ * The sanitised live tail while the tool runs
48
+ * (agent-activity-presentation-live §3.3.4).
49
+ *
50
+ * PASSED AS `live={entry.live}` AND NEVER `live={entry.live ?? []}` (P2-4).
51
+ * This component is `React.memo` with the DEFAULT comparator, so a fresh `[]`
52
+ * per render is a changed prop and defeats the boundary for every settled card
53
+ * in the transcript — which is exactly why `EMPTY_LINES` above exists.
54
+ */
55
+ live?: readonly string[];
56
+ /** Epoch ms of the last output row; with `nowSec`, the stall row's clock. */
57
+ lastOutputAt?: number;
58
+ /**
59
+ * Wall-clock SECONDS, passed ONLY to running entries (D-29 / D-36).
60
+ *
61
+ * Seconds and not milliseconds, and `undefined` on every other card, for the
62
+ * same reason: a prop that changed five times a second on every entry would
63
+ * defeat this memo boundary transcript-wide. The value is wall-clock rather
64
+ * than the ticker's `elapsedMs` because the stall is `now - lastOutputAt` and
65
+ * `lastOutputAt` is epoch ms; the ticker's job is to cause the render.
66
+ */
67
+ nowSec?: number;
31
68
  theme: Theme;
32
69
  caps: TermCapabilities;
33
70
  }
34
71
  export declare function statusColor(status: ToolStatus, theme: Theme): string | undefined;
35
72
  /** The tool's own glyph, used by `EntryView` as the entry's rail marker. */
36
73
  export declare function toolEntryGlyph(name: string, caps: TermCapabilities): string;
37
- export declare function ToolCard(props: ToolCardProps): React.ReactElement;
74
+ declare function ToolCardImpl(props: ToolCardProps): React.ReactElement;
75
+ /**
76
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
77
+ * Every prop is a scalar except `args` (a stable reference on a settled entry),
78
+ * `patch` and `theme` / `caps` (both `useMemo`d in `App.tsx` — I-L2-1).
79
+ *
80
+ * `patch` QUALIFIES ONLY BECAUSE THE REDUCER WRITES IT ONCE, at `toolExecEnd`,
81
+ * and never rebuilds it — `mapEntry` preserves object identity for untouched
82
+ * entries. A future action that recomputed or normalized a patch would turn this
83
+ * boundary into a no-op WITH NOTHING FAILING, which is the same trap I-L2-1
84
+ * exists to catch for `theme` and `caps` (P2-9).
85
+ */
86
+ export declare const ToolCard: React.MemoExoticComponent<typeof ToolCardImpl>;
38
87
  export {};
@@ -1,12 +1,33 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Tool-call card (spec §3.5): a per-tool glyph, the tool name, a compact args
4
+ * summary, a colored status badge, and a duration, over a collapsible rich
5
+ * preview delegated to `<ToolPreview>`. Collapsed to the first 8 lines with a
6
+ * `+M lines (Ctrl+O)` footer; `Ctrl+O` / `/expand` toggle the full preview.
7
+ *
8
+ * Two v0.4.0 changes (§4.1 / §4.3):
9
+ * - capabilities arrive as a prop. This used to infer Unicode support by
10
+ * comparing `theme.symbols.gaugeFull` against a literal block character, so
11
+ * editing an unrelated gauge glyph would have silently sent every tool icon
12
+ * back to ASCII;
13
+ * - the preview's round border is gone. It cost 2 rows and 4 columns per card,
14
+ * and nested visibly inside markdown code blocks. A left rail carries the
15
+ * same grouping in 1 column and 0 rows.
16
+ */
17
+ import React from 'react';
2
18
  import { Box, Text } from 'ink';
3
19
  import Spinner from 'ink-spinner';
4
20
  import { pickGlyphs, toolGlyph } from '../glyphs.js';
5
21
  import { railBorderProps } from '../layout/Gutter.js';
22
+ import { splitLinesCached } from '../render-cache.js';
6
23
  import { formatDuration } from '../../agent/usage.js';
7
24
  import { ToolPreview, bashBadge } from './ToolPreview.js';
25
+ import { DiffView } from './DiffView.js';
26
+ import { STALL_AFTER_MS } from '../../tools/tool-output-store.js';
8
27
  /** Number of preview lines shown before collapse. */
9
28
  const COLLAPSED_LINES = 8;
29
+ /** Shared empty slice, so an argument-less card allocates nothing per frame. */
30
+ const EMPTY_LINES = [];
10
31
  export function statusColor(status, theme) {
11
32
  switch (status) {
12
33
  case 'pending':
@@ -58,15 +79,91 @@ function badgeLabel(name, status, preview) {
58
79
  export function toolEntryGlyph(name, caps) {
59
80
  return toolGlyph(pickGlyphs(caps), name);
60
81
  }
61
- export function ToolCard(props) {
62
- const { name, argsRaw, args, status, preview, durationMs, isError, expanded, reducedMotion, theme, caps, } = props;
82
+ function ToolCardImpl(props) {
83
+ const { name, argsRaw, args, status, preview, patch, durationMs, isError, expanded, reducedMotion, liveClampRows, live, lastOutputAt, nowSec, theme, caps, } = props;
63
84
  const glyphs = pickGlyphs(caps);
64
85
  const color = statusColor(status, theme);
65
86
  const previewText = preview ?? '';
66
- const previewLines = previewText.length > 0 ? previewText.split('\n') : [];
67
- const visibleLines = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES);
87
+ // `splitLinesCached` rather than `previewText.split('\n')`: a stored preview
88
+ // is immutable once written, and this used to reallocate up to 200 strings per
89
+ // card per frame (R3).
90
+ const previewLines = previewText.length > 0 ? splitLinesCached(previewText) : EMPTY_LINES;
91
+ // Head-first, exactly as before: the collapsed card shows the FIRST 8 lines.
92
+ // The inline clamp only lowers the ceiling — it never changes which end is
93
+ // kept, because `Ctrl+O` on a 5 000-line bash result is precisely how an
94
+ // inline session reaches `ink.js:121` (I-L5-1) and the fix must not also
95
+ // reshuffle what a full-screen user has been reading for four releases.
96
+ const ceiling = Math.min(expanded ? previewLines.length : COLLAPSED_LINES, liveClampRows ?? Number.POSITIVE_INFINITY);
97
+ const visibleLines = ceiling >= previewLines.length ? previewLines : previewLines.slice(0, ceiling);
68
98
  const hidden = previewLines.length - visibleLines.length;
69
- const showBody = previewLines.length > 0 && (status === 'done' || status === 'error');
99
+ const settled = status === 'done' || status === 'error';
100
+ const showPatch = !!patch && settled;
101
+ // The live tail (agent-activity-presentation-live §3.3.4). `expanded` is
102
+ // deliberately ABSENT from this arithmetic: `Ctrl+O` is inert on a live card
103
+ // (D-26), because expansion is a promise about STORED content and the store
104
+ // deliberately holds eight rows. A key that silently does nothing is better
105
+ // than a key that promises the rest of a 400 MB stream.
106
+ //
107
+ // THE TAIL IS THE **LAST** ROWS, and the settled preview above keeps its
108
+ // head-first slice (E-21). Two different ends because they answer two
109
+ // different questions — "what did this command do" versus "what is it doing
110
+ // NOW" (D-25). Head-first while live would freeze on the first eight lines a
111
+ // build ever printed and then never change again.
112
+ const liveRows = live ?? EMPTY_LINES;
113
+ const liveCeiling = Math.min(COLLAPSED_LINES,
114
+ // Inline only: `liveClampRows` already subtracts one row per live entry to
115
+ // pay for a marker (`Transcript.tsx:534-538`), which is what this card's
116
+ // unconditional footer spends (AC-36 / P2-7).
117
+ liveClampRows ?? Number.POSITIVE_INFINITY);
118
+ const visibleLive = liveRows.length > liveCeiling ? liveRows.slice(-Math.max(1, liveCeiling)) : liveRows;
119
+ const showLive = !settled && visibleLive.length > 0;
120
+ // THE OUTER GUARD WIDENS WITH THE BRANCH (P2-2). It was `previewLines.length >
121
+ // 0 && ...`, and the reducer now truncates the stored preview to ONE LINE when
122
+ // a patch is attached (D-19) — so a card whose preview were ever empty would
123
+ // render its header and swallow the diff, silently. It widens a second time
124
+ // here, for the third branch: without `|| showLive` the tail would be computed
125
+ // and never drawn.
126
+ const showBody = ((previewLines.length > 0 || showPatch) && settled) || showLive;
127
+ // ONE FOOTER ROW, ALWAYS, matching `estimateEntryRows`'s arithmetic exactly: a
128
+ // CONSTANT row count is what lets the estimate be exact rather than an upper
129
+ // bound. `nowSec` is absent until the first tick, which reads as "not stalled".
130
+ const stalledFor = nowSec !== undefined && lastOutputAt !== undefined
131
+ ? nowSec * 1000 - lastOutputAt
132
+ : 0;
133
+ const liveFooter = stalledFor >= STALL_AFTER_MS
134
+ ? `no output for ${Math.floor(stalledFor / 1000)}s`
135
+ : '(running)';
70
136
  const badge = status === 'running' && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " running"] })) : (_jsxs(Text, { color: color, children: [status === 'running' ? `${glyphs.spinnerStill} ` : '', badgeLabel(name, status, previewText)] }));
71
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: name }), _jsxs(Text, { color: theme.muted, children: [" ", summarizeArgs(name, glyphs.ellipsis, args, argsRaw), " "] }), badge, durationMs !== undefined && (_jsxs(Text, { color: theme.muted, children: [" (", formatDuration(durationMs), ")"] }))] }), showBody && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, isError ? theme.toolError : theme.border), children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] }))] }));
137
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: name }), _jsxs(Text, { color: theme.muted, children: [" ", summarizeArgs(name, glyphs.ellipsis, args, argsRaw), " "] }), badge, durationMs !== undefined && (_jsxs(Text, { color: theme.muted, children: [" (", formatDuration(durationMs), ")"] }))] }), showBody && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, isError ? theme.toolError : theme.border), children: showLive ? (_jsxs(_Fragment, { children: [visibleLive.map((row, i) => (
138
+ // The row is already terminal-safe and clipped at
139
+ // `LIVE_ROW_MAX_CHARS` by `sanitizeChunk`, so `wrap="truncate"`
140
+ // is an exact height rather than a hope — which is the coupling
141
+ // `estimateEntryRows`'s live branch relies on.
142
+ //
143
+ // The index IS the key: these rows are positional slots in a
144
+ // ring, not identities, and the whole array is replaced on every
145
+ // chunk anyway.
146
+ //
147
+ // AN EMPTY ROW RENDERS AS A SPACE, and that is not cosmetic:
148
+ // Ink measures a `<Text>` with an empty child as ZERO rows, so a
149
+ // blank separator line — which `npm test`, `cargo` and every
150
+ // build tool print — would make the card shorter than
151
+ // `estimateEntryRows` charged for it. That is an over-estimate,
152
+ // the safe direction, but it would quietly cost §3.3.4's
153
+ // "constant row count" the exactness its arithmetic claims. A
154
+ // space is also what the terminal itself shows for a blank line.
155
+ // eslint-disable-next-line react/no-array-index-key
156
+ _jsx(Text, { wrap: "truncate", color: theme.muted, children: row.length > 0 ? row : ' ' }, i))), _jsx(Text, { color: theme.muted, children: liveFooter })] })) : showPatch ? (_jsx(DiffView, { patch: patch, expanded: expanded, liveClampRows: liveClampRows, theme: theme })) : (_jsxs(_Fragment, { children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] })) }))] }));
72
157
  }
158
+ /**
159
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
160
+ * Every prop is a scalar except `args` (a stable reference on a settled entry),
161
+ * `patch` and `theme` / `caps` (both `useMemo`d in `App.tsx` — I-L2-1).
162
+ *
163
+ * `patch` QUALIFIES ONLY BECAUSE THE REDUCER WRITES IT ONCE, at `toolExecEnd`,
164
+ * and never rebuilds it — `mapEntry` preserves object identity for untouched
165
+ * entries. A future action that recomputed or normalized a patch would turn this
166
+ * boundary into a no-op WITH NOTHING FAILING, which is the same trap I-L2-1
167
+ * exists to catch for `theme` and `caps` (P2-9).
168
+ */
169
+ export const ToolCard = React.memo(ToolCardImpl);
@@ -35,7 +35,7 @@ export declare function parseWriteFile(text: string): {
35
35
  path: string;
36
36
  } | null;
37
37
  /** `3 questions · Postgres · REST` — or `dismissed`. */
38
- export declare function summarizeAskUser(lines: string[]): string | null;
38
+ export declare function summarizeAskUser(lines: readonly string[]): string | null;
39
39
  export interface ListRow {
40
40
  kind: 'dir' | 'file';
41
41
  name: string;
@@ -51,7 +51,7 @@ export declare function parseListRow(line: string): ListRow | null;
51
51
  */
52
52
  interface ToolPreviewProps {
53
53
  name: string;
54
- lines: string[];
54
+ lines: readonly string[];
55
55
  theme: Theme;
56
56
  isError?: boolean;
57
57
  }
@@ -8,7 +8,15 @@
8
8
  */
9
9
  import React from 'react';
10
10
  import type { Theme } from '../theme.js';
11
- export declare function UserEntry({ text, theme }: {
11
+ declare function UserEntryImpl({ text, theme }: {
12
12
  text: string;
13
13
  theme: Theme;
14
14
  }): React.ReactElement;
15
+ /**
16
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
17
+ * `mapEntry` already preserves object identity for untouched entries, so the
18
+ * array props below are stable references on a settled card, and `theme` /
19
+ * `caps` are `useMemo`d in `App.tsx` (I-L2-1).
20
+ */
21
+ export declare const UserEntry: React.MemoExoticComponent<typeof UserEntryImpl>;
22
+ export {};