@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
package/dist/ui/App.js
CHANGED
|
@@ -28,10 +28,21 @@ 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';
|
|
37
|
+
import { UpdateLine } from './UpdateLine.js';
|
|
38
|
+
// The updater's two PURE modules. `shouldRenderUpdateLine` decides the row's
|
|
39
|
+
// presence AT THE CALL SITE (C-15), and `UPDATE_LIMITS` owns the width
|
|
40
|
+
// threshold - both are values, and both are resolved here rather than inside
|
|
41
|
+
// `UpdateLine` so that component's only edge into `update/` stays erasable by
|
|
42
|
+
// tsc (cli-auto-update section 3.1 rule 1 / IF-1). Neither module opens a
|
|
43
|
+
// socket, spawns a process or creates a timer.
|
|
44
|
+
import { shouldRenderUpdateLine } from '../update/types.js';
|
|
45
|
+
import { UPDATE_LIMITS } from '../update/limits.js';
|
|
35
46
|
import { AppShell } from './layout/AppShell.js';
|
|
36
47
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
37
48
|
import { todoRailRows, todoRailWidth } from './layout/rail.js';
|
|
@@ -43,14 +54,13 @@ import { useRenderGovernor } from './use-render-governor.js';
|
|
|
43
54
|
import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
|
|
44
55
|
import { frameHeight, MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
45
56
|
import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
46
|
-
import { UNMEASURED_GEOMETRY } from './layout/regions.js';
|
|
47
57
|
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
48
58
|
import { installConsoleBridge } from './console-bridge.js';
|
|
49
59
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
50
60
|
import { OverlayFrame } from './layout/OverlayFrame.js';
|
|
51
61
|
import { helpRows } from './overlays/HelpOverlay.js';
|
|
52
62
|
import { ModelPicker } from './overlays/ModelPicker.js';
|
|
53
|
-
import { SettingsScreen } from './overlays/SettingsScreen.js';
|
|
63
|
+
import { SettingsScreen, fastSettingsFrom, readFastSettings, } from './overlays/SettingsScreen.js';
|
|
54
64
|
import { ConfirmDialog } from './overlays/ConfirmDialog.js';
|
|
55
65
|
import { QuestionOverlay } from './overlays/QuestionOverlay.js';
|
|
56
66
|
import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
|
|
@@ -93,7 +103,14 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
93
103
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
94
104
|
};
|
|
95
105
|
}
|
|
96
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The lazy `useReducer` initializer. Module scope, so the reference is stable
|
|
108
|
+
* across renders and React never re-runs it.
|
|
109
|
+
*/
|
|
110
|
+
function seedViewState(thinkingVisible) {
|
|
111
|
+
return initialViewState({ thinkingVisible });
|
|
112
|
+
}
|
|
113
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, updateBridge, }) {
|
|
97
114
|
const { exit } = useApp();
|
|
98
115
|
const { stdout } = useStdout();
|
|
99
116
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -110,9 +127,21 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
110
127
|
});
|
|
111
128
|
/** The transcript height cache, owned here and read by `TranscriptList` (L3). */
|
|
112
129
|
const heights = useHeightStore();
|
|
113
|
-
|
|
130
|
+
// SEEDED FROM CONFIG, not dispatched after mount (§3.1.1): a post-mount
|
|
131
|
+
// `toggleThinking` would let `/resume` flash a restored thinking block on
|
|
132
|
+
// frame 1. `seedViewState` is module-scope so the lazy initializer is a stable
|
|
133
|
+
// function reference.
|
|
134
|
+
const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
|
|
114
135
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
115
136
|
const [confirmState, setConfirmState] = useState(null);
|
|
137
|
+
/**
|
|
138
|
+
* The updater's latest snapshot, or `null` while there is no service
|
|
139
|
+
* (cli-auto-update section 6.1).
|
|
140
|
+
*
|
|
141
|
+
* `UpdateSnapshot` is a VALUE OBJECT the service replaces wholesale on every
|
|
142
|
+
* transition, so this state can use referential equality and never diff fields.
|
|
143
|
+
*/
|
|
144
|
+
const [updateSnapshot, setUpdateSnapshot] = useState(null);
|
|
116
145
|
/** The request the question / plan overlay is currently rendering. */
|
|
117
146
|
const [humanRequest, setHumanRequest] = useState(null);
|
|
118
147
|
// Seeded from the history file, not from `CliConfig`: the list is no longer
|
|
@@ -121,8 +150,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
121
150
|
const [promptHistory, setPromptHistory] = useState(() => loadPromptHistory());
|
|
122
151
|
// Scroll plumbing: intent goes down, one derived display number comes back.
|
|
123
152
|
const [scrollIntent, setScrollIntent] = useState();
|
|
124
|
-
/** One prompt-history step per wheel notch over the composer band (§4.7). */
|
|
125
|
-
const [historyIntent, setHistoryIntent] = useState();
|
|
126
153
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
127
154
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
128
155
|
// I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
|
|
@@ -155,12 +182,21 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
155
182
|
const ctrlCArmed = useRef(false);
|
|
156
183
|
const ctrlCTimer = useRef(null);
|
|
157
184
|
const runBaselineOut = useRef(0);
|
|
185
|
+
/**
|
|
186
|
+
* Epoch ms the current run began — the activity phrase's seed.
|
|
187
|
+
*
|
|
188
|
+
* ASSIGNED IN RENDER SCOPE, NOT IN THE ELAPSED EFFECT (P1-7). That effect runs
|
|
189
|
+
* AFTER the render that first sees `status === 'running'`, so a ref it wrote
|
|
190
|
+
* would be one frame late — and the first frame of a run is exactly when the
|
|
191
|
+
* phrase is chosen. `Transcript.tsx` updates `highWater` / `prevLen` during
|
|
192
|
+
* render for the same reason: a value derived from a transition is needed by
|
|
193
|
+
* the very render that observes the transition.
|
|
194
|
+
*/
|
|
195
|
+
const runStartedAt = useRef(0);
|
|
158
196
|
const toastTimers = useRef(new Map());
|
|
159
197
|
/** Every outstanding `gate.request()`. Held in a ref, never in state: it is
|
|
160
198
|
* bookkeeping for promises, and re-rendering on it would be noise. */
|
|
161
199
|
const pendingHuman = useRef(new Set());
|
|
162
|
-
/** Written by `AppShell`'s layout effect, read at wheel-event time (§4.5). */
|
|
163
|
-
const geometryRef = useRef(UNMEASURED_GEOMETRY);
|
|
164
200
|
/**
|
|
165
201
|
* What `/perf` reports. Assigned during render, exactly like `stateRef` above,
|
|
166
202
|
* because a slash command runs outside the render pass and a value captured in
|
|
@@ -181,6 +217,15 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
181
217
|
// `ViewState` is a field `restoreEntries`, `/clear`, `/reset` and the session
|
|
182
218
|
// round-trip each have to have an opinion about, and none of them has an
|
|
183
219
|
// opinion about a 3-second timer.
|
|
220
|
+
/**
|
|
221
|
+
* Whether the one `installing -> ready` toast has already been pushed
|
|
222
|
+
* (cli-auto-update section 6.3).
|
|
223
|
+
*
|
|
224
|
+
* A REF AND NOT STATE: this must survive every re-render without causing one,
|
|
225
|
+
* and it is the whole of AC-19 - ten re-renders while the phase sits at
|
|
226
|
+
* `ready` must still produce exactly one toast.
|
|
227
|
+
*/
|
|
228
|
+
const updateToastPushed = useRef(false);
|
|
184
229
|
const budgetRef = useRef(emptyBudget());
|
|
185
230
|
const followTimer = useRef(null);
|
|
186
231
|
/** True while the run in flight was started by the grace timer, not the user. */
|
|
@@ -289,10 +334,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
289
334
|
for (const action of merged)
|
|
290
335
|
dispatch(action);
|
|
291
336
|
}, []);
|
|
337
|
+
/**
|
|
338
|
+
* The CLI-local diff side channel (§3.3.7). Memoized once, beside the `cost`
|
|
339
|
+
* lookup below, because `reduceEvent` TAKES a patch rather than reading one:
|
|
340
|
+
* the store is consume-once, so this object must not be rebuilt per event.
|
|
341
|
+
*/
|
|
342
|
+
const patchSource = useMemo(() => ({ take: (id) => controller.takeFilePatch(id) }), [controller]);
|
|
292
343
|
useEffect(() => {
|
|
293
344
|
const unsubscribe = controller.subscribe((event) => {
|
|
294
345
|
const cost = controller.getModelInfo().cost;
|
|
295
|
-
for (const action of reduceEvent(event, cost)) {
|
|
346
|
+
for (const action of reduceEvent(event, cost, patchSource)) {
|
|
296
347
|
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
297
348
|
// `ViewState.errorNoticed`, rather than read back from the reducer one
|
|
298
349
|
// commit later (C-11). This is the whole of the batching-independence
|
|
@@ -406,7 +457,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
406
457
|
cancelFollowThrough();
|
|
407
458
|
unsubscribe();
|
|
408
459
|
};
|
|
409
|
-
}, [controller, flushPending, cancelPendingHuman, cancelFollowThrough]);
|
|
460
|
+
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough]);
|
|
410
461
|
// --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
|
|
411
462
|
//
|
|
412
463
|
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
|
|
@@ -440,16 +491,32 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
440
491
|
dispatch({ type: 'teamUpdate', snapshot });
|
|
441
492
|
break;
|
|
442
493
|
}
|
|
443
|
-
case 'usage':
|
|
494
|
+
case 'usage': {
|
|
444
495
|
// Child spend is REAL SPEND and must reach the status bar, or the
|
|
445
496
|
// session cost readout under-reports by however much the team consumed
|
|
446
497
|
// — the single most misleading possible failure of this feature (R-5).
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
498
|
+
//
|
|
499
|
+
// THE COST TABLE IS SELECTED BY TIER (fast-model-tier §3.6 / R-7).
|
|
500
|
+
// Always using the lead's would over-report a Haiku child under a
|
|
501
|
+
// Sonnet lead by roughly an order of magnitude, which is a different
|
|
502
|
+
// way of being wrong about the same number.
|
|
503
|
+
// FROM THE CONTROLLER, NOT FROM `cfg` (the rule the `agent_end`
|
|
504
|
+
// handler above records): this effect's dependency list never changes,
|
|
505
|
+
// so it closes over the `cfg` object of the render it mounted in, and
|
|
506
|
+
// `setModel` builds a NEW one.
|
|
507
|
+
const live = controller.getConfig();
|
|
508
|
+
const fastTier = event.tier === 'fast' ? controller.getFastStatus().tier : null;
|
|
509
|
+
const ref = fastTier && fastTier.ok
|
|
510
|
+
? fastTier.ref
|
|
511
|
+
: { providerId: live.provider, modelId: live.model };
|
|
512
|
+
// Unknown pricing contributes ZERO rather than a fabricated figure
|
|
513
|
+
// (C-11 / RV-4); `/fast status` and the dispatch report say `unknown`.
|
|
514
|
+
const costDelta = controller.isPricedModel(ref)
|
|
515
|
+
? computeCost(event.usage, controller.getModelInfoFor(ref).cost)
|
|
516
|
+
: 0;
|
|
517
|
+
dispatch({ type: 'teamUsage', usage: event.usage, costDelta });
|
|
452
518
|
break;
|
|
519
|
+
}
|
|
453
520
|
case 'dispatch_end':
|
|
454
521
|
dispatch({ type: 'teamEnd', outcome: event.outcome });
|
|
455
522
|
break;
|
|
@@ -491,6 +558,83 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
491
558
|
}
|
|
492
559
|
});
|
|
493
560
|
}, [controller]);
|
|
561
|
+
// --- Fast-tier event stream (fast-model-tier §5.3). --------------------
|
|
562
|
+
//
|
|
563
|
+
// A FOURTH SUBSCRIPTION, for the reason the team and todo ones record:
|
|
564
|
+
// `FastEvent` is deliberately not a member of core's `AgentEvent` union
|
|
565
|
+
// (D-10), so it cannot arrive through `controller.subscribe`.
|
|
566
|
+
//
|
|
567
|
+
// `subscribeFast` returns a no-op unsubscribe when the tier was off at
|
|
568
|
+
// construction, so this can subscribe unconditionally and an ordinary session
|
|
569
|
+
// pays one closure for the whole run.
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
// Seed the chip from the tier resolved at construction, so a fast-enabled
|
|
572
|
+
// session shows it on the first frame rather than after the first review.
|
|
573
|
+
const initial = controller.getFastStatus();
|
|
574
|
+
if (initial.registered)
|
|
575
|
+
dispatch({ type: 'fastTier', snapshot: initial.snapshot });
|
|
576
|
+
return controller.subscribeFast((event) => {
|
|
577
|
+
switch (event.type) {
|
|
578
|
+
case 'review_start':
|
|
579
|
+
dispatch({
|
|
580
|
+
type: 'fastStart',
|
|
581
|
+
index: event.index,
|
|
582
|
+
turn: event.turn,
|
|
583
|
+
model: controller.getFastStatus().snapshot.model,
|
|
584
|
+
});
|
|
585
|
+
break;
|
|
586
|
+
case 'review_end':
|
|
587
|
+
dispatch({ type: 'fastEnd', review: event.review });
|
|
588
|
+
// The card settled, so the chip's `inFlight` and the session totals
|
|
589
|
+
// both moved. One dispatch rather than two: `fastTier` carries them.
|
|
590
|
+
dispatch({ type: 'fastTier', snapshot: controller.getFastStatus().snapshot });
|
|
591
|
+
break;
|
|
592
|
+
case 'usage': {
|
|
593
|
+
// Review spend is REAL SPEND at the FAST model's price (§3.6 / AC-18).
|
|
594
|
+
const ref = controller.getFastStatus().tier;
|
|
595
|
+
const costDelta = ref.ok && controller.isPricedModel(ref.ref)
|
|
596
|
+
? computeCost(event.usage, controller.getModelInfoFor(ref.ref).cost)
|
|
597
|
+
: 0;
|
|
598
|
+
dispatch({ type: 'fastUsage', usage: event.usage, costDelta });
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
case 'tier_changed':
|
|
602
|
+
dispatch({ type: 'fastTier', snapshot: event.snapshot });
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}, [controller]);
|
|
607
|
+
// --- Live tool output (agent-activity-presentation-live §3.3.2). --------
|
|
608
|
+
//
|
|
609
|
+
// A FIFTH SUBSCRIPTION, and THE ONE THAT DOES GO THROUGH THE STREAMING
|
|
610
|
+
// COALESCER (D-34 / P0-2). The three above each carry a discrete act at human
|
|
611
|
+
// frequency — a roster change, a `todo_write`, a review verdict — and each has
|
|
612
|
+
// a comment refusing the buffer for that reason. This one carries a STREAM at
|
|
613
|
+
// pipe speed: the producer is a child process's stdout, so dispatching per
|
|
614
|
+
// chunk would be one React commit per `read()`.
|
|
615
|
+
//
|
|
616
|
+
// THE EFFECT PUSHES INTO `pending.current` AND ARMS `flushTimer` ITSELF.
|
|
617
|
+
// There is no "buffered path for every other action" to fall into —
|
|
618
|
+
// `App.tsx:436-447` buffers exactly `textDelta` / `thinkingDelta` and
|
|
619
|
+
// dispatches everything else immediately — so writing this the way its three
|
|
620
|
+
// siblings are written leaves `mergeDeltas`'s new clause dead code and R-1
|
|
621
|
+
// with one bound instead of two. AC-38 is the pin, mutation-checked against
|
|
622
|
+
// the direct-dispatch form.
|
|
623
|
+
//
|
|
624
|
+
// `Date.now()` IS READ HERE, in the listener, not in the reducer, which is
|
|
625
|
+
// what keeps `viewReducer` pure.
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
return controller.subscribeToolOutput(({ toolCallId, rows }) => {
|
|
628
|
+
pending.current.push({ type: 'toolOutputDelta', toolCallId, rows, at: Date.now() });
|
|
629
|
+
if (!flushTimer.current) {
|
|
630
|
+
// READ FROM THE REF AT ARM TIME, for the reason `App.tsx:439-441`
|
|
631
|
+
// records: this effect's dependency list never changes, so a captured
|
|
632
|
+
// number would be pinned to the mount render and the governor's ladder
|
|
633
|
+
// would move nothing.
|
|
634
|
+
flushTimer.current = setTimeout(() => flushPending(), governorInterval.current);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}, [controller, flushPending, governorInterval]);
|
|
494
638
|
// --- Elapsed timer + tokens/sec baseline while running. ----------------
|
|
495
639
|
useEffect(() => {
|
|
496
640
|
if (state.status !== 'running') {
|
|
@@ -503,6 +647,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
503
647
|
const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 200);
|
|
504
648
|
return () => clearInterval(id);
|
|
505
649
|
}, [state.status]);
|
|
650
|
+
// --- Retry countdown tick (llm-api-retry-backoff §6.5). ----------------
|
|
651
|
+
//
|
|
652
|
+
// ONLY WHILE A BACKOFF IS ACTUALLY WAITING, and the effect's teardown clears it —
|
|
653
|
+
// so an idle session, and even a session that is retrying but has already fired
|
|
654
|
+
// its next attempt, has NO timer at all. 1 Hz rather than the elapsed timer's
|
|
655
|
+
// 200 ms because the card shows whole seconds and a five-times-finer tick would
|
|
656
|
+
// buy four identical frames.
|
|
657
|
+
useEffect(() => {
|
|
658
|
+
if (state.retry?.phase !== 'waiting')
|
|
659
|
+
return undefined;
|
|
660
|
+
const id = setInterval(() => dispatch({ type: 'retryTick' }), 1000);
|
|
661
|
+
return () => clearInterval(id);
|
|
662
|
+
}, [state.retry?.phase]);
|
|
506
663
|
// --- Toast auto-dismiss (single effect keyed on the toast list). -------
|
|
507
664
|
useEffect(() => {
|
|
508
665
|
for (const t of state.toasts) {
|
|
@@ -670,6 +827,48 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
670
827
|
// --- Helpers -----------------------------------------------------------
|
|
671
828
|
const notify = (level, text) => dispatch({ type: 'notice', level, text });
|
|
672
829
|
const toast = (level, text) => dispatch({ type: 'pushToast', level, text });
|
|
830
|
+
// --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
|
|
831
|
+
//
|
|
832
|
+
// TWO ARRIVAL ORDERS, AND BOTH HAPPEN. The service is built by a dynamic
|
|
833
|
+
// `import()` fired after `render()`, so on a warm module cache it can be
|
|
834
|
+
// attached BEFORE this effect runs, and on a cold one long after. Reading
|
|
835
|
+
// `bridge.service` first and installing `onAttach` second covers both without
|
|
836
|
+
// a race - the same shape `confirmBridge` uses one screen up.
|
|
837
|
+
useEffect(() => {
|
|
838
|
+
if (!updateBridge)
|
|
839
|
+
return undefined;
|
|
840
|
+
const bridge = updateBridge;
|
|
841
|
+
let unsubscribe = null;
|
|
842
|
+
const attach = (service) => {
|
|
843
|
+
setUpdateSnapshot(service.snapshot());
|
|
844
|
+
unsubscribe = service.subscribe(setUpdateSnapshot);
|
|
845
|
+
};
|
|
846
|
+
if (bridge.service)
|
|
847
|
+
attach(bridge.service);
|
|
848
|
+
else
|
|
849
|
+
bridge.onAttach = attach;
|
|
850
|
+
return () => {
|
|
851
|
+
bridge.onAttach = null;
|
|
852
|
+
unsubscribe?.();
|
|
853
|
+
};
|
|
854
|
+
}, [updateBridge]);
|
|
855
|
+
// ONE TOAST, ON THE `installing -> ready` EDGE ONLY (section 6.3 / AC-19).
|
|
856
|
+
//
|
|
857
|
+
// It exists because the moment of completion may arrive while the user is
|
|
858
|
+
// reading the transcript with the row occupied by a run: the toast wins the row
|
|
859
|
+
// for its TTL and then hands it back to the persistent line. The ref is what
|
|
860
|
+
// makes ten re-renders at `ready` push exactly one.
|
|
861
|
+
useEffect(() => {
|
|
862
|
+
if (updateSnapshot?.phase !== 'ready')
|
|
863
|
+
return;
|
|
864
|
+
if (updateToastPushed.current)
|
|
865
|
+
return;
|
|
866
|
+
updateToastPushed.current = true;
|
|
867
|
+
toast('info', `${updateSnapshot.latestVersion ?? 'Update'} installed - restart aragon to apply.`);
|
|
868
|
+
// `toast` is a stable dispatch wrapper; depending on it would re-run this on
|
|
869
|
+
// every render and defeat the ref guard's purpose rather than its mechanism.
|
|
870
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
871
|
+
}, [updateSnapshot?.phase, updateSnapshot?.latestVersion]);
|
|
673
872
|
const persistConfig = (patch) => {
|
|
674
873
|
try {
|
|
675
874
|
updatePersistedConfig(patch);
|
|
@@ -727,6 +926,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
727
926
|
// Read at command time, so `/todo status` reports the live count rather than
|
|
728
927
|
// whatever it was when this context was built.
|
|
729
928
|
followBudget: budgetRef.current,
|
|
929
|
+
// Read at command time for the same reason, and additionally because the
|
|
930
|
+
// service ATTACHES LATE (section 3.8): a value captured when this closure was
|
|
931
|
+
// first created would be `undefined` for the whole session on a cold module
|
|
932
|
+
// cache, and `/update` would report "disabled" on a session where it is on.
|
|
933
|
+
// `?? undefined` because the bridge holds `null` and the port is optional.
|
|
934
|
+
update: updateBridge?.service ?? undefined,
|
|
730
935
|
});
|
|
731
936
|
/**
|
|
732
937
|
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
@@ -803,6 +1008,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
803
1008
|
controller.setModel(values.provider, values.model, values.baseUrl || undefined);
|
|
804
1009
|
controller.setThinkingLevel(values.thinkingLevel);
|
|
805
1010
|
controller.setMaxTokens(cap.applied);
|
|
1011
|
+
// THE SETTER AND THE PERSIST ARE BOTH REQUIRED, the pair `/todo panel`
|
|
1012
|
+
// documents: persisting alone reports success and changes nothing until the
|
|
1013
|
+
// next launch. `thinkingVisible` lives in `ViewState`, not in the config the
|
|
1014
|
+
// controller holds, so the live half is a dispatch rather than a setter —
|
|
1015
|
+
// and it is a no-op when the row already agrees with the current state.
|
|
1016
|
+
if ((values.showThinking === 'on') !== stateRef.current.thinkingVisible) {
|
|
1017
|
+
dispatch({ type: 'toggleThinking' });
|
|
1018
|
+
}
|
|
806
1019
|
const key = values.apiKey.trim();
|
|
807
1020
|
if (key.length > 0) {
|
|
808
1021
|
// Registration site 4 (§4.4.3), and the one that matters most: this is how
|
|
@@ -817,6 +1030,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
817
1030
|
model: values.model,
|
|
818
1031
|
baseUrl: values.baseUrl.trim() ? values.baseUrl.trim() : null,
|
|
819
1032
|
thinkingLevel: values.thinkingLevel,
|
|
1033
|
+
showThinking: values.showThinking === 'on',
|
|
1034
|
+
// PERSIST ONLY, with no paired live setter — the store is allocated in
|
|
1035
|
+
// `AgentController`'s constructor, so this row takes effect on the next
|
|
1036
|
+
// launch. That is what manual row 8 asks for, and it is why the seed above
|
|
1037
|
+
// reads `cfg` rather than `ViewState`.
|
|
1038
|
+
liveToolOutput: values.liveToolOutput === 'on',
|
|
820
1039
|
// Omitted entirely when the field was unusable, so the previous value
|
|
821
1040
|
// survives instead of being overwritten with a guess.
|
|
822
1041
|
...(cap.persist !== 'skip' ? { maxTokens: cap.persist } : {}),
|
|
@@ -827,6 +1046,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
827
1046
|
// merges it — a shallow merge here would reset `redactSecrets` every time
|
|
828
1047
|
// someone touched the level.
|
|
829
1048
|
patch.log = { level: values.logLevel };
|
|
1049
|
+
// --- Fast tier (fast-model-tier §4.5) ---------------------------------
|
|
1050
|
+
//
|
|
1051
|
+
// THE SETTER AND THE PERSIST ARE BOTH REQUIRED, and this is the pair `/todo
|
|
1052
|
+
// panel` documents (P1-2): `App` reads `controller.getConfig()` at render
|
|
1053
|
+
// time, so persisting alone reports success and changes nothing until the
|
|
1054
|
+
// next launch, while the setter alone forgets by morning.
|
|
1055
|
+
//
|
|
1056
|
+
// `setFastConfig` also re-resolves the tier, which matters HERE more than
|
|
1057
|
+
// anywhere: this same Enter press may have changed the main provider, the
|
|
1058
|
+
// main model or the API key, and `fast.provider: ''` inherits from all
|
|
1059
|
+
// three (§3.2 rule 8 / RV-3).
|
|
1060
|
+
const fastPatch = readFastSettings(values);
|
|
1061
|
+
controller.setFastConfig(fastPatch);
|
|
1062
|
+
patch.fast = fastPatch;
|
|
830
1063
|
persistConfig(patch);
|
|
831
1064
|
getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
|
|
832
1065
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
@@ -864,26 +1097,24 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
864
1097
|
}
|
|
865
1098
|
resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
|
|
866
1099
|
};
|
|
867
|
-
// --- Mouse wheel
|
|
868
|
-
// hook (D-13); only the wiring it cannot own stays here.
|
|
869
|
-
// fine — the hook re-reads its options through a ref every
|
|
870
|
-
// `Math.max(0, …)` mirrors the keyboard branch below, and for the
|
|
871
|
-
// reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
1100
|
+
// --- Mouse wheel routing (wheel-scrolls-transcript-only §4.1). Table and
|
|
1101
|
+
// coalescer live in the hook (D-13); only the wiring it cannot own stays here.
|
|
1102
|
+
// Inline arrows are fine — the hook re-reads its options through a ref every
|
|
1103
|
+
// render. The `Math.max(0, …)` mirrors the keyboard branch below, and for the
|
|
1104
|
+
// same reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
872
1105
|
const { clearCoalescer } = useWheelRouting({
|
|
873
1106
|
mouseSource,
|
|
874
|
-
geometryRef,
|
|
875
|
-
geometryOverride,
|
|
876
1107
|
getOverlay: () => stateRef.current.overlay,
|
|
877
1108
|
onScroll: scrollBy,
|
|
878
1109
|
onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
|
|
879
|
-
onHistoryStep: (dir) => setHistoryIntent((prev) => ({ dir, nonce: (prev?.nonce ?? 0) + 1 })),
|
|
880
1110
|
});
|
|
881
1111
|
// A fresh overlay always opens at the top; carrying the previous overlay's
|
|
882
1112
|
// offset over would open Help mid-list for no reason the user can see.
|
|
883
1113
|
//
|
|
884
1114
|
// `clearCoalescer()` rides along here rather than adding a second subscriber
|
|
885
|
-
// to the same transition (§4.
|
|
886
|
-
// transcript scroll must not survive an overlay
|
|
1115
|
+
// to the same transition (wheel-scrolls-transcript-only §4.3): an
|
|
1116
|
+
// accumulated-but-unflushed transcript scroll must not survive an overlay
|
|
1117
|
+
// opening.
|
|
887
1118
|
useEffect(() => {
|
|
888
1119
|
setOverlayScroll(0);
|
|
889
1120
|
clearCoalescer();
|
|
@@ -991,7 +1222,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
991
1222
|
}
|
|
992
1223
|
}
|
|
993
1224
|
if (key.ctrl && (input === 't' || input === 'T')) {
|
|
1225
|
+
const revealing = !stateRef.current.thinkingVisible;
|
|
994
1226
|
dispatch({ type: 'toggleThinking' });
|
|
1227
|
+
// THE ACK IS MODE-AWARE FOR THE SAME REASON THE MARKER IS (D-16 / P0-2).
|
|
1228
|
+
// Inline mode cannot repaint what `<Static>` has already printed, so the
|
|
1229
|
+
// toast says what it DID rather than what the user hoped. The hide
|
|
1230
|
+
// direction needs no qualifier: hiding applies to everything drawn from
|
|
1231
|
+
// here on, and nothing is claimed about scrollback either way.
|
|
1232
|
+
if (!revealing)
|
|
1233
|
+
toast('info', 'Thinking hidden.');
|
|
1234
|
+
else if (fullscreen)
|
|
1235
|
+
toast('info', 'Thinking shown.');
|
|
1236
|
+
else
|
|
1237
|
+
toast('info', 'Thinking shown for new output.');
|
|
995
1238
|
return;
|
|
996
1239
|
}
|
|
997
1240
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
@@ -1056,6 +1299,40 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1056
1299
|
const tokPerSec = state.status === 'running' && elapsedMs > 500
|
|
1057
1300
|
? Math.max(0, Math.round((state.usageTotal.outputTokens - runBaselineOut.current) / (elapsedMs / 1000)))
|
|
1058
1301
|
: 0;
|
|
1302
|
+
// --- The activity line's seed (§3.2.3 / P1-7). --------------------------
|
|
1303
|
+
//
|
|
1304
|
+
// RENDER SCOPE. Without this the ref holds `0` on the first run (an arbitrary
|
|
1305
|
+
// word) and the PREVIOUS run's start on every run after, so every run opens on
|
|
1306
|
+
// a wrong phrase for one frame and then jumps — a flicker at the start of
|
|
1307
|
+
// every single turn, which is the one moment this feature exists to make feel
|
|
1308
|
+
// calm.
|
|
1309
|
+
const running = state.status === 'running';
|
|
1310
|
+
if (running && runStartedAt.current === 0)
|
|
1311
|
+
runStartedAt.current = Date.now();
|
|
1312
|
+
if (!running && runStartedAt.current !== 0)
|
|
1313
|
+
runStartedAt.current = 0;
|
|
1314
|
+
// --- The running tool (agent-activity-presentation-live §3.4 / L4). -----
|
|
1315
|
+
//
|
|
1316
|
+
// A REVERSE LOOP WITH AN EARLY EXIT, NOT `.filter(...).pop()` (P2-3).
|
|
1317
|
+
// `state.entries` is bounded by `transcriptRetain`, which DEFAULTS TO 1000,
|
|
1318
|
+
// and this runs in render scope at up to 30 fps — an allocating full scan
|
|
1319
|
+
// there is the exact cost `tui-render-performance` exists to remove. The
|
|
1320
|
+
// running tool, when there is one, is within a few entries of the end.
|
|
1321
|
+
let runningTool;
|
|
1322
|
+
if (running) {
|
|
1323
|
+
for (let i = state.entries.length - 1; i >= 0; i -= 1) {
|
|
1324
|
+
const e = state.entries[i];
|
|
1325
|
+
if (e.kind === 'tool' && e.status === 'running') {
|
|
1326
|
+
runningTool = e.name;
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
// WALL-CLOCK SECONDS, and the 200 ms ticker above is what causes the render
|
|
1332
|
+
// that re-reads it — so no timer is added (D-36 / E-15). Seconds rather than
|
|
1333
|
+
// milliseconds because `ToolCard` is `React.memo`'d with the default
|
|
1334
|
+
// comparator; `Transcript` passes it on only to running tool entries.
|
|
1335
|
+
const nowSec = running ? Math.floor(Date.now() / 1000) : undefined;
|
|
1059
1336
|
// Row budget for the viewport and everything sized against it (§4.2a). The
|
|
1060
1337
|
// inline path has no fixed frame, hence no height to fit into: `Infinity`
|
|
1061
1338
|
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
@@ -1100,18 +1377,33 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1100
1377
|
model: cfg.model,
|
|
1101
1378
|
baseUrl: cfg.baseUrl ?? '',
|
|
1102
1379
|
thinkingLevel: cfg.thinkingLevel,
|
|
1380
|
+
// SEEDED AND READ BACK, both halves (§4.3): omitting either is the
|
|
1381
|
+
// documented silent half of adding a row — the field renders empty and
|
|
1382
|
+
// an untouched save writes a value the user never chose.
|
|
1383
|
+
//
|
|
1384
|
+
// FROM `ViewState`, NOT FROM `cfg`. `thinkingVisible` is session state
|
|
1385
|
+
// that `Ctrl+T` also writes, and there is no controller setter to keep
|
|
1386
|
+
// `cfg` in step — so seeding from the config would show the LAUNCH
|
|
1387
|
+
// value under a transcript that has been toggled since, and an
|
|
1388
|
+
// untouched save would then quietly toggle it back.
|
|
1389
|
+
showThinking: state.thinkingVisible ? 'on' : 'off',
|
|
1390
|
+
// FROM `cfg`, unlike its neighbour, and the difference is real: the
|
|
1391
|
+
// live tail is decided at construction (the store is allocated there
|
|
1392
|
+
// and there is no setter), so the config IS the current value and
|
|
1393
|
+
// nothing can have toggled it since launch.
|
|
1394
|
+
liveToolOutput: cfg.liveToolOutput ? 'on' : 'off',
|
|
1103
1395
|
// `auto`, not an empty string: an empty field reads as "unset", which
|
|
1104
1396
|
// is indistinguishable from AUTO and is not a state this can be in.
|
|
1105
1397
|
maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : 'auto',
|
|
1106
1398
|
apiKey: '',
|
|
1107
1399
|
logLevel: cfg.log.level,
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1400
|
+
// The four fast rows, seeded through the pure mapper so the screen and
|
|
1401
|
+
// the save handler cannot disagree about what `(inherit)` means.
|
|
1402
|
+
...fastSettingsFrom(cfg.fast),
|
|
1403
|
+
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave,
|
|
1404
|
+
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1405
|
+
// while the screen is open shows the new value on the next paint.
|
|
1406
|
+
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;
|
|
1115
1407
|
// A resize can drop below the usable floor. Do NOT leave the alt-screen —
|
|
1116
1408
|
// entering and leaving it on every drag is worse than a placeholder — and take
|
|
1117
1409
|
// the height from `frameHeight()` like everything else, because this is the
|
|
@@ -1148,13 +1440,32 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1148
1440
|
viewportRows: fullscreen ? viewportBudget : rows,
|
|
1149
1441
|
offset: scrolledLines,
|
|
1150
1442
|
};
|
|
1151
|
-
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, 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, theme: theme, caps: caps }), overlayNode] }));
|
|
1443
|
+
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] }));
|
|
1152
1444
|
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
1153
1445
|
// rows; unbounded, it pushes the status bar past the frame height where
|
|
1154
1446
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
1155
1447
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
1156
|
-
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,
|
|
1157
|
-
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols,
|
|
1448
|
+
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' }) }) }));
|
|
1449
|
+
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,
|
|
1450
|
+
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
1451
|
+
// the full-screen branch replaces the whole viewport with the overlay,
|
|
1452
|
+
// and a working line under a settings screen is noise attached to a
|
|
1453
|
+
// surface the run is not visible on. (Inline overlays are part of the
|
|
1454
|
+
// document flow, so there the suppression is conservative rather than
|
|
1455
|
+
// necessary — P2-5.)
|
|
1456
|
+
activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null,
|
|
1457
|
+
// PRESENCE IS DECIDED HERE, NOT BY `UpdateLine` RETURNING `null`
|
|
1458
|
+
// (C-15 / cli-auto-update P0-1 / D-19). `BottomStatusRow` tests this
|
|
1459
|
+
// prop for truthiness, and a React element is truthy however it
|
|
1460
|
+
// renders - so a component that rendered nothing would give the row
|
|
1461
|
+
// ZERO rows and silently unbalance the frame budget. Same rule the
|
|
1462
|
+
// activity line above follows with `running && !overlayNode`.
|
|
1463
|
+
//
|
|
1464
|
+
// `!overlayNode` for the same reason it appears there: the full-screen
|
|
1465
|
+
// branch replaces the whole viewport with the overlay, and an update
|
|
1466
|
+
// notice under a settings screen is noise attached to a surface it is
|
|
1467
|
+
// not visible on.
|
|
1468
|
+
update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, 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
|
|
1158
1469
|
? {
|
|
1159
1470
|
teamActive: {
|
|
1160
1471
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -1164,5 +1475,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1164
1475
|
}
|
|
1165
1476
|
: {}), ...(state.todos && !showRail
|
|
1166
1477
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
1478
|
+
: {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.retry
|
|
1479
|
+
? {
|
|
1480
|
+
retryActive: {
|
|
1481
|
+
attempt: state.retry.attempt,
|
|
1482
|
+
max: state.retry.maxRetries,
|
|
1483
|
+
// Recomputed at RENDER from the absolute `resumeAt`, which is
|
|
1484
|
+
// what keeps the chip and the transcript card showing the same
|
|
1485
|
+
// number without either of them owning a second clock.
|
|
1486
|
+
secondsLeft: secondsLeft(state.retry.resumeAt, Date.now()),
|
|
1487
|
+
},
|
|
1488
|
+
}
|
|
1167
1489
|
: {}) }) }));
|
|
1168
1490
|
}
|