@aragon-agent/cli 0.5.6 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +356 -0
- package/README.md +1429 -1105
- 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/entry-limits.d.ts +65 -0
- package/dist/agent/entry-limits.js +133 -0
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +253 -3
- package/dist/agent/reducer.js +553 -28
- package/dist/agent/retry-view.d.ts +68 -0
- package/dist/agent/retry-view.js +95 -0
- package/dist/agent/system-prompt.d.ts +11 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +122 -4
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +313 -2
- package/dist/commands/perf.d.ts +45 -0
- package/dist/commands/perf.js +78 -0
- package/dist/config/cli-commands.d.ts +53 -2
- package/dist/config/cli-commands.js +136 -3
- package/dist/config/env.d.ts +6 -1
- package/dist/config/env.js +131 -1
- package/dist/config/load.d.ts +63 -0
- package/dist/config/load.js +125 -2
- package/dist/config/schema.d.ts +273 -10
- package/dist/config/schema.js +250 -7
- package/dist/config/store.js +20 -1
- package/dist/fast/critique.d.ts +43 -0
- package/dist/fast/critique.js +62 -0
- package/dist/fast/digest.d.ts +42 -0
- package/dist/fast/digest.js +85 -0
- package/dist/fast/frames.d.ts +99 -0
- package/dist/fast/frames.js +180 -0
- package/dist/fast/limits.d.ts +98 -0
- package/dist/fast/limits.js +98 -0
- package/dist/fast/prompt.d.ts +72 -0
- package/dist/fast/prompt.js +94 -0
- package/dist/fast/resolve.d.ts +43 -0
- package/dist/fast/resolve.js +105 -0
- package/dist/fast/review-call.d.ts +99 -0
- package/dist/fast/review-call.js +130 -0
- package/dist/fast/reviewer.d.ts +248 -0
- package/dist/fast/reviewer.js +664 -0
- package/dist/fast/types.d.ts +128 -0
- package/dist/fast/types.js +14 -0
- package/dist/fast/wiring.d.ts +170 -0
- package/dist/fast/wiring.js +316 -0
- package/dist/logging/install.d.ts +15 -1
- package/dist/logging/install.js +37 -4
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +57 -1
- package/dist/team/normalize.d.ts +26 -1
- package/dist/team/normalize.js +23 -3
- package/dist/team/prompt.d.ts +11 -0
- package/dist/team/prompt.js +10 -0
- package/dist/team/report.d.ts +18 -0
- package/dist/team/report.js +27 -1
- package/dist/team/runtime.d.ts +13 -2
- package/dist/team/runtime.js +33 -6
- package/dist/team/subagent.d.ts +19 -1
- package/dist/team/subagent.js +72 -10
- package/dist/team/task-tool.d.ts +26 -0
- package/dist/team/task-tool.js +48 -4
- package/dist/team/types.d.ts +80 -1
- package/dist/tools/bash-tool.js +19 -3
- package/dist/tools/diff.d.ts +25 -5
- package/dist/tools/diff.js +72 -41
- package/dist/tools/file-change-store.d.ts +34 -0
- package/dist/tools/file-change-store.js +57 -0
- package/dist/tools/fs-tools.d.ts +26 -0
- package/dist/tools/fs-tools.js +62 -5
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch.d.ts +115 -0
- package/dist/tools/patch.js +453 -0
- package/dist/tools/terminal-output.d.ts +107 -0
- package/dist/tools/terminal-output.js +198 -0
- package/dist/tools/tool-output-store.d.ts +68 -0
- package/dist/tools/tool-output-store.js +102 -0
- package/dist/ui/ActivityLine.d.ts +48 -0
- package/dist/ui/ActivityLine.js +22 -0
- package/dist/ui/App.d.ts +1 -4
- package/dist/ui/App.js +357 -44
- package/dist/ui/BottomStatusRow.d.ts +43 -0
- package/dist/ui/BottomStatusRow.js +14 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Markdown.d.ts +7 -1
- package/dist/ui/Markdown.js +15 -48
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +50 -0
- package/dist/ui/StatusBar.js +7 -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 +80 -5
- package/dist/ui/Transcript.js +179 -11
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +38 -1
- package/dist/ui/entries/AssistantEntry.js +34 -3
- 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.d.ts +9 -1
- package/dist/ui/entries/TeamCard.js +28 -2
- package/dist/ui/entries/TodoCard.d.ts +9 -1
- package/dist/ui/entries/TodoCard.js +23 -1
- package/dist/ui/entries/ToolCard.d.ts +50 -1
- package/dist/ui/entries/ToolCard.js +104 -7
- package/dist/ui/entries/ToolPreview.d.ts +2 -2
- package/dist/ui/entries/UserEntry.d.ts +9 -1
- package/dist/ui/entries/UserEntry.js +17 -1
- package/dist/ui/exit-snapshot.d.ts +7 -0
- 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/MeasuredEntry.d.ts +21 -0
- package/dist/ui/layout/MeasuredEntry.js +30 -0
- package/dist/ui/layout/ScrollViewport.js +8 -2
- package/dist/ui/layout/viewport-geometry.d.ts +28 -0
- package/dist/ui/layout/viewport-geometry.js +27 -0
- package/dist/ui/layout/virtual-window.d.ts +100 -0
- package/dist/ui/layout/virtual-window.js +308 -0
- package/dist/ui/live-clamp.d.ts +42 -0
- package/dist/ui/live-clamp.js +74 -0
- package/dist/ui/markdown-blocks.d.ts +25 -1
- package/dist/ui/markdown-blocks.js +43 -0
- 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/render-cache.d.ts +81 -0
- package/dist/ui/render-cache.js +195 -0
- package/dist/ui/render-governor.d.ts +54 -0
- package/dist/ui/render-governor.js +78 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +32 -0
- package/dist/ui/use-height-store.d.ts +31 -0
- package/dist/ui/use-height-store.js +76 -0
- package/dist/ui/use-render-governor.d.ts +39 -0
- package/dist/ui/use-render-governor.js +57 -0
- package/dist/ui/use-startup-notices.d.ts +9 -1
- package/dist/ui/use-startup-notices.js +35 -1
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- package/package.json +2 -2
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
package/dist/ui/App.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
|
11
11
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
12
12
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
13
13
|
import { computeCost } from '../agent/usage.js';
|
|
14
|
-
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
14
|
+
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
15
15
|
import { updatePersistedConfig } from '../config/store.js';
|
|
16
16
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
17
17
|
import { bumpSubmitCount, setMouseNoticeSeen } from '../config/ui-state.js';
|
|
@@ -28,26 +28,30 @@ import { Transcript, TranscriptList } from './Transcript.js';
|
|
|
28
28
|
import { PromptInput } from './PromptInput.js';
|
|
29
29
|
import { Composer } from './Composer.js';
|
|
30
30
|
import { StatusBar } from './StatusBar.js';
|
|
31
|
+
import { secondsLeft } from '../agent/retry-view.js';
|
|
31
32
|
import { TeamPanel } from './TeamPanel.js';
|
|
32
33
|
import { TodoPanel } from './TodoPanel.js';
|
|
33
34
|
import { TodoStrip } from './TodoStrip.js';
|
|
34
|
-
import {
|
|
35
|
+
import { ActivityLine } from './ActivityLine.js';
|
|
36
|
+
import { BottomStatusRow } from './BottomStatusRow.js';
|
|
35
37
|
import { AppShell } from './layout/AppShell.js';
|
|
36
38
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
37
39
|
import { todoRailRows, todoRailWidth } from './layout/rail.js';
|
|
38
40
|
import { TODO_LIMITS } from '../todo/limits.js';
|
|
39
41
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
40
42
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
43
|
+
import { useHeightStore } from './use-height-store.js';
|
|
44
|
+
import { useRenderGovernor } from './use-render-governor.js';
|
|
45
|
+
import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
|
|
41
46
|
import { frameHeight, MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
42
47
|
import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
43
|
-
import { UNMEASURED_GEOMETRY } from './layout/regions.js';
|
|
44
48
|
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
45
49
|
import { installConsoleBridge } from './console-bridge.js';
|
|
46
50
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
47
51
|
import { OverlayFrame } from './layout/OverlayFrame.js';
|
|
48
52
|
import { helpRows } from './overlays/HelpOverlay.js';
|
|
49
53
|
import { ModelPicker } from './overlays/ModelPicker.js';
|
|
50
|
-
import { SettingsScreen } from './overlays/SettingsScreen.js';
|
|
54
|
+
import { SettingsScreen, fastSettingsFrom, readFastSettings, } from './overlays/SettingsScreen.js';
|
|
51
55
|
import { ConfirmDialog } from './overlays/ConfirmDialog.js';
|
|
52
56
|
import { QuestionOverlay } from './overlays/QuestionOverlay.js';
|
|
53
57
|
import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
|
|
@@ -90,14 +94,35 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
90
94
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The lazy `useReducer` initializer. Module scope, so the reference is stable
|
|
99
|
+
* across renders and React never re-runs it.
|
|
100
|
+
*/
|
|
101
|
+
function seedViewState(thinkingVisible) {
|
|
102
|
+
return initialViewState({ thinkingVisible });
|
|
103
|
+
}
|
|
104
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, }) {
|
|
96
105
|
const { exit } = useApp();
|
|
97
106
|
const { stdout } = useStdout();
|
|
98
107
|
const fullscreen = mode === 'fullscreen';
|
|
99
108
|
const { rows, cols } = useTerminalSize();
|
|
100
|
-
|
|
109
|
+
// --- Render governor (tui-render-performance L4). ------------------------
|
|
110
|
+
//
|
|
111
|
+
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
112
|
+
// `performance.now()` during render and reads it again in a layout effect, so
|
|
113
|
+
// the span it measures is only the whole commit if nothing precedes it here.
|
|
114
|
+
const cfg = controller.getConfig();
|
|
115
|
+
const governor = useRenderGovernor({
|
|
116
|
+
enabled: cfg.renderGovernor,
|
|
117
|
+
maxIntervalMs: cfg.maxRenderIntervalMs,
|
|
118
|
+
});
|
|
119
|
+
/** The transcript height cache, owned here and read by `TranscriptList` (L3). */
|
|
120
|
+
const heights = useHeightStore();
|
|
121
|
+
// SEEDED FROM CONFIG, not dispatched after mount (§3.1.1): a post-mount
|
|
122
|
+
// `toggleThinking` would let `/resume` flash a restored thinking block on
|
|
123
|
+
// frame 1. `seedViewState` is module-scope so the lazy initializer is a stable
|
|
124
|
+
// function reference.
|
|
125
|
+
const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
|
|
101
126
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
102
127
|
const [confirmState, setConfirmState] = useState(null);
|
|
103
128
|
/** The request the question / plan overlay is currently rendering. */
|
|
@@ -108,8 +133,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
108
133
|
const [promptHistory, setPromptHistory] = useState(() => loadPromptHistory());
|
|
109
134
|
// Scroll plumbing: intent goes down, one derived display number comes back.
|
|
110
135
|
const [scrollIntent, setScrollIntent] = useState();
|
|
111
|
-
/** One prompt-history step per wheel notch over the composer band (§4.7). */
|
|
112
|
-
const [historyIntent, setHistoryIntent] = useState();
|
|
113
136
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
114
137
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
115
138
|
// I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
|
|
@@ -124,7 +147,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
124
147
|
// the completion list are memoized against it, so a skill installed mid-session
|
|
125
148
|
// gets its `/<name>` command and its autocomplete entry immediately.
|
|
126
149
|
const [skillsNonce, setSkillsNonce] = useState(0);
|
|
127
|
-
const cfg = controller.getConfig();
|
|
128
150
|
const caps = useMemo(() => {
|
|
129
151
|
const detected = detectCapabilities(process.env, stdout);
|
|
130
152
|
return {
|
|
@@ -143,12 +165,35 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
143
165
|
const ctrlCArmed = useRef(false);
|
|
144
166
|
const ctrlCTimer = useRef(null);
|
|
145
167
|
const runBaselineOut = useRef(0);
|
|
168
|
+
/**
|
|
169
|
+
* Epoch ms the current run began — the activity phrase's seed.
|
|
170
|
+
*
|
|
171
|
+
* ASSIGNED IN RENDER SCOPE, NOT IN THE ELAPSED EFFECT (P1-7). That effect runs
|
|
172
|
+
* AFTER the render that first sees `status === 'running'`, so a ref it wrote
|
|
173
|
+
* would be one frame late — and the first frame of a run is exactly when the
|
|
174
|
+
* phrase is chosen. `Transcript.tsx` updates `highWater` / `prevLen` during
|
|
175
|
+
* render for the same reason: a value derived from a transition is needed by
|
|
176
|
+
* the very render that observes the transition.
|
|
177
|
+
*/
|
|
178
|
+
const runStartedAt = useRef(0);
|
|
146
179
|
const toastTimers = useRef(new Map());
|
|
147
180
|
/** Every outstanding `gate.request()`. Held in a ref, never in state: it is
|
|
148
181
|
* bookkeeping for promises, and re-rendering on it would be noise. */
|
|
149
182
|
const pendingHuman = useRef(new Set());
|
|
150
|
-
/**
|
|
151
|
-
|
|
183
|
+
/**
|
|
184
|
+
* What `/perf` reports. Assigned during render, exactly like `stateRef` above,
|
|
185
|
+
* because a slash command runs outside the render pass and a value captured in
|
|
186
|
+
* an effect closure would be one commit stale on every readout.
|
|
187
|
+
*/
|
|
188
|
+
const perfRef = useRef(null);
|
|
189
|
+
/**
|
|
190
|
+
* How many entries `TranscriptList` actually mounted this frame.
|
|
191
|
+
*
|
|
192
|
+
* A ref written during the child's render, not state: this is the number
|
|
193
|
+
* `render-budget.test.tsx` gates on (AC-1 / AC-9), and turning it into state
|
|
194
|
+
* would make reading the render budget cost a render.
|
|
195
|
+
*/
|
|
196
|
+
const mountedCount = useRef(0);
|
|
152
197
|
// --- Follow-through (todo-plan-followthrough §3.4). ---------------------
|
|
153
198
|
//
|
|
154
199
|
// THREE REFS AND NO VIEW STATE, DELIBERATELY (§5.2). Every field added to
|
|
@@ -249,6 +294,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
249
294
|
// --- Streaming coalescer + controller subscription. --------------------
|
|
250
295
|
const pending = useRef([]);
|
|
251
296
|
const flushTimer = useRef(null);
|
|
297
|
+
/** Alias so the subscription effect below reads one stable ref, not a hook. */
|
|
298
|
+
const governorInterval = governor.intervalMs;
|
|
252
299
|
const flushPending = useCallback(() => {
|
|
253
300
|
if (flushTimer.current) {
|
|
254
301
|
clearTimeout(flushTimer.current);
|
|
@@ -261,10 +308,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
261
308
|
for (const action of merged)
|
|
262
309
|
dispatch(action);
|
|
263
310
|
}, []);
|
|
311
|
+
/**
|
|
312
|
+
* The CLI-local diff side channel (§3.3.7). Memoized once, beside the `cost`
|
|
313
|
+
* lookup below, because `reduceEvent` TAKES a patch rather than reading one:
|
|
314
|
+
* the store is consume-once, so this object must not be rebuilt per event.
|
|
315
|
+
*/
|
|
316
|
+
const patchSource = useMemo(() => ({ take: (id) => controller.takeFilePatch(id) }), [controller]);
|
|
264
317
|
useEffect(() => {
|
|
265
318
|
const unsubscribe = controller.subscribe((event) => {
|
|
266
319
|
const cost = controller.getModelInfo().cost;
|
|
267
|
-
for (const action of reduceEvent(event, cost)) {
|
|
320
|
+
for (const action of reduceEvent(event, cost, patchSource)) {
|
|
268
321
|
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
269
322
|
// `ViewState.errorNoticed`, rather than read back from the reducer one
|
|
270
323
|
// commit later (C-11). This is the whole of the batching-independence
|
|
@@ -276,7 +329,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
276
329
|
if (action.type === 'textDelta' || action.type === 'thinkingDelta') {
|
|
277
330
|
pending.current.push(action);
|
|
278
331
|
if (!flushTimer.current) {
|
|
279
|
-
|
|
332
|
+
// READ FROM THE REF AT ARM TIME (L4). This effect's dependency list
|
|
333
|
+
// never changes by construction, so a captured number would be
|
|
334
|
+
// pinned to the mount render and the ladder would move nothing.
|
|
335
|
+
flushTimer.current = setTimeout(() => flushPending(), governorInterval.current);
|
|
280
336
|
}
|
|
281
337
|
}
|
|
282
338
|
else {
|
|
@@ -375,7 +431,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
375
431
|
cancelFollowThrough();
|
|
376
432
|
unsubscribe();
|
|
377
433
|
};
|
|
378
|
-
}, [controller, flushPending, cancelPendingHuman, cancelFollowThrough]);
|
|
434
|
+
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough]);
|
|
379
435
|
// --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
|
|
380
436
|
//
|
|
381
437
|
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
|
|
@@ -409,16 +465,32 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
409
465
|
dispatch({ type: 'teamUpdate', snapshot });
|
|
410
466
|
break;
|
|
411
467
|
}
|
|
412
|
-
case 'usage':
|
|
468
|
+
case 'usage': {
|
|
413
469
|
// Child spend is REAL SPEND and must reach the status bar, or the
|
|
414
470
|
// session cost readout under-reports by however much the team consumed
|
|
415
471
|
// — the single most misleading possible failure of this feature (R-5).
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
472
|
+
//
|
|
473
|
+
// THE COST TABLE IS SELECTED BY TIER (fast-model-tier §3.6 / R-7).
|
|
474
|
+
// Always using the lead's would over-report a Haiku child under a
|
|
475
|
+
// Sonnet lead by roughly an order of magnitude, which is a different
|
|
476
|
+
// way of being wrong about the same number.
|
|
477
|
+
// FROM THE CONTROLLER, NOT FROM `cfg` (the rule the `agent_end`
|
|
478
|
+
// handler above records): this effect's dependency list never changes,
|
|
479
|
+
// so it closes over the `cfg` object of the render it mounted in, and
|
|
480
|
+
// `setModel` builds a NEW one.
|
|
481
|
+
const live = controller.getConfig();
|
|
482
|
+
const fastTier = event.tier === 'fast' ? controller.getFastStatus().tier : null;
|
|
483
|
+
const ref = fastTier && fastTier.ok
|
|
484
|
+
? fastTier.ref
|
|
485
|
+
: { providerId: live.provider, modelId: live.model };
|
|
486
|
+
// Unknown pricing contributes ZERO rather than a fabricated figure
|
|
487
|
+
// (C-11 / RV-4); `/fast status` and the dispatch report say `unknown`.
|
|
488
|
+
const costDelta = controller.isPricedModel(ref)
|
|
489
|
+
? computeCost(event.usage, controller.getModelInfoFor(ref).cost)
|
|
490
|
+
: 0;
|
|
491
|
+
dispatch({ type: 'teamUsage', usage: event.usage, costDelta });
|
|
421
492
|
break;
|
|
493
|
+
}
|
|
422
494
|
case 'dispatch_end':
|
|
423
495
|
dispatch({ type: 'teamEnd', outcome: event.outcome });
|
|
424
496
|
break;
|
|
@@ -460,6 +532,83 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
460
532
|
}
|
|
461
533
|
});
|
|
462
534
|
}, [controller]);
|
|
535
|
+
// --- Fast-tier event stream (fast-model-tier §5.3). --------------------
|
|
536
|
+
//
|
|
537
|
+
// A FOURTH SUBSCRIPTION, for the reason the team and todo ones record:
|
|
538
|
+
// `FastEvent` is deliberately not a member of core's `AgentEvent` union
|
|
539
|
+
// (D-10), so it cannot arrive through `controller.subscribe`.
|
|
540
|
+
//
|
|
541
|
+
// `subscribeFast` returns a no-op unsubscribe when the tier was off at
|
|
542
|
+
// construction, so this can subscribe unconditionally and an ordinary session
|
|
543
|
+
// pays one closure for the whole run.
|
|
544
|
+
useEffect(() => {
|
|
545
|
+
// Seed the chip from the tier resolved at construction, so a fast-enabled
|
|
546
|
+
// session shows it on the first frame rather than after the first review.
|
|
547
|
+
const initial = controller.getFastStatus();
|
|
548
|
+
if (initial.registered)
|
|
549
|
+
dispatch({ type: 'fastTier', snapshot: initial.snapshot });
|
|
550
|
+
return controller.subscribeFast((event) => {
|
|
551
|
+
switch (event.type) {
|
|
552
|
+
case 'review_start':
|
|
553
|
+
dispatch({
|
|
554
|
+
type: 'fastStart',
|
|
555
|
+
index: event.index,
|
|
556
|
+
turn: event.turn,
|
|
557
|
+
model: controller.getFastStatus().snapshot.model,
|
|
558
|
+
});
|
|
559
|
+
break;
|
|
560
|
+
case 'review_end':
|
|
561
|
+
dispatch({ type: 'fastEnd', review: event.review });
|
|
562
|
+
// The card settled, so the chip's `inFlight` and the session totals
|
|
563
|
+
// both moved. One dispatch rather than two: `fastTier` carries them.
|
|
564
|
+
dispatch({ type: 'fastTier', snapshot: controller.getFastStatus().snapshot });
|
|
565
|
+
break;
|
|
566
|
+
case 'usage': {
|
|
567
|
+
// Review spend is REAL SPEND at the FAST model's price (§3.6 / AC-18).
|
|
568
|
+
const ref = controller.getFastStatus().tier;
|
|
569
|
+
const costDelta = ref.ok && controller.isPricedModel(ref.ref)
|
|
570
|
+
? computeCost(event.usage, controller.getModelInfoFor(ref.ref).cost)
|
|
571
|
+
: 0;
|
|
572
|
+
dispatch({ type: 'fastUsage', usage: event.usage, costDelta });
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
case 'tier_changed':
|
|
576
|
+
dispatch({ type: 'fastTier', snapshot: event.snapshot });
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
}, [controller]);
|
|
581
|
+
// --- Live tool output (agent-activity-presentation-live §3.3.2). --------
|
|
582
|
+
//
|
|
583
|
+
// A FIFTH SUBSCRIPTION, and THE ONE THAT DOES GO THROUGH THE STREAMING
|
|
584
|
+
// COALESCER (D-34 / P0-2). The three above each carry a discrete act at human
|
|
585
|
+
// frequency — a roster change, a `todo_write`, a review verdict — and each has
|
|
586
|
+
// a comment refusing the buffer for that reason. This one carries a STREAM at
|
|
587
|
+
// pipe speed: the producer is a child process's stdout, so dispatching per
|
|
588
|
+
// chunk would be one React commit per `read()`.
|
|
589
|
+
//
|
|
590
|
+
// THE EFFECT PUSHES INTO `pending.current` AND ARMS `flushTimer` ITSELF.
|
|
591
|
+
// There is no "buffered path for every other action" to fall into —
|
|
592
|
+
// `App.tsx:436-447` buffers exactly `textDelta` / `thinkingDelta` and
|
|
593
|
+
// dispatches everything else immediately — so writing this the way its three
|
|
594
|
+
// siblings are written leaves `mergeDeltas`'s new clause dead code and R-1
|
|
595
|
+
// with one bound instead of two. AC-38 is the pin, mutation-checked against
|
|
596
|
+
// the direct-dispatch form.
|
|
597
|
+
//
|
|
598
|
+
// `Date.now()` IS READ HERE, in the listener, not in the reducer, which is
|
|
599
|
+
// what keeps `viewReducer` pure.
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
return controller.subscribeToolOutput(({ toolCallId, rows }) => {
|
|
602
|
+
pending.current.push({ type: 'toolOutputDelta', toolCallId, rows, at: Date.now() });
|
|
603
|
+
if (!flushTimer.current) {
|
|
604
|
+
// READ FROM THE REF AT ARM TIME, for the reason `App.tsx:439-441`
|
|
605
|
+
// records: this effect's dependency list never changes, so a captured
|
|
606
|
+
// number would be pinned to the mount render and the governor's ladder
|
|
607
|
+
// would move nothing.
|
|
608
|
+
flushTimer.current = setTimeout(() => flushPending(), governorInterval.current);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}, [controller, flushPending, governorInterval]);
|
|
463
612
|
// --- Elapsed timer + tokens/sec baseline while running. ----------------
|
|
464
613
|
useEffect(() => {
|
|
465
614
|
if (state.status !== 'running') {
|
|
@@ -472,6 +621,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
472
621
|
const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 200);
|
|
473
622
|
return () => clearInterval(id);
|
|
474
623
|
}, [state.status]);
|
|
624
|
+
// --- Retry countdown tick (llm-api-retry-backoff §6.5). ----------------
|
|
625
|
+
//
|
|
626
|
+
// ONLY WHILE A BACKOFF IS ACTUALLY WAITING, and the effect's teardown clears it —
|
|
627
|
+
// so an idle session, and even a session that is retrying but has already fired
|
|
628
|
+
// its next attempt, has NO timer at all. 1 Hz rather than the elapsed timer's
|
|
629
|
+
// 200 ms because the card shows whole seconds and a five-times-finer tick would
|
|
630
|
+
// buy four identical frames.
|
|
631
|
+
useEffect(() => {
|
|
632
|
+
if (state.retry?.phase !== 'waiting')
|
|
633
|
+
return undefined;
|
|
634
|
+
const id = setInterval(() => dispatch({ type: 'retryTick' }), 1000);
|
|
635
|
+
return () => clearInterval(id);
|
|
636
|
+
}, [state.retry?.phase]);
|
|
475
637
|
// --- Toast auto-dismiss (single effect keyed on the toast list). -------
|
|
476
638
|
useEffect(() => {
|
|
477
639
|
for (const t of state.toasts) {
|
|
@@ -510,7 +672,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
510
672
|
// `state.json`, not `config.json`: whether a one-shot notice has been shown
|
|
511
673
|
// is bookkeeping, and the comment on the old persisted key said so.
|
|
512
674
|
onSeen: () => setMouseNoticeSeen(true),
|
|
513
|
-
}
|
|
675
|
+
},
|
|
676
|
+
// Present only when `loadConfig` had to raise the user's own
|
|
677
|
+
// `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
|
|
678
|
+
cfg.transcriptRetainRequested !== undefined
|
|
679
|
+
? { requested: cfg.transcriptRetainRequested, resolved: cfg.transcriptRetain }
|
|
680
|
+
: undefined);
|
|
514
681
|
// --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
|
|
515
682
|
useEffect(() => {
|
|
516
683
|
publishExitSnapshot({
|
|
@@ -519,8 +686,30 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
519
686
|
provider: cfg.provider,
|
|
520
687
|
model: cfg.model,
|
|
521
688
|
startedAt: startedAt.current,
|
|
689
|
+
// Carried so the exit replay can say what the retain ring removed. A
|
|
690
|
+
// transcript that silently omits entries is worse than one that names the
|
|
691
|
+
// count (K-6).
|
|
692
|
+
droppedEntries: state.droppedEntries,
|
|
693
|
+
});
|
|
694
|
+
}, [state.entries, state.usageTotal, state.droppedEntries, cfg.provider, cfg.model]);
|
|
695
|
+
// --- `/perf` snapshot channel (tui-render-performance §5.4). ------------
|
|
696
|
+
//
|
|
697
|
+
// The deps are the two `useCallback`d members, NOT the handle objects: both
|
|
698
|
+
// hooks return a fresh literal per render, so depending on them would re-run
|
|
699
|
+
// this effect on every frame.
|
|
700
|
+
const governorReset = governor.reset;
|
|
701
|
+
const heightsClear = heights.clear;
|
|
702
|
+
useEffect(() => {
|
|
703
|
+
setPerfSnapshotProvider(() => perfRef.current);
|
|
704
|
+
setPerfResetHook(() => {
|
|
705
|
+
governorReset();
|
|
706
|
+
heightsClear();
|
|
522
707
|
});
|
|
523
|
-
|
|
708
|
+
return () => {
|
|
709
|
+
setPerfSnapshotProvider(null);
|
|
710
|
+
setPerfResetHook(null);
|
|
711
|
+
};
|
|
712
|
+
}, [governorReset, heightsClear]);
|
|
524
713
|
// --- Confirm bridge (confirmTools mode + skill approvals). -------------
|
|
525
714
|
useEffect(() => {
|
|
526
715
|
if (!confirmBridge)
|
|
@@ -745,6 +934,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
745
934
|
controller.setModel(values.provider, values.model, values.baseUrl || undefined);
|
|
746
935
|
controller.setThinkingLevel(values.thinkingLevel);
|
|
747
936
|
controller.setMaxTokens(cap.applied);
|
|
937
|
+
// THE SETTER AND THE PERSIST ARE BOTH REQUIRED, the pair `/todo panel`
|
|
938
|
+
// documents: persisting alone reports success and changes nothing until the
|
|
939
|
+
// next launch. `thinkingVisible` lives in `ViewState`, not in the config the
|
|
940
|
+
// controller holds, so the live half is a dispatch rather than a setter —
|
|
941
|
+
// and it is a no-op when the row already agrees with the current state.
|
|
942
|
+
if ((values.showThinking === 'on') !== stateRef.current.thinkingVisible) {
|
|
943
|
+
dispatch({ type: 'toggleThinking' });
|
|
944
|
+
}
|
|
748
945
|
const key = values.apiKey.trim();
|
|
749
946
|
if (key.length > 0) {
|
|
750
947
|
// Registration site 4 (§4.4.3), and the one that matters most: this is how
|
|
@@ -759,6 +956,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
759
956
|
model: values.model,
|
|
760
957
|
baseUrl: values.baseUrl.trim() ? values.baseUrl.trim() : null,
|
|
761
958
|
thinkingLevel: values.thinkingLevel,
|
|
959
|
+
showThinking: values.showThinking === 'on',
|
|
960
|
+
// PERSIST ONLY, with no paired live setter — the store is allocated in
|
|
961
|
+
// `AgentController`'s constructor, so this row takes effect on the next
|
|
962
|
+
// launch. That is what manual row 8 asks for, and it is why the seed above
|
|
963
|
+
// reads `cfg` rather than `ViewState`.
|
|
964
|
+
liveToolOutput: values.liveToolOutput === 'on',
|
|
762
965
|
// Omitted entirely when the field was unusable, so the previous value
|
|
763
966
|
// survives instead of being overwritten with a guess.
|
|
764
967
|
...(cap.persist !== 'skip' ? { maxTokens: cap.persist } : {}),
|
|
@@ -769,6 +972,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
769
972
|
// merges it — a shallow merge here would reset `redactSecrets` every time
|
|
770
973
|
// someone touched the level.
|
|
771
974
|
patch.log = { level: values.logLevel };
|
|
975
|
+
// --- Fast tier (fast-model-tier §4.5) ---------------------------------
|
|
976
|
+
//
|
|
977
|
+
// THE SETTER AND THE PERSIST ARE BOTH REQUIRED, and this is the pair `/todo
|
|
978
|
+
// panel` documents (P1-2): `App` reads `controller.getConfig()` at render
|
|
979
|
+
// time, so persisting alone reports success and changes nothing until the
|
|
980
|
+
// next launch, while the setter alone forgets by morning.
|
|
981
|
+
//
|
|
982
|
+
// `setFastConfig` also re-resolves the tier, which matters HERE more than
|
|
983
|
+
// anywhere: this same Enter press may have changed the main provider, the
|
|
984
|
+
// main model or the API key, and `fast.provider: ''` inherits from all
|
|
985
|
+
// three (§3.2 rule 8 / RV-3).
|
|
986
|
+
const fastPatch = readFastSettings(values);
|
|
987
|
+
controller.setFastConfig(fastPatch);
|
|
988
|
+
patch.fast = fastPatch;
|
|
772
989
|
persistConfig(patch);
|
|
773
990
|
getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
|
|
774
991
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
@@ -806,26 +1023,24 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
806
1023
|
}
|
|
807
1024
|
resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
|
|
808
1025
|
};
|
|
809
|
-
// --- Mouse wheel
|
|
810
|
-
// hook (D-13); only the wiring it cannot own stays here.
|
|
811
|
-
// fine — the hook re-reads its options through a ref every
|
|
812
|
-
// `Math.max(0, …)` mirrors the keyboard branch below, and for the
|
|
813
|
-
// reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
1026
|
+
// --- Mouse wheel routing (wheel-scrolls-transcript-only §4.1). Table and
|
|
1027
|
+
// coalescer live in the hook (D-13); only the wiring it cannot own stays here.
|
|
1028
|
+
// Inline arrows are fine — the hook re-reads its options through a ref every
|
|
1029
|
+
// render. The `Math.max(0, …)` mirrors the keyboard branch below, and for the
|
|
1030
|
+
// same reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
814
1031
|
const { clearCoalescer } = useWheelRouting({
|
|
815
1032
|
mouseSource,
|
|
816
|
-
geometryRef,
|
|
817
|
-
geometryOverride,
|
|
818
1033
|
getOverlay: () => stateRef.current.overlay,
|
|
819
1034
|
onScroll: scrollBy,
|
|
820
1035
|
onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
|
|
821
|
-
onHistoryStep: (dir) => setHistoryIntent((prev) => ({ dir, nonce: (prev?.nonce ?? 0) + 1 })),
|
|
822
1036
|
});
|
|
823
1037
|
// A fresh overlay always opens at the top; carrying the previous overlay's
|
|
824
1038
|
// offset over would open Help mid-list for no reason the user can see.
|
|
825
1039
|
//
|
|
826
1040
|
// `clearCoalescer()` rides along here rather than adding a second subscriber
|
|
827
|
-
// to the same transition (§4.
|
|
828
|
-
// transcript scroll must not survive an overlay
|
|
1041
|
+
// to the same transition (wheel-scrolls-transcript-only §4.3): an
|
|
1042
|
+
// accumulated-but-unflushed transcript scroll must not survive an overlay
|
|
1043
|
+
// opening.
|
|
829
1044
|
useEffect(() => {
|
|
830
1045
|
setOverlayScroll(0);
|
|
831
1046
|
clearCoalescer();
|
|
@@ -933,7 +1148,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
933
1148
|
}
|
|
934
1149
|
}
|
|
935
1150
|
if (key.ctrl && (input === 't' || input === 'T')) {
|
|
1151
|
+
const revealing = !stateRef.current.thinkingVisible;
|
|
936
1152
|
dispatch({ type: 'toggleThinking' });
|
|
1153
|
+
// THE ACK IS MODE-AWARE FOR THE SAME REASON THE MARKER IS (D-16 / P0-2).
|
|
1154
|
+
// Inline mode cannot repaint what `<Static>` has already printed, so the
|
|
1155
|
+
// toast says what it DID rather than what the user hoped. The hide
|
|
1156
|
+
// direction needs no qualifier: hiding applies to everything drawn from
|
|
1157
|
+
// here on, and nothing is claimed about scrollback either way.
|
|
1158
|
+
if (!revealing)
|
|
1159
|
+
toast('info', 'Thinking hidden.');
|
|
1160
|
+
else if (fullscreen)
|
|
1161
|
+
toast('info', 'Thinking shown.');
|
|
1162
|
+
else
|
|
1163
|
+
toast('info', 'Thinking shown for new output.');
|
|
937
1164
|
return;
|
|
938
1165
|
}
|
|
939
1166
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
@@ -998,6 +1225,40 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
998
1225
|
const tokPerSec = state.status === 'running' && elapsedMs > 500
|
|
999
1226
|
? Math.max(0, Math.round((state.usageTotal.outputTokens - runBaselineOut.current) / (elapsedMs / 1000)))
|
|
1000
1227
|
: 0;
|
|
1228
|
+
// --- The activity line's seed (§3.2.3 / P1-7). --------------------------
|
|
1229
|
+
//
|
|
1230
|
+
// RENDER SCOPE. Without this the ref holds `0` on the first run (an arbitrary
|
|
1231
|
+
// word) and the PREVIOUS run's start on every run after, so every run opens on
|
|
1232
|
+
// a wrong phrase for one frame and then jumps — a flicker at the start of
|
|
1233
|
+
// every single turn, which is the one moment this feature exists to make feel
|
|
1234
|
+
// calm.
|
|
1235
|
+
const running = state.status === 'running';
|
|
1236
|
+
if (running && runStartedAt.current === 0)
|
|
1237
|
+
runStartedAt.current = Date.now();
|
|
1238
|
+
if (!running && runStartedAt.current !== 0)
|
|
1239
|
+
runStartedAt.current = 0;
|
|
1240
|
+
// --- The running tool (agent-activity-presentation-live §3.4 / L4). -----
|
|
1241
|
+
//
|
|
1242
|
+
// A REVERSE LOOP WITH AN EARLY EXIT, NOT `.filter(...).pop()` (P2-3).
|
|
1243
|
+
// `state.entries` is bounded by `transcriptRetain`, which DEFAULTS TO 1000,
|
|
1244
|
+
// and this runs in render scope at up to 30 fps — an allocating full scan
|
|
1245
|
+
// there is the exact cost `tui-render-performance` exists to remove. The
|
|
1246
|
+
// running tool, when there is one, is within a few entries of the end.
|
|
1247
|
+
let runningTool;
|
|
1248
|
+
if (running) {
|
|
1249
|
+
for (let i = state.entries.length - 1; i >= 0; i -= 1) {
|
|
1250
|
+
const e = state.entries[i];
|
|
1251
|
+
if (e.kind === 'tool' && e.status === 'running') {
|
|
1252
|
+
runningTool = e.name;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
// WALL-CLOCK SECONDS, and the 200 ms ticker above is what causes the render
|
|
1258
|
+
// that re-reads it — so no timer is added (D-36 / E-15). Seconds rather than
|
|
1259
|
+
// milliseconds because `ToolCard` is `React.memo`'d with the default
|
|
1260
|
+
// comparator; `Transcript` passes it on only to running tool entries.
|
|
1261
|
+
const nowSec = running ? Math.floor(Date.now() / 1000) : undefined;
|
|
1001
1262
|
// Row budget for the viewport and everything sized against it (§4.2a). The
|
|
1002
1263
|
// inline path has no fixed frame, hence no height to fit into: `Infinity`
|
|
1003
1264
|
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
@@ -1042,18 +1303,33 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1042
1303
|
model: cfg.model,
|
|
1043
1304
|
baseUrl: cfg.baseUrl ?? '',
|
|
1044
1305
|
thinkingLevel: cfg.thinkingLevel,
|
|
1306
|
+
// SEEDED AND READ BACK, both halves (§4.3): omitting either is the
|
|
1307
|
+
// documented silent half of adding a row — the field renders empty and
|
|
1308
|
+
// an untouched save writes a value the user never chose.
|
|
1309
|
+
//
|
|
1310
|
+
// FROM `ViewState`, NOT FROM `cfg`. `thinkingVisible` is session state
|
|
1311
|
+
// that `Ctrl+T` also writes, and there is no controller setter to keep
|
|
1312
|
+
// `cfg` in step — so seeding from the config would show the LAUNCH
|
|
1313
|
+
// value under a transcript that has been toggled since, and an
|
|
1314
|
+
// untouched save would then quietly toggle it back.
|
|
1315
|
+
showThinking: state.thinkingVisible ? 'on' : 'off',
|
|
1316
|
+
// FROM `cfg`, unlike its neighbour, and the difference is real: the
|
|
1317
|
+
// live tail is decided at construction (the store is allocated there
|
|
1318
|
+
// and there is no setter), so the config IS the current value and
|
|
1319
|
+
// nothing can have toggled it since launch.
|
|
1320
|
+
liveToolOutput: cfg.liveToolOutput ? 'on' : 'off',
|
|
1045
1321
|
// `auto`, not an empty string: an empty field reads as "unset", which
|
|
1046
1322
|
// is indistinguishable from AUTO and is not a state this can be in.
|
|
1047
1323
|
maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : 'auto',
|
|
1048
1324
|
apiKey: '',
|
|
1049
1325
|
logLevel: cfg.log.level,
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1326
|
+
// The four fast rows, seeded through the pure mapper so the screen and
|
|
1327
|
+
// the save handler cannot disagree about what `(inherit)` means.
|
|
1328
|
+
...fastSettingsFrom(cfg.fast),
|
|
1329
|
+
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave,
|
|
1330
|
+
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1331
|
+
// while the screen is open shows the new value on the next paint.
|
|
1332
|
+
retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
|
|
1057
1333
|
// A resize can drop below the usable floor. Do NOT leave the alt-screen —
|
|
1058
1334
|
// entering and leaving it on every drag is worse than a placeholder — and take
|
|
1059
1335
|
// the height from `frameHeight()` like everything else, because this is the
|
|
@@ -1071,13 +1347,39 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1071
1347
|
// the header the moment the first message lands.
|
|
1072
1348
|
const opener = empty ? (_jsx(SessionOpener, { variant: fullscreen ? pickOpenerVariant(viewportBudget, contentCols, caps) : 'none', version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: fullscreen ? viewportBudget : Number.POSITIVE_INFINITY, theme: theme, caps: caps })) : null;
|
|
1073
1349
|
const density = cfg.density;
|
|
1074
|
-
const
|
|
1350
|
+
const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
|
|
1351
|
+
// Assigned during render so `/perf` reads the CURRENT frame (see `perfRef`).
|
|
1352
|
+
// `mountedEntries` is the same number `render-budget.test.tsx` asserts (AC-9).
|
|
1353
|
+
perfRef.current = {
|
|
1354
|
+
rung: governor.rung,
|
|
1355
|
+
intervalMs: governor.intervalMs.current,
|
|
1356
|
+
lastCommitMs: governor.lastCommitMs(),
|
|
1357
|
+
governorEnabled: cfg.renderGovernor,
|
|
1358
|
+
totalEntries: state.entries.length,
|
|
1359
|
+
retain: cfg.transcriptRetain ?? DEFAULT_TRANSCRIPT_RETAIN,
|
|
1360
|
+
droppedEntries: state.droppedEntries,
|
|
1361
|
+
mountedEntries: mountedCount.current,
|
|
1362
|
+
heightsMeasured: heights.stats().measured,
|
|
1363
|
+
heightsEstimated: heights.stats().estimated,
|
|
1364
|
+
cols: contentCols,
|
|
1365
|
+
mode: fullscreen ? 'fullscreen' : 'inline',
|
|
1366
|
+
viewportRows: fullscreen ? viewportBudget : rows,
|
|
1367
|
+
offset: scrolledLines,
|
|
1368
|
+
};
|
|
1369
|
+
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
|
|
1075
1370
|
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
1076
1371
|
// rows; unbounded, it pushes the status bar past the frame height where
|
|
1077
1372
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
1078
1373
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
1079
|
-
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows,
|
|
1080
|
-
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols,
|
|
1374
|
+
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
1375
|
+
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: state.status === 'running', reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(BottomStatusRow, { mode: mode, toasts: state.toasts,
|
|
1376
|
+
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
1377
|
+
// the full-screen branch replaces the whole viewport with the overlay,
|
|
1378
|
+
// and a working line under a settings screen is noise attached to a
|
|
1379
|
+
// surface the run is not visible on. (Inline overlays are part of the
|
|
1380
|
+
// document flow, so there the suppression is conservative rather than
|
|
1381
|
+
// necessary — P2-5.)
|
|
1382
|
+
activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
|
|
1081
1383
|
? {
|
|
1082
1384
|
teamActive: {
|
|
1083
1385
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -1087,5 +1389,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1087
1389
|
}
|
|
1088
1390
|
: {}), ...(state.todos && !showRail
|
|
1089
1391
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
1392
|
+
: {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.retry
|
|
1393
|
+
? {
|
|
1394
|
+
retryActive: {
|
|
1395
|
+
attempt: state.retry.attempt,
|
|
1396
|
+
max: state.retry.maxRetries,
|
|
1397
|
+
// Recomputed at RENDER from the absolute `resumeAt`, which is
|
|
1398
|
+
// what keeps the chip and the transcript card showing the same
|
|
1399
|
+
// number without either of them owning a second clock.
|
|
1400
|
+
secondsLeft: secondsLeft(state.retry.resumeAt, Date.now()),
|
|
1401
|
+
},
|
|
1402
|
+
}
|
|
1090
1403
|
: {}) }) }));
|
|
1091
1404
|
}
|