@aragon-agent/cli 0.6.2 → 0.6.4
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 +321 -0
- package/README.md +297 -2
- package/dist/agent/controller.d.ts +193 -1
- package/dist/agent/controller.js +475 -2
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +189 -0
- package/dist/agent/reducer.js +301 -2
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +297 -16
- package/dist/commands/builtins.js +165 -24
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +118 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +355 -0
- package/dist/compaction/compactor.d.ts +302 -0
- package/dist/compaction/compactor.js +810 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +215 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +264 -0
- package/dist/compaction/wiring.js +577 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +5 -2
- package/dist/config/env.js +140 -3
- package/dist/config/load.d.ts +42 -0
- package/dist/config/load.js +101 -1
- package/dist/config/schema.d.ts +244 -0
- package/dist/config/schema.js +170 -0
- package/dist/config/store.js +37 -1
- package/dist/config/ui-state.d.ts +28 -1
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/runner.js +42 -0
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +53 -1
- package/dist/logging/install.js +81 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +55 -1
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +62 -2
- package/dist/ui/App.js +546 -16
- package/dist/ui/Composer.d.ts +66 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +82 -3
- package/dist/ui/PromptInput.js +232 -49
- package/dist/ui/StatusBar.d.ts +59 -0
- package/dist/ui/StatusBar.js +16 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +61 -3
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +44 -9
- package/dist/ui/layout/budget.js +42 -10
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +85 -1
- package/dist/ui/overlays/HelpOverlay.js +9 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +74 -2
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +38 -0
- package/dist/ui/screen.js +113 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +72 -0
- package/dist/ui/use-startup-notices.d.ts +27 -1
- package/dist/ui/use-startup-notices.js +43 -11
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/dist/ui/use-wheel-routing.js +8 -0
- package/package.json +4 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -42
- package/dist/input/stdin-mouse-filter.js +0 -153
package/dist/ui/App.js
CHANGED
|
@@ -11,14 +11,16 @@ 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 { isApproximate } from '../compaction/pressure.js';
|
|
14
15
|
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
15
16
|
import { updatePersistedConfig } from '../config/store.js';
|
|
16
17
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
17
|
-
import { bumpSubmitCount,
|
|
18
|
+
import { bumpSubmitCount, setMouseNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
|
|
18
19
|
import { getLogger } from '../logging/logger.js';
|
|
20
|
+
import { isTerminalStatus } from '../proc/types.js';
|
|
19
21
|
import { registerSecret } from '../logging/secret-registry.js';
|
|
20
22
|
import { detectCapabilities } from './capabilities.js';
|
|
21
|
-
import { useStartupNotices, VT_INPUT_NOTICE_VERSION } from './use-startup-notices.js';
|
|
23
|
+
import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
|
|
22
24
|
import { getTheme } from './theme.js';
|
|
23
25
|
import { pickGlyphs } from './glyphs.js';
|
|
24
26
|
import { Header } from './Header.js';
|
|
@@ -60,7 +62,7 @@ import { publishExitSnapshot } from './exit-snapshot.js';
|
|
|
60
62
|
import { OverlayFrame } from './layout/OverlayFrame.js';
|
|
61
63
|
import { helpRows } from './overlays/HelpOverlay.js';
|
|
62
64
|
import { ModelPicker } from './overlays/ModelPicker.js';
|
|
63
|
-
import { SettingsScreen, fastSettingsFrom, readFastSettings, } from './overlays/SettingsScreen.js';
|
|
65
|
+
import { SettingsScreen, compactionSettingsFrom, fastSettingsFrom, readCompactionSettings, readFastSettings, } from './overlays/SettingsScreen.js';
|
|
64
66
|
import { ConfirmDialog } from './overlays/ConfirmDialog.js';
|
|
65
67
|
import { QuestionOverlay } from './overlays/QuestionOverlay.js';
|
|
66
68
|
import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
|
|
@@ -68,6 +70,7 @@ import { CommandRegistry, runSlashInput } from '../commands/registry.js';
|
|
|
68
70
|
import { registerBuiltinCommands } from '../commands/builtins.js';
|
|
69
71
|
import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
|
|
70
72
|
import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
|
|
73
|
+
import { emptyTailState } from './layout/follow-state.js';
|
|
71
74
|
/**
|
|
72
75
|
* Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
|
|
73
76
|
* discard the rest of the form.
|
|
@@ -103,6 +106,24 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
103
106
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
104
107
|
};
|
|
105
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* How long rung two of the Esc ladder stays armed after rung one (§5.5).
|
|
111
|
+
*
|
|
112
|
+
* Four seconds: long enough that a user who watched the abort fail can react,
|
|
113
|
+
* short enough that a stray Esc minutes later cannot force-stop a healthy run.
|
|
114
|
+
* `runEnd` / `agent_end` disarm it too, so the window is an upper bound rather
|
|
115
|
+
* than the only protection (R-9).
|
|
116
|
+
*/
|
|
117
|
+
const ESC_ARM_MS = 4000;
|
|
118
|
+
/** Services in a non-terminal state, from the live snapshot map. */
|
|
119
|
+
function liveServiceCount(services) {
|
|
120
|
+
let n = 0;
|
|
121
|
+
for (const service of Object.values(services)) {
|
|
122
|
+
if (!isTerminalStatus(service.status))
|
|
123
|
+
n += 1;
|
|
124
|
+
}
|
|
125
|
+
return n;
|
|
126
|
+
}
|
|
106
127
|
/**
|
|
107
128
|
* The lazy `useReducer` initializer. Module scope, so the reference is stable
|
|
108
129
|
* across renders and React never re-runs it.
|
|
@@ -110,7 +131,7 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
110
131
|
function seedViewState(thinkingVisible) {
|
|
111
132
|
return initialViewState({ thinkingVisible });
|
|
112
133
|
}
|
|
113
|
-
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, }) {
|
|
134
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, }) {
|
|
114
135
|
const { exit } = useApp();
|
|
115
136
|
const { stdout } = useStdout();
|
|
116
137
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -133,6 +154,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
133
154
|
// function reference.
|
|
134
155
|
const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
|
|
135
156
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
157
|
+
/**
|
|
158
|
+
* Forces the render that re-reads `Date.now()` for the live compaction card
|
|
159
|
+
* while the run is IDLE (hardening W5). The value itself is never read - the
|
|
160
|
+
* card's elapsed figure is derived from `nowSec` like the running tool card's.
|
|
161
|
+
*/
|
|
162
|
+
const [, setCompactionClock] = useState(0);
|
|
136
163
|
const [confirmState, setConfirmState] = useState(null);
|
|
137
164
|
/**
|
|
138
165
|
* The updater's latest snapshot, or `null` while there is no service
|
|
@@ -152,6 +179,15 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
152
179
|
const [scrollIntent, setScrollIntent] = useState();
|
|
153
180
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
154
181
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
182
|
+
/**
|
|
183
|
+
* True while a selection drag holds the viewport
|
|
184
|
+
* (tui-selection-and-scroll-follow §4.3.1 / §4.4).
|
|
185
|
+
*
|
|
186
|
+
* REACT STATE RATHER THAN A REF, because `ScrollViewport` has to re-arm its
|
|
187
|
+
* resume timer on both edges and a ref cannot drive an effect. The controller
|
|
188
|
+
* is the owner; this is a mirror, pushed by `onHoldChange`.
|
|
189
|
+
*/
|
|
190
|
+
const [selectionHold, setSelectionHold] = useState(false);
|
|
155
191
|
// I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
|
|
156
192
|
// identical output twice over — so it bumps this instead, and StatusBar turns
|
|
157
193
|
// it into a one-byte, zero-width change that both dedupe gates let through.
|
|
@@ -181,6 +217,37 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
181
217
|
confirmRef.current = confirmState;
|
|
182
218
|
const ctrlCArmed = useRef(false);
|
|
183
219
|
const ctrlCTimer = useRef(null);
|
|
220
|
+
/**
|
|
221
|
+
* Rung two of Esc is armed (background-service-supervision §3.6).
|
|
222
|
+
*
|
|
223
|
+
* CLEARED BY `runEnd` / `agent_end` AS WELL AS BY ITS OWN TIMER, and both
|
|
224
|
+
* halves matter: a healthy run that ended between the two presses must not be
|
|
225
|
+
* force-stoppable retroactively, and in every healthy run the second press
|
|
226
|
+
* lands on the idle branch and does nothing at all (R-9). Rung two only ever
|
|
227
|
+
* fires while the run is STILL running after rung one - i.e. only when the
|
|
228
|
+
* first press provably failed.
|
|
229
|
+
*/
|
|
230
|
+
const escArmed = useRef(false);
|
|
231
|
+
const escArmTimer = useRef(null);
|
|
232
|
+
/**
|
|
233
|
+
* Live service snapshots, keyed by id, for the status chip and the Ctrl+C rung.
|
|
234
|
+
*
|
|
235
|
+
* A `useState` MAP rather than a ref, because the chip has to re-render when it
|
|
236
|
+
* changes; and separate from `ViewState`, because a service is not a
|
|
237
|
+
* transcript entry - the transcript records EVENTS about a service (D-11) and
|
|
238
|
+
* this is the live state those events are about.
|
|
239
|
+
*/
|
|
240
|
+
const [services, setServices] = useState({});
|
|
241
|
+
/**
|
|
242
|
+
* Read by the Ctrl+C handler, which `useInput` registers ONCE.
|
|
243
|
+
*
|
|
244
|
+
* The same reason `stateRef` exists two lines up: a value captured in that
|
|
245
|
+
* closure would be pinned to the mount render, so the key handler would
|
|
246
|
+
* believe there are never any services and Ctrl+C would arm exit while a dev
|
|
247
|
+
* server was up.
|
|
248
|
+
*/
|
|
249
|
+
const servicesRef = useRef(services);
|
|
250
|
+
servicesRef.current = services;
|
|
184
251
|
const runBaselineOut = useRef(0);
|
|
185
252
|
/**
|
|
186
253
|
* Epoch ms the current run began — the activity phrase's seed.
|
|
@@ -211,6 +278,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
211
278
|
* would make reading the render budget cost a render.
|
|
212
279
|
*/
|
|
213
280
|
const mountedCount = useRef(0);
|
|
281
|
+
/**
|
|
282
|
+
* Rows the transcript has appended AT ITS TAIL — Rule A's only legal input
|
|
283
|
+
* (tui-selection-and-scroll-follow §4.3.1a).
|
|
284
|
+
*
|
|
285
|
+
* Owned here because it has TWO readers one level down each: `TranscriptList`
|
|
286
|
+
* writes it during its render, and `ScrollViewport` reads it in the layout
|
|
287
|
+
* effect that runs afterwards. A ref rather than state, exactly like
|
|
288
|
+
* `mountedCount` above and for the same reason.
|
|
289
|
+
*/
|
|
290
|
+
const tailSink = useRef(emptyTailState());
|
|
214
291
|
// --- Follow-through (todo-plan-followthrough §3.4). ---------------------
|
|
215
292
|
//
|
|
216
293
|
// THREE REFS AND NO VIEW STATE, DELIBERATELY (§5.2). Every field added to
|
|
@@ -255,6 +332,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
255
332
|
aborted: false,
|
|
256
333
|
errored: false,
|
|
257
334
|
});
|
|
335
|
+
/**
|
|
336
|
+
* Disarm rung two of the Esc ladder.
|
|
337
|
+
*
|
|
338
|
+
* A `useCallback` so the event subscription's dependency list can name it, and
|
|
339
|
+
* so it is one function rather than three copies of "clear the flag and the
|
|
340
|
+
* timer" that can drift.
|
|
341
|
+
*/
|
|
342
|
+
const disarmEsc = useCallback(() => {
|
|
343
|
+
escArmed.current = false;
|
|
344
|
+
if (escArmTimer.current) {
|
|
345
|
+
clearTimeout(escArmTimer.current);
|
|
346
|
+
escArmTimer.current = null;
|
|
347
|
+
}
|
|
348
|
+
}, []);
|
|
258
349
|
/**
|
|
259
350
|
* Disarm an armed continuation. Idempotent; NEVER touches the budget — a
|
|
260
351
|
* cancelled continuation was never issued, so it must not be charged (AC-18).
|
|
@@ -341,7 +432,28 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
341
432
|
*/
|
|
342
433
|
const patchSource = useMemo(() => ({ take: (id) => controller.takeFilePatch(id) }), [controller]);
|
|
343
434
|
useEffect(() => {
|
|
435
|
+
// CAPTURED AT SUBSCRIBE TIME AND COMPARED ON EVERY EVENT (I-7 / P1-7).
|
|
436
|
+
//
|
|
437
|
+
// `forceStop()` bumps the controller's generation, and everything the engine
|
|
438
|
+
// emits while it unwinds - `turn_start`, `tool_call_start`, `turn_end`,
|
|
439
|
+
// `agent_end` - belongs to the run the user just abandoned. `runStart` /
|
|
440
|
+
// `turnStart` would set `status: 'running'` again, bouncing the view straight
|
|
441
|
+
// back out of the `idle` rung two just gave them, and stray entries would
|
|
442
|
+
// append to a transcript they believe is finished.
|
|
443
|
+
//
|
|
444
|
+
// A LOCAL `let`, RE-READ RATHER THAN CAPTURED ONCE: the generation this
|
|
445
|
+
// listener considers current advances with every submit, so a force-stop
|
|
446
|
+
// makes stale events inert WITHOUT making the next run's events inert too.
|
|
447
|
+
let generation = controller.runGeneration;
|
|
344
448
|
const unsubscribe = controller.subscribe((event) => {
|
|
449
|
+
if (controller.runGeneration !== generation) {
|
|
450
|
+
// The engine is unwinding a force-stopped run. Adopt the new generation
|
|
451
|
+
// only once it has finished doing so, so a late `agent_end` cannot be
|
|
452
|
+
// mistaken for the next run's.
|
|
453
|
+
if (event.type === 'agent_end')
|
|
454
|
+
generation = controller.runGeneration;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
345
457
|
const cost = controller.getModelInfo().cost;
|
|
346
458
|
for (const action of reduceEvent(event, cost, patchSource)) {
|
|
347
459
|
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
@@ -368,6 +480,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
368
480
|
}
|
|
369
481
|
if (event.type !== 'agent_end')
|
|
370
482
|
return;
|
|
483
|
+
// RUNG TWO DISARMS WITH THE RUN (§3.6 / R-9). A healthy run that ended
|
|
484
|
+
// between the two Esc presses must not be force-stoppable retroactively:
|
|
485
|
+
// the second press then lands on the idle branch, which is what makes the
|
|
486
|
+
// ladder free to take rather than a footgun.
|
|
487
|
+
disarmEsc();
|
|
371
488
|
// NO OVERLAY OUTLIVES ITS RUN (P1-4). `ctx.signal` already covers the
|
|
372
489
|
// abort path, so this is DELIBERATE REDUNDANCY: it makes "the card cannot
|
|
373
490
|
// survive the run that opened it" a property of the App's lifecycle
|
|
@@ -457,7 +574,37 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
457
574
|
cancelFollowThrough();
|
|
458
575
|
unsubscribe();
|
|
459
576
|
};
|
|
460
|
-
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough]);
|
|
577
|
+
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough, disarmEsc]);
|
|
578
|
+
// --- Background services (background-service-supervision §3.8). ---------
|
|
579
|
+
//
|
|
580
|
+
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `ProcEvent` is
|
|
581
|
+
// deliberately not a member of core's `AgentEvent` union (I-5 / D-1), so it
|
|
582
|
+
// cannot arrive through `controller.subscribe`.
|
|
583
|
+
//
|
|
584
|
+
// TWO SINKS PER EVENT, and they are not redundant. `setServices` holds the
|
|
585
|
+
// LIVE state the status chip, the composer hint and the Ctrl+C rung read;
|
|
586
|
+
// `dispatch` writes the TRANSCRIPT, which records events about a service
|
|
587
|
+
// rather than mirroring it (D-11). Feeding one from the other would make the
|
|
588
|
+
// chip lie about a card that has already been printed into `<Static>`.
|
|
589
|
+
//
|
|
590
|
+
// NOT ROUTED THROUGH THE STREAMING COALESCER. `output` is already coalesced at
|
|
591
|
+
// the SOURCE, inside the supervisor, because `/bg` and `bash_output` consume
|
|
592
|
+
// the same event (§6); a second buffer here would only add latency.
|
|
593
|
+
useEffect(() => {
|
|
594
|
+
return controller.subscribeProc((event) => {
|
|
595
|
+
const service = event.service;
|
|
596
|
+
setServices((prev) => ({ ...prev, [service.id]: service }));
|
|
597
|
+
if (event.type === 'started') {
|
|
598
|
+
dispatch({ type: 'serviceStart', service });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (event.type === 'exited' || event.type === 'stopped') {
|
|
602
|
+
dispatch({ type: 'serviceEnd', service });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
dispatch({ type: 'serviceUpdate', service });
|
|
606
|
+
});
|
|
607
|
+
}, [controller]);
|
|
461
608
|
// --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
|
|
462
609
|
//
|
|
463
610
|
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
|
|
@@ -604,6 +751,97 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
604
751
|
}
|
|
605
752
|
});
|
|
606
753
|
}, [controller]);
|
|
754
|
+
// --- Compaction event stream (context-auto-compaction §5.2). -------------
|
|
755
|
+
//
|
|
756
|
+
// A SIXTH SUBSCRIPTION, for the reason the team, todo and fast ones record:
|
|
757
|
+
// `CompactionEvent` is deliberately not a member of core's `AgentEvent` union,
|
|
758
|
+
// so it cannot arrive through `controller.subscribe`.
|
|
759
|
+
//
|
|
760
|
+
// AND IT IS THE **SOLE** DISPATCHER OF THE FIVE COMPACTION ACTIONS (D-20 /
|
|
761
|
+
// P1-6). `reduceEvent` gains no cases for core's `compaction_start` /
|
|
762
|
+
// `compaction_end` — its `default: return []` is already correct for them —
|
|
763
|
+
// because one compaction arriving through two channels is two transcript cards
|
|
764
|
+
// and a doubled `tokensReclaimed`, with nothing naming the authority. This
|
|
765
|
+
// stream is the right authority because it is strictly richer: the wiring IS
|
|
766
|
+
// the `ContextManager`, so it knows the summarizer's model, its cost and the
|
|
767
|
+
// session totals, and it learns `applied` by subscribing to the core event one
|
|
768
|
+
// layer earlier.
|
|
769
|
+
//
|
|
770
|
+
// `subscribeCompaction` returns a no-op unsubscribe when compaction was off at
|
|
771
|
+
// construction, so this can subscribe unconditionally and an ordinary session
|
|
772
|
+
// pays one closure for the whole run.
|
|
773
|
+
useEffect(() => {
|
|
774
|
+
// Seed the chip and the gauge marks from the snapshot resolved at
|
|
775
|
+
// construction, so a compaction-enabled session shows them on the FIRST
|
|
776
|
+
// frame rather than after the first turn.
|
|
777
|
+
if (controller.isCompactionRegistered()) {
|
|
778
|
+
dispatch({ type: 'compactionSnapshot', snapshot: controller.getCompactionSnapshot() });
|
|
779
|
+
}
|
|
780
|
+
return controller.subscribeCompaction((event) => {
|
|
781
|
+
switch (event.type) {
|
|
782
|
+
case 'compaction_start':
|
|
783
|
+
dispatch({
|
|
784
|
+
type: 'compactionStart',
|
|
785
|
+
index: event.index,
|
|
786
|
+
trigger: event.trigger,
|
|
787
|
+
model: event.model,
|
|
788
|
+
});
|
|
789
|
+
break;
|
|
790
|
+
case 'compaction_end': {
|
|
791
|
+
dispatch({ type: 'compactionEnd', record: event.record });
|
|
792
|
+
// THE GAUGE MUST FALL IMMEDIATELY (§6.2 / AC-5), and it must fall to a
|
|
793
|
+
// number in the SAME UNIT as the one it replaces (P1-11). The value
|
|
794
|
+
// being overwritten came from `occupiedTokens(usage)` — a provider
|
|
795
|
+
// measurement that includes the tool schemas — while
|
|
796
|
+
// `record.tokensAfter` comes from core's `estimatePromptTokens`, which
|
|
797
|
+
// never counts them. Writing the raw estimate makes the bar fall too
|
|
798
|
+
// far and then visibly climb again on the next `turn_end` for no reason
|
|
799
|
+
// the user can see, which is the same is-this-thing-working doubt the
|
|
800
|
+
// immediate drop exists to prevent, one turn later.
|
|
801
|
+
if (event.record.applied) {
|
|
802
|
+
const offset = controller.getCompactionSnapshot().pressure.estimateOffset ?? 0;
|
|
803
|
+
dispatch({ type: 'contextTokensEstimated', tokens: event.record.tokensAfter + offset });
|
|
804
|
+
}
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
case 'usage': {
|
|
808
|
+
// Compaction spend is REAL SPEND at the SUMMARIZER's price (§6.4 /
|
|
809
|
+
// AC-15), which may be neither the lead's nor the fast tier's — so the
|
|
810
|
+
// ref is ASKED FOR rather than assumed. `getModelInfo()` here would
|
|
811
|
+
// price a Haiku summarization at the lead's Opus rates, which is the
|
|
812
|
+
// same class of lie as the `$0.00` `pricingUnknown` exists to prevent,
|
|
813
|
+
// just in the other direction.
|
|
814
|
+
const snapshot = controller.getCompactionSnapshot();
|
|
815
|
+
const ref = controller.getCompactionSummarizerRef();
|
|
816
|
+
const costDelta = snapshot.pricingUnknown || !ref
|
|
817
|
+
? 0
|
|
818
|
+
: computeCost(event.usage, controller.getModelInfoFor(ref).cost);
|
|
819
|
+
dispatch({ type: 'compactionUsage', usage: event.usage, costDelta });
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
case 'snapshot':
|
|
823
|
+
dispatch({ type: 'compactionSnapshot', snapshot: event.snapshot });
|
|
824
|
+
// ONE NUMBER ON SCREEN AND IN THE TRIGGER (R-11 / hardening §3.2.4).
|
|
825
|
+
// The checkpoint's occupancy is a measured base PLUS an estimate of
|
|
826
|
+
// what was appended after it, and without this the gauge would still
|
|
827
|
+
// show the bare base from the last `turn_end` - so a user watching
|
|
828
|
+
// 76 % would see compaction fire and conclude the feature is broken.
|
|
829
|
+
//
|
|
830
|
+
// `contextTokensEstimated` IS REUSED RATHER THAN A SECOND WRITER ADDED:
|
|
831
|
+
// it is the one existing action for "this figure is derived", and it
|
|
832
|
+
// already sets the `~`. Gated on `isApproximate`, which is precisely
|
|
833
|
+
// the case it was built for - a wholly measured figure belongs to
|
|
834
|
+
// `turnEnd`.
|
|
835
|
+
if (isApproximate(event.snapshot.pressure)) {
|
|
836
|
+
dispatch({
|
|
837
|
+
type: 'contextTokensEstimated',
|
|
838
|
+
tokens: event.snapshot.pressure.occupied,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}, [controller]);
|
|
607
845
|
// --- Live tool output (agent-activity-presentation-live §3.3.2). --------
|
|
608
846
|
//
|
|
609
847
|
// A FIFTH SUBSCRIPTION, and THE ONE THAT DOES GO THROUGH THE STREAMING
|
|
@@ -647,6 +885,17 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
647
885
|
const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 200);
|
|
648
886
|
return () => clearInterval(id);
|
|
649
887
|
}, [state.status]);
|
|
888
|
+
// --- Live compaction elapsed tick (hardening §3.6 / W5). ---------------
|
|
889
|
+
//
|
|
890
|
+
// ONLY WHILE A CARD IS LIVE AND THE RUN IS NOT, so an ordinary session has no
|
|
891
|
+
// timer and an in-loop compaction reuses the 200 ms elapsed ticker rather than
|
|
892
|
+
// adding a second one. 1 Hz, because the card shows whole seconds.
|
|
893
|
+
useEffect(() => {
|
|
894
|
+
if (state.compactionEntryId === undefined || state.status === 'running')
|
|
895
|
+
return undefined;
|
|
896
|
+
const id = setInterval(() => setCompactionClock(Date.now()), 1000);
|
|
897
|
+
return () => clearInterval(id);
|
|
898
|
+
}, [state.compactionEntryId, state.status]);
|
|
650
899
|
// --- Retry countdown tick (llm-api-retry-backoff §6.5). ----------------
|
|
651
900
|
//
|
|
652
901
|
// ONLY WHILE A BACKOFF IS ACTUALLY WAITING, and the effect's teardown clears it —
|
|
@@ -697,9 +946,17 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
697
946
|
// so on a console that reports nothing there is no source and no advice.
|
|
698
947
|
useStartupNotices(dispatch, {
|
|
699
948
|
enabled: !!mouseSource,
|
|
949
|
+
// NOT re-derived here (see `MouseNoticeOptions`): `cli.tsx` computes
|
|
950
|
+
// `mouseFilter !== null && cfg.mouseSelect` once and four things read it,
|
|
951
|
+
// so a second derivation is a second chance for them to disagree.
|
|
952
|
+
selectEnabled: terminal?.mouseSelect ?? false,
|
|
700
953
|
// `state.json`, not `config.json`: whether a one-shot notice has been shown
|
|
701
954
|
// is bookkeeping, and the comment on the old persisted key said so.
|
|
702
|
-
|
|
955
|
+
//
|
|
956
|
+
// THE VERSION, NOT `true` (P1-8). The gate compares against
|
|
957
|
+
// `MOUSE_NOTICE_VERSION`, so storing anything else either replays the
|
|
958
|
+
// notice forever or silences a future revision of the text.
|
|
959
|
+
onSeen: () => setMouseNoticeVersion(MOUSE_NOTICE_VERSION),
|
|
703
960
|
},
|
|
704
961
|
// Present only when `loadConfig` had to raise the user's own
|
|
705
962
|
// `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
|
|
@@ -846,6 +1103,56 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
846
1103
|
// --- Helpers -----------------------------------------------------------
|
|
847
1104
|
const notify = (level, text) => dispatch({ type: 'notice', level, text });
|
|
848
1105
|
const toast = (level, text) => dispatch({ type: 'pushToast', level, text });
|
|
1106
|
+
// --- Paste bridge (tui-paste-handling section 5.1.2 / I-15). -------------
|
|
1107
|
+
//
|
|
1108
|
+
// The filter exists before this component does, so it holds a slot rather than
|
|
1109
|
+
// a callback. Cleared on unmount: a rejection that somehow arrived after the
|
|
1110
|
+
// tree came down would otherwise dispatch into a dead reducer.
|
|
1111
|
+
useEffect(() => {
|
|
1112
|
+
if (!pasteBridge)
|
|
1113
|
+
return undefined;
|
|
1114
|
+
const bridge = pasteBridge;
|
|
1115
|
+
bridge.notify = (level, text) => dispatch({ type: 'notice', level, text });
|
|
1116
|
+
return () => {
|
|
1117
|
+
bridge.notify = null;
|
|
1118
|
+
};
|
|
1119
|
+
}, [pasteBridge]);
|
|
1120
|
+
// --- The composer's height (tui-paste-handling section 5.5 / D-12). ------
|
|
1121
|
+
//
|
|
1122
|
+
// GROWTH IS IMMEDIATE, SHRINK IS DEFERRED BY ONE TICK (R-12). Growing late
|
|
1123
|
+
// overdraws the frame for a frame; shrinking early makes a `Backspace` that
|
|
1124
|
+
// crosses a wrap boundary bounce the whole transcript, and a run of deletions
|
|
1125
|
+
// bounce it repeatedly. Deferring coalesces the run into one move.
|
|
1126
|
+
//
|
|
1127
|
+
// The ref mirrors the state so the decision is made OUTSIDE the updater: React
|
|
1128
|
+
// may call an updater more than once, and scheduling a timer from inside one
|
|
1129
|
+
// would arm it twice.
|
|
1130
|
+
const [draftRows, setDraftRows] = useState(1);
|
|
1131
|
+
const draftRowsRef = useRef(1);
|
|
1132
|
+
const shrinkTimer = useRef(null);
|
|
1133
|
+
const onDraftRows = useCallback((next) => {
|
|
1134
|
+
const wanted = Math.max(1, next);
|
|
1135
|
+
if (shrinkTimer.current) {
|
|
1136
|
+
clearTimeout(shrinkTimer.current);
|
|
1137
|
+
shrinkTimer.current = null;
|
|
1138
|
+
}
|
|
1139
|
+
if (wanted === draftRowsRef.current)
|
|
1140
|
+
return;
|
|
1141
|
+
if (wanted > draftRowsRef.current) {
|
|
1142
|
+
draftRowsRef.current = wanted;
|
|
1143
|
+
setDraftRows(wanted);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
shrinkTimer.current = setTimeout(() => {
|
|
1147
|
+
shrinkTimer.current = null;
|
|
1148
|
+
draftRowsRef.current = wanted;
|
|
1149
|
+
setDraftRows(wanted);
|
|
1150
|
+
}, 0);
|
|
1151
|
+
}, []);
|
|
1152
|
+
useEffect(() => () => {
|
|
1153
|
+
if (shrinkTimer.current)
|
|
1154
|
+
clearTimeout(shrinkTimer.current);
|
|
1155
|
+
}, []);
|
|
849
1156
|
// --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
|
|
850
1157
|
//
|
|
851
1158
|
// TWO ARRIVAL ORDERS, AND BOTH HAPPEN. The service is built by a dynamic
|
|
@@ -888,6 +1195,83 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
888
1195
|
// every render and defeat the ref guard's purpose rather than its mechanism.
|
|
889
1196
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
890
1197
|
}, [updateSnapshot?.phase, updateSnapshot?.latestVersion]);
|
|
1198
|
+
// --- Selection (tui-selection-and-scroll-follow §4.4). -------------------
|
|
1199
|
+
//
|
|
1200
|
+
// The controller is built by `cli.tsx` before `render()`, so what it is
|
|
1201
|
+
// missing when it starts is exactly the two things that only exist once this
|
|
1202
|
+
// component is mounted: somewhere to put a toast, and a way to ask for a React
|
|
1203
|
+
// redraw when the frame differ has no cache to repaint from. Both are assigned
|
|
1204
|
+
// here and cleared on unmount, the same shape `updateBridge` uses.
|
|
1205
|
+
const selectionBridge = terminal?.selection;
|
|
1206
|
+
useEffect(() => {
|
|
1207
|
+
if (!selectionBridge)
|
|
1208
|
+
return undefined;
|
|
1209
|
+
selectionBridge.onCopied = (via, lines, chars) => {
|
|
1210
|
+
// IT NAMES THE MECHANISM RATHER THAN CLAIMING SUCCESS (R-7). Neither OSC 52
|
|
1211
|
+
// nor a spawned `xclip` is detectable: tmux without `set -g set-clipboard
|
|
1212
|
+
// on` swallows the first silently, and the second is best-effort by
|
|
1213
|
+
// construction. A toast that said "copied successfully" would be guessing.
|
|
1214
|
+
const noun = lines === 1 ? 'line' : 'lines';
|
|
1215
|
+
if (via === 'none') {
|
|
1216
|
+
toast('warn', 'Nothing could be copied - no clipboard mechanism is available.');
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const how = via === 'osc52' ? 'terminal clipboard' : 'system clipboard';
|
|
1220
|
+
toast('success', `Sent ${lines} ${noun} (${chars} chars) to the ${how}.`);
|
|
1221
|
+
};
|
|
1222
|
+
// I-5 of `frame-differ.ts`: writing escapes here would be a foreign write.
|
|
1223
|
+
// The nonce is the carrier Ctrl+L already uses (§4.13).
|
|
1224
|
+
selectionBridge.requestRedraw = () => setRedrawNonce((n) => n + 1);
|
|
1225
|
+
const unsubscribe = selectionBridge.controller?.onHoldChange(setSelectionHold);
|
|
1226
|
+
return () => {
|
|
1227
|
+
selectionBridge.onCopied = null;
|
|
1228
|
+
selectionBridge.requestRedraw = null;
|
|
1229
|
+
unsubscribe?.();
|
|
1230
|
+
};
|
|
1231
|
+
// `toast` is a stable dispatch wrapper; depending on it would rebuild this
|
|
1232
|
+
// subscription on every render.
|
|
1233
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1234
|
+
}, [selectionBridge]);
|
|
1235
|
+
const selectionController = selectionBridge?.controller ?? null;
|
|
1236
|
+
/**
|
|
1237
|
+
* I-9, the DERIVED half — and the one that matters (D-14 / P1-4).
|
|
1238
|
+
*
|
|
1239
|
+
* `shiftUp` changes exactly when the rows on screen move: a wheel notch, a key,
|
|
1240
|
+
* PgDn, the bottom snap, `pinToBottomNonce`, the idle auto-resume, a height
|
|
1241
|
+
* correction above the reading position. An enumerated list of gestures was
|
|
1242
|
+
* already incomplete on the day it was written — it missed this feature's OWN
|
|
1243
|
+
* auto-resume, which would have left a five-second-old highlight sitting over
|
|
1244
|
+
* rows that had scrolled out from under it — and a derived condition cannot be
|
|
1245
|
+
* forgotten by the next person who adds a way to move the viewport.
|
|
1246
|
+
*
|
|
1247
|
+
* Rule A holds `shiftUp` CONSTANT while a drag is in progress, which is why a
|
|
1248
|
+
* selection survives output streaming in below it and only that.
|
|
1249
|
+
*
|
|
1250
|
+
* Referentially stable, because `ScrollViewport` publishes it from an effect
|
|
1251
|
+
* keyed on the callback.
|
|
1252
|
+
*/
|
|
1253
|
+
const onViewportShiftChange = useCallback(() => {
|
|
1254
|
+
selectionController?.clear();
|
|
1255
|
+
}, [selectionController]);
|
|
1256
|
+
// I-9, the ENUMERATED half. `shiftUp` covers every movement of the rows and is
|
|
1257
|
+
// wired straight into `ScrollViewport` below; these three are the cases that
|
|
1258
|
+
// change the screen while leaving `shiftUp` untouched — a key that scrolls
|
|
1259
|
+
// nothing, an overlay swap, and a re-wrap (P1-7).
|
|
1260
|
+
useEffect(() => {
|
|
1261
|
+
if (!selectionController)
|
|
1262
|
+
return;
|
|
1263
|
+
// Screen coordinates stop meaning anything the moment every line re-wraps,
|
|
1264
|
+
// and a resize is exactly that (N6).
|
|
1265
|
+
selectionController.clear();
|
|
1266
|
+
}, [selectionController, rows, cols]);
|
|
1267
|
+
useEffect(() => {
|
|
1268
|
+
if (!selectionController)
|
|
1269
|
+
return;
|
|
1270
|
+
// An overlay replaces the viewport, so a selection over it would be a
|
|
1271
|
+
// highlight over rows that are no longer there (N4).
|
|
1272
|
+
selectionController.setEnabled(state.overlay === null);
|
|
1273
|
+
selectionController.clear();
|
|
1274
|
+
}, [selectionController, state.overlay]);
|
|
891
1275
|
const persistConfig = (patch) => {
|
|
892
1276
|
try {
|
|
893
1277
|
updatePersistedConfig(patch);
|
|
@@ -905,6 +1289,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
905
1289
|
};
|
|
906
1290
|
const doExit = () => {
|
|
907
1291
|
controller.abort();
|
|
1292
|
+
// G5. `force: true` means `reapSync()` SEMANTICS - immediate SIGKILL, no
|
|
1293
|
+
// grace ladder - because THIS FUNCTION AWAITS NOTHING AND MUST NOT START
|
|
1294
|
+
// (§3.6 step 3). An async `stopAll` with a 3 s escalation would still be
|
|
1295
|
+
// running when `exit()` tore the tree down, and every child would survive.
|
|
1296
|
+
//
|
|
1297
|
+
// IT IS DELIBERATELY REDUNDANT with the signal hook the controller
|
|
1298
|
+
// registered: a normal quit reaches both, and `reapSync` is idempotent
|
|
1299
|
+
// precisely so that is safe. The hook covers SIGINT/SIGTERM and a crash;
|
|
1300
|
+
// this covers `/exit` and `Ctrl+C x2`, which are not signals at all.
|
|
1301
|
+
void controller.stopAllServices({ force: true });
|
|
908
1302
|
exit();
|
|
909
1303
|
};
|
|
910
1304
|
/**
|
|
@@ -951,6 +1345,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
951
1345
|
// cache, and `/update` would report "disabled" on a session where it is on.
|
|
952
1346
|
// `?? undefined` because the bridge holds `null` and the port is optional.
|
|
953
1347
|
update: updateBridge?.service ?? undefined,
|
|
1348
|
+
// Read at command time so `/mouse` reports the LIVE state rather than
|
|
1349
|
+
// whatever it was when this closure was first built — `/mouse off` followed
|
|
1350
|
+
// by a bare `/mouse` has to say "off".
|
|
1351
|
+
...(terminal?.setMouseCapture
|
|
1352
|
+
? {
|
|
1353
|
+
mouse: {
|
|
1354
|
+
captured: () => terminal.isMouseCaptured?.() ?? true,
|
|
1355
|
+
selectEnabled: () => terminal.mouseSelect,
|
|
1356
|
+
setCapture: (on) => terminal.setMouseCapture?.(on),
|
|
1357
|
+
},
|
|
1358
|
+
}
|
|
1359
|
+
: {}),
|
|
1360
|
+
...(terminal?.writeForeign ? { writeForeign: terminal.writeForeign } : {}),
|
|
954
1361
|
});
|
|
955
1362
|
/**
|
|
956
1363
|
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
@@ -1079,6 +1486,23 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1079
1486
|
const fastPatch = readFastSettings(values);
|
|
1080
1487
|
controller.setFastConfig(fastPatch);
|
|
1081
1488
|
patch.fast = fastPatch;
|
|
1489
|
+
// --- Context compaction (context-auto-compaction §4.5) ------------------
|
|
1490
|
+
//
|
|
1491
|
+
// THE SAME SETTER-AND-PERSIST PAIR, and the same reason. `setCompactionConfig`
|
|
1492
|
+
// additionally re-emits the snapshot, which is what moves the status chip and
|
|
1493
|
+
// the gauge's marks without waiting for the next turn.
|
|
1494
|
+
//
|
|
1495
|
+
// `enabled` here flips the LIVE switch only. Whether the wiring exists at all
|
|
1496
|
+
// was decided at construction and cannot be undone in this session (§3.2), so
|
|
1497
|
+
// a user who turns it ON in a `--no-compaction` session gets the persisted
|
|
1498
|
+
// value for next launch and nothing this session — which is exactly what
|
|
1499
|
+
// `/compact on` reports in words.
|
|
1500
|
+
const compactionPatch = readCompactionSettings(values);
|
|
1501
|
+
controller.setCompactionConfig(compactionPatch);
|
|
1502
|
+
if (compactionPatch.enabled !== undefined) {
|
|
1503
|
+
controller.setCompactionEnabled(compactionPatch.enabled);
|
|
1504
|
+
}
|
|
1505
|
+
patch.compaction = compactionPatch;
|
|
1082
1506
|
persistConfig(patch);
|
|
1083
1507
|
getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
|
|
1084
1508
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
@@ -1152,7 +1576,37 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1152
1576
|
}, []);
|
|
1153
1577
|
// --- Global keys -------------------------------------------------------
|
|
1154
1578
|
useInput((input, key) => {
|
|
1579
|
+
// I-9, the third clear trigger, and FIRST IN THE HANDLER so no `return`
|
|
1580
|
+
// below can skip it. Any key at all drops the selection: most of them move
|
|
1581
|
+
// the rows, and the ones that do not have still ended the gesture. It is
|
|
1582
|
+
// also the way out of a `hold` that lost its release (I-11), alongside the
|
|
1583
|
+
// watchdog.
|
|
1584
|
+
//
|
|
1585
|
+
// `Esc` IS DELIBERATELY NOT GIVEN A SECOND MEANING (§4.4.6). It aborts a
|
|
1586
|
+
// run, and an emergency exit is the last key that should silently absorb a
|
|
1587
|
+
// press; it clears the selection here like every other key and nothing more.
|
|
1588
|
+
selectionController?.clear();
|
|
1155
1589
|
if (key.ctrl && (input === 'c' || input === 'C')) {
|
|
1590
|
+
// RUNG ONE: Ctrl+C acquires the meaning a terminal user already expects
|
|
1591
|
+
// it to have (§3.6 / G4 / D-5). While services are live it stops THEM, and
|
|
1592
|
+
// only once nothing is running does it fall through to the existing
|
|
1593
|
+
// arm/exit ladder.
|
|
1594
|
+
//
|
|
1595
|
+
// IT DOES NOT ARM EXIT. Stopping a server and quitting the app are
|
|
1596
|
+
// different intentions, and a user who pressed Ctrl+C to kill a dev server
|
|
1597
|
+
// must not find themselves one keystroke from losing the session. The
|
|
1598
|
+
// composer hint still names an exit clause, so quitting stays discoverable
|
|
1599
|
+
// (P1-4).
|
|
1600
|
+
//
|
|
1601
|
+
// THE GRACEFUL LADDER, not `force`: this path has a live event loop to run
|
|
1602
|
+
// the SIGTERM -> SIGKILL escalation on, and a dev server given a chance to
|
|
1603
|
+
// shut down cleanly releases its port.
|
|
1604
|
+
const live = liveServiceCount(servicesRef.current);
|
|
1605
|
+
if (live > 0) {
|
|
1606
|
+
void controller.stopAllServices();
|
|
1607
|
+
toast('warn', `Stopping ${live} service${live === 1 ? '' : 's'}.`);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1156
1610
|
if (ctrlCArmed.current) {
|
|
1157
1611
|
doExit();
|
|
1158
1612
|
return;
|
|
@@ -1266,13 +1720,29 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1266
1720
|
// `team` joins `tool` here (team-subagents §6.3): a dispatch card collapses
|
|
1267
1721
|
// its per-agent summaries behind the same key, and `toggleExpand` is keyed
|
|
1268
1722
|
// on entry id so it needs no change.
|
|
1723
|
+
// `compaction` joins `tool` and `team` here (context-auto-compaction §6.3 /
|
|
1724
|
+
// C-15 / P1-8). `expandedToolIds` is keyed by entry id so the STORAGE was
|
|
1725
|
+
// already generic — but both surfaces that CHOOSE a target filter by kind,
|
|
1726
|
+
// and without this the compaction card renders its own `ctrl+o to expand`
|
|
1727
|
+
// hint, the keystroke silently expands an older tool card instead, and
|
|
1728
|
+
// nothing errors. AC-21 is the assertion that the hint does not lie.
|
|
1729
|
+
// `service` joins the closed list (P1-3). Omitting it is not a missing
|
|
1730
|
+
// feature but an actively wrong one: `ServiceCard` draws its own
|
|
1731
|
+
// `ctrl+o log` hint, the keystroke would silently expand AN OLDER TOOL
|
|
1732
|
+
// CARD, and nothing would error. The terminal one-row record is skipped -
|
|
1733
|
+
// it has nothing to reveal, and expanding it would consume the press.
|
|
1269
1734
|
const last = [...stateRef.current.entries]
|
|
1270
1735
|
.reverse()
|
|
1271
|
-
.find((e) => e.kind === 'tool' ||
|
|
1736
|
+
.find((e) => e.kind === 'tool' ||
|
|
1737
|
+
e.kind === 'team' ||
|
|
1738
|
+
e.kind === 'compaction' ||
|
|
1739
|
+
(e.kind === 'service' && !e.terminal));
|
|
1272
1740
|
if (last)
|
|
1273
1741
|
dispatch({ type: 'toggleExpand', id: last.id });
|
|
1742
|
+
// WIDENED FROM `No tool output to expand.`: with three expandable kinds the
|
|
1743
|
+
// old wording would be wrong in two of the three empty cases.
|
|
1274
1744
|
else
|
|
1275
|
-
toast('info', '
|
|
1745
|
+
toast('info', 'Nothing to expand.');
|
|
1276
1746
|
return;
|
|
1277
1747
|
}
|
|
1278
1748
|
if (key.escape) {
|
|
@@ -1292,6 +1762,34 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1292
1762
|
return;
|
|
1293
1763
|
}
|
|
1294
1764
|
if (stateRef.current.status === 'running') {
|
|
1765
|
+
// RUNG TWO (§3.6 / G3). The first press asked the engine to stop; if the
|
|
1766
|
+
// run is STILL running when the second arrives, that request provably
|
|
1767
|
+
// failed - so this one does not ask, it takes.
|
|
1768
|
+
//
|
|
1769
|
+
// THREE THINGS HAPPEN, AND THE THIRD IS THE BELT-AND-BRACES.
|
|
1770
|
+
// `controller.forceStop()` aborts, hard-kills every tracked foreground
|
|
1771
|
+
// `bash` child (which is what actually unblocks a wedged
|
|
1772
|
+
// `await tool.execute`) and bumps the run generation so the unwinding
|
|
1773
|
+
// engine can no longer move the view. The local `runEnd` is dispatched
|
|
1774
|
+
// UNCONDITIONALLY on top: even if the engine is blocked somewhere
|
|
1775
|
+
// nothing can reach, the view returns to `idle`, the composer is usable,
|
|
1776
|
+
// and the reducer releases every live tail. Making the view's usability
|
|
1777
|
+
// conditional on the engine recovering is what produced the reported
|
|
1778
|
+
// screenshot (D-6).
|
|
1779
|
+
//
|
|
1780
|
+
// AND THE NEXT MESSAGE MUST STILL WORK. `AgentController.prompt()` waits
|
|
1781
|
+
// a still-running engine out and never rejects (I-8) - without that,
|
|
1782
|
+
// "Esc, Esc, then type" would reach `Agent.prompt()`'s synchronous throw,
|
|
1783
|
+
// become an unhandled rejection, and EXIT THE PROCESS (P0-1).
|
|
1784
|
+
if (escArmed.current) {
|
|
1785
|
+
disarmEsc();
|
|
1786
|
+
controller.forceStop();
|
|
1787
|
+
endReasonRef.current.aborted = true;
|
|
1788
|
+
dispatch({ type: 'abortMark' });
|
|
1789
|
+
dispatch({ type: 'runEnd' });
|
|
1790
|
+
toast('warn', 'Force-stopped.');
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1295
1793
|
controller.abort();
|
|
1296
1794
|
// Next to `abortMark`, in the same callback, for the reason
|
|
1297
1795
|
// `endReasonRef`'s own comment gives (C-11). D-5 turns this into
|
|
@@ -1299,7 +1797,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1299
1797
|
// restating what they interrupted is the CLI arguing.
|
|
1300
1798
|
endReasonRef.current.aborted = true;
|
|
1301
1799
|
dispatch({ type: 'abortMark' });
|
|
1302
|
-
|
|
1800
|
+
escArmed.current = true;
|
|
1801
|
+
if (escArmTimer.current)
|
|
1802
|
+
clearTimeout(escArmTimer.current);
|
|
1803
|
+
escArmTimer.current = setTimeout(() => {
|
|
1804
|
+
escArmed.current = false;
|
|
1805
|
+
escArmTimer.current = null;
|
|
1806
|
+
}, ESC_ARM_MS);
|
|
1807
|
+
toast('info', 'Run aborted. Esc again to force-stop.');
|
|
1303
1808
|
return;
|
|
1304
1809
|
}
|
|
1305
1810
|
// IDLE. Today this branch does nothing at all, which is what makes it free
|
|
@@ -1321,6 +1826,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1321
1826
|
const overlay = state.overlay;
|
|
1322
1827
|
const hasKey = controller.hasApiKey();
|
|
1323
1828
|
const empty = state.entries.length === 0;
|
|
1829
|
+
// RENDER SCOPE, so the chip and the composer hint agree with what the Ctrl+C
|
|
1830
|
+
// handler will do on the very next keystroke.
|
|
1831
|
+
const liveServices = liveServiceCount(services);
|
|
1324
1832
|
const tokPerSec = state.status === 'running' && elapsedMs > 500
|
|
1325
1833
|
? Math.max(0, Math.round((state.usageTotal.outputTokens - runBaselineOut.current) / (elapsedMs / 1000)))
|
|
1326
1834
|
: 0;
|
|
@@ -1332,6 +1840,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1332
1840
|
// every single turn, which is the one moment this feature exists to make feel
|
|
1333
1841
|
// calm.
|
|
1334
1842
|
const running = state.status === 'running';
|
|
1843
|
+
// The live compaction card's clock (hardening W5). While the run is going the
|
|
1844
|
+
// 200 ms elapsed ticker already causes the render that re-reads this; an IDLE
|
|
1845
|
+
// `/compact` has no ticker at all, which is what `compactionClock` is for.
|
|
1846
|
+
const compactionLive = state.compactionEntryId !== undefined;
|
|
1335
1847
|
if (running && runStartedAt.current === 0)
|
|
1336
1848
|
runStartedAt.current = Date.now();
|
|
1337
1849
|
if (!running && runStartedAt.current !== 0)
|
|
@@ -1357,11 +1869,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1357
1869
|
// that re-reads it — so no timer is added (D-36 / E-15). Seconds rather than
|
|
1358
1870
|
// milliseconds because `ToolCard` is `React.memo`'d with the default
|
|
1359
1871
|
// comparator; `Transcript` passes it on only to running tool entries.
|
|
1360
|
-
const nowSec = running ? Math.floor(Date.now() / 1000) : undefined;
|
|
1872
|
+
const nowSec = running || compactionLive ? Math.floor(Date.now() / 1000) : undefined;
|
|
1361
1873
|
// Row budget for the viewport and everything sized against it (§4.2a). The
|
|
1362
1874
|
// inline path has no fixed frame, hence no height to fit into: `Infinity`
|
|
1363
1875
|
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
1364
|
-
|
|
1876
|
+
// `draftRows` is the number `PromptInput` just reported for the rows it is
|
|
1877
|
+
// ACTUALLY RENDERING (I-8). Defaulting it to 1 is what makes the inline path
|
|
1878
|
+
// and every test that never types byte-identical to before.
|
|
1879
|
+
const viewportBudget = computeViewportRows(rows, draftRows);
|
|
1365
1880
|
const overlayMaxRows = fullscreen ? viewportBudget : Number.POSITIVE_INFINITY;
|
|
1366
1881
|
// --- The todo rail (todo-plan-execution §3.9). --------------------------
|
|
1367
1882
|
//
|
|
@@ -1425,6 +1940,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1425
1940
|
// The four fast rows, seeded through the pure mapper so the screen and
|
|
1426
1941
|
// the save handler cannot disagree about what `(inherit)` means.
|
|
1427
1942
|
...fastSettingsFrom(cfg.fast),
|
|
1943
|
+
...compactionSettingsFrom(cfg.compaction),
|
|
1428
1944
|
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave,
|
|
1429
1945
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1430
1946
|
// while the screen is open shows the new value on the next paint.
|
|
@@ -1500,7 +2016,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1500
2016
|
viewportRows: fullscreen ? viewportBudget : rows,
|
|
1501
2017
|
offset: scrolledLines,
|
|
1502
2018
|
};
|
|
1503
|
-
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: viewReducedMotion, 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: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
|
|
2019
|
+
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
|
|
1504
2020
|
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
1505
2021
|
// rows; unbounded, it pushes the status bar past the frame height where
|
|
1506
2022
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
@@ -1515,7 +2031,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1515
2031
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
1516
2032
|
// only decides which one the user is taught.
|
|
1517
2033
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
1518
|
-
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, modeToggleKey: modeToggleKey, 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' }) }) }));
|
|
2034
|
+
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, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, scrolledLines: scrolledLines, onDraftRows: onDraftRows, onNotice: notify, 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' }) }) }));
|
|
1519
2035
|
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: viewReducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, 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,
|
|
1520
2036
|
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
1521
2037
|
// the full-screen branch replaces the whole viewport with the overlay,
|
|
@@ -1528,7 +2044,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1528
2044
|
// SIGNAL IS DERIVED FROM (D-4): inlining it here again is how the two
|
|
1529
2045
|
// drift apart, and the drift is silent in whichever branch is not
|
|
1530
2046
|
// edited.
|
|
1531
|
-
activity: activityVisible ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool,
|
|
2047
|
+
activity: activityVisible ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool,
|
|
2048
|
+
// THE ROW MUST NEVER SAY SOMETHING FALSE (§6.1). Compaction runs
|
|
2049
|
+
// BEFORE `turn_start`, so `status` is `'running'` with no tool in
|
|
2050
|
+
// flight — precisely the window in which the rotating phrase
|
|
2051
|
+
// would claim the model is thinking while it is actually waiting
|
|
2052
|
+
// on a 30k-token summarization.
|
|
2053
|
+
compacting: state.compaction?.inFlight === true, theme: theme, caps: caps })) : null,
|
|
1532
2054
|
// THE FORM THE LIFE SIGNAL TAKES WHEN A TOAST HAS THE ROW
|
|
1533
2055
|
// (`activity-spinner-vanishes-behind-toast`). Passed UNCONDITIONALLY:
|
|
1534
2056
|
// `BottomStatusRow` reads it only when `activity` above is non-null,
|
|
@@ -1548,7 +2070,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1548
2070
|
// branch replaces the whole viewport with the overlay, and an update
|
|
1549
2071
|
// notice under a settings screen is noise attached to a surface it is
|
|
1550
2072
|
// not visible on.
|
|
1551
|
-
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
|
|
2073
|
+
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, contextEstimated: state.contextTokensEstimated, 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
|
|
1552
2074
|
? {
|
|
1553
2075
|
teamActive: {
|
|
1554
2076
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -1558,7 +2080,15 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1558
2080
|
}
|
|
1559
2081
|
: {}), ...(state.todos && !showRail
|
|
1560
2082
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
1561
|
-
: {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.
|
|
2083
|
+
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.compaction?.live
|
|
2084
|
+
? {
|
|
2085
|
+
compactionActive: { inFlight: state.compaction.inFlight },
|
|
2086
|
+
gaugeMarks: {
|
|
2087
|
+
warn: cfg.compaction.warnThreshold * 100,
|
|
2088
|
+
high: cfg.compaction.threshold * 100,
|
|
2089
|
+
},
|
|
2090
|
+
}
|
|
2091
|
+
: {}), ...(state.retry
|
|
1562
2092
|
? {
|
|
1563
2093
|
retryActive: {
|
|
1564
2094
|
attempt: state.retry.attempt,
|