@aragon-agent/cli 0.6.8 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +89 -44
- package/dist/agent/controller.d.ts +1 -1
- package/dist/agent/reducer.d.ts +31 -113
- package/dist/agent/reducer.js +41 -63
- package/dist/cli.js +53 -155
- package/dist/commands/builtins.d.ts +2 -1
- package/dist/commands/builtins.js +47 -2
- package/dist/commands/perf.d.ts +32 -2
- package/dist/commands/perf.js +32 -2
- package/dist/commands/registry.d.ts +2 -2
- package/dist/config/env.js +0 -130
- package/dist/config/load.d.ts +0 -2
- package/dist/config/load.js +2 -31
- package/dist/config/schema.d.ts +1 -14
- package/dist/config/schema.js +0 -1
- package/dist/config/store.js +2 -0
- package/dist/input/enter-sequences.d.ts +63 -0
- package/dist/input/enter-sequences.js +151 -0
- package/dist/input/limits.d.ts +11 -0
- package/dist/input/limits.js +11 -0
- package/dist/input/stdin-filter.js +19 -1
- package/dist/logging/install.d.ts +2 -2
- package/dist/logging/install.js +2 -2
- package/dist/session/persist.js +20 -0
- package/dist/todo/follow-through.d.ts +0 -4
- package/dist/todo/follow-through.js +0 -4
- package/dist/todo/limits.d.ts +0 -10
- package/dist/todo/limits.js +0 -10
- package/dist/todo/prompt.d.ts +0 -15
- package/dist/ui/ActivityLine.d.ts +26 -34
- package/dist/ui/ActivityLine.js +30 -19
- package/dist/ui/App.d.ts +1 -41
- package/dist/ui/App.js +94 -129
- package/dist/ui/BottomStatusRow.d.ts +1 -3
- package/dist/ui/BottomStatusRow.js +4 -8
- package/dist/ui/Composer.d.ts +28 -1
- package/dist/ui/Composer.js +46 -17
- package/dist/ui/Header.d.ts +2 -16
- package/dist/ui/Header.js +3 -18
- package/dist/ui/Logo.d.ts +1 -17
- package/dist/ui/Logo.js +0 -16
- package/dist/ui/ModeChip.d.ts +7 -18
- package/dist/ui/ModeChip.js +9 -0
- package/dist/ui/PromptInput.d.ts +2 -28
- package/dist/ui/PromptInput.js +38 -9
- package/dist/ui/StatusBar.d.ts +0 -11
- package/dist/ui/ToastStack.d.ts +2 -18
- package/dist/ui/ToastStack.js +4 -11
- package/dist/ui/Transcript.d.ts +5 -71
- package/dist/ui/Transcript.js +15 -199
- package/dist/ui/enter-frames.d.ts +63 -0
- package/dist/ui/enter-frames.js +136 -0
- package/dist/ui/entries/AssistantEntry.d.ts +0 -23
- package/dist/ui/entries/AssistantEntry.js +3 -4
- package/dist/ui/entries/DiffView.d.ts +1 -7
- package/dist/ui/entries/DiffView.js +2 -2
- package/dist/ui/entries/QueuedEntry.d.ts +27 -0
- package/dist/ui/entries/QueuedEntry.js +30 -0
- package/dist/ui/entries/ToolCard.d.ts +0 -6
- package/dist/ui/entries/ToolCard.js +4 -13
- package/dist/ui/frame-differ.d.ts +13 -0
- package/dist/ui/frame-differ.js +36 -6
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/ink-runtime.d.ts +1 -0
- package/dist/ui/ink-runtime.js +21 -0
- package/dist/ui/layout/AppShell.d.ts +1 -10
- package/dist/ui/layout/AppShell.js +2 -5
- package/dist/ui/layout/OverlayFrame.d.ts +1 -25
- package/dist/ui/layout/OverlayFrame.js +0 -24
- package/dist/ui/layout/ScrollViewport.d.ts +3 -0
- package/dist/ui/layout/ScrollViewport.js +17 -5
- package/dist/ui/layout/frame.d.ts +0 -22
- package/dist/ui/layout/frame.js +0 -36
- package/dist/ui/layout/todo-layout.d.ts +1 -3
- package/dist/ui/layout/todo-layout.js +7 -8
- package/dist/ui/layout/viewport-geometry.d.ts +1 -1
- package/dist/ui/layout/viewport-geometry.js +1 -1
- package/dist/ui/live-clamp.d.ts +1 -29
- package/dist/ui/live-clamp.js +1 -29
- package/dist/ui/overlays/HelpOverlay.js +3 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +5 -1
- package/dist/ui/overlays/QuestionOverlay.js +7 -2
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/run-status-row.d.ts +61 -0
- package/dist/ui/run-status-row.js +70 -0
- package/dist/ui/selection/selection-controller.d.ts +23 -7
- package/dist/ui/selection/selection-controller.js +68 -16
- package/dist/ui/transcript-text.d.ts +0 -8
- package/dist/ui/transcript-text.js +0 -8
- package/dist/ui/use-startup-notices.d.ts +6 -58
- package/dist/ui/use-startup-notices.js +10 -57
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/ui/TodoStrip.d.ts +0 -36
- package/dist/ui/TodoStrip.js +0 -32
package/dist/agent/reducer.js
CHANGED
|
@@ -289,6 +289,29 @@ function nextId(state) {
|
|
|
289
289
|
function mapEntry(entries, id, fn) {
|
|
290
290
|
return entries.map((e) => (e.id === id ? fn(e) : e));
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Rewrite every `queued` entry in place as the user message it became
|
|
294
|
+
* (tui-shift-enter-copy-queue 5.2.3).
|
|
295
|
+
*
|
|
296
|
+
* SAME ID, NEW OBJECT -- the `mapEntry` discipline for a non-append change:
|
|
297
|
+
* the position (and therefore the settled boundary, which advances by
|
|
298
|
+
* POSITION and never by identity) is untouched, while the new object
|
|
299
|
+
* reference is what lets `React.memo` and the height cache see that this
|
|
300
|
+
* one row changed. ALL of them at once, because `drainSteering` is a full
|
|
301
|
+
* drain: converting only some would show a partial acceptance that never
|
|
302
|
+
* happened. Identity-preserving when nothing is queued -- `turnStart` fires
|
|
303
|
+
* every turn, and this must stay free for the common case.
|
|
304
|
+
*/
|
|
305
|
+
function convertQueuedEntries(entries) {
|
|
306
|
+
let touched = false;
|
|
307
|
+
const next = entries.map((e) => {
|
|
308
|
+
if (e.kind !== 'queued')
|
|
309
|
+
return e;
|
|
310
|
+
touched = true;
|
|
311
|
+
return { id: e.id, kind: 'user', text: e.text };
|
|
312
|
+
});
|
|
313
|
+
return touched ? next : entries;
|
|
314
|
+
}
|
|
292
315
|
/**
|
|
293
316
|
* Seal `thinkingMs`, once, as a spreadable patch (§3.1.2).
|
|
294
317
|
*
|
|
@@ -382,31 +405,6 @@ function settleRetryCard(state, entries, phase) {
|
|
|
382
405
|
retryEntryId: undefined,
|
|
383
406
|
};
|
|
384
407
|
}
|
|
385
|
-
/**
|
|
386
|
-
* EVERY PATH THAT REACHES `status: 'idle'` RELEASES THE LIVE TAIL (D-35 / AC-40).
|
|
387
|
-
*
|
|
388
|
-
* `runEnd` finalizes only the streaming ASSISTANT entry and `abortMark` maps only
|
|
389
|
-
* assistant entries, so neither touches a tool entry stranded at
|
|
390
|
-
* `status: 'running'` -- the engine's own idle watchdog, a provider error mid-call
|
|
391
|
-
* and `Esc` all produce one. Today that costs a single spinner row. With a tail
|
|
392
|
-
* attached it costs a permanent multi-row card whose stall counter is frozen at
|
|
393
|
-
* its last value, because the 200 ms ticker that feeds `nowSec` is torn down the
|
|
394
|
-
* instant `state.status` leaves `running` -- and, worse, `computeSettledCount`
|
|
395
|
-
* breaks on a non-settled tool entry, so that card pins `Transcript`'s MONOTONIC
|
|
396
|
-
* settled boundary and re-renders the tail every frame for the rest of the
|
|
397
|
-
* session. The rule and the stakes are `settleRetryCard`'s, verbatim.
|
|
398
|
-
*
|
|
399
|
-
* THE STATUS IS DELIBERATELY NOT CHANGED. That is a different decision with
|
|
400
|
-
* different consequences for `bashBadge` and for the run's own bookkeeping, and
|
|
401
|
-
* it is not this round's to make. Only the tail is released.
|
|
402
|
-
*
|
|
403
|
-
* `liveSeq` is BUMPED rather than cleared, and that is not bookkeeping: clearing
|
|
404
|
-
* `live` is a non-append mutation, and with the status untouched nothing else in
|
|
405
|
-
* the tool revision moves -- so without the bump the height cache would keep
|
|
406
|
-
* serving the multi-row measurement for a card that now draws one row (I-L3-1).
|
|
407
|
-
* Entries with no tail are returned by identity, which is what keeps
|
|
408
|
-
* `EntryView`'s memo boundary intact for the rest of the transcript.
|
|
409
|
-
*/
|
|
410
408
|
function releaseLiveTails(entries) {
|
|
411
409
|
let touched = false;
|
|
412
410
|
const next = entries.map((e) => {
|
|
@@ -450,6 +448,15 @@ export function viewReducer(state, action) {
|
|
|
450
448
|
aborted: false,
|
|
451
449
|
};
|
|
452
450
|
}
|
|
451
|
+
case 'steerQueued': {
|
|
452
|
+
const { id, seq } = nextId(state);
|
|
453
|
+
const entry = { id, kind: 'queued', text: action.text };
|
|
454
|
+
// NOTHING ELSE RESETS. This is not a new turn -- `turnProduced`,
|
|
455
|
+
// `errorNoticed` and `aborted` describe the run that is still in
|
|
456
|
+
// flight, and clearing them here would let the next `runEnd` failure
|
|
457
|
+
// guard misfire on a turn that produced plenty.
|
|
458
|
+
return { ...state, seq, ...appendEntry(state, state.entries, entry) };
|
|
459
|
+
}
|
|
453
460
|
case 'runStart':
|
|
454
461
|
return {
|
|
455
462
|
...state,
|
|
@@ -460,6 +467,14 @@ export function viewReducer(state, action) {
|
|
|
460
467
|
};
|
|
461
468
|
case 'turnStart': {
|
|
462
469
|
const { id, seq } = nextId(state);
|
|
470
|
+
// CONVERT, THEN APPEND, IN THAT ORDER AND IN THIS ONE ACTION
|
|
471
|
+
// (5.2.3): `turn_start` is the authoritative "the steering queue was
|
|
472
|
+
// drained into the conversation" signal (the loop drains at its top,
|
|
473
|
+
// immediately before emitting it), so the user message and the answer
|
|
474
|
+
// that follows it must land in the same commit, in render order, or a
|
|
475
|
+
// frame can exist where the message has vanished and the answer has
|
|
476
|
+
// not appeared.
|
|
477
|
+
const entries = convertQueuedEntries(state.entries);
|
|
463
478
|
const entry = {
|
|
464
479
|
id,
|
|
465
480
|
kind: 'assistant',
|
|
@@ -468,7 +483,7 @@ export function viewReducer(state, action) {
|
|
|
468
483
|
thinkingOpen: false,
|
|
469
484
|
streaming: true,
|
|
470
485
|
};
|
|
471
|
-
return { ...state, seq, ...appendEntry(state,
|
|
486
|
+
return { ...state, seq, ...appendEntry(state, entries, entry), streamingId: id };
|
|
472
487
|
}
|
|
473
488
|
case 'thinkingStart': {
|
|
474
489
|
if (!state.streamingId)
|
|
@@ -1218,22 +1233,6 @@ export function viewReducer(state, action) {
|
|
|
1218
1233
|
entries: mapEntry(state.entries, known, (e) => e.kind === 'retry' ? { ...e, phase: 'retrying', resumeAt: undefined } : e),
|
|
1219
1234
|
};
|
|
1220
1235
|
}
|
|
1221
|
-
/**
|
|
1222
|
-
* Rewind the view for a mid-stream restart.
|
|
1223
|
-
*
|
|
1224
|
-
* THE ONLY ACTION IN THIS REDUCER THAT SHRINKS `entries`, and it is safe for
|
|
1225
|
-
* one specific reason (§6.4 / AC-22). `Transcript` clamps its settled boundary
|
|
1226
|
-
* with a MONOTONIC high-water mark: if `highWater` ever exceeded
|
|
1227
|
-
* `entries.length` the clamp would mark EVERYTHING settled — including the
|
|
1228
|
-
* still-streaming assistant entry — and `<Static>` would duplicate it.
|
|
1229
|
-
*
|
|
1230
|
-
* It cannot, and the ORDERING is why: the entries removed here are
|
|
1231
|
-
* `kind: 'tool'` cards created by `toolCallStart` AFTER `turnStart` created the
|
|
1232
|
-
* streaming assistant entry, so they sit strictly above it in the array; and
|
|
1233
|
-
* the boundary scan already breaks at that entry. Removing only entries above
|
|
1234
|
-
* the boundary is the invariant. A future action that removed a SETTLED entry
|
|
1235
|
-
* would break `<Static>` and nothing would report it.
|
|
1236
|
-
*/
|
|
1237
1236
|
case 'streamRestart': {
|
|
1238
1237
|
const discarded = new Set(action.discardedToolCallIds);
|
|
1239
1238
|
const entries = state.entries.filter((e) => !(e.kind === 'tool' && discarded.has(e.toolCallId)));
|
|
@@ -1326,18 +1325,6 @@ export function viewReducer(state, action) {
|
|
|
1326
1325
|
droppedEntries: appended.droppedEntries,
|
|
1327
1326
|
};
|
|
1328
1327
|
}
|
|
1329
|
-
/**
|
|
1330
|
-
* Rewrite the LIVE card in place.
|
|
1331
|
-
*
|
|
1332
|
-
* SAFE ONLY BECAUSE OF D-11'S OTHER HALF: a `ready` / `running` card has
|
|
1333
|
-
* reached `<Static>` and cannot be re-printed, so `serviceEnd` appends a new
|
|
1334
|
-
* entry instead of routing through here. What this handles is the window in
|
|
1335
|
-
* which the card is still in the live region — `starting`, and the one
|
|
1336
|
-
* transition out of it — plus the output tail while it is there.
|
|
1337
|
-
*
|
|
1338
|
-
* The lookup is by `serviceId` and SKIPS terminal entries, so a stray update
|
|
1339
|
-
* cannot rewrite the one-row record `serviceEnd` appended.
|
|
1340
|
-
*/
|
|
1341
1328
|
case 'serviceUpdate': {
|
|
1342
1329
|
const target = findServiceEntryId(state, action.service.id);
|
|
1343
1330
|
if (!target)
|
|
@@ -1347,15 +1334,6 @@ export function viewReducer(state, action) {
|
|
|
1347
1334
|
entries: mapEntry(state.entries, target, (e) => e.kind === 'service' ? { ...e, ...serviceFields(action.service) } : e),
|
|
1348
1335
|
};
|
|
1349
1336
|
}
|
|
1350
|
-
/**
|
|
1351
|
-
* A service reached a terminal state.
|
|
1352
|
-
*
|
|
1353
|
-
* TWO WRITES, AND BOTH ARE REQUIRED. The live card is settled in place so
|
|
1354
|
-
* `computeSettledCount` can move past it and `estimateEntryRows` stops
|
|
1355
|
-
* charging for a tail nothing will add to; the NEW one-row entry is what the
|
|
1356
|
-
* user actually reads, because the card may already be printed into
|
|
1357
|
-
* `<Static>` and `<Static>` cannot un-print (D-11 / R-13).
|
|
1358
|
-
*/
|
|
1359
1337
|
case 'serviceEnd': {
|
|
1360
1338
|
const target = findServiceEntryId(state, action.service.id);
|
|
1361
1339
|
const settled = target
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,8 @@ import { dirname, join } from 'node:path';
|
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import process from 'node:process';
|
|
17
17
|
import '../runtime/insecure-tls-warning.cjs';
|
|
18
|
-
|
|
18
|
+
// Must precede every other dependency that imports Ink: CI detection is cached.
|
|
19
|
+
import { render } from './ui/ink-runtime.js';
|
|
19
20
|
import { Command } from 'commander';
|
|
20
21
|
import { loadConfig } from './config/load.js';
|
|
21
22
|
import { getConfigPath, readConfigFile, updatePersistedConfig } from './config/store.js';
|
|
@@ -40,7 +41,6 @@ import { registerExecCommand } from './exec/cli-commands.js';
|
|
|
40
41
|
import { registerSessionsCommand } from './session/cli-commands.js';
|
|
41
42
|
import { registerDoctorCommand, registerInfoCommand } from './diagnostics/cli-commands.js';
|
|
42
43
|
import { App } from './ui/App.js';
|
|
43
|
-
import { decideRenderMode } from './ui/layout/frame.js';
|
|
44
44
|
import { createFrameDiffer } from './ui/frame-differ.js';
|
|
45
45
|
import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
|
|
46
46
|
import { setFrameStatsProvider } from './commands/perf.js';
|
|
@@ -56,7 +56,6 @@ import { renderTranscriptText } from './ui/transcript-text.js';
|
|
|
56
56
|
import { detectCapabilities } from './ui/capabilities.js';
|
|
57
57
|
import { pickGlyphs } from './ui/glyphs.js';
|
|
58
58
|
import { getTheme } from './ui/theme.js';
|
|
59
|
-
import { copyText } from './ui/clipboard.js';
|
|
60
59
|
import { createSelectionController, } from './ui/selection/selection-controller.js';
|
|
61
60
|
import { DENY_ALL_HUMAN_INPUT } from './tools/human-input.js';
|
|
62
61
|
// A VALUE import, and the one deliberate exception to the paragraph above
|
|
@@ -96,7 +95,6 @@ function toFlags(opts) {
|
|
|
96
95
|
confirm: opts.confirm,
|
|
97
96
|
toolTimeout: opts.toolTimeout,
|
|
98
97
|
idleTimeout: opts.idleTimeout,
|
|
99
|
-
fullscreen: opts.fullscreen,
|
|
100
98
|
exitTranscript: opts.exitTranscript,
|
|
101
99
|
compact: opts.compact,
|
|
102
100
|
hints: opts.hints,
|
|
@@ -230,14 +228,8 @@ function makeController(flags, opts = { interactive: true }) {
|
|
|
230
228
|
// resolved a bootstrap level — it ran before commander and before `.env` were
|
|
231
229
|
// readable — and without this hand-off the fully resolved one never applies.
|
|
232
230
|
getLogger().reconfigure(config.log);
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
// is pure and `runInteractive` calls it again with the same inputs, so
|
|
236
|
-
// computing it here rather than threading it back costs nothing and keeps the
|
|
237
|
-
// controller's construction self-contained.
|
|
238
|
-
const todoPanelCapable = opts.interactive &&
|
|
239
|
-
decideRenderMode({ fullscreen: config.fullscreen }, process.env, process.stdout) ===
|
|
240
|
-
'fullscreen';
|
|
231
|
+
// Headless controllers never expose the task panel.
|
|
232
|
+
const todoPanelCapable = opts.interactive;
|
|
241
233
|
const confirmBridge = { handler: null };
|
|
242
234
|
// `cancelPending` is replaced by the App's own implementation on mount; this
|
|
243
235
|
// no-op keeps the object total so nothing has to null-check it.
|
|
@@ -389,11 +381,15 @@ function forceVtInputForThisConsole() {
|
|
|
389
381
|
* renders Ink, `runOneShot()` goes to `runHeadless()`, which never does.
|
|
390
382
|
*/
|
|
391
383
|
function runInteractive(flags, extras = {}) {
|
|
384
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
385
|
+
process.stderr.write('Interactive UI requires a TTY. Use -p "<prompt>", exec, or config list / config set.\n');
|
|
386
|
+
process.exitCode = 2;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
392
389
|
const { controller, confirmBridge, humanInputBridge } = makeController(flags);
|
|
393
390
|
const config = controller.getConfig();
|
|
394
|
-
const mode = decideRenderMode({ fullscreen: config.fullscreen }, process.env, process.stdout);
|
|
395
391
|
const logger = getLogger();
|
|
396
|
-
logger.info('cli', 'run_interactive', { mode, provider: config.provider, model: config.model });
|
|
392
|
+
logger.info('cli', 'run_interactive', { mode: 'fullscreen', provider: config.provider, model: config.model });
|
|
397
393
|
attachAgentEvents(logger, controller);
|
|
398
394
|
// The team stream is CLI-local (D-10), so `attachAgentEvents` cannot see it.
|
|
399
395
|
// Without this a dispatch leaves NO trace in the log file at all — the wrong
|
|
@@ -442,8 +438,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
442
438
|
// a console this process just repaired.
|
|
443
439
|
const vtInputByNode = supportsWindowsVtInput(process.platform, process.versions.node);
|
|
444
440
|
const vtInputSupported = vtInputByNode || forceVtInputForThisConsole();
|
|
445
|
-
const wantMouse =
|
|
446
|
-
config.mouse &&
|
|
441
|
+
const wantMouse = config.mouse &&
|
|
447
442
|
!!process.stdout.isTTY &&
|
|
448
443
|
!!process.stdin.isTTY &&
|
|
449
444
|
vtInputSupported;
|
|
@@ -492,21 +487,13 @@ function runInteractive(flags, extras = {}) {
|
|
|
492
487
|
onCopied: null,
|
|
493
488
|
requestRedraw: null,
|
|
494
489
|
};
|
|
495
|
-
|
|
496
|
-
//
|
|
497
|
-
// FULL-SCREEN ONLY, and that is invariant I-3 rather than caution: inline mode
|
|
498
|
-
// prints settled entries through Ink's `<Static>`, starts wherever the shell's
|
|
499
|
-
// cursor happened to be and scrolls, so row k of a frame is not absolute row k
|
|
500
|
-
// there and never will be. `--no-diff-render` skips construction entirely,
|
|
501
|
-
// which is what makes rung 2 of the fail-safe ladder byte-identical to a
|
|
502
|
-
// pre-fix build rather than merely equivalent.
|
|
503
|
-
//
|
|
504
|
-
// `rows` is a THUNK, not a snapshot: it is the only input to the I-9 geometry
|
|
505
|
-
// guard and it changes under the differ's feet on every resize.
|
|
506
|
-
const frameWriter = mode === 'fullscreen' && config.diffRender
|
|
490
|
+
const frameWriter = config.diffRender
|
|
507
491
|
? wrapStdoutForFrames(process.stdout, createFrameDiffer({
|
|
508
492
|
sync: config.syncOutput && process.env.TERM !== 'dumb',
|
|
509
493
|
rows: () => process.stdout.rows,
|
|
494
|
+
// Rows that fill the line skip `CSI K` so the scrollbar's last column
|
|
495
|
+
// is not erased on pending-wrap terminals (frame-differ I-4).
|
|
496
|
+
cols: () => process.stdout.columns,
|
|
510
497
|
// The console bridge (`App.tsx`, full-screen only) turns this into
|
|
511
498
|
// `dispatch({type:'notice', level:'warn'})`, which is the only legal
|
|
512
499
|
// user-visible channel with the TUI mounted — a direct write would
|
|
@@ -539,11 +526,11 @@ function runInteractive(flags, extras = {}) {
|
|
|
539
526
|
// than printing zeroes that read like a writer doing nothing (§5.4).
|
|
540
527
|
if (frameWriter)
|
|
541
528
|
setFrameStatsProvider(frameWriter.stats);
|
|
542
|
-
const frameObserver =
|
|
529
|
+
const frameObserver = createFrameObserver({
|
|
543
530
|
stdout: frameWriter?.stdout ?? process.stdout, terminal: process.stdout, scrollbar,
|
|
544
|
-
})
|
|
531
|
+
});
|
|
545
532
|
const disposeFrameWriter = () => {
|
|
546
|
-
frameObserver
|
|
533
|
+
frameObserver.dispose();
|
|
547
534
|
pointerRouter?.dispose();
|
|
548
535
|
scrollbar.controller?.dispose();
|
|
549
536
|
if (!frameWriter)
|
|
@@ -552,7 +539,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
552
539
|
frameWriter.dispose();
|
|
553
540
|
};
|
|
554
541
|
const replayTranscript = () => {
|
|
555
|
-
if (replayed ||
|
|
542
|
+
if (replayed || !config.exitTranscript)
|
|
556
543
|
return;
|
|
557
544
|
replayed = true;
|
|
558
545
|
const snapshot = readExitSnapshot();
|
|
@@ -571,7 +558,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
571
558
|
...(snapshot.droppedEntries ? { droppedEntries: snapshot.droppedEntries } : {}),
|
|
572
559
|
}));
|
|
573
560
|
};
|
|
574
|
-
|
|
561
|
+
{
|
|
575
562
|
// `mouse` here means "a mouse-parsing filter is installed", NOT "the user
|
|
576
563
|
// wants mouse support" — see `AltScreenOptions`. `screen.ts` must never
|
|
577
564
|
// derive it for itself, or a later refactor re-opens the gap I-8 closes.
|
|
@@ -585,7 +572,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
585
572
|
screen = enterAltScreen(process.stdout, {
|
|
586
573
|
mouse: mouseOn,
|
|
587
574
|
motion: mouseOn,
|
|
588
|
-
bracketedPaste: pasteOn
|
|
575
|
+
bracketedPaste: pasteOn,
|
|
589
576
|
});
|
|
590
577
|
const restore = () => screen?.restore();
|
|
591
578
|
// Four idempotent restore paths (§4.4). The signal hook is NOT redundant:
|
|
@@ -610,42 +597,6 @@ function runInteractive(flags, extras = {}) {
|
|
|
610
597
|
process.exit(128 + signo);
|
|
611
598
|
});
|
|
612
599
|
}
|
|
613
|
-
// --- Auto-update (cli-auto-update section 3.8). --------------------------
|
|
614
|
-
//
|
|
615
|
-
// THE ONLY CONSTRUCTION SITE. `runOneShot` / `runHeadless` never reach this
|
|
616
|
-
// function, so `aragon -p` executes zero update code and its stdout is
|
|
617
|
-
// byte-identical to a pre-feature build (AC-1); `aragon config <subcommand>`,
|
|
618
|
-
// `aragon logs`, `aragon skills` and `aragon --version` likewise. Bare
|
|
619
|
-
// `aragon config` DOES construct it, because it is a full interactive session
|
|
620
|
-
// with a settings overlay on top - which is correct, and is why AC-1's claim
|
|
621
|
-
// names the SUBCOMMAND form.
|
|
622
|
-
//
|
|
623
|
-
// `CI` is excluded on top of the two TTY tests because a CI runner with a pty
|
|
624
|
-
// allocated is otherwise a machine that silently mutates its own global
|
|
625
|
-
// toolchain mid-pipeline (D-11).
|
|
626
|
-
// --- The foreign-write door (§4.4.5). ------------------------------------
|
|
627
|
-
//
|
|
628
|
-
// Where bytes that are OURS but are NOT a frame go — today, the OSC 52
|
|
629
|
-
// clipboard sequence. THREE SHAPES, ONE DOOR, and the third one is the reason
|
|
630
|
-
// this is a resolved value rather than an inline arrow:
|
|
631
|
-
//
|
|
632
|
-
// * with the differ in front of stdout it MUST be `frameWriter.writeForeign`,
|
|
633
|
-
// which invalidates the cache and skips the fallback accounting — through
|
|
634
|
-
// the proxy the same bytes are an unrecognised chunk, which is
|
|
635
|
-
// `passThrough(true)`, which prints `FRAME_FALLBACK_NOTICE` at a user who
|
|
636
|
-
// did nothing but copy something (P1-6);
|
|
637
|
-
// * without one — `--no-diff-render`, and inline mode, which never builds a
|
|
638
|
-
// differ at all — stdout IS the real stream, so a direct write is both safe
|
|
639
|
-
// and what §4.4.5 prescribes. OSC 52 emits no newline, so Ink's own line
|
|
640
|
-
// accounting is untouched and there is nothing to invalidate;
|
|
641
|
-
// * on a non-TTY there is no terminal to accept it, so the door is ABSENT.
|
|
642
|
-
//
|
|
643
|
-
// It must never be a function that accepts a chunk and writes NOWHERE.
|
|
644
|
-
// `copyText` reports `'osc52'` whenever its door took the text, so a silent
|
|
645
|
-
// no-op door — which `frameWriter?.writeForeign(chunk)` is under
|
|
646
|
-
// `--no-diff-render` — makes the toast name a mechanism that was never
|
|
647
|
-
// attempted. Naming the mechanism honestly is the entire reason that return
|
|
648
|
-
// value exists (R-7), and a door that lies is worse than no door.
|
|
649
600
|
const writeForeign = frameWriter
|
|
650
601
|
? (text) => { frameObserver?.invalidate(); frameWriter.writeForeign(text); }
|
|
651
602
|
: process.stdout.isTTY
|
|
@@ -678,10 +629,10 @@ function runInteractive(flags, extras = {}) {
|
|
|
678
629
|
// correct. That is rung 2 of the fail-safe ladder doing its job.
|
|
679
630
|
repaint: () => frameWriter?.repaint() ?? false,
|
|
680
631
|
requestRedraw: () => selectionBridge.requestRedraw?.(),
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
|
|
684
|
-
|
|
632
|
+
// The copy decision moved UP to `App` (tui-shift-enter-copy-queue
|
|
633
|
+
// 4.2.4): Ctrl+C with a settled selection is routed there, through
|
|
634
|
+
// the SAME `copyText` door and the SAME bridge `onCopied` toast
|
|
635
|
+
// funnel -- the controller keeps only the selection state machine.
|
|
685
636
|
// Read at PAINT time, not captured: `/theme` rebuilds the theme mid-session.
|
|
686
637
|
theme: () => getTheme(config.theme, detectCapabilities(process.env, process.stdout)),
|
|
687
638
|
caps: detectCapabilities(process.env, process.stdout),
|
|
@@ -695,45 +646,30 @@ function runInteractive(flags, extras = {}) {
|
|
|
695
646
|
selectionBridge.controller?.dispose();
|
|
696
647
|
selectionBridge.controller = null;
|
|
697
648
|
};
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
// A released mouse cannot be dragging, and a selection left
|
|
723
|
-
// painted over a screen the terminal is now selecting on its
|
|
724
|
-
// own would be two highlights claiming the same rows.
|
|
725
|
-
if (!on)
|
|
726
|
-
selectionBridge.controller?.clear();
|
|
727
|
-
},
|
|
728
|
-
isMouseCaptured: () => mouseCaptured,
|
|
729
|
-
}
|
|
730
|
-
: {}),
|
|
731
|
-
// The same door the controller copies through, so `/copy` and a
|
|
732
|
-
// drag-release can never disagree about which stream OSC 52 goes to.
|
|
733
|
-
...(writeForeign ? { writeForeign } : {}),
|
|
734
|
-
...(selectionBridge.controller ? { selection: selectionBridge } : {}),
|
|
735
|
-
}
|
|
736
|
-
: undefined;
|
|
649
|
+
const terminalBridge = {
|
|
650
|
+
mouseSelect: wantSelect,
|
|
651
|
+
scrollbar,
|
|
652
|
+
...(mouseOn
|
|
653
|
+
? {
|
|
654
|
+
setMouseCapture: (on) => {
|
|
655
|
+
screen?.setMouseCapture(on);
|
|
656
|
+
mouseCaptured = on;
|
|
657
|
+
if (!on)
|
|
658
|
+
scrollbar.controller?.cancel();
|
|
659
|
+
// A released mouse cannot be dragging, and a selection left
|
|
660
|
+
// painted over a screen the terminal is now selecting on its
|
|
661
|
+
// own would be two highlights claiming the same rows.
|
|
662
|
+
if (!on)
|
|
663
|
+
selectionBridge.controller?.clear();
|
|
664
|
+
},
|
|
665
|
+
isMouseCaptured: () => mouseCaptured,
|
|
666
|
+
}
|
|
667
|
+
: {}),
|
|
668
|
+
// The same door the controller copies through, so `/copy` and a
|
|
669
|
+
// drag-release can never disagree about which stream OSC 52 goes to.
|
|
670
|
+
...(writeForeign ? { writeForeign } : {}),
|
|
671
|
+
...(selectionBridge.controller ? { selection: selectionBridge } : {}),
|
|
672
|
+
};
|
|
737
673
|
const updateBridge = { service: null, onAttach: null };
|
|
738
674
|
let updateService = null;
|
|
739
675
|
let interactiveClosed = false;
|
|
@@ -761,12 +697,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
761
697
|
config.update.mode !== 'off';
|
|
762
698
|
let instance;
|
|
763
699
|
try {
|
|
764
|
-
instance = render(_jsx(App, { controller: controller, version: VERSION,
|
|
765
|
-
// `mouseOn`, NOT the handle (I-11). `App.tsx:186-188` documents this
|
|
766
|
-
// prop's contract as "absent under `--no-mouse`, inline, or non-TTY", and
|
|
767
|
-
// it feeds `enabled: !!mouseSource` on a wheel subscription that would
|
|
768
|
-
// otherwise be a channel no event can ever arrive on.
|
|
769
|
-
mouseSource: mouseOn && filter ? filter.source : undefined,
|
|
700
|
+
instance = render(_jsx(App, { controller: controller, version: VERSION, confirmBridge: confirmBridge, humanInputBridge: humanInputBridge, initialPrompt: extras.initialPrompt, initialOverlay: extras.initialOverlay, mouseSource: mouseOn && filter ? filter.source : undefined,
|
|
770
701
|
// Present ONLY when this console swallows `CSI Z` and mouse reports.
|
|
771
702
|
// Passed in rather than probed inside `App` so that every test renders
|
|
772
703
|
// the same tree on every machine — reading `process.versions.node` from
|
|
@@ -780,17 +711,11 @@ function runInteractive(flags, extras = {}) {
|
|
|
780
711
|
// straight to stdout and permanently shifts the fixed frame's accounting.
|
|
781
712
|
{
|
|
782
713
|
exitOnCtrlC: false,
|
|
783
|
-
patchConsole:
|
|
714
|
+
patchConsole: false,
|
|
784
715
|
// I-5: with mouse support off the REAL stdin is handed over unwrapped, so
|
|
785
716
|
// every existing path is provably unchanged.
|
|
786
717
|
stdin: filter?.stdin ?? process.stdin,
|
|
787
|
-
|
|
788
|
-
// `stdout` at all before, and Ink defaults it to `process.stdout`; omitting
|
|
789
|
-
// the key entirely when the writer is off keeps the options object the same
|
|
790
|
-
// SHAPE it has always had, which is what makes AC-5 / AC-6 ("byte-identical
|
|
791
|
-
// with --no-diff-render, inline mode and `aragon -p`") a statement about
|
|
792
|
-
// the code rather than about Ink's defaulting.
|
|
793
|
-
...(frameObserver ? { stdout: frameObserver.stdout } : {}),
|
|
718
|
+
stdout: frameObserver.stdout,
|
|
794
719
|
});
|
|
795
720
|
}
|
|
796
721
|
catch (error) {
|
|
@@ -968,7 +893,6 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
968
893
|
'confirmTools',
|
|
969
894
|
'toolTimeoutMs',
|
|
970
895
|
'idleTimeoutMs',
|
|
971
|
-
'fullscreen',
|
|
972
896
|
'exitTranscript',
|
|
973
897
|
'transcriptWindow',
|
|
974
898
|
// tui-render-performance L1 / L4. Listed here AND cased in the switch
|
|
@@ -1189,9 +1113,6 @@ function runConfigSet(key, value) {
|
|
|
1189
1113
|
case 'idleTimeoutMs':
|
|
1190
1114
|
patch.idleTimeoutMs = coercePositiveInt(value, 210_000);
|
|
1191
1115
|
break;
|
|
1192
|
-
case 'fullscreen':
|
|
1193
|
-
patch.fullscreen = value === 'true' || value === '1';
|
|
1194
|
-
break;
|
|
1195
1116
|
case 'exitTranscript':
|
|
1196
1117
|
patch.exitTranscript = value === 'true' || value === '1';
|
|
1197
1118
|
break;
|
|
@@ -1480,31 +1401,17 @@ function buildProgram() {
|
|
|
1480
1401
|
.option('--tool-timeout <ms>', 'Per-tool executor ceiling (default 180000)')
|
|
1481
1402
|
.option('--idle-timeout <ms>', 'Watchdog idle timeout (auto-raised to >= tool-timeout+30s)')
|
|
1482
1403
|
.option('--theme <name>', 'auto|warm|cool|light ("dark" is an alias for "cool")')
|
|
1483
|
-
// Same tri-state shape as `--fullscreen`: declaring the positive form first
|
|
1484
|
-
// keeps the default `undefined`, so "no opinion" stays distinguishable from
|
|
1485
|
-
// an explicit choice and the config layer below can still win.
|
|
1486
1404
|
.option('--compact', 'Compact transcript density (no blank rows between turns)')
|
|
1487
1405
|
.option('--no-compact', 'Comfortable transcript density')
|
|
1488
1406
|
.option('--hints', 'Always show the composer hint row')
|
|
1489
1407
|
.option('--no-hints', 'Hide the composer hint row')
|
|
1490
|
-
// Positive form first, same tri-state reason as `--fullscreen` above.
|
|
1491
1408
|
.option('--show-thinking', 'Show the reasoning blocks the model returns')
|
|
1492
1409
|
.option('--no-show-thinking', 'Hide reasoning blocks (the default)')
|
|
1493
|
-
// Positive form first, same tri-state reason as `--fullscreen` above. The
|
|
1494
|
-
// pair matters more here than for its neighbour because this key defaults to
|
|
1495
|
-
// ON: a lone negative form would let commander synthesize `true` and
|
|
1496
|
-
// overwrite a stored `false` on every run that passed no flag at all.
|
|
1497
1410
|
.option('--live-tool-output', 'Show a running command output tail (the default)')
|
|
1498
1411
|
.option('--no-live-tool-output', 'Keep a running tool card to one line')
|
|
1499
|
-
// Positive form first, same tri-state reason as `--fullscreen` above.
|
|
1500
1412
|
.option('--mouse', 'Wheel scrolls the transcript (full-screen mode)')
|
|
1501
1413
|
.option('--no-mouse', 'Leave the mouse to the terminal (the wheel does nothing)')
|
|
1502
|
-
|
|
1503
|
-
// below: `mouseSelect` is persisted and defaults to `true`, so a lone
|
|
1504
|
-
// `--no-mouse-select` would make commander default `opts.mouseSelect` to
|
|
1505
|
-
// `true` — indistinguishable from silence, and silently overriding a stored
|
|
1506
|
-
// `false` on every run that passes no flag at all.
|
|
1507
|
-
.option('--mouse-select', 'Drag with the mouse to select text; releasing copies it')
|
|
1414
|
+
.option('--mouse-select', 'Drag with the mouse to select text; Ctrl+C copies it')
|
|
1508
1415
|
.option('--no-mouse-select', 'Keep wheel scrolling, but leave drag-select off')
|
|
1509
1416
|
// Positive form FIRST, same tri-state reason as `--mouse-select` above:
|
|
1510
1417
|
// `paste` is persisted and defaults to `true`, so a lone `--no-paste` would
|
|
@@ -1512,16 +1419,7 @@ function buildProgram() {
|
|
|
1512
1419
|
// silence, and silently overriding a stored `false` on every run.
|
|
1513
1420
|
.option('--paste', 'Collapse large pastes into a placeholder (the default)')
|
|
1514
1421
|
.option('--no-paste', 'Treat pasted bytes as keystrokes (pre-0.6.3 behavior)')
|
|
1515
|
-
// Declaring `--fullscreen` BEFORE `--no-fullscreen` keeps the default
|
|
1516
|
-
// `undefined` instead of `true`, which is what makes the tri-state work:
|
|
1517
|
-
// "no opinion" must stay distinguishable from "force it".
|
|
1518
|
-
.option('--fullscreen', 'Force the full-screen TUI (overrides auto-downgrade, except non-TTY)')
|
|
1519
|
-
.option('--no-fullscreen', 'Force the inline renderer (v0.2.0 behavior)')
|
|
1520
1422
|
.option('--no-exit-transcript', 'Do not replay the session summary after exiting')
|
|
1521
|
-
// Same tri-state shape as `--fullscreen`: the positive form is declared
|
|
1522
|
-
// FIRST so the default stays `undefined` and "no opinion" remains
|
|
1523
|
-
// distinguishable from an explicit `--no-plan`, which is what lets the
|
|
1524
|
-
// config layer win when neither flag was given.
|
|
1525
1423
|
.option('--plan', 'Start the session in PLAN mode (read-only research + review)')
|
|
1526
1424
|
.option('--no-plan', 'Start the session in BUILD mode (overrides planModeDefault)')
|
|
1527
1425
|
// BOTH FORMS, positive first, and the pair is mandatory (team-subagents
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in slash commands (spec §5.2):
|
|
3
3
|
* /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
|
|
4
|
-
* /resume /copy /exit (/quit), plus the /team, /fast and
|
|
4
|
+
* /resume /copy /terminal-setup /exit (/quit), plus the /team, /fast and
|
|
5
|
+
* /todo subsystems.
|
|
5
6
|
*/
|
|
6
7
|
import { CommandRegistry } from './registry.js';
|
|
7
8
|
export declare function registerBuiltinCommands(registry: CommandRegistry): void;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in slash commands (spec §5.2):
|
|
3
3
|
* /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
|
|
4
|
-
* /resume /copy /exit (/quit), plus the /team, /fast and
|
|
4
|
+
* /resume /copy /terminal-setup /exit (/quit), plus the /team, /fast and
|
|
5
|
+
* /todo subsystems.
|
|
5
6
|
*/
|
|
6
7
|
import { existsSync, statSync } from 'node:fs';
|
|
7
8
|
import { isAbsolute, resolve } from 'node:path';
|
|
@@ -1013,7 +1014,10 @@ const COMMANDS = [
|
|
|
1013
1014
|
const captured = port.captured();
|
|
1014
1015
|
const lines = [
|
|
1015
1016
|
`Mouse capture: ${captured ? 'on' : 'off'}`,
|
|
1016
|
-
|
|
1017
|
+
// The wording follows tui-shift-enter-copy-queue 4.4: releasing no
|
|
1018
|
+
// longer copies, so this row must name the commit key -- leaving the
|
|
1019
|
+
// bare `on` would keep teaching the gesture that was removed.
|
|
1020
|
+
` drag-select ${port.selectEnabled() ? 'on (drag to select, ctrl+c to copy)' : 'off (mouseSelect is false)'}`,
|
|
1017
1021
|
' /mouse off hand the mouse to your terminal for this session',
|
|
1018
1022
|
' /mouse on take it back',
|
|
1019
1023
|
' --no-mouse never capture it (also ARAGON_MOUSE=0)',
|
|
@@ -1022,6 +1026,47 @@ const COMMANDS = [
|
|
|
1022
1026
|
ctx.notify('info', lines.join('\n'));
|
|
1023
1027
|
},
|
|
1024
1028
|
},
|
|
1029
|
+
{
|
|
1030
|
+
name: 'terminal-setup',
|
|
1031
|
+
description: 'How to make Shift+Enter send a newline in your terminal',
|
|
1032
|
+
run: (ctx) => {
|
|
1033
|
+
// READ-ONLY BY DESIGN (3.6): pushing kitty keyboard mode or
|
|
1034
|
+
// modifyOtherKeys=2 would re-encode EVERY modified key as CSI-u and
|
|
1035
|
+
// blind Ink's parser for the whole session. One user-side binding
|
|
1036
|
+
// reaches the same result for the one key that needs it.
|
|
1037
|
+
//
|
|
1038
|
+
// Ctrl+J and Alt+Enter work with no setup at all; CSI-u terminals
|
|
1039
|
+
// (kitty / WezTerm / foot with keyboard enhancement on) need none
|
|
1040
|
+
// either. The lines below are for the terminals that send a plain
|
|
1041
|
+
// CR for Shift+Enter and CAN be configured.
|
|
1042
|
+
const lines = [
|
|
1043
|
+
'Shift+Enter newline - terminal setup',
|
|
1044
|
+
'',
|
|
1045
|
+
'Ctrl+J and Alt+Enter insert a newline in every terminal, with no',
|
|
1046
|
+
'setup. To get a dedicated Shift+Enter key, make the terminal send',
|
|
1047
|
+
'the CSI-u sequence \\u001b[13;2u for it:',
|
|
1048
|
+
'',
|
|
1049
|
+
' Windows Terminal settings.json, actions array:',
|
|
1050
|
+
' { "keys": "shift+enter", "command": { "action": "sendInput",',
|
|
1051
|
+
' "input": "\\u001b[13;2u" } }',
|
|
1052
|
+
' VS Code terminal keybindings.json:',
|
|
1053
|
+
' { "key": "shift+enter",',
|
|
1054
|
+
' "command": "workbench.action.terminal.sendSequence",',
|
|
1055
|
+
' "args": { "text": "\\u001b[13;2u" } }',
|
|
1056
|
+
' iTerm2 Settings > Profiles > Keys > Key Mappings:',
|
|
1057
|
+
' Shift+Enter -> Send Escape Sequence: [13;2u',
|
|
1058
|
+
' kitty/WezTerm/foot native once keyboard enhancement is on',
|
|
1059
|
+
' (kitty: map shift+enter send_text all \\u001b[13;2u)',
|
|
1060
|
+
' Alacritty alacritty.toml, [keyboard] bindings:',
|
|
1061
|
+
' { key = "Enter", mods = "Shift", chars = "\\u001b[13;2u" }',
|
|
1062
|
+
' macOS Terminal.app / conhost cannot rebind Enter; use Ctrl+J',
|
|
1063
|
+
' or Alt+Enter',
|
|
1064
|
+
'',
|
|
1065
|
+
'This command only prints advice; it changes no terminal modes.',
|
|
1066
|
+
];
|
|
1067
|
+
ctx.notify('info', lines.join('\n'));
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1025
1070
|
{
|
|
1026
1071
|
name: 'logs',
|
|
1027
1072
|
description: 'Show where this session is being logged',
|