@aragon-agent/cli 0.6.3 → 0.6.7
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 +142 -0
- package/README.md +203 -13
- package/dist/agent/controller.d.ts +158 -2
- package/dist/agent/controller.js +400 -1
- package/dist/agent/reducer.d.ts +102 -19
- package/dist/agent/reducer.js +162 -71
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +132 -23
- package/dist/commands/builtins.js +101 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +94 -24
- package/dist/compaction/compactor.js +247 -98
- package/dist/compaction/context-command.d.ts +33 -0
- package/dist/compaction/context-command.js +166 -0
- package/dist/compaction/meter.d.ts +251 -0
- package/dist/compaction/meter.js +428 -0
- package/dist/compaction/pressure.d.ts +6 -0
- package/dist/compaction/pressure.js +4 -0
- package/dist/compaction/types.d.ts +77 -0
- package/dist/compaction/wiring.d.ts +56 -0
- package/dist/compaction/wiring.js +107 -25
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +63 -2
- package/dist/config/load.d.ts +28 -0
- package/dist/config/load.js +83 -1
- package/dist/config/schema.d.ts +129 -0
- package/dist/config/schema.js +69 -0
- package/dist/config/store.js +47 -1
- package/dist/exec/events.d.ts +138 -1
- package/dist/exec/events.js +42 -0
- package/dist/exec/index.js +43 -6
- package/dist/exec/progress-reporter.d.ts +23 -0
- package/dist/exec/progress-reporter.js +88 -0
- package/dist/exec/runner.d.ts +34 -0
- package/dist/exec/runner.js +137 -25
- package/dist/fast/reviewer.d.ts +19 -1
- package/dist/fast/reviewer.js +22 -0
- package/dist/fast/types.d.ts +15 -0
- package/dist/fast/wiring.d.ts +10 -1
- package/dist/fast/wiring.js +18 -1
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- 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 +2 -0
- package/dist/logging/install.js +53 -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 +36 -1
- package/dist/todo/limits.d.ts +4 -17
- package/dist/todo/limits.js +4 -17
- package/dist/todo/panel-rows.d.ts +8 -6
- package/dist/todo/panel-rows.js +19 -6
- 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/App.d.ts +15 -2
- package/dist/ui/App.js +330 -69
- package/dist/ui/AutocompletePopup.d.ts +3 -1
- package/dist/ui/AutocompletePopup.js +9 -11
- package/dist/ui/BottomStatusRow.d.ts +1 -1
- package/dist/ui/Composer.d.ts +56 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +52 -3
- package/dist/ui/PromptInput.js +201 -55
- package/dist/ui/StatusBar.d.ts +40 -15
- package/dist/ui/StatusBar.js +51 -9
- package/dist/ui/TeamPanel.d.ts +3 -1
- package/dist/ui/TeamPanel.js +13 -9
- package/dist/ui/TodoPanel.d.ts +5 -5
- package/dist/ui/TodoPanel.js +56 -24
- package/dist/ui/Transcript.js +29 -1
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- 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/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/exit-snapshot.d.ts +9 -0
- package/dist/ui/layout/ScrollViewport.d.ts +1 -1
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/team-panel.d.ts +18 -0
- package/dist/ui/layout/team-panel.js +22 -0
- package/dist/ui/layout/todo-layout.d.ts +24 -0
- package/dist/ui/layout/todo-layout.js +27 -0
- package/dist/ui/layout/virtual-window.js +51 -1
- package/dist/ui/overlays/HelpOverlay.js +3 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.js +14 -1
- 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 +16 -0
- package/dist/ui/screen.js +45 -2
- package/dist/ui/selection/selection-controller.d.ts +1 -1
- package/dist/ui/transcript-text.js +55 -2
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -47
- package/dist/input/stdin-mouse-filter.js +0 -153
package/dist/ui/App.js
CHANGED
|
@@ -11,12 +11,12 @@ 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';
|
|
15
14
|
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
16
15
|
import { updatePersistedConfig } from '../config/store.js';
|
|
17
16
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
18
17
|
import { bumpSubmitCount, setMouseNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
|
|
19
18
|
import { getLogger } from '../logging/logger.js';
|
|
19
|
+
import { isTerminalStatus } from '../proc/types.js';
|
|
20
20
|
import { registerSecret } from '../logging/secret-registry.js';
|
|
21
21
|
import { detectCapabilities } from './capabilities.js';
|
|
22
22
|
import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
|
|
@@ -46,8 +46,8 @@ import { shouldRenderUpdateLine } from '../update/types.js';
|
|
|
46
46
|
import { UPDATE_LIMITS } from '../update/limits.js';
|
|
47
47
|
import { AppShell } from './layout/AppShell.js';
|
|
48
48
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
49
|
+
import { buildTodoRailLayout } from './layout/todo-layout.js';
|
|
50
|
+
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
51
51
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
52
52
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
53
53
|
import { useHeightStore } from './use-height-store.js';
|
|
@@ -105,6 +105,24 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
105
105
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* How long rung two of the Esc ladder stays armed after rung one (§5.5).
|
|
110
|
+
*
|
|
111
|
+
* Four seconds: long enough that a user who watched the abort fail can react,
|
|
112
|
+
* short enough that a stray Esc minutes later cannot force-stop a healthy run.
|
|
113
|
+
* `runEnd` / `agent_end` disarm it too, so the window is an upper bound rather
|
|
114
|
+
* than the only protection (R-9).
|
|
115
|
+
*/
|
|
116
|
+
const ESC_ARM_MS = 4000;
|
|
117
|
+
/** Services in a non-terminal state, from the live snapshot map. */
|
|
118
|
+
function liveServiceCount(services) {
|
|
119
|
+
let n = 0;
|
|
120
|
+
for (const service of Object.values(services)) {
|
|
121
|
+
if (!isTerminalStatus(service.status))
|
|
122
|
+
n += 1;
|
|
123
|
+
}
|
|
124
|
+
return n;
|
|
125
|
+
}
|
|
108
126
|
/**
|
|
109
127
|
* The lazy `useReducer` initializer. Module scope, so the reference is stable
|
|
110
128
|
* across renders and React never re-runs it.
|
|
@@ -112,7 +130,7 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
112
130
|
function seedViewState(thinkingVisible) {
|
|
113
131
|
return initialViewState({ thinkingVisible });
|
|
114
132
|
}
|
|
115
|
-
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, terminal, }) {
|
|
133
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, }) {
|
|
116
134
|
const { exit } = useApp();
|
|
117
135
|
const { stdout } = useStdout();
|
|
118
136
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -198,6 +216,37 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
198
216
|
confirmRef.current = confirmState;
|
|
199
217
|
const ctrlCArmed = useRef(false);
|
|
200
218
|
const ctrlCTimer = useRef(null);
|
|
219
|
+
/**
|
|
220
|
+
* Rung two of Esc is armed (background-service-supervision §3.6).
|
|
221
|
+
*
|
|
222
|
+
* CLEARED BY `runEnd` / `agent_end` AS WELL AS BY ITS OWN TIMER, and both
|
|
223
|
+
* halves matter: a healthy run that ended between the two presses must not be
|
|
224
|
+
* force-stoppable retroactively, and in every healthy run the second press
|
|
225
|
+
* lands on the idle branch and does nothing at all (R-9). Rung two only ever
|
|
226
|
+
* fires while the run is STILL running after rung one - i.e. only when the
|
|
227
|
+
* first press provably failed.
|
|
228
|
+
*/
|
|
229
|
+
const escArmed = useRef(false);
|
|
230
|
+
const escArmTimer = useRef(null);
|
|
231
|
+
/**
|
|
232
|
+
* Live service snapshots, keyed by id, for the status chip and the Ctrl+C rung.
|
|
233
|
+
*
|
|
234
|
+
* A `useState` MAP rather than a ref, because the chip has to re-render when it
|
|
235
|
+
* changes; and separate from `ViewState`, because a service is not a
|
|
236
|
+
* transcript entry - the transcript records EVENTS about a service (D-11) and
|
|
237
|
+
* this is the live state those events are about.
|
|
238
|
+
*/
|
|
239
|
+
const [services, setServices] = useState({});
|
|
240
|
+
/**
|
|
241
|
+
* Read by the Ctrl+C handler, which `useInput` registers ONCE.
|
|
242
|
+
*
|
|
243
|
+
* The same reason `stateRef` exists two lines up: a value captured in that
|
|
244
|
+
* closure would be pinned to the mount render, so the key handler would
|
|
245
|
+
* believe there are never any services and Ctrl+C would arm exit while a dev
|
|
246
|
+
* server was up.
|
|
247
|
+
*/
|
|
248
|
+
const servicesRef = useRef(services);
|
|
249
|
+
servicesRef.current = services;
|
|
201
250
|
const runBaselineOut = useRef(0);
|
|
202
251
|
/**
|
|
203
252
|
* Epoch ms the current run began — the activity phrase's seed.
|
|
@@ -282,6 +331,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
282
331
|
aborted: false,
|
|
283
332
|
errored: false,
|
|
284
333
|
});
|
|
334
|
+
/**
|
|
335
|
+
* Disarm rung two of the Esc ladder.
|
|
336
|
+
*
|
|
337
|
+
* A `useCallback` so the event subscription's dependency list can name it, and
|
|
338
|
+
* so it is one function rather than three copies of "clear the flag and the
|
|
339
|
+
* timer" that can drift.
|
|
340
|
+
*/
|
|
341
|
+
const disarmEsc = useCallback(() => {
|
|
342
|
+
escArmed.current = false;
|
|
343
|
+
if (escArmTimer.current) {
|
|
344
|
+
clearTimeout(escArmTimer.current);
|
|
345
|
+
escArmTimer.current = null;
|
|
346
|
+
}
|
|
347
|
+
}, []);
|
|
285
348
|
/**
|
|
286
349
|
* Disarm an armed continuation. Idempotent; NEVER touches the budget — a
|
|
287
350
|
* cancelled continuation was never issued, so it must not be charged (AC-18).
|
|
@@ -368,7 +431,28 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
368
431
|
*/
|
|
369
432
|
const patchSource = useMemo(() => ({ take: (id) => controller.takeFilePatch(id) }), [controller]);
|
|
370
433
|
useEffect(() => {
|
|
434
|
+
// CAPTURED AT SUBSCRIBE TIME AND COMPARED ON EVERY EVENT (I-7 / P1-7).
|
|
435
|
+
//
|
|
436
|
+
// `forceStop()` bumps the controller's generation, and everything the engine
|
|
437
|
+
// emits while it unwinds - `turn_start`, `tool_call_start`, `turn_end`,
|
|
438
|
+
// `agent_end` - belongs to the run the user just abandoned. `runStart` /
|
|
439
|
+
// `turnStart` would set `status: 'running'` again, bouncing the view straight
|
|
440
|
+
// back out of the `idle` rung two just gave them, and stray entries would
|
|
441
|
+
// append to a transcript they believe is finished.
|
|
442
|
+
//
|
|
443
|
+
// A LOCAL `let`, RE-READ RATHER THAN CAPTURED ONCE: the generation this
|
|
444
|
+
// listener considers current advances with every submit, so a force-stop
|
|
445
|
+
// makes stale events inert WITHOUT making the next run's events inert too.
|
|
446
|
+
let generation = controller.runGeneration;
|
|
371
447
|
const unsubscribe = controller.subscribe((event) => {
|
|
448
|
+
if (controller.runGeneration !== generation) {
|
|
449
|
+
// The engine is unwinding a force-stopped run. Adopt the new generation
|
|
450
|
+
// only once it has finished doing so, so a late `agent_end` cannot be
|
|
451
|
+
// mistaken for the next run's.
|
|
452
|
+
if (event.type === 'agent_end')
|
|
453
|
+
generation = controller.runGeneration;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
372
456
|
const cost = controller.getModelInfo().cost;
|
|
373
457
|
for (const action of reduceEvent(event, cost, patchSource)) {
|
|
374
458
|
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
@@ -395,6 +479,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
395
479
|
}
|
|
396
480
|
if (event.type !== 'agent_end')
|
|
397
481
|
return;
|
|
482
|
+
// RUNG TWO DISARMS WITH THE RUN (§3.6 / R-9). A healthy run that ended
|
|
483
|
+
// between the two Esc presses must not be force-stoppable retroactively:
|
|
484
|
+
// the second press then lands on the idle branch, which is what makes the
|
|
485
|
+
// ladder free to take rather than a footgun.
|
|
486
|
+
disarmEsc();
|
|
398
487
|
// NO OVERLAY OUTLIVES ITS RUN (P1-4). `ctx.signal` already covers the
|
|
399
488
|
// abort path, so this is DELIBERATE REDUNDANCY: it makes "the card cannot
|
|
400
489
|
// survive the run that opened it" a property of the App's lifecycle
|
|
@@ -484,7 +573,37 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
484
573
|
cancelFollowThrough();
|
|
485
574
|
unsubscribe();
|
|
486
575
|
};
|
|
487
|
-
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough]);
|
|
576
|
+
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough, disarmEsc]);
|
|
577
|
+
// --- Background services (background-service-supervision §3.8). ---------
|
|
578
|
+
//
|
|
579
|
+
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `ProcEvent` is
|
|
580
|
+
// deliberately not a member of core's `AgentEvent` union (I-5 / D-1), so it
|
|
581
|
+
// cannot arrive through `controller.subscribe`.
|
|
582
|
+
//
|
|
583
|
+
// TWO SINKS PER EVENT, and they are not redundant. `setServices` holds the
|
|
584
|
+
// LIVE state the status chip, the composer hint and the Ctrl+C rung read;
|
|
585
|
+
// `dispatch` writes the TRANSCRIPT, which records events about a service
|
|
586
|
+
// rather than mirroring it (D-11). Feeding one from the other would make the
|
|
587
|
+
// chip lie about a card that has already been printed into `<Static>`.
|
|
588
|
+
//
|
|
589
|
+
// NOT ROUTED THROUGH THE STREAMING COALESCER. `output` is already coalesced at
|
|
590
|
+
// the SOURCE, inside the supervisor, because `/bg` and `bash_output` consume
|
|
591
|
+
// the same event (§6); a second buffer here would only add latency.
|
|
592
|
+
useEffect(() => {
|
|
593
|
+
return controller.subscribeProc((event) => {
|
|
594
|
+
const service = event.service;
|
|
595
|
+
setServices((prev) => ({ ...prev, [service.id]: service }));
|
|
596
|
+
if (event.type === 'started') {
|
|
597
|
+
dispatch({ type: 'serviceStart', service });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (event.type === 'exited' || event.type === 'stopped') {
|
|
601
|
+
dispatch({ type: 'serviceEnd', service });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
dispatch({ type: 'serviceUpdate', service });
|
|
605
|
+
});
|
|
606
|
+
}, [controller]);
|
|
488
607
|
// --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
|
|
489
608
|
//
|
|
490
609
|
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
|
|
@@ -631,6 +750,27 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
631
750
|
}
|
|
632
751
|
});
|
|
633
752
|
}, [controller]);
|
|
753
|
+
// --- Context occupancy (context-usage-gauge-accuracy §3.3 / I-1). --------
|
|
754
|
+
//
|
|
755
|
+
// A SEVENTH SUBSCRIPTION, AND THE SOLE DISPATCHER OF `contextUsage`. The gauge
|
|
756
|
+
// used to be written from three reducer branches fed by two upstreams, which
|
|
757
|
+
// is what let a stale `snapshot` bounce the bar back to the pre-compaction
|
|
758
|
+
// figure one statement after a compaction correctly dropped it (P0-1). One
|
|
759
|
+
// writer makes that unrepresentable.
|
|
760
|
+
//
|
|
761
|
+
// IT SUBSCRIBES UNCONDITIONALLY. `subscribeContextUsage` is not a compaction
|
|
762
|
+
// forwarder - the meter exists in every session, including one started with
|
|
763
|
+
// `--no-compaction`, which is precisely the session whose gauge previously had
|
|
764
|
+
// one sample per turn and read 0 % after a `/resume`.
|
|
765
|
+
useEffect(() => {
|
|
766
|
+
// SEED ON MOUNT so a resumed session shows a real occupancy on frame 1
|
|
767
|
+
// rather than after its first completed turn. `getContextUsage()` measures
|
|
768
|
+
// on demand, so this works before any event has ever arrived.
|
|
769
|
+
dispatch({ type: 'contextUsage', snapshot: controller.getContextUsage() });
|
|
770
|
+
return controller.subscribeContextUsage((snapshot) => {
|
|
771
|
+
dispatch({ type: 'contextUsage', snapshot });
|
|
772
|
+
});
|
|
773
|
+
}, [controller]);
|
|
634
774
|
// --- Compaction event stream (context-auto-compaction §5.2). -------------
|
|
635
775
|
//
|
|
636
776
|
// A SIXTH SUBSCRIPTION, for the reason the team, todo and fast ones record:
|
|
@@ -667,23 +807,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
667
807
|
model: event.model,
|
|
668
808
|
});
|
|
669
809
|
break;
|
|
670
|
-
case 'compaction_end':
|
|
810
|
+
case 'compaction_end':
|
|
811
|
+
// THE CARD, AND NOTHING ELSE (context-usage-gauge-accuracy §3.3).
|
|
812
|
+
//
|
|
813
|
+
// A `contextTokensEstimated` dispatch used to live here, hand-building
|
|
814
|
+
// the post-compaction occupancy out of `record.tokensAfter` plus the
|
|
815
|
+
// snapshot's `estimateOffset`. It is gone because the gauge now has ONE
|
|
816
|
+
// writer: `ContextMeter` learns about the splice from
|
|
817
|
+
// `CompactionWiring.settlePending` itself and re-measures, so the bar
|
|
818
|
+
// falls in the same frame WITHOUT this branch and without the `snapshot`
|
|
819
|
+
// branch below racing it. Those two dispatches were the second and
|
|
820
|
+
// third writers of one number, and the loser of that race was the
|
|
821
|
+
// correct value (P0-1).
|
|
671
822
|
dispatch({ type: 'compactionEnd', record: event.record });
|
|
672
|
-
// THE GAUGE MUST FALL IMMEDIATELY (§6.2 / AC-5), and it must fall to a
|
|
673
|
-
// number in the SAME UNIT as the one it replaces (P1-11). The value
|
|
674
|
-
// being overwritten came from `occupiedTokens(usage)` — a provider
|
|
675
|
-
// measurement that includes the tool schemas — while
|
|
676
|
-
// `record.tokensAfter` comes from core's `estimatePromptTokens`, which
|
|
677
|
-
// never counts them. Writing the raw estimate makes the bar fall too
|
|
678
|
-
// far and then visibly climb again on the next `turn_end` for no reason
|
|
679
|
-
// the user can see, which is the same is-this-thing-working doubt the
|
|
680
|
-
// immediate drop exists to prevent, one turn later.
|
|
681
|
-
if (event.record.applied) {
|
|
682
|
-
const offset = controller.getCompactionSnapshot().pressure.estimateOffset ?? 0;
|
|
683
|
-
dispatch({ type: 'contextTokensEstimated', tokens: event.record.tokensAfter + offset });
|
|
684
|
-
}
|
|
685
823
|
break;
|
|
686
|
-
}
|
|
687
824
|
case 'usage': {
|
|
688
825
|
// Compaction spend is REAL SPEND at the SUMMARIZER's price (§6.4 /
|
|
689
826
|
// AC-15), which may be neither the lead's nor the fast tier's — so the
|
|
@@ -700,24 +837,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
700
837
|
break;
|
|
701
838
|
}
|
|
702
839
|
case 'snapshot':
|
|
840
|
+
// THE CHIP, THE COUNTS AND THE SUMMARIZER (§3.3). The occupancy that
|
|
841
|
+
// rides along on `snapshot.pressure` is deliberately NOT dispatched
|
|
842
|
+
// here: the meter that produced it publishes it on its own channel, and
|
|
843
|
+
// a second dispatch of the same number is the second writer this
|
|
844
|
+
// feature removed.
|
|
703
845
|
dispatch({ type: 'compactionSnapshot', snapshot: event.snapshot });
|
|
704
|
-
// ONE NUMBER ON SCREEN AND IN THE TRIGGER (R-11 / hardening §3.2.4).
|
|
705
|
-
// The checkpoint's occupancy is a measured base PLUS an estimate of
|
|
706
|
-
// what was appended after it, and without this the gauge would still
|
|
707
|
-
// show the bare base from the last `turn_end` - so a user watching
|
|
708
|
-
// 76 % would see compaction fire and conclude the feature is broken.
|
|
709
|
-
//
|
|
710
|
-
// `contextTokensEstimated` IS REUSED RATHER THAN A SECOND WRITER ADDED:
|
|
711
|
-
// it is the one existing action for "this figure is derived", and it
|
|
712
|
-
// already sets the `~`. Gated on `isApproximate`, which is precisely
|
|
713
|
-
// the case it was built for - a wholly measured figure belongs to
|
|
714
|
-
// `turnEnd`.
|
|
715
|
-
if (isApproximate(event.snapshot.pressure)) {
|
|
716
|
-
dispatch({
|
|
717
|
-
type: 'contextTokensEstimated',
|
|
718
|
-
tokens: event.snapshot.pressure.occupied,
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
846
|
break;
|
|
722
847
|
}
|
|
723
848
|
});
|
|
@@ -983,6 +1108,60 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
983
1108
|
// --- Helpers -----------------------------------------------------------
|
|
984
1109
|
const notify = (level, text) => dispatch({ type: 'notice', level, text });
|
|
985
1110
|
const toast = (level, text) => dispatch({ type: 'pushToast', level, text });
|
|
1111
|
+
// --- Paste bridge (tui-paste-handling section 5.1.2 / I-15). -------------
|
|
1112
|
+
//
|
|
1113
|
+
// The filter exists before this component does, so it holds a slot rather than
|
|
1114
|
+
// a callback. Cleared on unmount: a rejection that somehow arrived after the
|
|
1115
|
+
// tree came down would otherwise dispatch into a dead reducer.
|
|
1116
|
+
useEffect(() => {
|
|
1117
|
+
if (!pasteBridge)
|
|
1118
|
+
return undefined;
|
|
1119
|
+
const bridge = pasteBridge;
|
|
1120
|
+
bridge.notify = (level, text) => dispatch({ type: 'notice', level, text });
|
|
1121
|
+
return () => {
|
|
1122
|
+
bridge.notify = null;
|
|
1123
|
+
};
|
|
1124
|
+
}, [pasteBridge]);
|
|
1125
|
+
// --- The composer's height (tui-paste-handling section 5.5 / D-12). ------
|
|
1126
|
+
//
|
|
1127
|
+
// GROWTH IS IMMEDIATE, SHRINK IS DEFERRED BY ONE TICK (R-12). Growing late
|
|
1128
|
+
// overdraws the frame for a frame; shrinking early makes a `Backspace` that
|
|
1129
|
+
// crosses a wrap boundary bounce the whole transcript, and a run of deletions
|
|
1130
|
+
// bounce it repeatedly. Deferring coalesces the run into one move.
|
|
1131
|
+
//
|
|
1132
|
+
// The ref mirrors the state so the decision is made OUTSIDE the updater: React
|
|
1133
|
+
// may call an updater more than once, and scheduling a timer from inside one
|
|
1134
|
+
// would arm it twice.
|
|
1135
|
+
const [draftRows, setDraftRows] = useState(1);
|
|
1136
|
+
const [popupRows, setPopupRows] = useState(0);
|
|
1137
|
+
const onPopupRowsChange = useCallback((next) => {
|
|
1138
|
+
setPopupRows(previous => previous === next ? previous : next);
|
|
1139
|
+
}, []);
|
|
1140
|
+
const draftRowsRef = useRef(1);
|
|
1141
|
+
const shrinkTimer = useRef(null);
|
|
1142
|
+
const onDraftRows = useCallback((next) => {
|
|
1143
|
+
const wanted = Math.max(1, next);
|
|
1144
|
+
if (shrinkTimer.current) {
|
|
1145
|
+
clearTimeout(shrinkTimer.current);
|
|
1146
|
+
shrinkTimer.current = null;
|
|
1147
|
+
}
|
|
1148
|
+
if (wanted === draftRowsRef.current)
|
|
1149
|
+
return;
|
|
1150
|
+
if (wanted > draftRowsRef.current) {
|
|
1151
|
+
draftRowsRef.current = wanted;
|
|
1152
|
+
setDraftRows(wanted);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
shrinkTimer.current = setTimeout(() => {
|
|
1156
|
+
shrinkTimer.current = null;
|
|
1157
|
+
draftRowsRef.current = wanted;
|
|
1158
|
+
setDraftRows(wanted);
|
|
1159
|
+
}, 0);
|
|
1160
|
+
}, []);
|
|
1161
|
+
useEffect(() => () => {
|
|
1162
|
+
if (shrinkTimer.current)
|
|
1163
|
+
clearTimeout(shrinkTimer.current);
|
|
1164
|
+
}, []);
|
|
986
1165
|
// --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
|
|
987
1166
|
//
|
|
988
1167
|
// TWO ARRIVAL ORDERS, AND BOTH HAPPEN. The service is built by a dynamic
|
|
@@ -1119,6 +1298,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1119
1298
|
};
|
|
1120
1299
|
const doExit = () => {
|
|
1121
1300
|
controller.abort();
|
|
1301
|
+
// G5. `force: true` means `reapSync()` SEMANTICS - immediate SIGKILL, no
|
|
1302
|
+
// grace ladder - because THIS FUNCTION AWAITS NOTHING AND MUST NOT START
|
|
1303
|
+
// (§3.6 step 3). An async `stopAll` with a 3 s escalation would still be
|
|
1304
|
+
// running when `exit()` tore the tree down, and every child would survive.
|
|
1305
|
+
//
|
|
1306
|
+
// IT IS DELIBERATELY REDUNDANT with the signal hook the controller
|
|
1307
|
+
// registered: a normal quit reaches both, and `reapSync` is idempotent
|
|
1308
|
+
// precisely so that is safe. The hook covers SIGINT/SIGTERM and a crash;
|
|
1309
|
+
// this covers `/exit` and `Ctrl+C x2`, which are not signals at all.
|
|
1310
|
+
void controller.stopAllServices({ force: true });
|
|
1122
1311
|
exit();
|
|
1123
1312
|
};
|
|
1124
1313
|
/**
|
|
@@ -1407,6 +1596,26 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1407
1596
|
// press; it clears the selection here like every other key and nothing more.
|
|
1408
1597
|
selectionController?.clear();
|
|
1409
1598
|
if (key.ctrl && (input === 'c' || input === 'C')) {
|
|
1599
|
+
// RUNG ONE: Ctrl+C acquires the meaning a terminal user already expects
|
|
1600
|
+
// it to have (§3.6 / G4 / D-5). While services are live it stops THEM, and
|
|
1601
|
+
// only once nothing is running does it fall through to the existing
|
|
1602
|
+
// arm/exit ladder.
|
|
1603
|
+
//
|
|
1604
|
+
// IT DOES NOT ARM EXIT. Stopping a server and quitting the app are
|
|
1605
|
+
// different intentions, and a user who pressed Ctrl+C to kill a dev server
|
|
1606
|
+
// must not find themselves one keystroke from losing the session. The
|
|
1607
|
+
// composer hint still names an exit clause, so quitting stays discoverable
|
|
1608
|
+
// (P1-4).
|
|
1609
|
+
//
|
|
1610
|
+
// THE GRACEFUL LADDER, not `force`: this path has a live event loop to run
|
|
1611
|
+
// the SIGTERM -> SIGKILL escalation on, and a dev server given a chance to
|
|
1612
|
+
// shut down cleanly releases its port.
|
|
1613
|
+
const live = liveServiceCount(servicesRef.current);
|
|
1614
|
+
if (live > 0) {
|
|
1615
|
+
void controller.stopAllServices();
|
|
1616
|
+
toast('warn', `Stopping ${live} service${live === 1 ? '' : 's'}.`);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1410
1619
|
if (ctrlCArmed.current) {
|
|
1411
1620
|
doExit();
|
|
1412
1621
|
return;
|
|
@@ -1526,9 +1735,17 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1526
1735
|
// and without this the compaction card renders its own `ctrl+o to expand`
|
|
1527
1736
|
// hint, the keystroke silently expands an older tool card instead, and
|
|
1528
1737
|
// nothing errors. AC-21 is the assertion that the hint does not lie.
|
|
1738
|
+
// `service` joins the closed list (P1-3). Omitting it is not a missing
|
|
1739
|
+
// feature but an actively wrong one: `ServiceCard` draws its own
|
|
1740
|
+
// `ctrl+o log` hint, the keystroke would silently expand AN OLDER TOOL
|
|
1741
|
+
// CARD, and nothing would error. The terminal one-row record is skipped -
|
|
1742
|
+
// it has nothing to reveal, and expanding it would consume the press.
|
|
1529
1743
|
const last = [...stateRef.current.entries]
|
|
1530
1744
|
.reverse()
|
|
1531
|
-
.find((e) => e.kind === 'tool' ||
|
|
1745
|
+
.find((e) => e.kind === 'tool' ||
|
|
1746
|
+
e.kind === 'team' ||
|
|
1747
|
+
e.kind === 'compaction' ||
|
|
1748
|
+
(e.kind === 'service' && !e.terminal));
|
|
1532
1749
|
if (last)
|
|
1533
1750
|
dispatch({ type: 'toggleExpand', id: last.id });
|
|
1534
1751
|
// WIDENED FROM `No tool output to expand.`: with three expandable kinds the
|
|
@@ -1554,6 +1771,34 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1554
1771
|
return;
|
|
1555
1772
|
}
|
|
1556
1773
|
if (stateRef.current.status === 'running') {
|
|
1774
|
+
// RUNG TWO (§3.6 / G3). The first press asked the engine to stop; if the
|
|
1775
|
+
// run is STILL running when the second arrives, that request provably
|
|
1776
|
+
// failed - so this one does not ask, it takes.
|
|
1777
|
+
//
|
|
1778
|
+
// THREE THINGS HAPPEN, AND THE THIRD IS THE BELT-AND-BRACES.
|
|
1779
|
+
// `controller.forceStop()` aborts, hard-kills every tracked foreground
|
|
1780
|
+
// `bash` child (which is what actually unblocks a wedged
|
|
1781
|
+
// `await tool.execute`) and bumps the run generation so the unwinding
|
|
1782
|
+
// engine can no longer move the view. The local `runEnd` is dispatched
|
|
1783
|
+
// UNCONDITIONALLY on top: even if the engine is blocked somewhere
|
|
1784
|
+
// nothing can reach, the view returns to `idle`, the composer is usable,
|
|
1785
|
+
// and the reducer releases every live tail. Making the view's usability
|
|
1786
|
+
// conditional on the engine recovering is what produced the reported
|
|
1787
|
+
// screenshot (D-6).
|
|
1788
|
+
//
|
|
1789
|
+
// AND THE NEXT MESSAGE MUST STILL WORK. `AgentController.prompt()` waits
|
|
1790
|
+
// a still-running engine out and never rejects (I-8) - without that,
|
|
1791
|
+
// "Esc, Esc, then type" would reach `Agent.prompt()`'s synchronous throw,
|
|
1792
|
+
// become an unhandled rejection, and EXIT THE PROCESS (P0-1).
|
|
1793
|
+
if (escArmed.current) {
|
|
1794
|
+
disarmEsc();
|
|
1795
|
+
controller.forceStop();
|
|
1796
|
+
endReasonRef.current.aborted = true;
|
|
1797
|
+
dispatch({ type: 'abortMark' });
|
|
1798
|
+
dispatch({ type: 'runEnd' });
|
|
1799
|
+
toast('warn', 'Force-stopped.');
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1557
1802
|
controller.abort();
|
|
1558
1803
|
// Next to `abortMark`, in the same callback, for the reason
|
|
1559
1804
|
// `endReasonRef`'s own comment gives (C-11). D-5 turns this into
|
|
@@ -1561,7 +1806,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1561
1806
|
// restating what they interrupted is the CLI arguing.
|
|
1562
1807
|
endReasonRef.current.aborted = true;
|
|
1563
1808
|
dispatch({ type: 'abortMark' });
|
|
1564
|
-
|
|
1809
|
+
escArmed.current = true;
|
|
1810
|
+
if (escArmTimer.current)
|
|
1811
|
+
clearTimeout(escArmTimer.current);
|
|
1812
|
+
escArmTimer.current = setTimeout(() => {
|
|
1813
|
+
escArmed.current = false;
|
|
1814
|
+
escArmTimer.current = null;
|
|
1815
|
+
}, ESC_ARM_MS);
|
|
1816
|
+
toast('info', 'Run aborted. Esc again to force-stop.');
|
|
1565
1817
|
return;
|
|
1566
1818
|
}
|
|
1567
1819
|
// IDLE. Today this branch does nothing at all, which is what makes it free
|
|
@@ -1578,11 +1830,18 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1578
1830
|
}
|
|
1579
1831
|
});
|
|
1580
1832
|
// --- Render ------------------------------------------------------------
|
|
1581
|
-
const modelInfo = controller.getModelInfo()
|
|
1582
|
-
|
|
1833
|
+
// `const modelInfo = controller.getModelInfo()` AND ITS `modelKnown` COMPANION
|
|
1834
|
+
// USED TO LIVE HERE (context-usage-gauge-accuracy §4.3). They fed the status
|
|
1835
|
+
// bar's `contextWindow` / `contextWindowKnown` props, which no longer exist:
|
|
1836
|
+
// the denominator and its trustworthiness are resolved inside `ContextMeter`,
|
|
1837
|
+
// which is what lets the user's `contextWindow` override reach the gauge at
|
|
1838
|
+
// all. Nothing else in this render read them.
|
|
1583
1839
|
const overlay = state.overlay;
|
|
1584
1840
|
const hasKey = controller.hasApiKey();
|
|
1585
1841
|
const empty = state.entries.length === 0;
|
|
1842
|
+
// RENDER SCOPE, so the chip and the composer hint agree with what the Ctrl+C
|
|
1843
|
+
// handler will do on the very next keystroke.
|
|
1844
|
+
const liveServices = liveServiceCount(services);
|
|
1586
1845
|
const tokPerSec = state.status === 'running' && elapsedMs > 500
|
|
1587
1846
|
? Math.max(0, Math.round((state.usageTotal.outputTokens - runBaselineOut.current) / (elapsedMs / 1000)))
|
|
1588
1847
|
: 0;
|
|
@@ -1627,24 +1886,24 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1627
1886
|
// Row budget for the viewport and everything sized against it (§4.2a). The
|
|
1628
1887
|
// inline path has no fixed frame, hence no height to fit into: `Infinity`
|
|
1629
1888
|
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
1630
|
-
|
|
1889
|
+
// `draftRows` is the number `PromptInput` just reported for the rows it is
|
|
1890
|
+
// ACTUALLY RENDERING (I-8). Defaulting it to 1 is what makes the inline path
|
|
1891
|
+
// and every test that never types byte-identical to before.
|
|
1892
|
+
const viewportBudget = computeViewportRows(rows, draftRows);
|
|
1631
1893
|
const overlayMaxRows = fullscreen ? viewportBudget : Number.POSITIVE_INFINITY;
|
|
1632
|
-
//
|
|
1633
|
-
//
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
state.todos !== null && // no list -> no furniture, zero layout cost
|
|
1646
|
-
railWidth > 0 && // too narrow (under 80 columns)
|
|
1647
|
-
railRows >= TODO_LIMITS.panelMinRows; // too short to say anything useful
|
|
1894
|
+
// Renderers consume these same projections. Team capacity does not depend on
|
|
1895
|
+
// popup occupancy, so a menu cannot toggle the team's collapsed state.
|
|
1896
|
+
const teamLayout = buildTeamPanelLayout({
|
|
1897
|
+
snapshot: state.team, terminalRows: rows, availableRows: viewportBudget,
|
|
1898
|
+
});
|
|
1899
|
+
const railLayout = buildTodoRailLayout({
|
|
1900
|
+
mode, cols, panelEnabled: cfg.todo.panel, overlayOpen: overlay !== null,
|
|
1901
|
+
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
1902
|
+
teamRows: teamLayout.rowCount, popupRows,
|
|
1903
|
+
});
|
|
1904
|
+
// Every viewport consumer uses contentCols; header, composer, status and
|
|
1905
|
+
// overlays keep the full terminal width. Overlay visibility is in the gate.
|
|
1906
|
+
const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
1648
1907
|
// The inline plan strip (todo-plan-followthrough §3.7 / W2).
|
|
1649
1908
|
//
|
|
1650
1909
|
// DELIBERATELY NOT `!showRail`, which is true for FOUR different reasons and
|
|
@@ -1657,12 +1916,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1657
1916
|
// needless — this runs in RENDER scope, where `cfg` is re-read every frame.
|
|
1658
1917
|
// The asymmetry with the `agent_end` handler above is the whole of P1-1.
|
|
1659
1918
|
const showStrip = !fullscreen && cfg.todo.panel && state.todos !== null;
|
|
1660
|
-
// Every consumer INSIDE the viewport slot switches to this; the header,
|
|
1661
|
-
// composer, status bar, toasts and every overlay keep the full frame width
|
|
1662
|
-
// (C-7). Overlays keep it because `showRail` already includes `overlay ===
|
|
1663
|
-
// null`: splitting the screen with a status panel behind a modal is both worse
|
|
1664
|
-
// design and a second source of width-contract bugs in six components.
|
|
1665
|
-
const contentCols = cols - (showRail ? railWidth : 0);
|
|
1666
1919
|
const overlayNode = overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: cols, rows: helpRows(theme, caps), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { initial: {
|
|
1667
1920
|
provider: cfg.provider,
|
|
1668
1921
|
model: cfg.model,
|
|
@@ -1782,8 +2035,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1782
2035
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
1783
2036
|
// only decides which one the user is taught.
|
|
1784
2037
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
1785
|
-
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, scrolledLines: scrolledLines, 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' }) }) }));
|
|
1786
|
-
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,
|
|
2038
|
+
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, popupMaxHeight: railLayout.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, 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' }) }) }));
|
|
2039
|
+
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: fullscreen ? teamLayout : undefined, 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,
|
|
1787
2040
|
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
1788
2041
|
// the full-screen branch replaces the whole viewport with the overlay,
|
|
1789
2042
|
// and a working line under a settings screen is noise attached to a
|
|
@@ -1821,7 +2074,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1821
2074
|
// branch replaces the whole viewport with the overlay, and an update
|
|
1822
2075
|
// notice under a settings screen is noise attached to a surface it is
|
|
1823
2076
|
// not visible on.
|
|
1824
|
-
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,
|
|
2077
|
+
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,
|
|
2078
|
+
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2079
|
+
// projection of "many sources of truth" into the props layer, and a
|
|
2080
|
+
// caller could update three of them; a single reading cannot be
|
|
2081
|
+
// partially stale. The window and its `known` flag now travel INSIDE
|
|
2082
|
+
// it, resolved by the meter - which is what makes the user's
|
|
2083
|
+
// `contextWindow` override reach the bar.
|
|
2084
|
+
context: state.context, 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
|
|
1825
2085
|
? {
|
|
1826
2086
|
teamActive: {
|
|
1827
2087
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -1831,9 +2091,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1831
2091
|
}
|
|
1832
2092
|
: {}), ...(state.todos && !showRail
|
|
1833
2093
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
1834
|
-
: {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.compaction?.live
|
|
2094
|
+
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.compaction?.live
|
|
2095
|
+
? { compactionActive: { inFlight: state.compaction.inFlight } }
|
|
2096
|
+
: {}), ...(controller.isCompactionEnabled()
|
|
1835
2097
|
? {
|
|
1836
|
-
compactionActive: { inFlight: state.compaction.inFlight },
|
|
1837
2098
|
gaugeMarks: {
|
|
1838
2099
|
warn: cfg.compaction.warnThreshold * 100,
|
|
1839
2100
|
high: cfg.compaction.threshold * 100,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import type { Theme } from './theme.js';
|
|
8
8
|
import type { TermCapabilities } from './capabilities.js';
|
|
9
|
+
import { type AutocompleteLayout } from './layout/autocomplete.js';
|
|
9
10
|
export interface Suggestion {
|
|
10
11
|
label: string;
|
|
11
12
|
hint?: string;
|
|
@@ -16,6 +17,7 @@ interface AutocompletePopupProps {
|
|
|
16
17
|
theme: Theme;
|
|
17
18
|
caps: TermCapabilities;
|
|
18
19
|
maxRows?: number;
|
|
20
|
+
layout?: AutocompleteLayout;
|
|
19
21
|
}
|
|
20
|
-
export declare function AutocompletePopup({ items, selected, theme, caps, maxRows, }: AutocompletePopupProps): React.ReactElement | null;
|
|
22
|
+
export declare function AutocompletePopup({ items, selected, theme, caps, maxRows, layout, }: AutocompletePopupProps): React.ReactElement | null;
|
|
21
23
|
export {};
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { pickGlyphs } from './glyphs.js';
|
|
4
|
-
|
|
5
|
-
export function AutocompletePopup({ items, selected, theme, caps, maxRows
|
|
6
|
-
|
|
4
|
+
import { buildAutocompleteLayout } from './layout/autocomplete.js';
|
|
5
|
+
export function AutocompletePopup({ items, selected, theme, caps, maxRows, layout, }) {
|
|
6
|
+
const projection = layout ?? buildAutocompleteLayout({ itemCount: items.length, selected, maxRows });
|
|
7
|
+
if (projection.rowCount === 0)
|
|
7
8
|
return null;
|
|
8
9
|
const glyphs = pickGlyphs(caps);
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const visible = items.slice(windowStart, windowStart + maxRows);
|
|
13
|
-
const moreBelow = items.length - (windowStart + visible.length);
|
|
14
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: glyphs.boxStyle, borderColor: theme.border, paddingX: 1, children: [visible.map((item, i) => {
|
|
10
|
+
const { start: windowStart, count, moreBelow, showMore } = projection;
|
|
11
|
+
const visible = items.slice(windowStart, windowStart + count);
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: theme.border, paddingX: 1, children: [visible.map((item, i) => {
|
|
15
13
|
const idx = windowStart + i;
|
|
16
14
|
const active = idx === selected;
|
|
17
|
-
return (_jsxs(Text, { inverse: active, children: [_jsx(Text, { color: active ? undefined : theme.primary, children: item.label }), item.hint ? _jsxs(Text, { color: theme.muted, children: [" ", item.hint] }) : null] },
|
|
18
|
-
}),
|
|
15
|
+
return (_jsxs(Text, { inverse: active, wrap: "truncate", children: [_jsx(Text, { color: active ? undefined : theme.primary, children: item.label.replace(/\s+/g, ' ') }), item.hint ? _jsxs(Text, { color: theme.muted, children: [" ", item.hint.replace(/\s+/g, ' ')] }) : null] }, idx));
|
|
16
|
+
}), showMore && _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [" +", moreBelow, " more"] })] }));
|
|
19
17
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* box is its only `flexShrink={1}` child — so an extra chrome row does not make
|
|
12
12
|
* the frame taller, Yoga takes the row out of the TRANSCRIPT. Meanwhile
|
|
13
13
|
* `viewportRows()` keeps returning the old number to `ScrollViewport`,
|
|
14
|
-
* `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `
|
|
14
|
+
* `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `buildTodoRailLayout`.
|
|
15
15
|
* The transcript would draw one row shorter than every consumer believes, ONLY
|
|
16
16
|
* WHILE A RUN IS IN FLIGHT, so the layout would shift on submit and shift back on
|
|
17
17
|
* `agent_end`.
|