@aragon-agent/cli 0.6.3 → 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 +105 -0
- package/README.md +95 -2
- package/dist/agent/controller.d.ts +119 -0
- package/dist/agent/controller.js +275 -1
- package/dist/agent/reducer.d.ts +69 -0
- package/dist/agent/reducer.js +100 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +84 -22
- package/dist/commands/builtins.js +92 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +34 -0
- package/dist/compaction/compactor.js +158 -37
- package/dist/compaction/types.d.ts +36 -0
- package/dist/compaction/wiring.d.ts +9 -0
- package/dist/compaction/wiring.js +22 -1
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +48 -1
- package/dist/config/load.d.ts +11 -0
- package/dist/config/load.js +39 -1
- package/dist/config/schema.d.ts +85 -0
- package/dist/config/schema.js +33 -0
- package/dist/config/store.js +21 -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/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 +256 -7
- package/dist/ui/Composer.d.ts +54 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +49 -3
- package/dist/ui/PromptInput.js +184 -52
- package/dist/ui/StatusBar.d.ts +19 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/Transcript.js +29 -1
- 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/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- 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 +49 -0
- 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
|
@@ -17,6 +17,7 @@ import { updatePersistedConfig } from '../config/store.js';
|
|
|
17
17
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
18
18
|
import { bumpSubmitCount, setMouseNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
|
|
19
19
|
import { getLogger } from '../logging/logger.js';
|
|
20
|
+
import { isTerminalStatus } from '../proc/types.js';
|
|
20
21
|
import { registerSecret } from '../logging/secret-registry.js';
|
|
21
22
|
import { detectCapabilities } from './capabilities.js';
|
|
22
23
|
import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
|
|
@@ -105,6 +106,24 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
105
106
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
106
107
|
};
|
|
107
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
|
+
}
|
|
108
127
|
/**
|
|
109
128
|
* The lazy `useReducer` initializer. Module scope, so the reference is stable
|
|
110
129
|
* across renders and React never re-runs it.
|
|
@@ -112,7 +131,7 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
112
131
|
function seedViewState(thinkingVisible) {
|
|
113
132
|
return initialViewState({ thinkingVisible });
|
|
114
133
|
}
|
|
115
|
-
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, terminal, }) {
|
|
134
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, }) {
|
|
116
135
|
const { exit } = useApp();
|
|
117
136
|
const { stdout } = useStdout();
|
|
118
137
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -198,6 +217,37 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
198
217
|
confirmRef.current = confirmState;
|
|
199
218
|
const ctrlCArmed = useRef(false);
|
|
200
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;
|
|
201
251
|
const runBaselineOut = useRef(0);
|
|
202
252
|
/**
|
|
203
253
|
* Epoch ms the current run began — the activity phrase's seed.
|
|
@@ -282,6 +332,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
282
332
|
aborted: false,
|
|
283
333
|
errored: false,
|
|
284
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
|
+
}, []);
|
|
285
349
|
/**
|
|
286
350
|
* Disarm an armed continuation. Idempotent; NEVER touches the budget — a
|
|
287
351
|
* cancelled continuation was never issued, so it must not be charged (AC-18).
|
|
@@ -368,7 +432,28 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
368
432
|
*/
|
|
369
433
|
const patchSource = useMemo(() => ({ take: (id) => controller.takeFilePatch(id) }), [controller]);
|
|
370
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;
|
|
371
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
|
+
}
|
|
372
457
|
const cost = controller.getModelInfo().cost;
|
|
373
458
|
for (const action of reduceEvent(event, cost, patchSource)) {
|
|
374
459
|
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
@@ -395,6 +480,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
395
480
|
}
|
|
396
481
|
if (event.type !== 'agent_end')
|
|
397
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();
|
|
398
488
|
// NO OVERLAY OUTLIVES ITS RUN (P1-4). `ctx.signal` already covers the
|
|
399
489
|
// abort path, so this is DELIBERATE REDUNDANCY: it makes "the card cannot
|
|
400
490
|
// survive the run that opened it" a property of the App's lifecycle
|
|
@@ -484,7 +574,37 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
484
574
|
cancelFollowThrough();
|
|
485
575
|
unsubscribe();
|
|
486
576
|
};
|
|
487
|
-
}, [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]);
|
|
488
608
|
// --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
|
|
489
609
|
//
|
|
490
610
|
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
|
|
@@ -983,6 +1103,56 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
983
1103
|
// --- Helpers -----------------------------------------------------------
|
|
984
1104
|
const notify = (level, text) => dispatch({ type: 'notice', level, text });
|
|
985
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
|
+
}, []);
|
|
986
1156
|
// --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
|
|
987
1157
|
//
|
|
988
1158
|
// TWO ARRIVAL ORDERS, AND BOTH HAPPEN. The service is built by a dynamic
|
|
@@ -1119,6 +1289,16 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1119
1289
|
};
|
|
1120
1290
|
const doExit = () => {
|
|
1121
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 });
|
|
1122
1302
|
exit();
|
|
1123
1303
|
};
|
|
1124
1304
|
/**
|
|
@@ -1407,6 +1587,26 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1407
1587
|
// press; it clears the selection here like every other key and nothing more.
|
|
1408
1588
|
selectionController?.clear();
|
|
1409
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
|
+
}
|
|
1410
1610
|
if (ctrlCArmed.current) {
|
|
1411
1611
|
doExit();
|
|
1412
1612
|
return;
|
|
@@ -1526,9 +1726,17 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1526
1726
|
// and without this the compaction card renders its own `ctrl+o to expand`
|
|
1527
1727
|
// hint, the keystroke silently expands an older tool card instead, and
|
|
1528
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.
|
|
1529
1734
|
const last = [...stateRef.current.entries]
|
|
1530
1735
|
.reverse()
|
|
1531
|
-
.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));
|
|
1532
1740
|
if (last)
|
|
1533
1741
|
dispatch({ type: 'toggleExpand', id: last.id });
|
|
1534
1742
|
// WIDENED FROM `No tool output to expand.`: with three expandable kinds the
|
|
@@ -1554,6 +1762,34 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1554
1762
|
return;
|
|
1555
1763
|
}
|
|
1556
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
|
+
}
|
|
1557
1793
|
controller.abort();
|
|
1558
1794
|
// Next to `abortMark`, in the same callback, for the reason
|
|
1559
1795
|
// `endReasonRef`'s own comment gives (C-11). D-5 turns this into
|
|
@@ -1561,7 +1797,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1561
1797
|
// restating what they interrupted is the CLI arguing.
|
|
1562
1798
|
endReasonRef.current.aborted = true;
|
|
1563
1799
|
dispatch({ type: 'abortMark' });
|
|
1564
|
-
|
|
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.');
|
|
1565
1808
|
return;
|
|
1566
1809
|
}
|
|
1567
1810
|
// IDLE. Today this branch does nothing at all, which is what makes it free
|
|
@@ -1583,6 +1826,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1583
1826
|
const overlay = state.overlay;
|
|
1584
1827
|
const hasKey = controller.hasApiKey();
|
|
1585
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);
|
|
1586
1832
|
const tokPerSec = state.status === 'running' && elapsedMs > 500
|
|
1587
1833
|
? Math.max(0, Math.round((state.usageTotal.outputTokens - runBaselineOut.current) / (elapsedMs / 1000)))
|
|
1588
1834
|
: 0;
|
|
@@ -1627,7 +1873,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1627
1873
|
// Row budget for the viewport and everything sized against it (§4.2a). The
|
|
1628
1874
|
// inline path has no fixed frame, hence no height to fit into: `Infinity`
|
|
1629
1875
|
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
1630
|
-
|
|
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);
|
|
1631
1880
|
const overlayMaxRows = fullscreen ? viewportBudget : Number.POSITIVE_INFINITY;
|
|
1632
1881
|
// --- The todo rail (todo-plan-execution §3.9). --------------------------
|
|
1633
1882
|
//
|
|
@@ -1782,7 +2031,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1782
2031
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
1783
2032
|
// only decides which one the user is taught.
|
|
1784
2033
|
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' }) }) }));
|
|
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' }) }) }));
|
|
1786
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,
|
|
1787
2036
|
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
1788
2037
|
// the full-screen branch replaces the whole viewport with the overlay,
|
|
@@ -1831,7 +2080,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1831
2080
|
}
|
|
1832
2081
|
: {}), ...(state.todos && !showRail
|
|
1833
2082
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
1834
|
-
: {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.compaction?.live
|
|
2083
|
+
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.compaction?.live
|
|
1835
2084
|
? {
|
|
1836
2085
|
compactionActive: { inFlight: state.compaction.inFlight },
|
|
1837
2086
|
gaugeMarks: {
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import React from 'react';
|
|
18
18
|
import type { Theme } from './theme.js';
|
|
19
19
|
import type { TermCapabilities } from './capabilities.js';
|
|
20
|
+
import { pickGlyphs } from './glyphs.js';
|
|
20
21
|
import { type CommandOption } from './PromptInput.js';
|
|
21
22
|
import { type AgentMode } from '../agent/agent-mode.js';
|
|
22
23
|
export interface ComposerProps {
|
|
@@ -60,9 +61,61 @@ export interface ComposerProps {
|
|
|
60
61
|
* un-faded in `hintText`.
|
|
61
62
|
*/
|
|
62
63
|
scrolledLines?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Live background services (background-service-supervision §3.8), 0 when none.
|
|
66
|
+
*
|
|
67
|
+
* IT IS STATE, NOT A FADEABLE HINT, exactly like `scrolledLines` above: the
|
|
68
|
+
* clause it adds is the only visible way to stop something, and `hintText`
|
|
69
|
+
* below keeps it out of the faded form for the same reason `esc abort` is kept
|
|
70
|
+
* there.
|
|
71
|
+
*/
|
|
72
|
+
services?: number;
|
|
73
|
+
/**
|
|
74
|
+
* The composer's DRAFT row count, forwarded upward (tui-paste-handling
|
|
75
|
+
* section 5.5).
|
|
76
|
+
*
|
|
77
|
+
* `hasDraft` stays here, because the border reacts to it and routing that
|
|
78
|
+
* through `App` would re-render the transcript on the first keystroke of every
|
|
79
|
+
* message. The row count is the opposite case: `layout/budget.ts` needs it, so
|
|
80
|
+
* it has to reach `App` — and it changes on a line break rather than on a
|
|
81
|
+
* character, which is what keeps the re-render rate the same (R-6).
|
|
82
|
+
*/
|
|
83
|
+
onDraftRows?: (rows: number) => void;
|
|
84
|
+
/** Paste refusals from `PromptInput`, routed to `App`'s notice dispatch. */
|
|
85
|
+
onNotice?: (level: 'warn' | 'error', text: string) => void;
|
|
63
86
|
theme: Theme;
|
|
64
87
|
caps: TermCapabilities;
|
|
65
88
|
onSubmit: (text: string) => void;
|
|
66
89
|
onHelp?: () => void;
|
|
67
90
|
}
|
|
68
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Build the hint row.
|
|
93
|
+
*
|
|
94
|
+
* The `running` branch is NEVER abbreviated, no matter how experienced the user
|
|
95
|
+
* is. Once the status bar's hint cluster was removed, the composer became the
|
|
96
|
+
* only place that names the abort key — and fading that out after eight
|
|
97
|
+
* submissions would leave a long-running task with no visible way to stop it.
|
|
98
|
+
* Progressive disclosure is for teaching discoverable things, not for retiring
|
|
99
|
+
* an emergency exit (§4.6 / A-14 / M-10b).
|
|
100
|
+
*/
|
|
101
|
+
declare function hintText(opts: {
|
|
102
|
+
running: boolean;
|
|
103
|
+
submitCount: number;
|
|
104
|
+
glyphs: ReturnType<typeof pickGlyphs>;
|
|
105
|
+
mode: AgentMode;
|
|
106
|
+
toggleKey: string;
|
|
107
|
+
/** Live background services; 0 leaves both rows byte-identical to today's. */
|
|
108
|
+
services: number;
|
|
109
|
+
}): string;
|
|
110
|
+
/**
|
|
111
|
+
* `hintText`, for the interrupt-ladder tests (AC-43).
|
|
112
|
+
*
|
|
113
|
+
* EXPORTED UNDER A TEST-FACING NAME rather than exporting `hintText` itself, so
|
|
114
|
+
* the private helper stays private in the module graph and nothing in `src/`
|
|
115
|
+
* grows a second caller by accident. The row is the ONLY visible way to stop a
|
|
116
|
+
* process the agent left running, so it is worth asserting on directly rather
|
|
117
|
+
* than fishing the string out of a rendered frame.
|
|
118
|
+
*/
|
|
119
|
+
export declare const hintTextForTest: typeof hintText;
|
|
120
|
+
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
121
|
+
export {};
|
package/dist/ui/Composer.js
CHANGED
|
@@ -32,9 +32,28 @@ import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
|
|
|
32
32
|
* Progressive disclosure is for teaching discoverable things, not for retiring
|
|
33
33
|
* an emergency exit (§4.6 / A-14 / M-10b).
|
|
34
34
|
*/
|
|
35
|
-
function hintText(
|
|
35
|
+
function hintText(opts) {
|
|
36
|
+
const { running, submitCount, glyphs, mode, toggleKey, services } = opts;
|
|
36
37
|
const dot = ` ${glyphs.midDot} `;
|
|
38
|
+
// THE EXIT CLAUSE SURVIVES IN BOTH ROWS (P1-4). The obvious edit here is to
|
|
39
|
+
// replace `ctrl+c*2 exit` with `ctrl+c stop N` — and that would delete the
|
|
40
|
+
// only visible way to quit at exactly the moment Ctrl+C stops meaning "quit",
|
|
41
|
+
// on the screen a user reaches when something has already gone wrong. Both
|
|
42
|
+
// clauses are shown; the row is `wrap="truncate"` and they are ordered
|
|
43
|
+
// most-urgent-first, so a terminal too narrow for all of them drops the exit
|
|
44
|
+
// clause last rather than the abort key.
|
|
37
45
|
if (running) {
|
|
46
|
+
if (services > 0) {
|
|
47
|
+
return [
|
|
48
|
+
`${glyphs.enterKey} steer`,
|
|
49
|
+
// Rung two is named because the whole point of the ladder is that the
|
|
50
|
+
// first press can fail; a user who does not know there is a second press
|
|
51
|
+
// is left exactly where the reported screenshot left them.
|
|
52
|
+
`esc abort${glyphs.times}2 force`,
|
|
53
|
+
`ctrl+c stop ${services}`,
|
|
54
|
+
`ctrl+c${glyphs.times}2 exit`,
|
|
55
|
+
].join(dot);
|
|
56
|
+
}
|
|
38
57
|
return [`${glyphs.enterKey} steer`, 'esc abort', `ctrl+c${glyphs.times}2 exit`].join(dot);
|
|
39
58
|
}
|
|
40
59
|
// The hint names the DESTINATION, not the current state — the correct label
|
|
@@ -45,9 +64,14 @@ function hintText(running, submitCount, glyphs, mode, toggleKey) {
|
|
|
45
64
|
// retire things the user has demonstrably learned, and `submitCount` counts
|
|
46
65
|
// submissions — a user can send fifty messages without ever discovering
|
|
47
66
|
// Shift+Tab. Same argument that keeps `esc abort` un-faded above.
|
|
67
|
+
// NEVER FADED, for the reason the props doc gives: it is the only visible way
|
|
68
|
+
// to stop a process the agent left running, and progressive disclosure retires
|
|
69
|
+
// things the user has demonstrably learned — not emergency exits.
|
|
70
|
+
const stop = services > 0 ? [`ctrl+c stop ${services}`] : [];
|
|
48
71
|
if (submitCount >= HINT_FADE_AFTER)
|
|
49
|
-
return [toggle, '? help'].join(dot);
|
|
72
|
+
return [...stop, toggle, '? help'].join(dot);
|
|
50
73
|
return [
|
|
74
|
+
...stop,
|
|
51
75
|
`${glyphs.enterKey} send`,
|
|
52
76
|
`${glyphs.shiftEnter} newline`,
|
|
53
77
|
'/ commands',
|
|
@@ -56,9 +80,22 @@ function hintText(running, submitCount, glyphs, mode, toggleKey) {
|
|
|
56
80
|
'? help',
|
|
57
81
|
].join(dot);
|
|
58
82
|
}
|
|
59
|
-
|
|
83
|
+
/**
|
|
84
|
+
* `hintText`, for the interrupt-ladder tests (AC-43).
|
|
85
|
+
*
|
|
86
|
+
* EXPORTED UNDER A TEST-FACING NAME rather than exporting `hintText` itself, so
|
|
87
|
+
* the private helper stays private in the module graph and nothing in `src/`
|
|
88
|
+
* grows a second caller by accident. The row is the ONLY visible way to stop a
|
|
89
|
+
* process the agent left running, so it is worth asserting on directly rather
|
|
90
|
+
* than fishing the string out of a rendered frame.
|
|
91
|
+
*/
|
|
92
|
+
export const hintTextForTest = hintText;
|
|
93
|
+
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services = 0, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
|
|
60
94
|
const [hasDraft, setHasDraft] = useState(false);
|
|
61
|
-
const onDraftChange = useCallback((next) =>
|
|
95
|
+
const onDraftChange = useCallback((next) => {
|
|
96
|
+
setHasDraft(next.hasDraft);
|
|
97
|
+
onDraftRows?.(next.rows);
|
|
98
|
+
}, [onDraftRows]);
|
|
62
99
|
// Plan mode inserts ONE branch, above `draft` and below `running`. The
|
|
63
100
|
// ordering is load-bearing: an in-flight run's border must stay `toolRunning`,
|
|
64
101
|
// because "is it running" is more urgent than "which mode".
|
|
@@ -73,8 +110,15 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
|
|
|
73
110
|
: theme.idleBorder ?? theme.border;
|
|
74
111
|
const glyphs = pickGlyphs(caps);
|
|
75
112
|
const visible = showHint && hintsEnabled;
|
|
76
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, scrolledLines: scrolledLines }), visible && (
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
|
|
77
114
|
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
78
115
|
// costs zero rows on a 24-row terminal.
|
|
79
|
-
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(
|
|
116
|
+
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText({
|
|
117
|
+
running,
|
|
118
|
+
submitCount,
|
|
119
|
+
glyphs,
|
|
120
|
+
mode: agentMode,
|
|
121
|
+
toggleKey: modeToggleKey,
|
|
122
|
+
services,
|
|
123
|
+
}) })] }))] }));
|
|
80
124
|
}
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { TermCapabilities } from './capabilities.js';
|
|
|
24
24
|
import { pickGlyphs } from './glyphs.js';
|
|
25
25
|
import { type EditIntent } from '../input/keymap.js';
|
|
26
26
|
import { type Suggestion } from './AutocompletePopup.js';
|
|
27
|
+
import { type EditorState, type InputSegment } from './editor-reducer.js';
|
|
27
28
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
28
29
|
export interface CommandOption {
|
|
29
30
|
name: string;
|
|
@@ -64,8 +65,30 @@ interface PromptInputProps {
|
|
|
64
65
|
bordered?: {
|
|
65
66
|
color?: string;
|
|
66
67
|
};
|
|
67
|
-
/**
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Fires when the draft flips empty ⇄ non-empty (for the border color) OR when
|
|
70
|
+
* its ROW COUNT changes (for `layout/budget.ts`) — never per keystroke.
|
|
71
|
+
*
|
|
72
|
+
* Widened from `(hasDraft: boolean)` by tui-paste-handling section 5.5. The row
|
|
73
|
+
* count is what makes I-8 true: the composer's rendered height and the
|
|
74
|
+
* `draftRows` handed to `viewportRows` have to be the same number, and the only
|
|
75
|
+
* component that knows the first one is this one. It is reported ALREADY
|
|
76
|
+
* CLAMPED, so a 218-line draft and a 400-line draft report the same value and
|
|
77
|
+
* the transcript stops moving once the composer is full (R-12).
|
|
78
|
+
*/
|
|
79
|
+
onDraftChange?: (state: {
|
|
80
|
+
hasDraft: boolean;
|
|
81
|
+
rows: number;
|
|
82
|
+
}) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Tell the user a paste was refused (D-11 / P1-4).
|
|
85
|
+
*
|
|
86
|
+
* `PASTE_DRAFT_MAX_BYTES` and `PASTE_MAX_BLOCKS` cannot be enforced in the
|
|
87
|
+
* filter (it does not know the draft) and cannot be enforced in the reducer (it
|
|
88
|
+
* is pure, so it can neither raise a toast nor say why it refused). This
|
|
89
|
+
* component is the only place that holds both facts.
|
|
90
|
+
*/
|
|
91
|
+
onNotice?: (level: 'warn' | 'error', text: string) => void;
|
|
69
92
|
/**
|
|
70
93
|
* Rows of newer output hidden BELOW the viewport — `ScrollViewport`'s offset
|
|
71
94
|
* (tui-selection-and-scroll-follow §4.2 / G7).
|
|
@@ -99,6 +122,29 @@ export interface ScrollChip {
|
|
|
99
122
|
* scan fails the build if that rule is broken here.
|
|
100
123
|
*/
|
|
101
124
|
export declare function scrollChip(scrolledLines: number, cols: number, glyphs: ReturnType<typeof pickGlyphs>): ScrollChip | null;
|
|
125
|
+
export interface OverflowChip {
|
|
126
|
+
text: string;
|
|
127
|
+
/** FIXED width, so a number gaining a digit cannot re-wrap the draft. */
|
|
128
|
+
cells: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build the "there is more draft above / below this window" indicator, or `null`
|
|
132
|
+
* when the whole draft is on screen.
|
|
133
|
+
*
|
|
134
|
+
* Every glyph comes from `pickGlyphs` and none from a literal (I-5): on a legacy
|
|
135
|
+
* console this renders `^3 v7` rather than mojibake, and `glyphs.test.ts`'s
|
|
136
|
+
* static scan fails the build if that rule is broken here.
|
|
137
|
+
*/
|
|
138
|
+
export declare function overflowChip(hiddenAbove: number, hiddenBelow: number, glyphs: ReturnType<typeof pickGlyphs>): OverflowChip | null;
|
|
139
|
+
/**
|
|
140
|
+
* Would these segments push the draft past a limit? Returns the refusal text, or
|
|
141
|
+
* `null` when the paste is allowed.
|
|
142
|
+
*
|
|
143
|
+
* Checked BEFORE `dispatch`, so a refusal dispatches nothing at all and the draft
|
|
144
|
+
* is left byte-for-byte unchanged (AC-9). The one-dispatch-per-key rule is
|
|
145
|
+
* unaffected for the same reason.
|
|
146
|
+
*/
|
|
147
|
+
export declare function draftLimitRefusal(segments: InputSegment[], editor: Pick<EditorState, 'pastes'>): string | null;
|
|
102
148
|
/** Apply an edit intent as a pure buffer/cursor transform. */
|
|
103
149
|
export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
|
|
104
150
|
buffer: string;
|
|
@@ -127,5 +173,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
127
173
|
end: number;
|
|
128
174
|
query: string;
|
|
129
175
|
} | null;
|
|
130
|
-
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, scrolledLines, }: PromptInputProps): React.ReactElement;
|
|
176
|
+
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
|
|
131
177
|
export {};
|