@aragon-agent/cli 0.5.8 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +279 -0
- package/README.md +1429 -1163
- package/dist/agent/coalesce.d.ts +13 -0
- package/dist/agent/coalesce.js +50 -11
- package/dist/agent/controller.d.ts +141 -2
- package/dist/agent/controller.js +396 -4
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +245 -3
- package/dist/agent/reducer.js +490 -17
- package/dist/agent/retry-view.d.ts +68 -0
- package/dist/agent/retry-view.js +95 -0
- package/dist/agent/system-prompt.d.ts +11 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +86 -2
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +308 -2
- package/dist/config/cli-commands.d.ts +53 -2
- package/dist/config/cli-commands.js +136 -3
- package/dist/config/env.d.ts +6 -3
- package/dist/config/env.js +103 -3
- package/dist/config/load.d.ts +49 -0
- package/dist/config/load.js +83 -1
- package/dist/config/schema.d.ts +212 -2
- package/dist/config/schema.js +207 -1
- package/dist/config/store.js +20 -1
- package/dist/fast/critique.d.ts +43 -0
- package/dist/fast/critique.js +62 -0
- package/dist/fast/digest.d.ts +42 -0
- package/dist/fast/digest.js +85 -0
- package/dist/fast/frames.d.ts +99 -0
- package/dist/fast/frames.js +180 -0
- package/dist/fast/limits.d.ts +98 -0
- package/dist/fast/limits.js +98 -0
- package/dist/fast/prompt.d.ts +72 -0
- package/dist/fast/prompt.js +94 -0
- package/dist/fast/resolve.d.ts +43 -0
- package/dist/fast/resolve.js +105 -0
- package/dist/fast/review-call.d.ts +99 -0
- package/dist/fast/review-call.js +130 -0
- package/dist/fast/reviewer.d.ts +248 -0
- package/dist/fast/reviewer.js +664 -0
- package/dist/fast/types.d.ts +128 -0
- package/dist/fast/types.js +14 -0
- package/dist/fast/wiring.d.ts +170 -0
- package/dist/fast/wiring.js +316 -0
- package/dist/logging/install.d.ts +15 -1
- package/dist/logging/install.js +37 -4
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +57 -1
- package/dist/team/normalize.d.ts +26 -1
- package/dist/team/normalize.js +23 -3
- package/dist/team/prompt.d.ts +11 -0
- package/dist/team/prompt.js +10 -0
- package/dist/team/report.d.ts +18 -0
- package/dist/team/report.js +27 -1
- package/dist/team/runtime.d.ts +13 -2
- package/dist/team/runtime.js +33 -6
- package/dist/team/subagent.d.ts +19 -1
- package/dist/team/subagent.js +72 -10
- package/dist/team/task-tool.d.ts +26 -0
- package/dist/team/task-tool.js +48 -4
- package/dist/team/types.d.ts +80 -1
- package/dist/tools/bash-tool.js +19 -3
- package/dist/tools/diff.d.ts +25 -5
- package/dist/tools/diff.js +72 -41
- package/dist/tools/file-change-store.d.ts +34 -0
- package/dist/tools/file-change-store.js +57 -0
- package/dist/tools/fs-tools.d.ts +26 -0
- package/dist/tools/fs-tools.js +62 -5
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch.d.ts +115 -0
- package/dist/tools/patch.js +453 -0
- package/dist/tools/terminal-output.d.ts +107 -0
- package/dist/tools/terminal-output.js +198 -0
- package/dist/tools/tool-output-store.d.ts +68 -0
- package/dist/tools/tool-output-store.js +102 -0
- package/dist/ui/ActivityLine.d.ts +48 -0
- package/dist/ui/ActivityLine.js +22 -0
- package/dist/ui/App.d.ts +1 -4
- package/dist/ui/App.js +273 -37
- package/dist/ui/BottomStatusRow.d.ts +43 -0
- package/dist/ui/BottomStatusRow.js +14 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +37 -0
- package/dist/ui/StatusBar.js +5 -3
- package/dist/ui/TeamPanel.d.ts +5 -4
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/TodoPanel.d.ts +5 -4
- package/dist/ui/Transcript.d.ts +26 -3
- package/dist/ui/Transcript.js +76 -10
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +20 -0
- package/dist/ui/entries/AssistantEntry.js +3 -2
- package/dist/ui/entries/DiffView.d.ts +67 -0
- package/dist/ui/entries/DiffView.js +103 -0
- package/dist/ui/entries/FastCard.d.ts +53 -0
- package/dist/ui/entries/FastCard.js +87 -0
- package/dist/ui/entries/RetryCard.d.ts +56 -0
- package/dist/ui/entries/RetryCard.js +60 -0
- package/dist/ui/entries/TeamCard.js +6 -1
- package/dist/ui/entries/ToolCard.d.ts +38 -1
- package/dist/ui/entries/ToolCard.js +68 -5
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/AppShell.d.ts +13 -22
- package/dist/ui/layout/AppShell.js +3 -38
- package/dist/ui/layout/virtual-window.d.ts +1 -1
- package/dist/ui/layout/virtual-window.js +109 -5
- package/dist/ui/overlays/HelpOverlay.js +16 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
- package/dist/ui/overlays/SettingsScreen.js +147 -1
- package/dist/ui/transcript-text.js +27 -0
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- package/package.json +2 -2
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
package/dist/ui/PromptInput.js
CHANGED
|
@@ -127,7 +127,7 @@ function isControlSeq(input) {
|
|
|
127
127
|
// ---------------------------------------------------------------------------
|
|
128
128
|
// Component
|
|
129
129
|
// ---------------------------------------------------------------------------
|
|
130
|
-
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange,
|
|
130
|
+
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }) {
|
|
131
131
|
const [buffer, setBuffer] = useState('');
|
|
132
132
|
const [cursor, setCursor] = useState(0);
|
|
133
133
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
@@ -261,43 +261,6 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
261
261
|
recallDown();
|
|
262
262
|
}
|
|
263
263
|
};
|
|
264
|
-
// --- Wheel-driven history step (mouse-wheel-region-routing §5.6). ---------
|
|
265
|
-
// The applier closes over the buffer, the cursor and the popup state, so it
|
|
266
|
-
// is re-created on every render and reached through a ref: depending on it
|
|
267
|
-
// directly would re-fire the effect on every keystroke.
|
|
268
|
-
const applyHistoryIntent = useRef(() => { });
|
|
269
|
-
applyHistoryIntent.current = (dir) => {
|
|
270
|
-
// Mirrors the keyboard branch exactly — with the completion popup open,
|
|
271
|
-
// Up/Down move the selection rather than recalling history.
|
|
272
|
-
if (popupItems.length > 0) {
|
|
273
|
-
setSel(dir === 'up'
|
|
274
|
-
? Math.max(0, clampedSel - 1)
|
|
275
|
-
: Math.min(popupItems.length - 1, clampedSel + 1));
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
verticalOrHistory(dir);
|
|
279
|
-
};
|
|
280
|
-
const historyIntentRef = useRef(historyIntent);
|
|
281
|
-
historyIntentRef.current = historyIntent;
|
|
282
|
-
// I-9: seeded from the incoming nonce during the FIRST render, so a mount
|
|
283
|
-
// that inherits a nonce cannot replay it. This component really does remount
|
|
284
|
-
// — `App` returns the terminal-too-small placeholder before `AppShell` —
|
|
285
|
-
// so without the guard, shrinking the window mid-wheel and growing it back
|
|
286
|
-
// recalls a history entry with no gesture behind it.
|
|
287
|
-
const lastHistoryNonce = useRef(historyIntent?.nonce);
|
|
288
|
-
const historyNonce = historyIntent?.nonce ?? 0;
|
|
289
|
-
useEffect(() => {
|
|
290
|
-
const current = historyIntentRef.current;
|
|
291
|
-
if (!current || !isActive)
|
|
292
|
-
return;
|
|
293
|
-
if (current.nonce === lastHistoryNonce.current)
|
|
294
|
-
return;
|
|
295
|
-
lastHistoryNonce.current = current.nonce;
|
|
296
|
-
applyHistoryIntent.current(current.dir);
|
|
297
|
-
// Keyed on the nonce alone, same reasoning as `ScrollViewport`'s intent
|
|
298
|
-
// effect: `historyIntent` is a fresh object on every parent render.
|
|
299
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
300
|
-
}, [historyNonce]);
|
|
301
264
|
// --- Input handling. -----------------------------------------------------
|
|
302
265
|
useInput((input, key) => {
|
|
303
266
|
// FIRST STATEMENT, BEFORE THE POPUP BRANCH, AND IT HAS TO STAY THERE.
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -88,6 +88,25 @@ interface StatusBarProps {
|
|
|
88
88
|
done: number;
|
|
89
89
|
total: number;
|
|
90
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Live retry state (llm-api-retry-backoff §6.6), or absent when nothing is
|
|
93
|
+
* retrying — an ordinary session's bar is then unchanged.
|
|
94
|
+
*
|
|
95
|
+
* LEFT CLUSTER, after `teamActive`, and it DEGRADES. It is a run-state fact like
|
|
96
|
+
* `agents 3/5`, and the left cluster is `flexShrink={0}` so every column it takes
|
|
97
|
+
* comes out of the context gauge; below `RETRY_UI.statusCompactCols` it renders
|
|
98
|
+
* as `[r3]`.
|
|
99
|
+
*
|
|
100
|
+
* This does NOT contradict the "no `cols >= N` breakpoint" rule on `agentMode`
|
|
101
|
+
* above: that rule exists because the MODE must never be unreportable. The retry
|
|
102
|
+
* has a second, fuller home in the transcript card, which is exactly why it is
|
|
103
|
+
* the kind of readout that can afford to shrink.
|
|
104
|
+
*/
|
|
105
|
+
retryActive?: {
|
|
106
|
+
attempt: number;
|
|
107
|
+
max: number;
|
|
108
|
+
secondsLeft: number;
|
|
109
|
+
};
|
|
91
110
|
/**
|
|
92
111
|
* Render-governor rung (tui-render-performance L4). `> 0` renders a muted
|
|
93
112
|
* `eco` chip.
|
|
@@ -101,6 +120,24 @@ interface StatusBarProps {
|
|
|
101
120
|
* eco chip on a 50-column terminal costs the context gauge more than it says.
|
|
102
121
|
*/
|
|
103
122
|
ecoRung?: number;
|
|
123
|
+
/**
|
|
124
|
+
* The live fast tier (fast-model-tier §6), or absent when it is off for this
|
|
125
|
+
* session — in which case an ordinary session's bar is byte-identical.
|
|
126
|
+
*
|
|
127
|
+
* RIGHT CLUSTER, next to `eco`, and it DEGRADES below
|
|
128
|
+
* `FAST_LIMITS.statusCompactCols`. This is the `teamActive` / `ecoRung`
|
|
129
|
+
* treatment rather than the `agentMode` one, and the distinction is the note
|
|
130
|
+
* on `agentMode` above: the mode must never be unreportable, whereas the fast
|
|
131
|
+
* tier has a fuller home in `/fast status` — which is the GUARANTEED reporting
|
|
132
|
+
* surface — and a chip on a 60-column terminal costs the context gauge more
|
|
133
|
+
* than it says.
|
|
134
|
+
*
|
|
135
|
+
* `inFlight` renders `fast*`, so a review that is taking a while is visible
|
|
136
|
+
* without a spinner competing with the run's own.
|
|
137
|
+
*/
|
|
138
|
+
fastActive?: {
|
|
139
|
+
inFlight: boolean;
|
|
140
|
+
};
|
|
104
141
|
}
|
|
105
142
|
export declare function StatusBar(props: StatusBarProps): React.ReactElement;
|
|
106
143
|
export {};
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -4,6 +4,8 @@ import { pickGlyphs } from './glyphs.js';
|
|
|
4
4
|
import { formatCost, formatTokens, formatDuration } from '../agent/usage.js';
|
|
5
5
|
import { buildGauge } from './gauge.js';
|
|
6
6
|
import { MODE_LABEL } from '../agent/agent-mode.js';
|
|
7
|
+
import { formatRetryChip } from '../agent/retry-view.js';
|
|
8
|
+
import { FAST_LIMITS } from '../fast/limits.js';
|
|
7
9
|
import { TEAM_LIMITS } from '../team/limits.js';
|
|
8
10
|
import { TODO_LIMITS } from '../todo/limits.js';
|
|
9
11
|
/** Below this the `eco` chip costs the context gauge more than it is worth. */
|
|
@@ -27,7 +29,7 @@ function redrawChar(nonce) {
|
|
|
27
29
|
return nonce % 2 === 0 ? ' ' : NBSP;
|
|
28
30
|
}
|
|
29
31
|
export function StatusBar(props) {
|
|
30
|
-
const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, ecoRung = 0, } = props;
|
|
32
|
+
const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, retryActive, ecoRung = 0, fastActive, } = props;
|
|
31
33
|
const { stdout } = useStdout();
|
|
32
34
|
const cols = stdout?.columns ?? 80;
|
|
33
35
|
const running = status === 'running';
|
|
@@ -53,7 +55,7 @@ export function StatusBar(props) {
|
|
|
53
55
|
// land in one predictable place.
|
|
54
56
|
_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [redrawChar(redrawNonce), statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), teamActive && (_jsxs(Text, { color: theme.toolRunning, bold: true, children: [' ', cols >= TEAM_LIMITS.statusCompactCols
|
|
55
57
|
? `agents ${teamActive.running}/${teamActive.total}`
|
|
56
|
-
: `[${teamActive.running}]`] })), todoActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= TODO_LIMITS.statusCompactCols
|
|
58
|
+
: `[${teamActive.running}]`] })), retryActive && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', formatRetryChip(retryActive, cols)] })), todoActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= TODO_LIMITS.statusCompactCols
|
|
57
59
|
? `todo ${todoActive.done}/${todoActive.total}`
|
|
58
|
-
: `[${todoActive.done}/${todoActive.total}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
|
|
60
|
+
: `[${todoActive.done}/${todoActive.total}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
|
|
59
61
|
}
|
package/dist/ui/TeamPanel.d.ts
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* not mounted at all, so a session that never delegates has zero rows and zero
|
|
11
11
|
* layout shift.
|
|
12
12
|
*
|
|
13
|
-
* IT MUST BE MOUNTED
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* IT MUST BE MOUNTED IN `AppShell`'s BOTTOM CHROME (I-10), not in the middle
|
|
14
|
+
* band. The roster is transient status about the run in progress, so it belongs
|
|
15
|
+
* with the other transient rows — toast, composer, status bar — rather than
|
|
16
|
+
* inside the scrolling transcript, where it would be interleaved with history
|
|
17
|
+
* and scroll out of sight exactly when the user wants to watch it.
|
|
17
18
|
*/
|
|
18
19
|
import React from 'react';
|
|
19
20
|
import type { Theme } from './theme.js';
|
package/dist/ui/TeamPanel.js
CHANGED
|
@@ -120,7 +120,7 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
|
|
|
120
120
|
// `reducedMotion` and a non-Unicode terminal fall back to the same
|
|
121
121
|
// static marker, exactly as `AssistantEntry` and `ToolCard` already do.
|
|
122
122
|
const animate = isRunning(run) && !reducedMotion && caps.unicode;
|
|
123
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
|
|
123
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
|
|
124
124
|
}), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject] }))] }));
|
|
125
125
|
}
|
|
126
126
|
function marker(run, glyphs) {
|
package/dist/ui/TodoPanel.d.ts
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* this component is not rendered at all, so a session that never plans has zero
|
|
9
9
|
* columns and zero layout shift (R-g's "no furniture when idle" / AC-24).
|
|
10
10
|
*
|
|
11
|
-
* IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER
|
|
12
|
-
* (C-6 / I-4).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER IN THE BOTTOM CHROME
|
|
12
|
+
* (C-6 / I-4). This is a COLUMN BESIDE the viewport, and the middle band is the
|
|
13
|
+
* only row-wrapped region there is; moved down into the bottom chrome it would
|
|
14
|
+
* stop being a rail and start eating viewport height, growing and shrinking the
|
|
15
|
+
* transcript every time the list changed length.
|
|
15
16
|
*
|
|
16
17
|
* WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 18-36
|
|
17
18
|
* columns wide and the system prompt's last line is "Respond in the language the
|
package/dist/ui/Transcript.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { type Theme } from './theme.js';
|
|
|
30
30
|
import type { TermCapabilities } from './capabilities.js';
|
|
31
31
|
import { type DensityMode } from './density.js';
|
|
32
32
|
import type { Entry } from '../agent/reducer.js';
|
|
33
|
+
import type { RenderMode } from './layout/frame.js';
|
|
33
34
|
import type { HeightStore } from './use-height-store.js';
|
|
34
35
|
/**
|
|
35
36
|
* Largest prefix length P such that `entries[0..P)` are all terminal, none is
|
|
@@ -44,6 +45,25 @@ interface TranscriptProps {
|
|
|
44
45
|
thinkingVisible: boolean;
|
|
45
46
|
reducedMotion: boolean;
|
|
46
47
|
density: DensityMode;
|
|
48
|
+
/**
|
|
49
|
+
* The render mode, threaded from `App` as a plain scalar prop.
|
|
50
|
+
*
|
|
51
|
+
* It decides whether the collapsed thinking marker may offer `ctrl+t` (D-16):
|
|
52
|
+
* inline prints settled entries into `<Static>`, which cannot re-print them.
|
|
53
|
+
* NOT read from a module-level singleton — `render-memo.test.tsx` asserts prop
|
|
54
|
+
* stability across frames, and a hidden read would be invisible to it.
|
|
55
|
+
*/
|
|
56
|
+
mode: RenderMode;
|
|
57
|
+
/**
|
|
58
|
+
* Wall-clock seconds, threaded from `App`'s existing 200 ms ticker, for the
|
|
59
|
+
* live tool card's stall row (agent-activity-presentation-live §3.3.4 / D-36).
|
|
60
|
+
*
|
|
61
|
+
* BOTH MAP CALLBACKS BELOW DECIDE PER ENTRY WHETHER TO PASS IT ON, and neither
|
|
62
|
+
* may pass it unconditionally: `ToolCard` is `React.memo` with the default
|
|
63
|
+
* comparator, so a prop that changes every second on every card would defeat
|
|
64
|
+
* that boundary for the whole transcript.
|
|
65
|
+
*/
|
|
66
|
+
nowSec?: number;
|
|
47
67
|
theme: Theme;
|
|
48
68
|
caps: TermCapabilities;
|
|
49
69
|
}
|
|
@@ -55,12 +75,15 @@ export interface EntryViewProps {
|
|
|
55
75
|
thinkingVisible: boolean;
|
|
56
76
|
reducedMotion: boolean;
|
|
57
77
|
density: DensityMode;
|
|
78
|
+
mode: RenderMode;
|
|
58
79
|
theme: Theme;
|
|
59
80
|
caps: TermCapabilities;
|
|
60
81
|
/** Inline mode only (L5); `undefined` in full-screen, which means no clamp. */
|
|
61
82
|
liveClampRows?: number;
|
|
83
|
+
/** Wall-clock seconds; set ONLY on a running tool entry (§3.3.4 / P1-3). */
|
|
84
|
+
nowSec?: number;
|
|
62
85
|
}
|
|
63
|
-
declare function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, theme, caps, liveClampRows, }: EntryViewProps): React.ReactElement | null;
|
|
86
|
+
declare function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, mode, theme, caps, liveClampRows, nowSec, }: EntryViewProps): React.ReactElement | null;
|
|
64
87
|
/**
|
|
65
88
|
* THE memo boundary that makes a long transcript cheap (L2 / R3).
|
|
66
89
|
*
|
|
@@ -96,7 +119,7 @@ export declare const EntryView: React.MemoExoticComponent<typeof EntryViewImpl>;
|
|
|
96
119
|
* `selectWindow`, and the two spacers are what keep the scroll maths honest
|
|
97
120
|
* while they are absent.
|
|
98
121
|
*/
|
|
99
|
-
export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, windowSize, cols, heights, mountedSink, }: TranscriptProps & {
|
|
122
|
+
export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, }: TranscriptProps & {
|
|
100
123
|
windowSize: number;
|
|
101
124
|
/** The viewport's own width — narrower than the terminal when the rail is up. */
|
|
102
125
|
cols: number;
|
|
@@ -110,5 +133,5 @@ export declare function TranscriptList({ entries, expandedToolIds, thinkingVisib
|
|
|
110
133
|
current: number;
|
|
111
134
|
};
|
|
112
135
|
}): React.ReactElement;
|
|
113
|
-
export declare function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, }: TranscriptProps): React.ReactElement;
|
|
136
|
+
export declare function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, }: TranscriptProps): React.ReactElement;
|
|
114
137
|
export {};
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -37,6 +37,8 @@ import { AssistantEntry } from './entries/AssistantEntry.js';
|
|
|
37
37
|
import { ToolCard, statusColor, toolEntryGlyph } from './entries/ToolCard.js';
|
|
38
38
|
import { TeamCard, teamCardColor } from './entries/TeamCard.js';
|
|
39
39
|
import { TodoCard, todoCardColor } from './entries/TodoCard.js';
|
|
40
|
+
import { RetryCard, retryCardColor } from './entries/RetryCard.js';
|
|
41
|
+
import { FastCard, fastCardColor } from './entries/FastCard.js';
|
|
40
42
|
import { MeasuredEntry } from './layout/MeasuredEntry.js';
|
|
41
43
|
import { useViewportGeometry } from './layout/viewport-geometry.js';
|
|
42
44
|
import { estimateEntryRows, heightKey, selectWindow, VIRTUAL_LIMITS, } from './layout/virtual-window.js';
|
|
@@ -83,10 +85,34 @@ export function computeSettledCount(entries, expandedToolIds) {
|
|
|
83
85
|
// on every frame for the rest of the session.
|
|
84
86
|
if (e.kind === 'todo' && e.live)
|
|
85
87
|
break;
|
|
88
|
+
// Nor is a retry card that is still counting down or still in flight: it is
|
|
89
|
+
// rewritten on every `retryScheduled` / `retryAttempt` and once a second by
|
|
90
|
+
// `retryTick`, and `<Static>` cannot un-print what it has already drawn.
|
|
91
|
+
// `session/persist.ts` forces a loaded card to `interrupted`, which is the
|
|
92
|
+
// other half — a card that never settles is re-rendered on every frame for
|
|
93
|
+
// the rest of the session (R-10).
|
|
94
|
+
if (e.kind === 'retry' && (e.phase === 'waiting' || e.phase === 'retrying'))
|
|
95
|
+
break;
|
|
96
|
+
// A live review card is not settled either: it is rewritten once, when the
|
|
97
|
+
// call comes back, and `<Static>` cannot un-print what it has already drawn
|
|
98
|
+
// (C-5). `session/persist.ts` forces `live: false` on load, which is the
|
|
99
|
+
// other half.
|
|
100
|
+
if (e.kind === 'fast' && e.live)
|
|
101
|
+
break;
|
|
86
102
|
count = i + 1;
|
|
87
103
|
}
|
|
88
104
|
return count;
|
|
89
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The value `EntryView` should receive for `nowSec` on THIS entry.
|
|
108
|
+
*
|
|
109
|
+
* `undefined` for everything that is not a running tool, which is what keeps
|
|
110
|
+
* every other card's memo boundary intact — `undefined === undefined` in the
|
|
111
|
+
* comparator, on every frame, forever.
|
|
112
|
+
*/
|
|
113
|
+
function nowSecFor(entry, nowSec) {
|
|
114
|
+
return entry.kind === 'tool' && entry.status === 'running' ? nowSec : undefined;
|
|
115
|
+
}
|
|
90
116
|
function noticeColor(level, theme) {
|
|
91
117
|
if (level === 'error')
|
|
92
118
|
return theme.noticeError;
|
|
@@ -101,7 +127,7 @@ function noticeSymbol(level, theme) {
|
|
|
101
127
|
return theme.symbols.warn;
|
|
102
128
|
return theme.symbols.info;
|
|
103
129
|
}
|
|
104
|
-
function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, theme, caps, liveClampRows, }) {
|
|
130
|
+
function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, mode, theme, caps, liveClampRows, nowSec, }) {
|
|
105
131
|
const glyphs = pickGlyphs(caps);
|
|
106
132
|
const separation = separationRows(prev, entry, density);
|
|
107
133
|
const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
|
|
@@ -113,14 +139,37 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
113
139
|
// both an animation and a Unicode-only glyph, so reduced motion and an
|
|
114
140
|
// ASCII terminal fall back to the same static marker (P2-11 / A-10).
|
|
115
141
|
const animate = entry.streaming && !reducedMotion && caps.unicode;
|
|
116
|
-
return frame(animate ? _jsx(Spinner, { type: "dots" }) : glyphs.assistant, theme.primary, _jsx(AssistantEntry, { text: entry.text, thinking: entry.thinking, thinkingOpen: entry.thinkingOpen, thinkingVisible: thinkingVisible, streaming: entry.streaming, aborted: entry.aborted,
|
|
142
|
+
return frame(animate ? _jsx(Spinner, { type: "dots" }) : glyphs.assistant, theme.primary, _jsx(AssistantEntry, { text: entry.text, thinking: entry.thinking, thinkingOpen: entry.thinkingOpen, thinkingVisible: thinkingVisible, streaming: entry.streaming, aborted: entry.aborted,
|
|
143
|
+
// `EntryViewImpl` hands each renderer EXPLICIT SCALAR PROPS, so
|
|
144
|
+
// nothing on the entry reaches a component by itself (P1-6b).
|
|
145
|
+
thinkingMs: entry.thinkingMs, revealable: mode === 'fullscreen', liveClampRows: liveClampRows, theme: theme, caps: caps }));
|
|
117
146
|
}
|
|
118
147
|
case 'tool':
|
|
119
|
-
return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion, liveClampRows: liveClampRows,
|
|
148
|
+
return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, patch: entry.patch, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion, liveClampRows: liveClampRows,
|
|
149
|
+
// `entry.live` AND NOT `entry.live ?? []` (P2-4): `ToolCard`'s default
|
|
150
|
+
// comparator sees a fresh `[]` as a changed prop, which is why that
|
|
151
|
+
// file already keeps a shared `EMPTY_LINES`.
|
|
152
|
+
live: entry.live, lastOutputAt: entry.lastOutputAt, nowSec: nowSec, theme: theme, caps: caps }));
|
|
120
153
|
case 'team':
|
|
121
154
|
return frame(glyphs.teamAgent, teamCardColor(entry.runs, entry.active, entry.aborted, theme), _jsx(TeamCard, { requested: entry.requested, runs: entry.runs, aborted: entry.aborted, durationMs: entry.durationMs, active: entry.active, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
|
|
122
155
|
case 'todo':
|
|
123
156
|
return frame(toolGlyph(glyphs, 'todo_write'), todoCardColor(entry.doneCount, entry.total, entry.live, theme), _jsx(TodoCard, { items: entry.items, doneCount: entry.doneCount, total: entry.total, live: entry.live, interrupted: entry.interrupted, theme: theme, caps: caps }));
|
|
157
|
+
case 'retry':
|
|
158
|
+
return frame(entry.phase === 'exhausted' ? glyphs.error : glyphs.retry, retryCardColor(entry.phase, theme), _jsx(RetryCard, { attempt: entry.attempt, maxRetries: entry.maxRetries, errorType: entry.errorType, phase: entry.phase, resumeAt: entry.resumeAt, totalRetries: entry.totalRetries, elapsedMs: entry.elapsedMs,
|
|
159
|
+
// READ HERE, not threaded from `App`, and that is what keeps the memo
|
|
160
|
+
// boundary intact: a `now` prop on every `EntryView` would change on
|
|
161
|
+
// every frame and re-render the whole transcript. The `retryTick` action
|
|
162
|
+
// refreshes THIS entry's identity once a second, which re-renders this
|
|
163
|
+
// one card and gives it a fresh clock; nothing else moves.
|
|
164
|
+
now: Date.now(),
|
|
165
|
+
// NOT THREADED FROM `App`: reaching this renderer means a TUI is
|
|
166
|
+
// mounted, and the TUI always binds Esc to abort. Headless mode writes
|
|
167
|
+
// its retry lines to stderr and never mounts an entry component, and the
|
|
168
|
+
// exit replay goes through `transcript-text.ts`. `RetryCard` keeps the
|
|
169
|
+
// prop so the render tests can assert the suppressed form.
|
|
170
|
+
reducedMotion: reducedMotion, theme: theme, caps: caps }));
|
|
171
|
+
case 'fast':
|
|
172
|
+
return frame(entry.status === 'failed' ? glyphs.toolError : glyphs.teamAgent, fastCardColor(entry.status, theme), _jsx(FastCard, { reviewIndex: entry.reviewIndex, model: entry.model, status: entry.status, text: entry.text, detail: entry.detail, turn: entry.turn, durationMs: entry.durationMs, live: entry.live, reducedMotion: reducedMotion, theme: theme, caps: caps }));
|
|
124
173
|
case 'notice':
|
|
125
174
|
return frame(noticeSymbol(entry.level, theme), noticeColor(entry.level, theme), _jsx(Text, { color: noticeColor(entry.level, theme), children: entry.text }));
|
|
126
175
|
default:
|
|
@@ -148,9 +197,21 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
|
|
|
148
197
|
a.thinkingVisible === b.thinkingVisible &&
|
|
149
198
|
a.reducedMotion === b.reducedMotion &&
|
|
150
199
|
a.density === b.density &&
|
|
200
|
+
a.mode === b.mode &&
|
|
151
201
|
a.theme === b.theme &&
|
|
152
202
|
a.caps === b.caps &&
|
|
153
|
-
a.liveClampRows === b.liveClampRows
|
|
203
|
+
a.liveClampRows === b.liveClampRows &&
|
|
204
|
+
/*
|
|
205
|
+
* ELEVENTH TERM, AND OMITTING IT IS THE TRAP I-L2-1 DESCRIBES ONE COMMENT UP
|
|
206
|
+
* (P1-3). This comparator is a CLOSED LIST: a prop absent from it changes
|
|
207
|
+
* without invalidating the boundary, so `ToolCard` is never re-rendered and
|
|
208
|
+
* the stall row freezes at whatever second it first drew — AC-26's "and the
|
|
209
|
+
* seconds advance" failing with nothing reporting it.
|
|
210
|
+
*
|
|
211
|
+
* It is free: `nowSecFor` returns `undefined` for every entry that is not a
|
|
212
|
+
* running tool, and `undefined === undefined` on every frame.
|
|
213
|
+
*/
|
|
214
|
+
a.nowSec === b.nowSec);
|
|
154
215
|
});
|
|
155
216
|
/**
|
|
156
217
|
* Full-screen transcript body — the same entry renderers with NO `<Static>`.
|
|
@@ -172,7 +233,7 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
|
|
|
172
233
|
* `selectWindow`, and the two spacers are what keep the scroll maths honest
|
|
173
234
|
* while they are absent.
|
|
174
235
|
*/
|
|
175
|
-
export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, windowSize, cols, heights, mountedSink, }) {
|
|
236
|
+
export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, }) {
|
|
176
237
|
const size = Math.max(1, Math.floor(windowSize));
|
|
177
238
|
const visible = entries.length > size ? entries.slice(-size) : entries;
|
|
178
239
|
const collapsed = entries.length - visible.length;
|
|
@@ -190,7 +251,12 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
|
|
|
190
251
|
const { resolve, report } = heights;
|
|
191
252
|
const heightOf = useCallback((index) => {
|
|
192
253
|
const entry = visible[index];
|
|
193
|
-
|
|
254
|
+
// `thinkingVisible` IS PASSED, not left to the parameter's conservative
|
|
255
|
+
// default (P1-6a / condition 5). With thinking hidden the default would
|
|
256
|
+
// inflate every assistant entry that thought by its whole wrapped length,
|
|
257
|
+
// and an entry estimated at 400 rows against a real height of 1 is exactly
|
|
258
|
+
// the one `selectWindow` then never mounts and never re-measures.
|
|
259
|
+
return resolve(keyOf(entry), () => estimateEntryRows(entry, cols, density, !!expandedToolIds[entry.id], thinkingVisible));
|
|
194
260
|
},
|
|
195
261
|
// Memoised only incidentally: `visible` IS `entries` while the session is
|
|
196
262
|
// shorter than the horizon and a fresh slice once it is past it, so this
|
|
@@ -199,7 +265,7 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
|
|
|
199
265
|
// `keyOf` is omitted from the list because every input it closes over is
|
|
200
266
|
// already in it.
|
|
201
267
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
-
[visible, expandedToolIds, cols, density, flags, resolve]);
|
|
268
|
+
[visible, expandedToolIds, cols, density, flags, resolve, thinkingVisible]);
|
|
203
269
|
const selection = selectWindow({
|
|
204
270
|
entries: visible,
|
|
205
271
|
heightOf,
|
|
@@ -212,10 +278,10 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
|
|
|
212
278
|
mountedSink.current = slice.length;
|
|
213
279
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [collapsed > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: [glyphs.ellipsis, " ", collapsed, " earlier ", collapsed === 1 ? 'entry' : 'entries', " collapsed (/save exports the full session)"] })), selection.leadingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.leadingRows })), slice.map((entry, i) => {
|
|
214
280
|
const index = selection.startIndex + i;
|
|
215
|
-
return (_jsx(MeasuredEntry, { heightKey: keyOf(entry), onMeasure: report, children: _jsx(EntryView, { entry: entry, prev: index > 0 ? visible[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }) }, entry.id));
|
|
281
|
+
return (_jsx(MeasuredEntry, { heightKey: keyOf(entry), onMeasure: report, children: _jsx(EntryView, { entry: entry, prev: index > 0 ? visible[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSecFor(entry, nowSec), theme: theme, caps: caps }) }, entry.id));
|
|
216
282
|
}), selection.trailingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.trailingRows }))] }));
|
|
217
283
|
}
|
|
218
|
-
export function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, theme, caps, }) {
|
|
284
|
+
export function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, }) {
|
|
219
285
|
const highWater = useRef(0);
|
|
220
286
|
const prevLen = useRef(0);
|
|
221
287
|
const { rows } = useTerminalSize();
|
|
@@ -246,5 +312,5 @@ export function Transcript({ entries, expandedToolIds, thinkingVisible, reducedM
|
|
|
246
312
|
// a live entry at all, which is worse than a tall frame.
|
|
247
313
|
const liveBudget = Math.max(INLINE_LIVE_MIN_ROWS, rows - INLINE_LIVE_MARGIN);
|
|
248
314
|
const liveClampRows = Math.max(1, Math.floor(liveBudget / Math.max(1, liveEntries.length)) - 1);
|
|
249
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: settledEntries, children: (entry, index) => (_jsx(EntryView, { entry: entry, prev: index > 0 ? settledEntries[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }, entry.id)) }), _jsx(Box, { flexDirection: "column", children: liveEntries.map((entry, i) => (_jsx(EntryView, { entry: entry, prev: i > 0 ? liveEntries[i - 1] : entries[settled - 1], expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps, liveClampRows: liveClampRows }, entry.id))) })] }));
|
|
315
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: settledEntries, children: (entry, index) => (_jsx(EntryView, { entry: entry, prev: index > 0 ? settledEntries[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, theme: theme, caps: caps }, entry.id)) }), _jsx(Box, { flexDirection: "column", children: liveEntries.map((entry, i) => (_jsx(EntryView, { entry: entry, prev: i > 0 ? liveEntries[i - 1] : entries[settled - 1], expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, theme: theme, caps: caps, liveClampRows: liveClampRows, nowSec: nowSecFor(entry, nowSec) }, entry.id))) })] }));
|
|
250
316
|
}
|
|
@@ -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 {};
|