@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,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* RetryCard — the one-line retry entry (llm-api-retry-backoff §6.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`, and the line itself is built by
|
|
6
|
+
* the pure `formatRetryLine` in `agent/retry-view.ts` so it can be asserted
|
|
7
|
+
* without a terminal.
|
|
8
|
+
*
|
|
9
|
+
* ONE LINE. ALWAYS ONE LINE. Ten retries rendered as ten notices would bury the
|
|
10
|
+
* transcript in the one moment the user most needs it legible, so this card is
|
|
11
|
+
* rewritten in place for the whole turn and settles into a single row of history.
|
|
12
|
+
* Amber while in flight, muted once recovered, red once the ladder is exhausted.
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { Box, Text } from 'ink';
|
|
16
|
+
import Spinner from 'ink-spinner';
|
|
17
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
18
|
+
import { formatRetryLine } from '../../agent/retry-view.js';
|
|
19
|
+
/**
|
|
20
|
+
* The card's tone. `exhausted` is the only failure: an `interrupted` card records
|
|
21
|
+
* a user decision, and colouring that red would blame them for it.
|
|
22
|
+
*/
|
|
23
|
+
export function retryCardColor(phase, theme) {
|
|
24
|
+
if (phase === 'waiting' || phase === 'retrying')
|
|
25
|
+
return theme.noticeWarn;
|
|
26
|
+
if (phase === 'exhausted')
|
|
27
|
+
return theme.noticeError;
|
|
28
|
+
return theme.muted;
|
|
29
|
+
}
|
|
30
|
+
function RetryCardImpl(props) {
|
|
31
|
+
const { attempt, maxRetries, errorType, phase, resumeAt, totalRetries, elapsedMs, now,
|
|
32
|
+
// DEFAULTS TO TRUE: reaching this component means a TUI is mounted, and the
|
|
33
|
+
// TUI always binds Esc to abort. The prop exists so headless-shaped callers
|
|
34
|
+
// and the render tests can assert the suppressed form.
|
|
35
|
+
interactive = true, reducedMotion, theme, caps, } = props;
|
|
36
|
+
const glyphs = pickGlyphs(caps);
|
|
37
|
+
const color = retryCardColor(phase, theme);
|
|
38
|
+
const inFlight = phase === 'waiting' || phase === 'retrying';
|
|
39
|
+
const line = formatRetryLine({
|
|
40
|
+
attempt,
|
|
41
|
+
maxRetries,
|
|
42
|
+
errorType,
|
|
43
|
+
phase,
|
|
44
|
+
...(resumeAt !== undefined ? { resumeAt } : {}),
|
|
45
|
+
...(totalRetries !== undefined ? { totalRetries } : {}),
|
|
46
|
+
...(elapsedMs !== undefined ? { elapsedMs } : {}),
|
|
47
|
+
}, glyphs, now);
|
|
48
|
+
// REDUCED MOTION SUPPRESSES THE SPINNER AND KEEPS THE COUNTDOWN. A frozen number
|
|
49
|
+
// is not calmer, it is broken: reduced motion is about animation, and a
|
|
50
|
+
// countdown is information.
|
|
51
|
+
const animate = inFlight && !reducedMotion && caps.unicode;
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "row", children: [animate ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), ' '] })) : null, _jsx(Text, { color: color, wrap: "truncate", children: line }), phase === 'waiting' && interactive && (_jsxs(Text, { color: theme.muted, children: [' ', "Esc to cancel"] }))] }));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
56
|
+
* `mapEntry` preserves object identity for untouched entries, and `theme` / `caps`
|
|
57
|
+
* are `useMemo`d in `App.tsx` (I-L2-1). `now` changes once a second while the card
|
|
58
|
+
* is waiting, which is exactly when it must re-render.
|
|
59
|
+
*/
|
|
60
|
+
export const RetryCard = React.memo(RetryCardImpl);
|
|
@@ -48,7 +48,12 @@ function TeamCardImpl(props) {
|
|
|
48
48
|
: `${formatDuration(durationMs ?? 0)} (${ok} ok, ${failed} failed)`;
|
|
49
49
|
const count = `${runs.length} ${runs.length === 1 ? 'subagent' : 'subagents'}`;
|
|
50
50
|
const dropped = requested > runs.length ? ` (${runs.length} of ${requested} requested)` : '';
|
|
51
|
-
|
|
51
|
+
// ABSENT WHEN NO FAST CHILD RAN, so an ordinary dispatch's card is unchanged
|
|
52
|
+
// (fast-model-tier §6). `n fast` rather than a ratio: the total is already one
|
|
53
|
+
// column to the left.
|
|
54
|
+
const fastCount = runs.filter((r) => r.tier === 'fast').length;
|
|
55
|
+
const fastNote = fastCount > 0 ? ` ${fastCount} fast` : '';
|
|
56
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, fastNote, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isOk(run) ? theme.toolDone : theme.toolError, children: isOk(run) ? glyphs.toolDone : glyphs.toolError }), _jsxs(Text, { color: theme.primary, children: [' ', `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? 0) - run.startedAt)) : '', ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] }), run.retry && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', "retry ", run.retry.attempt, "/", run.retry.maxRetries] }))] }, run.label))), expanded &&
|
|
52
57
|
runs.map((run) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: [run.label, " ", glyphs.midDot, " ", run.description] }), _jsx(Text, { color: theme.muted, children: run.summary ?? '(no summary)' })] }, `s-${run.label}`))), !expanded && !active && runs.length > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", runs.length, " ", runs.length === 1 ? 'summary' : 'summaries', " (Ctrl+O)"] })), expanded && _jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" })] })] }));
|
|
53
58
|
}
|
|
54
59
|
/**
|
|
@@ -17,6 +17,7 @@ import React from 'react';
|
|
|
17
17
|
import type { Theme } from '../theme.js';
|
|
18
18
|
import type { TermCapabilities } from '../capabilities.js';
|
|
19
19
|
import type { ToolStatus } from '../../agent/reducer.js';
|
|
20
|
+
import type { FilePatch } from '../../tools/patch.js';
|
|
20
21
|
interface ToolCardProps {
|
|
21
22
|
name: string;
|
|
22
23
|
label: string;
|
|
@@ -24,6 +25,14 @@ interface ToolCardProps {
|
|
|
24
25
|
args?: Record<string, unknown>;
|
|
25
26
|
status: ToolStatus;
|
|
26
27
|
preview?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The structured file diff, when the tool produced one (§3.4). Present it and
|
|
30
|
+
* `DiffView` draws INSTEAD of `ToolPreview`; absent — a session saved by an
|
|
31
|
+
* older build, an entry the store's 64-write ring evicted, or a toolset built
|
|
32
|
+
* without a recorder — and the text preview renders exactly what it does
|
|
33
|
+
* today. Nothing ever renders blank.
|
|
34
|
+
*/
|
|
35
|
+
patch?: FilePatch;
|
|
27
36
|
durationMs?: number;
|
|
28
37
|
isError?: boolean;
|
|
29
38
|
expanded?: boolean;
|
|
@@ -34,6 +43,28 @@ interface ToolCardProps {
|
|
|
34
43
|
* means no clamp and byte-identical output.
|
|
35
44
|
*/
|
|
36
45
|
liveClampRows?: number;
|
|
46
|
+
/**
|
|
47
|
+
* The sanitised live tail while the tool runs
|
|
48
|
+
* (agent-activity-presentation-live §3.3.4).
|
|
49
|
+
*
|
|
50
|
+
* PASSED AS `live={entry.live}` AND NEVER `live={entry.live ?? []}` (P2-4).
|
|
51
|
+
* This component is `React.memo` with the DEFAULT comparator, so a fresh `[]`
|
|
52
|
+
* per render is a changed prop and defeats the boundary for every settled card
|
|
53
|
+
* in the transcript — which is exactly why `EMPTY_LINES` above exists.
|
|
54
|
+
*/
|
|
55
|
+
live?: readonly string[];
|
|
56
|
+
/** Epoch ms of the last output row; with `nowSec`, the stall row's clock. */
|
|
57
|
+
lastOutputAt?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Wall-clock SECONDS, passed ONLY to running entries (D-29 / D-36).
|
|
60
|
+
*
|
|
61
|
+
* Seconds and not milliseconds, and `undefined` on every other card, for the
|
|
62
|
+
* same reason: a prop that changed five times a second on every entry would
|
|
63
|
+
* defeat this memo boundary transcript-wide. The value is wall-clock rather
|
|
64
|
+
* than the ticker's `elapsedMs` because the stall is `now - lastOutputAt` and
|
|
65
|
+
* `lastOutputAt` is epoch ms; the ticker's job is to cause the render.
|
|
66
|
+
*/
|
|
67
|
+
nowSec?: number;
|
|
37
68
|
theme: Theme;
|
|
38
69
|
caps: TermCapabilities;
|
|
39
70
|
}
|
|
@@ -44,7 +75,13 @@ declare function ToolCardImpl(props: ToolCardProps): React.ReactElement;
|
|
|
44
75
|
/**
|
|
45
76
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
46
77
|
* Every prop is a scalar except `args` (a stable reference on a settled entry),
|
|
47
|
-
* `
|
|
78
|
+
* `patch` and `theme` / `caps` (both `useMemo`d in `App.tsx` — I-L2-1).
|
|
79
|
+
*
|
|
80
|
+
* `patch` QUALIFIES ONLY BECAUSE THE REDUCER WRITES IT ONCE, at `toolExecEnd`,
|
|
81
|
+
* and never rebuilds it — `mapEntry` preserves object identity for untouched
|
|
82
|
+
* entries. A future action that recomputed or normalized a patch would turn this
|
|
83
|
+
* boundary into a no-op WITH NOTHING FAILING, which is the same trap I-L2-1
|
|
84
|
+
* exists to catch for `theme` and `caps` (P2-9).
|
|
48
85
|
*/
|
|
49
86
|
export declare const ToolCard: React.MemoExoticComponent<typeof ToolCardImpl>;
|
|
50
87
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Tool-call card (spec §3.5): a per-tool glyph, the tool name, a compact args
|
|
4
4
|
* summary, a colored status badge, and a duration, over a collapsible rich
|
|
@@ -22,6 +22,8 @@ import { railBorderProps } from '../layout/Gutter.js';
|
|
|
22
22
|
import { splitLinesCached } from '../render-cache.js';
|
|
23
23
|
import { formatDuration } from '../../agent/usage.js';
|
|
24
24
|
import { ToolPreview, bashBadge } from './ToolPreview.js';
|
|
25
|
+
import { DiffView } from './DiffView.js';
|
|
26
|
+
import { STALL_AFTER_MS } from '../../tools/tool-output-store.js';
|
|
25
27
|
/** Number of preview lines shown before collapse. */
|
|
26
28
|
const COLLAPSED_LINES = 8;
|
|
27
29
|
/** Shared empty slice, so an argument-less card allocates nothing per frame. */
|
|
@@ -78,7 +80,7 @@ export function toolEntryGlyph(name, caps) {
|
|
|
78
80
|
return toolGlyph(pickGlyphs(caps), name);
|
|
79
81
|
}
|
|
80
82
|
function ToolCardImpl(props) {
|
|
81
|
-
const { name, argsRaw, args, status, preview, durationMs, isError, expanded, reducedMotion, liveClampRows, theme, caps, } = props;
|
|
83
|
+
const { name, argsRaw, args, status, preview, patch, durationMs, isError, expanded, reducedMotion, liveClampRows, live, lastOutputAt, nowSec, theme, caps, } = props;
|
|
82
84
|
const glyphs = pickGlyphs(caps);
|
|
83
85
|
const color = statusColor(status, theme);
|
|
84
86
|
const previewText = preview ?? '';
|
|
@@ -94,13 +96,74 @@ function ToolCardImpl(props) {
|
|
|
94
96
|
const ceiling = Math.min(expanded ? previewLines.length : COLLAPSED_LINES, liveClampRows ?? Number.POSITIVE_INFINITY);
|
|
95
97
|
const visibleLines = ceiling >= previewLines.length ? previewLines : previewLines.slice(0, ceiling);
|
|
96
98
|
const hidden = previewLines.length - visibleLines.length;
|
|
97
|
-
const
|
|
99
|
+
const settled = status === 'done' || status === 'error';
|
|
100
|
+
const showPatch = !!patch && settled;
|
|
101
|
+
// The live tail (agent-activity-presentation-live §3.3.4). `expanded` is
|
|
102
|
+
// deliberately ABSENT from this arithmetic: `Ctrl+O` is inert on a live card
|
|
103
|
+
// (D-26), because expansion is a promise about STORED content and the store
|
|
104
|
+
// deliberately holds eight rows. A key that silently does nothing is better
|
|
105
|
+
// than a key that promises the rest of a 400 MB stream.
|
|
106
|
+
//
|
|
107
|
+
// THE TAIL IS THE **LAST** ROWS, and the settled preview above keeps its
|
|
108
|
+
// head-first slice (E-21). Two different ends because they answer two
|
|
109
|
+
// different questions — "what did this command do" versus "what is it doing
|
|
110
|
+
// NOW" (D-25). Head-first while live would freeze on the first eight lines a
|
|
111
|
+
// build ever printed and then never change again.
|
|
112
|
+
const liveRows = live ?? EMPTY_LINES;
|
|
113
|
+
const liveCeiling = Math.min(COLLAPSED_LINES,
|
|
114
|
+
// Inline only: `liveClampRows` already subtracts one row per live entry to
|
|
115
|
+
// pay for a marker (`Transcript.tsx:534-538`), which is what this card's
|
|
116
|
+
// unconditional footer spends (AC-36 / P2-7).
|
|
117
|
+
liveClampRows ?? Number.POSITIVE_INFINITY);
|
|
118
|
+
const visibleLive = liveRows.length > liveCeiling ? liveRows.slice(-Math.max(1, liveCeiling)) : liveRows;
|
|
119
|
+
const showLive = !settled && visibleLive.length > 0;
|
|
120
|
+
// THE OUTER GUARD WIDENS WITH THE BRANCH (P2-2). It was `previewLines.length >
|
|
121
|
+
// 0 && ...`, and the reducer now truncates the stored preview to ONE LINE when
|
|
122
|
+
// a patch is attached (D-19) — so a card whose preview were ever empty would
|
|
123
|
+
// render its header and swallow the diff, silently. It widens a second time
|
|
124
|
+
// here, for the third branch: without `|| showLive` the tail would be computed
|
|
125
|
+
// and never drawn.
|
|
126
|
+
const showBody = ((previewLines.length > 0 || showPatch) && settled) || showLive;
|
|
127
|
+
// ONE FOOTER ROW, ALWAYS, matching `estimateEntryRows`'s arithmetic exactly: a
|
|
128
|
+
// CONSTANT row count is what lets the estimate be exact rather than an upper
|
|
129
|
+
// bound. `nowSec` is absent until the first tick, which reads as "not stalled".
|
|
130
|
+
const stalledFor = nowSec !== undefined && lastOutputAt !== undefined
|
|
131
|
+
? nowSec * 1000 - lastOutputAt
|
|
132
|
+
: 0;
|
|
133
|
+
const liveFooter = stalledFor >= STALL_AFTER_MS
|
|
134
|
+
? `no output for ${Math.floor(stalledFor / 1000)}s`
|
|
135
|
+
: '(running)';
|
|
98
136
|
const badge = status === 'running' && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " running"] })) : (_jsxs(Text, { color: color, children: [status === 'running' ? `${glyphs.spinnerStill} ` : '', badgeLabel(name, status, previewText)] }));
|
|
99
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: name }), _jsxs(Text, { color: theme.muted, children: [" ", summarizeArgs(name, glyphs.ellipsis, args, argsRaw), " "] }), badge, durationMs !== undefined && (_jsxs(Text, { color: theme.muted, children: [" (", formatDuration(durationMs), ")"] }))] }), showBody && (
|
|
137
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: name }), _jsxs(Text, { color: theme.muted, children: [" ", summarizeArgs(name, glyphs.ellipsis, args, argsRaw), " "] }), badge, durationMs !== undefined && (_jsxs(Text, { color: theme.muted, children: [" (", formatDuration(durationMs), ")"] }))] }), showBody && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, isError ? theme.toolError : theme.border), children: showLive ? (_jsxs(_Fragment, { children: [visibleLive.map((row, i) => (
|
|
138
|
+
// The row is already terminal-safe and clipped at
|
|
139
|
+
// `LIVE_ROW_MAX_CHARS` by `sanitizeChunk`, so `wrap="truncate"`
|
|
140
|
+
// is an exact height rather than a hope — which is the coupling
|
|
141
|
+
// `estimateEntryRows`'s live branch relies on.
|
|
142
|
+
//
|
|
143
|
+
// The index IS the key: these rows are positional slots in a
|
|
144
|
+
// ring, not identities, and the whole array is replaced on every
|
|
145
|
+
// chunk anyway.
|
|
146
|
+
//
|
|
147
|
+
// AN EMPTY ROW RENDERS AS A SPACE, and that is not cosmetic:
|
|
148
|
+
// Ink measures a `<Text>` with an empty child as ZERO rows, so a
|
|
149
|
+
// blank separator line — which `npm test`, `cargo` and every
|
|
150
|
+
// build tool print — would make the card shorter than
|
|
151
|
+
// `estimateEntryRows` charged for it. That is an over-estimate,
|
|
152
|
+
// the safe direction, but it would quietly cost §3.3.4's
|
|
153
|
+
// "constant row count" the exactness its arithmetic claims. A
|
|
154
|
+
// space is also what the terminal itself shows for a blank line.
|
|
155
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
156
|
+
_jsx(Text, { wrap: "truncate", color: theme.muted, children: row.length > 0 ? row : ' ' }, i))), _jsx(Text, { color: theme.muted, children: liveFooter })] })) : showPatch ? (_jsx(DiffView, { patch: patch, expanded: expanded, liveClampRows: liveClampRows, theme: theme })) : (_jsxs(_Fragment, { children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] })) }))] }));
|
|
100
157
|
}
|
|
101
158
|
/**
|
|
102
159
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
103
160
|
* Every prop is a scalar except `args` (a stable reference on a settled entry),
|
|
104
|
-
* `
|
|
161
|
+
* `patch` and `theme` / `caps` (both `useMemo`d in `App.tsx` — I-L2-1).
|
|
162
|
+
*
|
|
163
|
+
* `patch` QUALIFIES ONLY BECAUSE THE REDUCER WRITES IT ONCE, at `toolExecEnd`,
|
|
164
|
+
* and never rebuilds it — `mapEntry` preserves object identity for untouched
|
|
165
|
+
* entries. A future action that recomputed or normalized a patch would turn this
|
|
166
|
+
* boundary into a no-op WITH NOTHING FAILING, which is the same trap I-L2-1
|
|
167
|
+
* exists to catch for `theme` and `caps` (P2-9).
|
|
105
168
|
*/
|
|
106
169
|
export const ToolCard = React.memo(ToolCardImpl);
|
package/dist/ui/glyphs.d.ts
CHANGED
|
@@ -108,6 +108,15 @@ export interface Glyphs {
|
|
|
108
108
|
todoPending: string;
|
|
109
109
|
todoActive: string;
|
|
110
110
|
todoDone: string;
|
|
111
|
+
/**
|
|
112
|
+
* API retry (llm-api-retry-backoff §6.5). The rail marker on the retry card.
|
|
113
|
+
*
|
|
114
|
+
* HERE RATHER THAN IN `RetryCard`, for the reason this file's header gives: a
|
|
115
|
+
* literal in a component bypasses `pickGlyphs` entirely and shows mojibake on a
|
|
116
|
+
* legacy console whatever the capability probe said. `glyphs.test.ts`'s static
|
|
117
|
+
* scan fails the build if either spelling appears anywhere else.
|
|
118
|
+
*/
|
|
119
|
+
retry: string;
|
|
111
120
|
/**
|
|
112
121
|
* Ink `borderStyle` name for every boxed region (composer, completion popup,
|
|
113
122
|
* overlay frame).
|
package/dist/ui/glyphs.js
CHANGED
|
@@ -64,6 +64,7 @@ const UNICODE_GLYPHS = {
|
|
|
64
64
|
todoPending: '○',
|
|
65
65
|
todoActive: '▸',
|
|
66
66
|
todoDone: '✔',
|
|
67
|
+
retry: '↻',
|
|
67
68
|
tool: {
|
|
68
69
|
read_file: '▤',
|
|
69
70
|
write_file: '✎',
|
|
@@ -125,6 +126,7 @@ const ASCII_GLYPHS = {
|
|
|
125
126
|
todoPending: '[ ]',
|
|
126
127
|
todoActive: '>',
|
|
127
128
|
todoDone: '[x]',
|
|
129
|
+
retry: '[r]',
|
|
128
130
|
tool: {
|
|
129
131
|
read_file: '=',
|
|
130
132
|
write_file: '+',
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { type RenderMode } from './frame.js';
|
|
14
|
-
import { type FrameGeometry } from './regions.js';
|
|
15
14
|
export interface AppShellProps {
|
|
16
15
|
mode: RenderMode;
|
|
17
16
|
rows: number;
|
|
@@ -21,11 +20,10 @@ export interface AppShellProps {
|
|
|
21
20
|
/**
|
|
22
21
|
* The live subagent roster (team-subagents §6.1), or nothing.
|
|
23
22
|
*
|
|
24
|
-
* IT RENDERS
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* composer band boundary stale the instant a dispatch started.
|
|
23
|
+
* IT RENDERS IN THE BOTTOM CHROME OF THE FULL-SCREEN BRANCH, above `toast` and
|
|
24
|
+
* below the viewport: the roster is transient status about the run in progress,
|
|
25
|
+
* so it belongs with the other transient rows rather than inside the scrolling
|
|
26
|
+
* transcript, where it would be interleaved with history and scroll away.
|
|
29
27
|
*/
|
|
30
28
|
team?: React.ReactNode;
|
|
31
29
|
/**
|
|
@@ -36,10 +34,11 @@ export interface AppShellProps {
|
|
|
36
34
|
* primitive and has no business knowing what a todo is. `team` above already
|
|
37
35
|
* leaks content vocabulary in here; there is no reason to repeat it.
|
|
38
36
|
*
|
|
39
|
-
* IT MUST
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
37
|
+
* IT MUST BE RENDERED IN THE MIDDLE BAND, BESIDE THE VIEWPORT (C-6 / I-4) —
|
|
38
|
+
* that is what makes it a rail rather than another row of bottom chrome. Moved
|
|
39
|
+
* down into the bottom box it would stop being a column beside the transcript
|
|
40
|
+
* and start eating viewport height, growing and shrinking the transcript every
|
|
41
|
+
* time the list changed length.
|
|
43
42
|
*
|
|
44
43
|
* The INLINE branch does not include it at all — a structural guarantee rather
|
|
45
44
|
* than a promise that the caller passes `null` (non-goal 4).
|
|
@@ -56,21 +55,13 @@ export interface AppShellProps {
|
|
|
56
55
|
* both on screen showing the same list (I-6). Fullscreen has the rail; that is
|
|
57
56
|
* the whole design.
|
|
58
57
|
*
|
|
59
|
-
* It sits between `viewport` and `team`,
|
|
60
|
-
* other branch
|
|
61
|
-
*
|
|
58
|
+
* It sits between `viewport` and `team`, mirroring where the rail sits in the
|
|
59
|
+
* other branch: last thing above the bottom chrome, so the strip and the
|
|
60
|
+
* roster read top-to-bottom in the same order in both modes.
|
|
62
61
|
*/
|
|
63
62
|
strip?: React.ReactNode;
|
|
64
63
|
toast: React.ReactNode;
|
|
65
64
|
composer: React.ReactNode;
|
|
66
65
|
status: React.ReactNode;
|
|
67
|
-
/**
|
|
68
|
-
* Where the wheel router reads the composer band boundary from (mouse-wheel
|
|
69
|
-
* §4.5). Written in a LAYOUT EFFECT to a MUTABLE REF, never to state: the
|
|
70
|
-
* measurement happens on every commit, and routing it through `setState`
|
|
71
|
-
* would add a re-render to the streaming path for a value nothing renders
|
|
72
|
-
* (R-7).
|
|
73
|
-
*/
|
|
74
|
-
geometryRef?: React.MutableRefObject<FrameGeometry>;
|
|
75
66
|
}
|
|
76
|
-
export declare function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status,
|
|
67
|
+
export declare function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, }: AppShellProps): React.ReactElement;
|
|
@@ -1,44 +1,9 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
* AppShell (spec §4.3) — the frame that makes R2 true.
|
|
4
|
-
*
|
|
5
|
-
* In `fullscreen` the root box has a FIXED height of `frameHeight(rows)` and the
|
|
6
|
-
* middle viewport is the only `flexGrow` child, so the bottom chrome (toast /
|
|
7
|
-
* composer / status bar) is physically the last thing on the screen from the
|
|
8
|
-
* very first frame — empty session, long session, mid-scroll, overlay open.
|
|
9
|
-
*
|
|
10
|
-
* In `inline` the same children are emitted in the same order with no height
|
|
11
|
-
* constraint, i.e. byte-for-byte the v0.2.0 document-flow behavior.
|
|
12
|
-
*/
|
|
13
|
-
import { useLayoutEffect, useRef } from 'react';
|
|
14
|
-
import { Box, measureElement } from 'ink';
|
|
2
|
+
import { Box } from 'ink';
|
|
15
3
|
import { frameHeight } from './frame.js';
|
|
16
|
-
|
|
17
|
-
export function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, geometryRef, }) {
|
|
18
|
-
const bottomRef = useRef(null);
|
|
19
|
-
// DECLARED ABOVE THE INLINE EARLY RETURN, and it has to stay there: React
|
|
20
|
-
// keys hooks by call order, and a hook that only runs in one mode changes
|
|
21
|
-
// that order between modes.
|
|
22
|
-
//
|
|
23
|
-
// Ink computes yoga layout in `onRender`, which React runs BEFORE layout
|
|
24
|
-
// effects, so this reflects the commit that just happened — the same
|
|
25
|
-
// mechanism `ScrollViewport` already relies on.
|
|
26
|
-
useLayoutEffect(() => {
|
|
27
|
-
if (!geometryRef)
|
|
28
|
-
return;
|
|
29
|
-
if (mode === 'inline' || !bottomRef.current) {
|
|
30
|
-
geometryRef.current = UNMEASURED_GEOMETRY;
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
geometryRef.current = {
|
|
34
|
-
// 1 in the alternate screen — see the note on `FrameGeometry.top`.
|
|
35
|
-
top: 1,
|
|
36
|
-
height: frameHeight(rows),
|
|
37
|
-
bottom: measureElement(bottomRef.current).height,
|
|
38
|
-
};
|
|
39
|
-
});
|
|
4
|
+
export function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, }) {
|
|
40
5
|
if (mode === 'inline') {
|
|
41
6
|
return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, strip, team, toast, composer, status] }));
|
|
42
7
|
}
|
|
43
|
-
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), rail] }), _jsxs(Box, {
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), rail] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [team, toast, composer, status] })] }));
|
|
44
9
|
}
|
|
@@ -73,7 +73,7 @@ export declare function heightKey(entry: Entry, cols: number, expanded: boolean,
|
|
|
73
73
|
* after the entry is measured, while an under-estimate can put the newest output
|
|
74
74
|
* off the bottom of a viewport the user believes is pinned.
|
|
75
75
|
*/
|
|
76
|
-
export declare function estimateEntryRows(entry: Entry, cols: number, density: DensityMode, expanded: boolean): number;
|
|
76
|
+
export declare function estimateEntryRows(entry: Entry, cols: number, density: DensityMode, expanded: boolean, thinkingVisible?: boolean): number;
|
|
77
77
|
export interface WindowSelection {
|
|
78
78
|
/** First mounted entry, inclusive. */
|
|
79
79
|
startIndex: number;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* unchanged. No scroll-anchoring compensation pass is required. Do NOT
|
|
22
22
|
* "simplify" `offset` to count from the top.
|
|
23
23
|
*/
|
|
24
|
+
import { FAST_LIMITS } from '../../fast/limits.js';
|
|
24
25
|
import { GUTTER_WIDTH } from './Gutter.js';
|
|
25
26
|
export const VIRTUAL_LIMITS = {
|
|
26
27
|
/**
|
|
@@ -54,16 +55,52 @@ export function entryRevision(entry) {
|
|
|
54
55
|
return 'u';
|
|
55
56
|
case 'notice':
|
|
56
57
|
return 'n';
|
|
58
|
+
// `thinkingMs` is O(1) and it CHANGES THE ROW: sealing the clock turns
|
|
59
|
+
// `* thought` into `* thought for 12s`, and with thinking hidden that row is
|
|
60
|
+
// the entry's whole rendered height.
|
|
57
61
|
case 'assistant':
|
|
58
|
-
return `a${entry.text.length}.${entry.thinking?.length ?? 0}.${entry.streaming ? 1 : 0}.${entry.aborted ? 1 : 0}`;
|
|
62
|
+
return `a${entry.text.length}.${entry.thinking?.length ?? 0}.${entry.streaming ? 1 : 0}.${entry.aborted ? 1 : 0}.${entry.thinkingMs ?? -1}`;
|
|
63
|
+
// `patch.lineCount` is PRECOMPUTED for exactly this term: summing
|
|
64
|
+
// `hunks[].lines.length` per entry per frame is the cost the height cache
|
|
65
|
+
// exists to remove, and I-L3-1 forbids hashing.
|
|
66
|
+
// `liveSeq` IS A COUNTER AND NOT `live.length`, and that is precisely the
|
|
67
|
+
// NON-APPEND mutation I-L3-1 above says must extend the revision: a
|
|
68
|
+
// fixed-size tail evicting its oldest row while appending a new one of the
|
|
69
|
+
// same width leaves the joined length unchanged, so a length term would go
|
|
70
|
+
// on matching while the card's content changed under it.
|
|
71
|
+
//
|
|
72
|
+
// SIX TERMS NOW, the longest revision in this file (P2-12). Still O(1) per
|
|
73
|
+
// entry per frame, which is the property I-L3-1 requires -- but the next
|
|
74
|
+
// field makes it seven, and a term that is not O(1) would put the cost this
|
|
75
|
+
// module exists to remove back on every frame.
|
|
59
76
|
case 'tool':
|
|
60
|
-
return `t${entry.status}.${entry.argsRaw.length}.${entry.preview?.length ?? 0}.${entry.durationMs ?? -1}`;
|
|
77
|
+
return `t${entry.status}.${entry.argsRaw.length}.${entry.preview?.length ?? 0}.${entry.durationMs ?? -1}.${entry.patch?.lineCount ?? -1}.${entry.liveSeq ?? -1}`;
|
|
61
78
|
case 'team':
|
|
62
79
|
return `m${entry.active ? 1 : 0}.${entry.runs.length}.${entry.runs
|
|
63
80
|
.map((r) => r.phase[0])
|
|
64
81
|
.join('')}.${entry.runs.reduce((acc, r) => acc + r.toolCalls, 0)}`;
|
|
65
82
|
case 'todo':
|
|
66
83
|
return `d${entry.live ? 1 : 0}.${entry.doneCount}.${entry.total}.${entry.items.length}`;
|
|
84
|
+
/**
|
|
85
|
+
* The retry card is ONE ROW IN EVERY PHASE, so its revision carries the phase
|
|
86
|
+
* and the counter but deliberately NOT the countdown: `resumeAt` changes the
|
|
87
|
+
* rendered SECONDS once a second while the height stays 1, and folding it in
|
|
88
|
+
* here would invalidate the height cache 30 times per backoff to re-measure a
|
|
89
|
+
* number that cannot change the answer.
|
|
90
|
+
*
|
|
91
|
+
* `phase` and `attempt` ARE in, because they change the row's text and a stale
|
|
92
|
+
* measurement would be wrong in a way nothing reports (I-L3-1).
|
|
93
|
+
*/
|
|
94
|
+
case 'retry':
|
|
95
|
+
return `r${entry.phase}.${entry.attempt}.${entry.maxRetries}`;
|
|
96
|
+
/**
|
|
97
|
+
* `status`, `live` and the TEXT LENGTH, because all three change the rows the
|
|
98
|
+
* card draws: `advice` renders a body, everything else renders one line, and
|
|
99
|
+
* the body's length decides how many rows it wraps to. `durationMs` is in the
|
|
100
|
+
* header, which is `wrap="truncate"`, so it can never change the height.
|
|
101
|
+
*/
|
|
102
|
+
case 'fast':
|
|
103
|
+
return `f${entry.status}.${entry.live ? 1 : 0}.${entry.text?.length ?? 0}.${entry.detail?.length ?? 0}`;
|
|
67
104
|
default:
|
|
68
105
|
return 'x';
|
|
69
106
|
}
|
|
@@ -101,6 +138,10 @@ function wrappedRows(text, usable) {
|
|
|
101
138
|
}
|
|
102
139
|
/** Collapsed preview rows in `ToolCard`; duplicated as a NUMBER, not imported. */
|
|
103
140
|
const TOOL_COLLAPSED_LINES = 8;
|
|
141
|
+
/** Collapsed diff rows in `DiffView`; duplicated for the same reason as above. */
|
|
142
|
+
const DIFF_COLLAPSED_LINES = 12;
|
|
143
|
+
/** Live tail rows in `ToolCard`; duplicated as a NUMBER, for the same reason. */
|
|
144
|
+
const TOOL_LIVE_TAIL_ROWS = 8;
|
|
104
145
|
/**
|
|
105
146
|
* Pure UPPER-BOUND estimate of an entry's height, used until a real measurement
|
|
106
147
|
* exists.
|
|
@@ -110,7 +151,7 @@ const TOOL_COLLAPSED_LINES = 8;
|
|
|
110
151
|
* after the entry is measured, while an under-estimate can put the newest output
|
|
111
152
|
* off the bottom of a viewport the user believes is pinned.
|
|
112
153
|
*/
|
|
113
|
-
export function estimateEntryRows(entry, cols, density, expanded) {
|
|
154
|
+
export function estimateEntryRows(entry, cols, density, expanded, thinkingVisible = true) {
|
|
114
155
|
const usable = Math.max(1, cols - GUTTER_WIDTH);
|
|
115
156
|
// `separationRows` needs the PRECEDING entry, which a per-entry estimate does
|
|
116
157
|
// not have. One row is its maximum, so charging it unconditionally keeps this
|
|
@@ -122,12 +163,60 @@ export function estimateEntryRows(entry, cols, density, expanded) {
|
|
|
122
163
|
case 'notice':
|
|
123
164
|
return separation + wrappedRows(entry.text, usable);
|
|
124
165
|
case 'assistant': {
|
|
125
|
-
|
|
166
|
+
// THE PARAMETER'S `true` DEFAULT IS A FAIL-SAFE FOR A FORGOTTEN CALLER,
|
|
167
|
+
// NOT THE PLAN FOR THE ONE THERE IS (P1-6a). `Transcript.tsx` holds
|
|
168
|
+
// `thinkingVisible` as a prop and MUST pass it: with thinking hidden by
|
|
169
|
+
// default, an unpassed flag inflates every assistant entry that thought by
|
|
170
|
+
// `1 + wrappedRows(thinking)` — hundreds of rows at `thinkingLevel: high`
|
|
171
|
+
// against a real height of one. The self-correction argument does not
|
|
172
|
+
// rescue that, because an entry is only measured once `selectWindow`
|
|
173
|
+
// mounts it, and an entry estimated at 400 rows is exactly the one that
|
|
174
|
+
// never gets mounted.
|
|
175
|
+
const thinking = !entry.thinking
|
|
176
|
+
? 0
|
|
177
|
+
: thinkingVisible
|
|
178
|
+
? 1 + wrappedRows(entry.thinking, usable)
|
|
179
|
+
: // The collapsed `thought for Ns` marker is exactly one row, and it is
|
|
180
|
+
// suppressed while the entry is still streaming (the activity line is
|
|
181
|
+
// the live surface then).
|
|
182
|
+
entry.streaming
|
|
183
|
+
? 0
|
|
184
|
+
: 1;
|
|
126
185
|
const body = entry.text.length > 0 ? wrappedRows(entry.text, usable) : 1;
|
|
127
186
|
return separation + thinking + body + (entry.aborted ? 1 : 0);
|
|
128
187
|
}
|
|
129
188
|
case 'tool': {
|
|
130
|
-
|
|
189
|
+
const settled = entry.status === 'done' || entry.status === 'error';
|
|
190
|
+
// WHEN A PATCH IS PRESENT IT, NOT THE PREVIEW, IS WHAT DRAWS (§3.4): the
|
|
191
|
+
// stored preview is one line by then, so estimating from it would report a
|
|
192
|
+
// 3-row card for a 12-row diff — an UNDER-estimate, the direction this
|
|
193
|
+
// file names as unsafe.
|
|
194
|
+
if (settled && entry.patch) {
|
|
195
|
+
const rows = entry.patch.lineCount + Math.max(0, entry.patch.hunks.length - 1);
|
|
196
|
+
const shown = expanded ? rows : Math.min(rows, DIFF_COLLAPSED_LINES);
|
|
197
|
+
// card header + `+N -M` summary + rows + footer
|
|
198
|
+
return separation + 1 + 1 + shown + 1;
|
|
199
|
+
}
|
|
200
|
+
// THE ONE-LINE `if (!entry.preview || !settled)` THIS REPLACES COVERED TWO
|
|
201
|
+
// CASES, and both must survive the split (P1-4): not settled, and settled
|
|
202
|
+
// with nothing to draw (an aborted call, a tool that returned an empty
|
|
203
|
+
// string). A `!settled` block alone would drop the second into
|
|
204
|
+
// `wrappedRows(entry.preview, usable)` with `undefined`.
|
|
205
|
+
if (!settled) {
|
|
206
|
+
const live = entry.live?.length ?? 0;
|
|
207
|
+
if (live === 0)
|
|
208
|
+
return separation + 1;
|
|
209
|
+
// Header + tail rows + the unconditional stall/`(running)` footer row.
|
|
210
|
+
//
|
|
211
|
+
// ROWS ARE COUNTED, NOT WRAPPED, which is legitimate only because every
|
|
212
|
+
// live row is rendered `wrap="truncate"` AND clipped at
|
|
213
|
+
// `LIVE_ROW_MAX_CHARS` on the way into the store -- the same argument
|
|
214
|
+
// `DiffView.tsx:6-11` makes for itself, and the same coupling: if a
|
|
215
|
+
// future change lets a live row wrap, this silently UNDER-counts, the
|
|
216
|
+
// direction this file names as the one the layout cannot absorb.
|
|
217
|
+
return separation + 1 + Math.min(live, TOOL_LIVE_TAIL_ROWS) + 1;
|
|
218
|
+
}
|
|
219
|
+
if (!entry.preview) {
|
|
131
220
|
return separation + 1;
|
|
132
221
|
}
|
|
133
222
|
const lines = wrappedRows(entry.preview, usable);
|
|
@@ -139,6 +228,21 @@ export function estimateEntryRows(entry, cols, density, expanded) {
|
|
|
139
228
|
return separation + 1 + entry.runs.length + (expanded ? entry.runs.length * 3 + 1 : 1);
|
|
140
229
|
case 'todo':
|
|
141
230
|
return separation + 1 + entry.items.length;
|
|
231
|
+
// ONE ROW, ALWAYS. The card is `wrap="truncate"` in every phase, so this is an
|
|
232
|
+
// exact height rather than an upper bound.
|
|
233
|
+
case 'retry':
|
|
234
|
+
return separation + 1;
|
|
235
|
+
// ADDING THE CASE TO ONLY ONE OF THIS FILE'S TWO SWITCHES YIELDS A
|
|
236
|
+
// TRANSCRIPT WHOSE SCROLL ARITHMETIC DISAGREES WITH WHAT IT DREW, which
|
|
237
|
+
// manifests as drift rather than as an error (RV-13). Header row, then
|
|
238
|
+
// either the wrapped critique (clamped to `cardTextRows`) or one status
|
|
239
|
+
// line — an upper bound, which is what this function owes its caller.
|
|
240
|
+
case 'fast': {
|
|
241
|
+
const body = entry.status === 'advice' && entry.text
|
|
242
|
+
? Math.min(FAST_LIMITS.cardTextRows, wrappedRows(entry.text, usable))
|
|
243
|
+
: 1;
|
|
244
|
+
return separation + 1 + body;
|
|
245
|
+
}
|
|
142
246
|
default:
|
|
143
247
|
return separation + 1;
|
|
144
248
|
}
|
|
@@ -10,10 +10,10 @@ function keyRows(times) {
|
|
|
10
10
|
['Shift+Tab', 'Toggle plan mode (same as /plan)'],
|
|
11
11
|
[`Ctrl+C ${times}2`, 'Exit'],
|
|
12
12
|
['Ctrl+L', 'Redraw the frame'],
|
|
13
|
-
['Ctrl+T', '
|
|
13
|
+
['Ctrl+T', 'Show/hide thinking (off by default)'],
|
|
14
14
|
['PgUp / PgDn', 'Scroll a page (transcript, or this overlay)'],
|
|
15
15
|
['Shift+Up / Shift+Down', 'Scroll the transcript a line (full-screen mode)'],
|
|
16
|
-
['Wheel', 'Scroll the transcript (
|
|
16
|
+
['Wheel', 'Scroll the transcript (or the open overlay)'],
|
|
17
17
|
['Shift+Wheel', 'Scroll the transcript a page'],
|
|
18
18
|
['Up / Down', 'Prompt history (empty input)'],
|
|
19
19
|
];
|
|
@@ -27,6 +27,7 @@ const COMMANDS = [
|
|
|
27
27
|
['/thinking <level>', 'Set thinking level'],
|
|
28
28
|
['/max-tokens [n|auto]', 'Output token cap; no argument reports the effective one'],
|
|
29
29
|
['/team [on|off|max n]', 'Team subagents: status, switch, fan-out width'],
|
|
30
|
+
['/fast [on|off|model|review]', 'Fast model tier: status, switch, model, review cadence'],
|
|
30
31
|
[
|
|
31
32
|
'/todo [on|off|panel|follow|clear|continue]',
|
|
32
33
|
'Todo planning: status, switches, the rail, follow-through',
|
|
@@ -67,6 +68,16 @@ const TEAM_ROWS = [
|
|
|
67
68
|
['--team / --no-team', 'Start a session with team mode on or off (also ARAGON_TEAM=0)'],
|
|
68
69
|
['team_send / team_wait', 'How subagents message each other; 6 each, one every 15s'],
|
|
69
70
|
];
|
|
71
|
+
const FAST_ROWS = [
|
|
72
|
+
['/fast', 'Status: on/off, the model, the cadence, and this session\'s totals'],
|
|
73
|
+
['/fast on | off', 'Switch the tier for this session (and save it)'],
|
|
74
|
+
['/fast model <id>', 'Set the fast model; accepts provider:model'],
|
|
75
|
+
['/fast same', 'Run the fast tier on the main model'],
|
|
76
|
+
['/fast review <n> | off', 'Turns between automatic reviews, or turn them off'],
|
|
77
|
+
['/fast delegate on | off', 'Allow model:"fast" on task subagents'],
|
|
78
|
+
['--fast / --no-fast', 'Start a session with the tier on or off (also ARAGON_FAST=1)'],
|
|
79
|
+
['<fast_review>', 'An automated second opinion from the fast model - advice, not the user'],
|
|
80
|
+
];
|
|
70
81
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
71
82
|
export function helpRows(theme, caps) {
|
|
72
83
|
const glyphs = pickGlyphs(caps);
|
|
@@ -85,6 +96,9 @@ export function helpRows(theme, caps) {
|
|
|
85
96
|
rows.push(section('Team subagents'));
|
|
86
97
|
for (const [c, d] of TEAM_ROWS)
|
|
87
98
|
rows.push(pair(c, d));
|
|
99
|
+
rows.push(section('Fast model tier'));
|
|
100
|
+
for (const [c, d] of FAST_ROWS)
|
|
101
|
+
rows.push(pair(c, d));
|
|
88
102
|
rows.push(section('Skills'));
|
|
89
103
|
for (const [c, d] of SKILL_ROWS)
|
|
90
104
|
rows.push(pair(c, d));
|