@aragon-agent/cli 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1157 -747
- package/README.md +1552 -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/boot/guard.d.ts +135 -0
- package/dist/boot/guard.js +218 -0
- package/dist/boot/rollback.d.ts +86 -0
- package/dist/boot/rollback.js +185 -0
- package/dist/cli.js +368 -3
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +426 -2
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +31 -0
- package/dist/config/app-paths.js +37 -0
- package/dist/config/cli-commands.d.ts +79 -2
- package/dist/config/cli-commands.js +185 -3
- package/dist/config/env.d.ts +15 -3
- package/dist/config/env.js +143 -3
- package/dist/config/load.d.ts +66 -0
- package/dist/config/load.js +107 -1
- package/dist/config/schema.d.ts +268 -2
- package/dist/config/schema.js +288 -1
- package/dist/config/store.js +32 -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/launcher.d.ts +14 -0
- package/dist/launcher.js +43 -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 +11 -4
- package/dist/ui/App.js +359 -37
- package/dist/ui/BottomStatusRow.d.ts +65 -0
- package/dist/ui/BottomStatusRow.js +17 -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/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- 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/dist/update/classify-failure.d.ts +32 -0
- package/dist/update/classify-failure.js +60 -0
- package/dist/update/install-lock.d.ts +55 -0
- package/dist/update/install-lock.js +184 -0
- package/dist/update/install-source.d.ts +97 -0
- package/dist/update/install-source.js +218 -0
- package/dist/update/installer.d.ts +108 -0
- package/dist/update/installer.js +242 -0
- package/dist/update/limits.d.ts +111 -0
- package/dist/update/limits.js +111 -0
- package/dist/update/npm-view.d.ts +62 -0
- package/dist/update/npm-view.js +133 -0
- package/dist/update/registry.d.ts +52 -0
- package/dist/update/registry.js +161 -0
- package/dist/update/semver.d.ts +89 -0
- package/dist/update/semver.js +295 -0
- package/dist/update/service.d.ts +193 -0
- package/dist/update/service.js +636 -0
- package/dist/update/state.d.ts +103 -0
- package/dist/update/state.js +142 -0
- package/dist/update/types.d.ts +169 -0
- package/dist/update/types.js +68 -0
- package/package.json +3 -3
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bottom status row — the one already-budgeted row above the composer, now
|
|
3
|
+
* shared by two occupants (agent-activity-presentation §3.2.3 / D-17 / P0-1).
|
|
4
|
+
*
|
|
5
|
+
* THE ACTIVITY LINE IS NOT A NEW `AppShell` SLOT, AND THIS FILE IS WHY.
|
|
6
|
+
*
|
|
7
|
+
* `viewportRows()` (`layout/budget.ts`) is the ONE function that says how many
|
|
8
|
+
* rows the viewport gets, and it enumerates exactly `header + toast + composer +
|
|
9
|
+
* status`. In full-screen `AppShell`'s root is `height={frameHeight(rows)}` with
|
|
10
|
+
* `overflow="hidden"`, the bottom chrome box is `flexShrink={0}` and the viewport
|
|
11
|
+
* box is its only `flexShrink={1}` child — so an extra chrome row does not make
|
|
12
|
+
* the frame taller, Yoga takes the row out of the TRANSCRIPT. Meanwhile
|
|
13
|
+
* `viewportRows()` keeps returning the old number to `ScrollViewport`,
|
|
14
|
+
* `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`.
|
|
15
|
+
* The transcript would draw one row shorter than every consumer believes, ONLY
|
|
16
|
+
* WHILE A RUN IS IN FLIGHT, so the layout would shift on submit and shift back on
|
|
17
|
+
* `agent_end`.
|
|
18
|
+
*
|
|
19
|
+
* `ToastStack.tsx` already legislates this for itself: *"in `fullscreen` this
|
|
20
|
+
* ALWAYS occupies exactly one row — an empty row when there is nothing to say.
|
|
21
|
+
* Rendering conditionally would make the viewport height (and therefore the whole
|
|
22
|
+
* transcript) jump every time a toast appears or expires."* A run starts and ends
|
|
23
|
+
* far more often than a toast appears and expires, so the activity line is the
|
|
24
|
+
* STRONGER case for that rule, not an exception to it.
|
|
25
|
+
*
|
|
26
|
+
* Sharing the row means `budget.ts` and `AppShell.tsx` come out of this round
|
|
27
|
+
* UNMODIFIED — and `budget.test.ts` asserts that they did (AC-8a, DoD #8).
|
|
28
|
+
*
|
|
29
|
+
* PRECEDENCE: toast > activity > update > blank. A toast wins because a
|
|
30
|
+
* transient ack is a RESPONSE TO THE USER, and the row nearest the input belongs
|
|
31
|
+
* to it. The update line is LAST because it is the only one of the three that is
|
|
32
|
+
* PERSISTENT (cli-auto-update D-2): it loses nothing by waiting for the run to
|
|
33
|
+
* end, whereas an activity line deferred is an activity line that never renders.
|
|
34
|
+
* That also means a user who is actively working never sees the updater at all
|
|
35
|
+
* until they stop, which is the whole of the silence requirement.
|
|
36
|
+
*/
|
|
37
|
+
import React from 'react';
|
|
38
|
+
import type { Theme } from './theme.js';
|
|
39
|
+
import type { Toast } from '../agent/reducer.js';
|
|
40
|
+
import type { RenderMode } from './layout/frame.js';
|
|
41
|
+
export interface BottomStatusRowProps {
|
|
42
|
+
mode: RenderMode;
|
|
43
|
+
toasts: Toast[];
|
|
44
|
+
/** `<ActivityLine>` while a run owns the screen, else `null`. */
|
|
45
|
+
activity: React.ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* `<UpdateLine>` when the updater has something to say, else `null` — AND THE
|
|
48
|
+
* CALLER DECIDES WHICH (C-15 / cli-auto-update P0-1).
|
|
49
|
+
*
|
|
50
|
+
* `if (update)` below tests a REACT ELEMENT, and an element whose own render
|
|
51
|
+
* returns `null` is still truthy: that branch would emit a box of ZERO rows,
|
|
52
|
+
* the frame would keep its fixed height, Yoga would hand the row to the
|
|
53
|
+
* transcript, and `viewportRows()` would go on reporting the old number to
|
|
54
|
+
* five consumers. So `UpdateLine` is typed `React.ReactElement` and `App`
|
|
55
|
+
* gates it with `shouldRenderUpdateLine(snapshot)`.
|
|
56
|
+
*
|
|
57
|
+
* OPTIONAL, because `bottom-status-row.test.tsx` and the `App.tsx` fixtures
|
|
58
|
+
* construct this component without it and the test tree is inside
|
|
59
|
+
* `tsconfig.test.json` (`typecheck-scope.test.ts`); a required prop turns
|
|
60
|
+
* `npm run typecheck` red for a reason unrelated to this feature (P2-1).
|
|
61
|
+
*/
|
|
62
|
+
update?: React.ReactNode;
|
|
63
|
+
theme: Theme;
|
|
64
|
+
}
|
|
65
|
+
export declare function BottomStatusRow({ mode, toasts, activity, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from 'ink';
|
|
3
|
+
import { ToastStack } from './ToastStack.js';
|
|
4
|
+
export function BottomStatusRow({ mode, toasts, activity, update, theme, }) {
|
|
5
|
+
// A toast wins the row outright.
|
|
6
|
+
if (toasts.length > 0)
|
|
7
|
+
return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
|
|
8
|
+
// Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
|
|
9
|
+
// branch is already a conditional 0-3 row stack, and these are simply one more
|
|
10
|
+
// conditional row above it.
|
|
11
|
+
if (activity)
|
|
12
|
+
return _jsx(Box, { flexShrink: 0, children: activity });
|
|
13
|
+
if (update)
|
|
14
|
+
return _jsx(Box, { flexShrink: 0, children: update });
|
|
15
|
+
// The blank full-screen row that IS the budget must keep being emitted.
|
|
16
|
+
return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
|
|
17
|
+
}
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -35,14 +35,9 @@ export interface ComposerProps {
|
|
|
35
35
|
/** Mirror of the controller's effective mode; drives the chip and the border. */
|
|
36
36
|
agentMode: AgentMode;
|
|
37
37
|
popupMaxRows?: number;
|
|
38
|
-
/** Pure pass-through to `PromptInput` (mouse-wheel-region-routing §5.6). */
|
|
39
|
-
historyIntent?: {
|
|
40
|
-
dir: 'up' | 'down';
|
|
41
|
-
nonce: number;
|
|
42
|
-
};
|
|
43
38
|
theme: Theme;
|
|
44
39
|
caps: TermCapabilities;
|
|
45
40
|
onSubmit: (text: string) => void;
|
|
46
41
|
onHelp?: () => void;
|
|
47
42
|
}
|
|
48
|
-
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows,
|
|
43
|
+
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
package/dist/ui/Composer.js
CHANGED
|
@@ -56,7 +56,7 @@ function hintText(running, submitCount, glyphs, mode) {
|
|
|
56
56
|
'? help',
|
|
57
57
|
].join(dot);
|
|
58
58
|
}
|
|
59
|
-
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows,
|
|
59
|
+
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
|
|
60
60
|
const [hasDraft, setHasDraft] = useState(false);
|
|
61
61
|
const onDraftChange = useCallback((next) => setHasDraft(next), []);
|
|
62
62
|
// Plan mode inserts ONE branch, above `draft` and below `running`. The
|
|
@@ -73,7 +73,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
|
|
|
73
73
|
: theme.idleBorder ?? theme.border;
|
|
74
74
|
const glyphs = pickGlyphs(caps);
|
|
75
75
|
const visible = showHint && hintsEnabled;
|
|
76
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange
|
|
76
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
|
|
77
77
|
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
78
78
|
// costs zero rows on a 24-row terminal.
|
|
79
79
|
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(running, submitCount, glyphs, agentMode) })] }))] }));
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -55,20 +55,6 @@ interface PromptInputProps {
|
|
|
55
55
|
};
|
|
56
56
|
/** Fires only when the draft flips empty ⇄ non-empty, for the border color. */
|
|
57
57
|
onDraftChange?: (hasDraft: boolean) => void;
|
|
58
|
-
/**
|
|
59
|
-
* One prompt-history step, pushed by the wheel router when a notch lands on
|
|
60
|
-
* the composer band (mouse-wheel-region-routing §5.6). `nonce` is monotonic
|
|
61
|
-
* so two notches in the same direction are two steps.
|
|
62
|
-
*
|
|
63
|
-
* Applied through the SAME `verticalOrHistory` / popup-selection branches the
|
|
64
|
-
* keyboard uses, deliberately: it means the wheel and `↑`/`↓` cannot drift
|
|
65
|
-
* apart, and the "never clobber an in-progress draft" guard protects the
|
|
66
|
-
* wheel for free.
|
|
67
|
-
*/
|
|
68
|
-
historyIntent?: {
|
|
69
|
-
dir: 'up' | 'down';
|
|
70
|
-
nonce: number;
|
|
71
|
-
};
|
|
72
58
|
}
|
|
73
59
|
/** Apply an edit intent as a pure buffer/cursor transform. */
|
|
74
60
|
export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
|
|
@@ -98,5 +84,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
98
84
|
end: number;
|
|
99
85
|
query: string;
|
|
100
86
|
} | null;
|
|
101
|
-
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange,
|
|
87
|
+
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }: PromptInputProps): React.ReactElement;
|
|
102
88
|
export {};
|
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,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The update line (cli-auto-update section 6.2) - the THIRD occupant of the
|
|
3
|
+
* already-budgeted bottom row, after the toast stack and the activity line.
|
|
4
|
+
*
|
|
5
|
+
* ONE ROW, ALWAYS, AND NEVER `null` (C-15 / P0-1 / D-19). The return type is
|
|
6
|
+
* `React.ReactElement` rather than `React.ReactElement | null`, so the compiler
|
|
7
|
+
* refuses the shortcut: `BottomStatusRow` decides presence with `if (update)`,
|
|
8
|
+
* which tests a REACT ELEMENT, and an element whose render returns `null` is
|
|
9
|
+
* still truthy. Taking that branch would emit a box of ZERO rows - the frame
|
|
10
|
+
* keeps its fixed height, Yoga hands the row to the transcript, and
|
|
11
|
+
* `viewportRows()` goes on reporting the old number to `ScrollViewport`,
|
|
12
|
+
* `selectWindow`, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`. The
|
|
13
|
+
* transcript would gain a row while the updater is idle and lose it the moment
|
|
14
|
+
* it has news. Presence is the CALLER's decision, through
|
|
15
|
+
* `shouldRenderUpdateLine`, exactly as `App.tsx` already decides the activity
|
|
16
|
+
* line's with `running && !overlayNode`.
|
|
17
|
+
*
|
|
18
|
+
* PURE: a function of `snapshot + compact + theme + caps`. No state, no timer,
|
|
19
|
+
* no clock. `<Text wrap="truncate">` so a narrow terminal degrades by truncation
|
|
20
|
+
* rather than by wrapping into a second row the fixed frame cannot afford.
|
|
21
|
+
*
|
|
22
|
+
* ASCII ONLY, and every user-visible character comes from `pickGlyphs(caps)`
|
|
23
|
+
* (C-3). No literal `...`, `+` or `~` may be spelled inline - `ActivityLine`'s
|
|
24
|
+
* routing of even the ellipsis through `pickGlyphs` is the precedent.
|
|
25
|
+
*
|
|
26
|
+
* `compact` RATHER THAN `cols` (IF-1). Section 6.2 describes the width tiers in
|
|
27
|
+
* terms of `cols`, but resolving them here would mean importing
|
|
28
|
+
* `UPDATE_LIMITS.statusCompactCols` - a VALUE - from `update/`, and section 3.1
|
|
29
|
+
* rule 1 requires this module's only edge into that tree to be erasable by tsc.
|
|
30
|
+
* The caller owns `UPDATE_LIMITS` already (it computes presence from it), so it
|
|
31
|
+
* resolves the threshold too. Same rule as presence: the call site decides.
|
|
32
|
+
*/
|
|
33
|
+
import React from 'react';
|
|
34
|
+
import type { Theme } from './theme.js';
|
|
35
|
+
import type { TermCapabilities } from './capabilities.js';
|
|
36
|
+
import type { UpdateSnapshot } from '../update/types.js';
|
|
37
|
+
export interface UpdateLineProps {
|
|
38
|
+
snapshot: UpdateSnapshot;
|
|
39
|
+
/** `cols < UPDATE_LIMITS.statusCompactCols`, resolved by the caller. */
|
|
40
|
+
compact: boolean;
|
|
41
|
+
theme: Theme;
|
|
42
|
+
caps: TermCapabilities;
|
|
43
|
+
}
|
|
44
|
+
export declare function UpdateLine({ snapshot, compact, theme, caps }: UpdateLineProps): React.ReactElement;
|