@aragon-agent/cli 0.6.29 → 0.6.32
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 +2223 -2103
- package/README.md +2957 -2759
- package/dist/agent/controller.d.ts +46 -0
- package/dist/agent/controller.js +175 -7
- package/dist/agent/job-notices.d.ts +143 -0
- package/dist/agent/job-notices.js +182 -0
- package/dist/agent/project-guidance-prompt.js +20 -20
- package/dist/agent/reducer.d.ts +21 -1
- package/dist/agent/reducer.js +7 -0
- package/dist/agent/system-prompt.d.ts +12 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +399 -62
- package/dist/commands/builtins.js +27 -8
- package/dist/commands/perf.d.ts +79 -2
- package/dist/commands/perf.js +51 -3
- package/dist/commands/registry.d.ts +6 -0
- package/dist/config/cli-commands.d.ts +27 -0
- package/dist/config/cli-commands.js +54 -1
- package/dist/config/env.js +42 -0
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +61 -1
- package/dist/config/schema.d.ts +100 -0
- package/dist/config/schema.js +43 -0
- package/dist/config/store.js +14 -1
- package/dist/config/ui-state.d.ts +9 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +29 -3
- package/dist/exec/events.js +3 -12
- package/dist/exec/runner.d.ts +24 -0
- package/dist/exec/runner.js +79 -0
- package/dist/input/cursor-report.d.ts +61 -0
- package/dist/input/cursor-report.js +97 -0
- package/dist/input/stdin-filter.d.ts +22 -0
- package/dist/input/stdin-filter.js +48 -3
- package/dist/proc/limits.d.ts +76 -2
- package/dist/proc/limits.js +83 -2
- package/dist/proc/log-mirror.d.ts +59 -0
- package/dist/proc/log-mirror.js +203 -0
- package/dist/proc/log-ring.d.ts +22 -0
- package/dist/proc/log-ring.js +20 -1
- package/dist/proc/notices.d.ts +57 -0
- package/dist/proc/notices.js +101 -0
- package/dist/proc/progress.d.ts +40 -0
- package/dist/proc/progress.js +135 -0
- package/dist/proc/prompt.d.ts +24 -2
- package/dist/proc/prompt.js +51 -2
- package/dist/proc/supervisor.d.ts +35 -2
- package/dist/proc/supervisor.js +142 -43
- package/dist/proc/types.d.ts +89 -1
- package/dist/proc/wait.d.ts +57 -0
- package/dist/proc/wait.js +133 -0
- package/dist/session/persist.js +3 -1
- package/dist/session/validate-session.js +17 -0
- package/dist/tools/bash-tool.d.ts +12 -1
- package/dist/tools/bash-tool.js +100 -13
- package/dist/tools/index.d.ts +26 -2
- package/dist/tools/index.js +43 -2
- package/dist/tools/job-tools.d.ts +54 -0
- package/dist/tools/job-tools.js +329 -0
- package/dist/tools/proc-tools.d.ts +1 -1
- package/dist/tools/proc-tools.js +49 -17
- package/dist/ui/ActivityLine.js +7 -0
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +181 -63
- package/dist/ui/ScrollbackShell.d.ts +54 -0
- package/dist/ui/ScrollbackShell.js +15 -0
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.d.ts +14 -0
- package/dist/ui/Transcript.js +14 -2
- package/dist/ui/entries/ServiceCard.d.ts +17 -1
- package/dist/ui/entries/ServiceCard.js +39 -4
- package/dist/ui/frame-differ.d.ts +12 -0
- package/dist/ui/layout/status-layout.d.ts +13 -1
- package/dist/ui/layout/status-layout.js +10 -0
- package/dist/ui/layout/virtual-window.js +14 -1
- package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
- package/dist/ui/overlays/HelpOverlay.js +21 -3
- package/dist/ui/run-status-row.d.ts +12 -0
- package/dist/ui/screen.d.ts +29 -0
- package/dist/ui/screen.js +40 -0
- package/dist/ui/scrollback/LiveTail.d.ts +64 -0
- package/dist/ui/scrollback/LiveTail.js +27 -0
- package/dist/ui/scrollback/band.d.ts +105 -0
- package/dist/ui/scrollback/band.js +109 -0
- package/dist/ui/scrollback/budget.d.ts +72 -0
- package/dist/ui/scrollback/budget.js +81 -0
- package/dist/ui/scrollback/canvas.d.ts +99 -0
- package/dist/ui/scrollback/canvas.js +113 -0
- package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
- package/dist/ui/scrollback/cursor-probe.js +133 -0
- package/dist/ui/scrollback/drain.d.ts +32 -0
- package/dist/ui/scrollback/drain.js +64 -0
- package/dist/ui/scrollback/exit-summary.d.ts +23 -0
- package/dist/ui/scrollback/exit-summary.js +25 -0
- package/dist/ui/scrollback/limits.d.ts +74 -0
- package/dist/ui/scrollback/limits.js +74 -0
- package/dist/ui/scrollback/live-preview.d.ts +26 -0
- package/dist/ui/scrollback/live-preview.js +106 -0
- package/dist/ui/scrollback/renderer.d.ts +38 -0
- package/dist/ui/scrollback/renderer.js +158 -0
- package/dist/ui/scrollback/scrollback-writer.d.ts +162 -0
- package/dist/ui/scrollback/scrollback-writer.js +549 -0
- package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
- package/dist/ui/scrollback/settled-boundary.js +106 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +103 -0
- package/dist/ui/scrollback/use-scrollback-view.js +294 -0
- package/dist/ui/scrollback/writer-shapes.d.ts +127 -0
- package/dist/ui/scrollback/writer-shapes.js +159 -0
- package/dist/ui/stdout-frame-writer.js +12 -2
- package/dist/ui/terminal-plan.d.ts +52 -0
- package/dist/ui/terminal-plan.js +49 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +21 -7
- package/dist/ui/use-startup-notices.d.ts +12 -1
- package/dist/ui/use-startup-notices.js +28 -2
- package/package.json +3 -3
- package/skills/project-indexer/SKILL.md +163 -163
- package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
- package/skills/project-indexer/templates/cleancode-template.md +94 -94
- package/skills/project-indexer/templates/config-template.md +69 -69
- package/skills/project-indexer/templates/index-template.md +129 -129
- package/skills/skill-creator/SKILL.md +96 -96
package/dist/cli.js
CHANGED
|
@@ -25,11 +25,11 @@ import { formatMigrationNotice, migrateLegacyState } from './config/migrate-lega
|
|
|
25
25
|
import { formatHomeMigrationNotice, migrateToHome } from './config/migrate-home.js';
|
|
26
26
|
import { migrateStateOutOfConfig } from './config/migrate-state-out-of-config.js';
|
|
27
27
|
import { runHistoryCommand } from './config/history-commands.js';
|
|
28
|
-
import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS, RETRY_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, UPDATE_CONFIG_SET_KEYS, applyCompactionConfigSet, applyFastConfigSet, applyLogConfigSet, applyRetryConfigSet, applyTeamConfigSet, applyUpdateConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
|
|
28
|
+
import { COMPACTION_CONFIG_SET_KEYS, FAST_CONFIG_SET_KEYS, LOG_CONFIG_SET_KEYS, RETRY_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, JOBS_CONFIG_SET_KEYS, UPDATE_CONFIG_SET_KEYS, applyCompactionConfigSet, applyFastConfigSet, applyLogConfigSet, applyRetryConfigSet, applyTeamConfigSet, applyJobsConfigSet, applyUpdateConfigSet, runConfigEdit, runConfigGet, runConfigHome, runConfigList, warnIfRedactionDisabled, } from './config/cli-commands.js';
|
|
29
29
|
import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
|
|
30
30
|
import { getLogger } from './logging/logger.js';
|
|
31
31
|
import { runLogsCommand } from './logging/cli-commands.js';
|
|
32
|
-
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampContextWindow, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, isAutoToken, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
|
|
32
|
+
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampContextWindow, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, isAutoToken, parseMaxTokensInput, DEFAULT_MAX_RENDER_INTERVAL_MS, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SCROLL_MODE, DEFAULT_SCROLL_RESUME_MS, DEFAULT_SKILLS_CONFIG, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from './config/schema.js';
|
|
33
33
|
import { makeGetApiKey } from './config/load.js';
|
|
34
34
|
import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
|
|
35
35
|
import { runSkillsCommand } from './skills/cli-commands.js';
|
|
@@ -49,13 +49,19 @@ import { createBranchLauncher } from './session/branch-launch.js';
|
|
|
49
49
|
import { EMPTY_USAGE_TOTAL } from './agent/reducer.js';
|
|
50
50
|
import { createFrameDiffer } from './ui/frame-differ.js';
|
|
51
51
|
import { wrapStdoutForFrames } from './ui/stdout-frame-writer.js';
|
|
52
|
-
import { setFrameStatsProvider } from './commands/perf.js';
|
|
52
|
+
import { setFrameStatsProvider, setScrollbackStatsProvider } from './commands/perf.js';
|
|
53
53
|
import { tryCreateStdinFilter } from './input/stdin-filter.js';
|
|
54
54
|
import { enableKeyboardEnhancement, NOOP_KEYBOARD_ENHANCEMENT, } from './input/keyboard-enhancement.js';
|
|
55
55
|
import { createScrollbarBridge } from './ui/scrollbar-controller.js';
|
|
56
56
|
import { createPointerRouter } from './input/pointer-router.js';
|
|
57
57
|
import { createFrameObserver } from './ui/frame-observer.js';
|
|
58
|
-
import { enterAltScreen, writeExitTranscript } from './ui/screen.js';
|
|
58
|
+
import { enterAltScreen, enterNativeScreen, writeExitTranscript, } from './ui/screen.js';
|
|
59
|
+
import { resolveTerminalPlan } from './ui/terminal-plan.js';
|
|
60
|
+
import { createScrollbackDrain } from './ui/scrollback/drain.js';
|
|
61
|
+
import { SCROLLBACK_LIMITS } from './ui/scrollback/limits.js';
|
|
62
|
+
import { createScrollbackWriter } from './ui/scrollback/scrollback-writer.js';
|
|
63
|
+
import { probeAnchorSpaceBelow, setAnchorProbeDisabled } from './ui/scrollback/cursor-probe.js';
|
|
64
|
+
import { createScrollbackBridge, } from './ui/scrollback/use-scrollback-view.js';
|
|
59
65
|
import { supportsWindowsVtInput } from './ui/win-vt-input.js';
|
|
60
66
|
import { forceWindowsVtInput } from './ui/win-vt-force.js';
|
|
61
67
|
import { readExitSnapshot } from './ui/exit-snapshot.js';
|
|
@@ -88,6 +94,19 @@ function readVersion() {
|
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
const VERSION = readVersion();
|
|
97
|
+
/**
|
|
98
|
+
* `--scroll-mode` is the one flag validated at parse time (tui-native-terminal-scroll
|
|
99
|
+
* §6.1): a typo must be an exit-2 sentence, not a silent fallback to the default.
|
|
100
|
+
*/
|
|
101
|
+
function parseScrollModeFlag(raw) {
|
|
102
|
+
if (raw === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
const mode = clampScrollMode(raw, DEFAULT_SCROLL_MODE);
|
|
105
|
+
if (mode === raw.trim().toLowerCase())
|
|
106
|
+
return mode;
|
|
107
|
+
process.stderr.write(`Invalid --scroll-mode "${raw}" (expected native or app).\n`);
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
91
110
|
function toFlags(opts) {
|
|
92
111
|
return {
|
|
93
112
|
provider: opts.provider,
|
|
@@ -186,6 +205,8 @@ function toFlags(opts) {
|
|
|
186
205
|
// user whose terminal renders the diffed frame wrongly, and a user who has
|
|
187
206
|
// been told to pass it and sees no change has no next move.
|
|
188
207
|
diffRender: opts.diffRender,
|
|
208
|
+
// Third site for `--scroll-mode`, the same trap the sentence above records.
|
|
209
|
+
scrollMode: parseScrollModeFlag(opts.scrollMode),
|
|
189
210
|
syncOutput: opts.syncOutput,
|
|
190
211
|
// Site three of three for `--update` / `--no-update` — SEVEN features, one
|
|
191
212
|
// trap (`mouse` P1-2, `team` P1-4, `todo` C-9, render L4, retry, fast, now
|
|
@@ -393,8 +414,16 @@ function forceVtInputForThisConsole() {
|
|
|
393
414
|
* "…" > out.txt`, `aragon config set`, and `aragon --version`, because the
|
|
394
415
|
* interactive and headless paths are disjoint branches: `runInteractive()`
|
|
395
416
|
* renders Ink, `runOneShot()` goes to `runHeadless()`, which never does.
|
|
417
|
+
*
|
|
418
|
+
* `async` FOR EXACTLY ONE `await` (tui-bottom-anchored-live-region P1-3): the
|
|
419
|
+
* cursor-position probe before the first frame. Both call sites are inside async
|
|
420
|
+
* Commander actions and use `void runInteractive(...)` — the session's lifetime is
|
|
421
|
+
* owned by `waitUntilExit()` and the four idempotent restore paths, never by the
|
|
422
|
+
* caller, so nothing awaits it. The `await` sits before the FIRST `stdout.write`
|
|
423
|
+
* and between it and `render()` there is no other suspension point, so making the
|
|
424
|
+
* function async introduces no new interleaving.
|
|
396
425
|
*/
|
|
397
|
-
function runInteractive(flags, extras = {}) {
|
|
426
|
+
async function runInteractive(flags, extras = {}) {
|
|
398
427
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
399
428
|
process.stderr.write('Interactive UI requires a TTY. Use -p "<prompt>", exec, or config list / config set.\n');
|
|
400
429
|
process.exitCode = 2;
|
|
@@ -431,8 +460,10 @@ function runInteractive(flags, extras = {}) {
|
|
|
431
460
|
const config = controller.getConfig();
|
|
432
461
|
const caps = resolveTuiCapabilities({}, config.color);
|
|
433
462
|
const logger = getLogger();
|
|
463
|
+
const scrollMode = config.scrollMode ?? DEFAULT_SCROLL_MODE;
|
|
434
464
|
logger.info('cli', 'run_interactive', {
|
|
435
|
-
mode: '
|
|
465
|
+
mode: scrollMode === 'native' ? 'native' : 'fullscreen',
|
|
466
|
+
provider: config.provider, model: config.model,
|
|
436
467
|
unicode: caps.unicode, colorLevel: caps.colorLevel,
|
|
437
468
|
});
|
|
438
469
|
attachAgentEvents(logger, controller);
|
|
@@ -513,6 +544,41 @@ function runInteractive(flags, extras = {}) {
|
|
|
513
544
|
};
|
|
514
545
|
let screen = null;
|
|
515
546
|
let replayed = false;
|
|
547
|
+
/**
|
|
548
|
+
* Native-scroll exit, shared by EVERY restore path (normal exit, `process.on
|
|
549
|
+
* ('exit')`, the signal terminator, the crash handler, `failInteractive`).
|
|
550
|
+
* Idempotent: the writer's `finalize` returns `''` the second time.
|
|
551
|
+
*
|
|
552
|
+
* Order (RV-9): publish the last unsettled entries to the feed, take the summary
|
|
553
|
+
* (lazily, and a failure only costs the summary), then write the finalize bytes
|
|
554
|
+
* through the REAL stdout. After `finalize` the writer swallows every Ink write,
|
|
555
|
+
* so it does not matter whether Ink has unmounted yet; callers must not add an
|
|
556
|
+
* asynchronous wait for it.
|
|
557
|
+
*/
|
|
558
|
+
const finalizeNativeScreen = () => {
|
|
559
|
+
// FIRST, before any bytes: the anchor probe must go quiet here (P1-6). A settle
|
|
560
|
+
// timer that survived this point would write `ESC[6n` at a terminal already handed
|
|
561
|
+
// back to the shell and nobody would strip the reply.
|
|
562
|
+
killAnchorProbe();
|
|
563
|
+
const bridge = scrollbackBridge;
|
|
564
|
+
if (!bridge)
|
|
565
|
+
return;
|
|
566
|
+
try {
|
|
567
|
+
bridge.settleAllNow();
|
|
568
|
+
}
|
|
569
|
+
catch { /* the feed keeps whatever was queued */ }
|
|
570
|
+
let summary = [];
|
|
571
|
+
try {
|
|
572
|
+
summary = bridge.getSummaryRows();
|
|
573
|
+
}
|
|
574
|
+
catch { /* degrade to no summary */ }
|
|
575
|
+
try {
|
|
576
|
+
const bytes = bridge.writer.finalize(summary);
|
|
577
|
+
if (bytes.length > 0)
|
|
578
|
+
process.stdout.write(bytes);
|
|
579
|
+
}
|
|
580
|
+
catch { /* a closed stdout must never turn into a crash on the exit path */ }
|
|
581
|
+
};
|
|
516
582
|
// --- Mouse wheel region routing (mouse-wheel-region-routing §4.2). -------
|
|
517
583
|
//
|
|
518
584
|
// BUILD THE FILTER FIRST, THEN ENABLE REPORTING. The order is invariant I-8
|
|
@@ -553,10 +619,23 @@ function runInteractive(flags, extras = {}) {
|
|
|
553
619
|
// a console this process just repaired.
|
|
554
620
|
const vtInputByNode = supportsWindowsVtInput(process.platform, process.versions.node);
|
|
555
621
|
const vtInputSupported = vtInputByNode || forceVtInputForThisConsole();
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
622
|
+
// THE ONE DECISION POINT for screen mode, mouse, paste, key enhancement and the
|
|
623
|
+
// frame writer (tui-native-terminal-scroll D10). In `app` mode every field below
|
|
624
|
+
// is the pre-feature expression term for term (`terminal-plan.test.ts` pins it).
|
|
625
|
+
const plan = resolveTerminalPlan({
|
|
626
|
+
scrollMode,
|
|
627
|
+
configMouse: config.mouse,
|
|
628
|
+
configMouseSelect: config.mouseSelect,
|
|
629
|
+
configPaste: config.paste,
|
|
630
|
+
configKeyboardEnhancement: config.keyboardEnhancement,
|
|
631
|
+
configDiffRender: config.diffRender,
|
|
632
|
+
stdinIsTTY: !!process.stdin.isTTY,
|
|
633
|
+
stdoutIsTTY: !!process.stdout.isTTY,
|
|
634
|
+
platform: process.platform,
|
|
635
|
+
vtInputSupported,
|
|
636
|
+
});
|
|
637
|
+
const native = plan.scrollMode === 'native';
|
|
638
|
+
const wantMouse = plan.mouseReporting;
|
|
560
639
|
// --- Paste handling (tui-paste-handling section 5.1.1 / D-13 / D-17). ----
|
|
561
640
|
//
|
|
562
641
|
// THE GATE IS WIDER THAN THE MOUSE'S, AND `paste` DEFAULTS TO TRUE. Paste
|
|
@@ -571,7 +650,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
571
650
|
// would flip true: `?1000h`/`?1006h`/`?1002h` written, terminal selection taken
|
|
572
651
|
// away (the ONLY reason anyone passes `--no-mouse`), and a startup notice
|
|
573
652
|
// advising the user about a mode they just disabled.
|
|
574
|
-
const wantPaste =
|
|
653
|
+
const wantPaste = plan.bracketedPaste;
|
|
575
654
|
const pasteBridge = { notify: null };
|
|
576
655
|
// --- Keyboard enhancement (Shift+Enter newline). --------------------------
|
|
577
656
|
//
|
|
@@ -584,11 +663,16 @@ function runInteractive(flags, extras = {}) {
|
|
|
584
663
|
// Elsewhere the push is kitty disambiguate + modifyOtherKeys, which a
|
|
585
664
|
// terminal that implements neither ignores (verified byte-identical on
|
|
586
665
|
// conhost), so no capability gate is possible or needed there.
|
|
587
|
-
const wantEnhancedKeys =
|
|
588
|
-
!!process.stdin.isTTY &&
|
|
589
|
-
(process.platform !== 'win32' || vtInputSupported);
|
|
666
|
+
const wantEnhancedKeys = plan.enhancedKeys;
|
|
590
667
|
const filter = process.stdin.isTTY
|
|
591
|
-
? tryCreateStdinFilter(process.stdin, {
|
|
668
|
+
? tryCreateStdinFilter(process.stdin, {
|
|
669
|
+
mouse: wantMouse,
|
|
670
|
+
paste: wantPaste,
|
|
671
|
+
enhancedKeys: wantEnhancedKeys,
|
|
672
|
+
// TOLD, never derived (I-A8): a native session strips the terminal's
|
|
673
|
+
// answer to `ESC[6n`, an `app` session is byte-identical to before.
|
|
674
|
+
cursorReport: plan.differ === 'scrollback',
|
|
675
|
+
}, (reason) => logger.warn('cli', 'stdin_filter_failed', { reason }), pasteBridge)
|
|
592
676
|
: null;
|
|
593
677
|
/**
|
|
594
678
|
* THE ONE EXPRESSION EVERY MOUSE SITE READS (I-11 / D-17).
|
|
@@ -602,11 +686,140 @@ function runInteractive(flags, extras = {}) {
|
|
|
602
686
|
const pasteOn = wantPaste && filter !== null;
|
|
603
687
|
/** Same discipline: the translation runs only when a filter fronts Ink. */
|
|
604
688
|
const enhancedKeysOn = wantEnhancedKeys && filter !== null;
|
|
605
|
-
|
|
689
|
+
// --- The bottom anchor's measurement pipeline (tui-bottom-anchored-live-region).
|
|
690
|
+
//
|
|
691
|
+
// Three pieces of host state, and every one of them exists to close a specific
|
|
692
|
+
// failure mode the writer cannot close by itself:
|
|
693
|
+
//
|
|
694
|
+
// `anchorProbeDead` — I-A9 / P1-6. After `finalizeNativeScreen()`,
|
|
695
|
+
// `disposeFrameWriter()`, `filter.dispose()` and the process `exit` hook, a
|
|
696
|
+
// query must never be written again: it would hit a terminal already handed
|
|
697
|
+
// back to the shell, where nothing strips the reply and the user's prompt gains
|
|
698
|
+
// a literal `^[[12;1R`.
|
|
699
|
+
// `anchorReprobeQueued` — P0-3. `invalidate()` is called from inside the writer's
|
|
700
|
+
// `transform`, i.e. inside Ink's own `stdout.write`, so the request is pushed out
|
|
701
|
+
// with `setImmediate` and everything inside that window is coalesced into one
|
|
702
|
+
// query instead of becoming a probe storm on every foreign write.
|
|
703
|
+
// `anchorReprobeTimer` — the 200 ms settle window (§4.6). A drag fires dozens of
|
|
704
|
+
// resize events; one CPR each would fight the input channel for bandwidth and
|
|
705
|
+
// make the region jitter.
|
|
706
|
+
let anchorProbeDead = false;
|
|
707
|
+
let anchorReprobeQueued = false;
|
|
708
|
+
let anchorReprobeTimer = null;
|
|
709
|
+
/** Stop measuring for the rest of the process. Idempotent; safe from any exit path. */
|
|
710
|
+
function killAnchorProbe() {
|
|
711
|
+
anchorProbeDead = true;
|
|
712
|
+
anchorReprobeQueued = false;
|
|
713
|
+
if (anchorReprobeTimer) {
|
|
714
|
+
clearTimeout(anchorReprobeTimer);
|
|
715
|
+
anchorReprobeTimer = null;
|
|
716
|
+
}
|
|
717
|
+
// The module-level switch is what makes a probe that is already in flight, or one
|
|
718
|
+
// whose timer fires during teardown, write zero bytes.
|
|
719
|
+
setAnchorProbeDisabled(true);
|
|
720
|
+
}
|
|
721
|
+
/** Ask for one measurement soon; coalesced, deferred out of Ink's write, and dead-safe. */
|
|
722
|
+
function requestAnchorProbe() {
|
|
723
|
+
if (anchorProbeDead || anchorReprobeQueued)
|
|
724
|
+
return;
|
|
725
|
+
anchorReprobeQueued = true;
|
|
726
|
+
setImmediate(() => {
|
|
727
|
+
anchorReprobeQueued = false;
|
|
728
|
+
if (anchorProbeDead)
|
|
729
|
+
return;
|
|
730
|
+
if (anchorReprobeTimer)
|
|
731
|
+
clearTimeout(anchorReprobeTimer);
|
|
732
|
+
anchorReprobeTimer = setTimeout(() => {
|
|
733
|
+
anchorReprobeTimer = null;
|
|
734
|
+
void runAnchorProbe();
|
|
735
|
+
}, SCROLLBACK_LIMITS.anchorReprobeDelayMs);
|
|
736
|
+
anchorReprobeTimer.unref?.();
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* One query, then consume the answer. Never throws and never fails the session: a
|
|
741
|
+
* terminal that does not answer simply keeps whatever anchor it had.
|
|
742
|
+
*/
|
|
743
|
+
async function runAnchorProbe() {
|
|
744
|
+
const bridge = scrollbackBridge;
|
|
745
|
+
if (anchorProbeDead || !bridge || !filter)
|
|
746
|
+
return;
|
|
747
|
+
filter.setExpectingCursorReport(true);
|
|
748
|
+
let spaceBelow = null;
|
|
749
|
+
try {
|
|
750
|
+
spaceBelow = await probeAnchorSpaceBelow({
|
|
751
|
+
stdin: process.stdin,
|
|
752
|
+
stdout: process.stdout,
|
|
753
|
+
rows: () => process.stdout.rows,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
logger.warn('cli', 'scroll_anchor_probe_failed', {
|
|
758
|
+
rows: process.stdout.rows ?? null, reason: String(error),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
finally {
|
|
762
|
+
filter.setExpectingCursorReport(false);
|
|
763
|
+
}
|
|
764
|
+
if (anchorProbeDead || spaceBelow === null)
|
|
765
|
+
return;
|
|
766
|
+
// `setAnchorSpaceBelow` records the re-probe count; the repaint is deferred for
|
|
767
|
+
// the same reason `requestRepaint` is (this can run while Ink is writing).
|
|
768
|
+
bridge.writer.setAnchorSpaceBelow(spaceBelow, 'probe');
|
|
769
|
+
logger.info('cli', 'scroll_anchor', {
|
|
770
|
+
source: 'probe', spaceBelow, rows: process.stdout.rows ?? null,
|
|
771
|
+
});
|
|
772
|
+
setImmediate(() => {
|
|
773
|
+
if (!anchorProbeDead)
|
|
774
|
+
frameWriter?.repaint();
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* THE ORDER HERE IS THE MECHANISM (P0-1 / I-A10). In canonical mode a terminal's
|
|
779
|
+
* reply to `ESC[6n` is line-buffered until a newline AND echoed, so the probe would
|
|
780
|
+
* time out on every Linux/macOS session while printing `^[[12;1R` on the screen.
|
|
781
|
+
* Raising raw first is also the direction `forceVtInputForThisConsole()` already
|
|
782
|
+
* goes (it raises the mode before Ink's own `setRawMode(true)` so libuv does not
|
|
783
|
+
* rewrite the console mode); a second raise is a no-op on both platforms.
|
|
784
|
+
*
|
|
785
|
+
* `false` means "no probe", never "no session": the caller falls back to the fill.
|
|
786
|
+
*/
|
|
787
|
+
function raiseRawModeForProbe() {
|
|
788
|
+
if (typeof process.stdin.setRawMode !== 'function')
|
|
789
|
+
return false;
|
|
790
|
+
try {
|
|
791
|
+
process.stdin.setRawMode(true);
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
logger.warn('cli', 'scroll_anchor_probe_failed', {
|
|
795
|
+
rows: process.stdout.rows ?? null, reason: `raw mode: ${String(error)}`,
|
|
796
|
+
});
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
// Ink lowers raw mode on unmount, which is the normal restore. A kill, a closed
|
|
800
|
+
// window or a crash never unmounts, so the same restore `forceVtInputForThisConsole`
|
|
801
|
+
// registers is registered here too; it is idempotent and cheap.
|
|
802
|
+
process.on('exit', () => {
|
|
803
|
+
try {
|
|
804
|
+
process.stdin.setRawMode(false);
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
/* Stream already torn down - nothing left to restore. */
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
const disposeStdinFilter = () => {
|
|
813
|
+
// Before the filter goes away: a probe settle timer must not fire into a stream
|
|
814
|
+
// nobody is stripping any more.
|
|
815
|
+
killAnchorProbe();
|
|
816
|
+
filter?.dispose();
|
|
817
|
+
};
|
|
606
818
|
// --- Drag-select (tui-selection-and-scroll-follow §4.4). ------------------
|
|
607
819
|
//
|
|
608
820
|
// Selection owns its controller, decoration and notice. Button motion follows
|
|
609
821
|
// mouseOn independently so --no-mouse-select still permits scrollbar dragging.
|
|
822
|
+
// `mouseOn` is already false in native mode, so this needs no `plan.dragSelect` term.
|
|
610
823
|
const wantSelect = mouseOn && config.mouseSelect;
|
|
611
824
|
let mouseCaptured = mouseOn;
|
|
612
825
|
const scrollbar = createScrollbarBridge(() => mouseCaptured);
|
|
@@ -618,59 +831,100 @@ function runInteractive(flags, extras = {}) {
|
|
|
618
831
|
onCopied: null,
|
|
619
832
|
requestRedraw: null,
|
|
620
833
|
};
|
|
621
|
-
|
|
622
|
-
|
|
834
|
+
// Native scroll: a drain + writer pair replaces the full-screen differ. The bridge
|
|
835
|
+
// is the only object shared with the React tree (tui-native-terminal-scroll §6.2).
|
|
836
|
+
let scrollbackBridge;
|
|
837
|
+
if (plan.differ === 'scrollback') {
|
|
838
|
+
const drain = createScrollbackDrain();
|
|
839
|
+
const writer = createScrollbackWriter({
|
|
623
840
|
sync: config.syncOutput && process.env.TERM !== 'dumb',
|
|
624
841
|
rows: () => process.stdout.rows,
|
|
625
|
-
// Rows that fill the line skip `CSI K` so the scrollbar's last column
|
|
626
|
-
// is not erased on pending-wrap terminals (frame-differ I-4).
|
|
627
842
|
cols: () => process.stdout.columns,
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// user-visible channel with the TUI mounted — a direct write would
|
|
631
|
-
// itself be the foreign write being reported. Latched on the 0->1
|
|
632
|
-
// edge inside the differ: a notice per fallback would be a repaint
|
|
633
|
-
// storm triggered by a repaint problem (§5.6).
|
|
843
|
+
fullEveryFrame: plan.fullEveryFrame,
|
|
844
|
+
drain,
|
|
634
845
|
onFirstFallback: () => console.warn(FRAME_FALLBACK_NOTICE),
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
:
|
|
654
|
-
|
|
655
|
-
|
|
846
|
+
onGuardTrip: () => scrollbackBridge?.onGuardTrip?.(),
|
|
847
|
+
// Deferred: static rows can be queued from inside a React commit, and the
|
|
848
|
+
// repaint writes to the stream Ink is in the middle of writing to.
|
|
849
|
+
requestRepaint: () => { setImmediate(() => frameWriter?.repaint()); },
|
|
850
|
+
// `invalidate()` calls this from inside `transform` (geometry lost), so the
|
|
851
|
+
// deferral is not optional here either (P0-3). The host coalesces it into one
|
|
852
|
+
// query per settle window and stops it for good once the screen is handed back.
|
|
853
|
+
requestAnchorProbe,
|
|
854
|
+
});
|
|
855
|
+
scrollbackBridge = createScrollbackBridge({
|
|
856
|
+
writer, drain, requestFlush: () => { setImmediate(() => frameWriter?.repaint()); },
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
const frameWriter = scrollbackBridge
|
|
860
|
+
? wrapStdoutForFrames(process.stdout, scrollbackBridge.writer)
|
|
861
|
+
: plan.differ === 'fullscreen'
|
|
862
|
+
? wrapStdoutForFrames(process.stdout, createFrameDiffer({
|
|
863
|
+
sync: config.syncOutput && process.env.TERM !== 'dumb',
|
|
864
|
+
rows: () => process.stdout.rows,
|
|
865
|
+
// Rows that fill the line skip `CSI K` so the scrollbar's last column
|
|
866
|
+
// is not erased on pending-wrap terminals (frame-differ I-4).
|
|
867
|
+
cols: () => process.stdout.columns,
|
|
868
|
+
// The console bridge (`App.tsx`, full-screen only) turns this into
|
|
869
|
+
// `dispatch({type:'notice', level:'warn'})`, which is the only legal
|
|
870
|
+
// user-visible channel with the TUI mounted — a direct write would
|
|
871
|
+
// itself be the foreign write being reported. Latched on the 0->1
|
|
872
|
+
// edge inside the differ: a notice per fallback would be a repaint
|
|
873
|
+
// storm triggered by a repaint problem (§5.6).
|
|
874
|
+
onFirstFallback: () => console.warn(FRAME_FALLBACK_NOTICE),
|
|
875
|
+
// ABSENT ENTIRELY without drag-select, not a no-op function: the
|
|
876
|
+
// differ's identity path then allocates nothing and copies nothing,
|
|
877
|
+
// which is what makes `mouseSelect: false` a statement about the code
|
|
878
|
+
// rather than about how cheap an empty function is.
|
|
879
|
+
...(wantSelect
|
|
880
|
+
? {
|
|
881
|
+
decorate: (lines) => selectionBridge.controller?.decorate(lines) ?? lines,
|
|
882
|
+
// P1-7: a pass-through frame writes the RAW Ink frame verbatim,
|
|
883
|
+
// so the highlight is wiped off the terminal while the
|
|
884
|
+
// controller still believes it is painted. Deferred out of the
|
|
885
|
+
// current stack because this fires from inside `transform`,
|
|
886
|
+
// which is inside Ink's own `stdout.write`.
|
|
887
|
+
onInvalidate: () => {
|
|
888
|
+
const controller = selectionBridge.controller;
|
|
889
|
+
if (controller)
|
|
890
|
+
setImmediate(() => controller.clear());
|
|
891
|
+
},
|
|
892
|
+
}
|
|
893
|
+
: {}),
|
|
894
|
+
}))
|
|
895
|
+
: null;
|
|
656
896
|
// `null` when no writer was built, so `/perf` can say "diff render off" rather
|
|
657
897
|
// than printing zeroes that read like a writer doing nothing (§5.4).
|
|
658
898
|
if (frameWriter)
|
|
659
899
|
setFrameStatsProvider(frameWriter.stats);
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
900
|
+
if (scrollbackBridge) {
|
|
901
|
+
const { writer } = scrollbackBridge;
|
|
902
|
+
setScrollbackStatsProvider(() => ({
|
|
903
|
+
...writer.scrollbackStats(), regionRows: writer.regionRows(),
|
|
904
|
+
}));
|
|
905
|
+
}
|
|
906
|
+
// The observer only serves the full-screen scrollbar geometry; native has none.
|
|
907
|
+
const frameObserver = native
|
|
908
|
+
? null
|
|
909
|
+
: createFrameObserver({
|
|
910
|
+
stdout: frameWriter?.stdout ?? process.stdout, terminal: process.stdout, scrollbar,
|
|
911
|
+
});
|
|
663
912
|
const disposeFrameWriter = () => {
|
|
664
|
-
|
|
913
|
+
// Same switch as `finalizeNativeScreen`: this is one of the four teardown paths.
|
|
914
|
+
killAnchorProbe();
|
|
915
|
+
frameObserver?.dispose();
|
|
665
916
|
pointerRouter?.dispose();
|
|
666
917
|
scrollbar.controller?.dispose();
|
|
667
918
|
if (!frameWriter)
|
|
668
919
|
return;
|
|
669
920
|
setFrameStatsProvider(null);
|
|
921
|
+
setScrollbackStatsProvider(null);
|
|
670
922
|
frameWriter.dispose();
|
|
671
923
|
};
|
|
672
924
|
const replayTranscript = () => {
|
|
673
|
-
|
|
925
|
+
// Native mode never replays: the conversation is already in the scrollback, and
|
|
926
|
+
// a replay would print it twice (D9).
|
|
927
|
+
if (replayed || !config.exitTranscript || native)
|
|
674
928
|
return;
|
|
675
929
|
replayed = true;
|
|
676
930
|
const snapshot = readExitSnapshot();
|
|
@@ -699,6 +953,49 @@ function runInteractive(flags, extras = {}) {
|
|
|
699
953
|
vtInputSupported,
|
|
700
954
|
})
|
|
701
955
|
: NOOP_KEYBOARD_ENHANCEMENT;
|
|
956
|
+
// --- Measure the bottom of the screen, THEN build the first frame (§4.4 / §8.1).
|
|
957
|
+
//
|
|
958
|
+
// ORDER, ALL OF IT LOAD-BEARING:
|
|
959
|
+
// 1. the filter exists (it is the only thing that strips the reply — D5);
|
|
960
|
+
// 2. raw mode is raised BEFORE the query (P0-1 / I-A10);
|
|
961
|
+
// 3. the probe is awaited, and any failure is `null`;
|
|
962
|
+
// 4. `enterNativeScreen` writes the one-off fill only when the probe failed;
|
|
963
|
+
// 5. the measurement is handed to the writer before the first batch.
|
|
964
|
+
//
|
|
965
|
+
// This is the only `await` in the whole startup path and it is bounded by
|
|
966
|
+
// `anchorProbeTimeoutMs` (150 ms), which only a terminal that cannot answer pays.
|
|
967
|
+
let anchorSpaceBelow = null;
|
|
968
|
+
if (native && filter && scrollbackBridge) {
|
|
969
|
+
if (raiseRawModeForProbe()) {
|
|
970
|
+
filter.setExpectingCursorReport(true);
|
|
971
|
+
try {
|
|
972
|
+
anchorSpaceBelow = await probeAnchorSpaceBelow({
|
|
973
|
+
stdin: process.stdin,
|
|
974
|
+
stdout: process.stdout,
|
|
975
|
+
rows: () => process.stdout.rows,
|
|
976
|
+
// The first frame is waiting on this: hold the event loop, or a session with
|
|
977
|
+
// nothing else referenced yet can exit before it draws anything.
|
|
978
|
+
holdProcess: true,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
catch (error) {
|
|
982
|
+
// The probe never throws by contract; this is the belt to that braces, and it
|
|
983
|
+
// must not be able to fail the launch.
|
|
984
|
+
logger.warn('cli', 'scroll_anchor_probe_failed', {
|
|
985
|
+
rows: process.stdout.rows ?? null, reason: String(error),
|
|
986
|
+
});
|
|
987
|
+
anchorSpaceBelow = null;
|
|
988
|
+
}
|
|
989
|
+
finally {
|
|
990
|
+
filter.setExpectingCursorReport(false);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
logger.info('cli', anchorSpaceBelow === null ? 'scroll_anchor_probe_failed' : 'scroll_anchor', {
|
|
994
|
+
source: anchorSpaceBelow === null ? 'fill' : 'probe',
|
|
995
|
+
spaceBelow: anchorSpaceBelow,
|
|
996
|
+
rows: process.stdout.rows ?? null,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
702
999
|
{
|
|
703
1000
|
// `mouse` here means "a mouse-parsing filter is installed", NOT "the user
|
|
704
1001
|
// wants mouse support" — see `AltScreenOptions`. `screen.ts` must never
|
|
@@ -710,17 +1007,31 @@ function runInteractive(flags, extras = {}) {
|
|
|
710
1007
|
// paste at its default `true` still builds a filter, and passing the handle
|
|
711
1008
|
// here would write `?1000h` at a user who disabled the mouse precisely to
|
|
712
1009
|
// keep their terminal's own selection.
|
|
713
|
-
screen =
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1010
|
+
screen = plan.altScreen
|
|
1011
|
+
? enterAltScreen(process.stdout, {
|
|
1012
|
+
mouse: mouseOn,
|
|
1013
|
+
motion: mouseOn,
|
|
1014
|
+
bracketedPaste: pasteOn,
|
|
1015
|
+
})
|
|
1016
|
+
// Native scroll: no alternate screen, no mouse (D6). DEC 2004, plus the
|
|
1017
|
+
// one-off fill when the cursor could not be measured — before anything else,
|
|
1018
|
+
// so the rows it scrolls away are the shell's, never this session's.
|
|
1019
|
+
: enterNativeScreen(process.stdout, {
|
|
1020
|
+
bracketedPaste: pasteOn,
|
|
1021
|
+
anchorFillRows: anchorSpaceBelow === null && scrollbackBridge
|
|
1022
|
+
? Math.max(0, (process.stdout.rows ?? 24) - 1)
|
|
1023
|
+
: 0,
|
|
1024
|
+
});
|
|
1025
|
+
// The measurement reaches the writer BEFORE the first batch, so frame one is
|
|
1026
|
+
// already pinned. `fill` says "we know where the bottom is because we went there".
|
|
1027
|
+
scrollbackBridge?.writer.setAnchorSpaceBelow(anchorSpaceBelow ?? 0, anchorSpaceBelow === null ? 'fill' : 'probe');
|
|
718
1028
|
// Input-encoding modes unwind FIRST, for the same reason the mouse
|
|
719
1029
|
// reporting does inside `screen.restore()`: the terminal must stop
|
|
720
1030
|
// sending enhanced encodings before anything else tears the reader
|
|
721
1031
|
// down, or a keystroke taken in that instant survives into the shell.
|
|
722
1032
|
const restore = () => {
|
|
723
1033
|
keyboardEnhancement.restore();
|
|
1034
|
+
finalizeNativeScreen();
|
|
724
1035
|
screen?.restore();
|
|
725
1036
|
};
|
|
726
1037
|
// Four idempotent restore paths (§4.4). The signal hook is NOT redundant:
|
|
@@ -838,7 +1149,8 @@ function runInteractive(flags, extras = {}) {
|
|
|
838
1149
|
interactiveClosed = true;
|
|
839
1150
|
logger.error('cli', 'render_failed', { error: String(error) });
|
|
840
1151
|
for (const cleanup of [
|
|
841
|
-
() => screen?.restore(), disposeSelection, disposeStdinFilter,
|
|
1152
|
+
finalizeNativeScreen, () => screen?.restore(), disposeSelection, disposeStdinFilter,
|
|
1153
|
+
disposeFrameWriter,
|
|
842
1154
|
() => controller.abort(), () => controller.dispose(), () => updateService?.dispose(),
|
|
843
1155
|
() => { process.stdin.pause(); process.stdin.unref?.(); },
|
|
844
1156
|
]) {
|
|
@@ -865,7 +1177,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
865
1177
|
vtInputWarning: vtInputSupported ? undefined : { nodeVersion: process.versions.node }, updateBridge: updateEligible ? updateBridge : undefined,
|
|
866
1178
|
// Absent when nothing can refuse a paste, so a `--no-paste` tree is
|
|
867
1179
|
// byte-identical to a build without this feature.
|
|
868
|
-
pasteBridge: pasteOn ? pasteBridge : undefined, terminal: terminalBridge, session: identity, sessionBridge: sessionBridge, ...(startup.initialEntries.length > 0 ? { initialEntries: startup.initialEntries } : {}), ...(startup.notice ? { sessionNotice: startup.notice } : {}) }),
|
|
1180
|
+
pasteBridge: pasteOn ? pasteBridge : undefined, terminal: terminalBridge, ...(scrollbackBridge ? { scrollMode: plan.scrollMode, scrollback: scrollbackBridge } : {}), session: identity, sessionBridge: sessionBridge, ...(startup.initialEntries.length > 0 ? { initialEntries: startup.initialEntries } : {}), ...(startup.notice ? { sessionNotice: startup.notice } : {}) }),
|
|
869
1181
|
// Full-screen owns console.* itself (I-4): Ink's patchConsole writes
|
|
870
1182
|
// straight to stdout and permanently shifts the fixed frame's accounting.
|
|
871
1183
|
{
|
|
@@ -874,7 +1186,7 @@ function runInteractive(flags, extras = {}) {
|
|
|
874
1186
|
// I-5: with mouse support off the REAL stdin is handed over unwrapped, so
|
|
875
1187
|
// every existing path is provably unchanged.
|
|
876
1188
|
stdin: filter?.stdin ?? process.stdin,
|
|
877
|
-
stdout: frameObserver.stdout,
|
|
1189
|
+
stdout: frameObserver?.stdout ?? frameWriter?.stdout ?? process.stdout,
|
|
878
1190
|
}, caps);
|
|
879
1191
|
}
|
|
880
1192
|
catch (error) {
|
|
@@ -955,6 +1267,9 @@ function runInteractive(flags, extras = {}) {
|
|
|
955
1267
|
// Also cover a successful immediate exit when launched via dist/cli.js,
|
|
956
1268
|
// which bypasses the launcher's code-zero boot-guard backstop.
|
|
957
1269
|
markBootHealthy(VERSION);
|
|
1270
|
+
// Native scroll: finalize BEFORE the screen handle (the writer erases the live
|
|
1271
|
+
// region and prints the summary; the handle then shows the cursor again).
|
|
1272
|
+
finalizeNativeScreen();
|
|
958
1273
|
screen?.restore();
|
|
959
1274
|
// BEFORE `disposeStdinFilter()`, which clears the listener set it subscribed to,
|
|
960
1275
|
// and after `restore()` for the reason that call records: the terminal must
|
|
@@ -1159,6 +1474,8 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
1159
1474
|
// tui-input-flicker-fix §5.1. Listed here AND cased in the switch below, for
|
|
1160
1475
|
// the reason `transcriptRetain` records three lines up.
|
|
1161
1476
|
'diffRender',
|
|
1477
|
+
// tui-native-terminal-scroll. Listed here AND cased in the switch below.
|
|
1478
|
+
'scrollMode',
|
|
1162
1479
|
'syncOutput',
|
|
1163
1480
|
// Without these two, `density` and `hints` would be settable only as one-shot
|
|
1164
1481
|
// flags and never persist -- inconsistent with every other config field.
|
|
@@ -1218,6 +1535,11 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
1218
1535
|
// a script at all: `config list` enumerates the section dynamically and so
|
|
1219
1536
|
// shows these keys whether they are here or not, while `config set` does not.
|
|
1220
1537
|
...COMPACTION_CONFIG_SET_KEYS,
|
|
1538
|
+
// `jobs.*` keys are handled by `applyJobsConfigSet`, same reasoning again — and
|
|
1539
|
+
// this spread is exactly why `JOBS_CONFIG_SET_KEYS ⊆ CONFIG_SET_KEYS` needs its
|
|
1540
|
+
// own assertion: the static guard in `config.test.ts` matches LITERAL keys in
|
|
1541
|
+
// this file, so a spread set is invisible to it (RV-P2-8).
|
|
1542
|
+
...JOBS_CONFIG_SET_KEYS,
|
|
1221
1543
|
]);
|
|
1222
1544
|
/**
|
|
1223
1545
|
* `aragon config set maxTokens <n|auto|default>`.
|
|
@@ -1357,6 +1679,17 @@ function runConfigSetValue(key, value) {
|
|
|
1357
1679
|
process.stdout.write(`Set ${key} = ${String(merged.compaction[field])}\n`);
|
|
1358
1680
|
return;
|
|
1359
1681
|
}
|
|
1682
|
+
const jobsPatch = applyJobsConfigSet(key, value);
|
|
1683
|
+
if (jobsPatch) {
|
|
1684
|
+
// Echoed from the STORED value, so `config set jobs.waitSeconds 1` prints 5
|
|
1685
|
+
// and `jobs.waitSeconds 9999` prints 60 — the rule every section above
|
|
1686
|
+
// states. It matters here too: `waitSeconds` is ALSO converged against the
|
|
1687
|
+
// tool ceiling by I-4, so the stored value is the only honest answer.
|
|
1688
|
+
const merged = updatePersistedConfig(jobsPatch);
|
|
1689
|
+
const field = key.slice('jobs.'.length);
|
|
1690
|
+
process.stdout.write(`Set ${key} = ${String(merged.jobs[field])}\n`);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1360
1693
|
const patch = {};
|
|
1361
1694
|
switch (key) {
|
|
1362
1695
|
case 'provider':
|
|
@@ -1401,6 +1734,9 @@ function runConfigSetValue(key, value) {
|
|
|
1401
1734
|
case 'diffRender':
|
|
1402
1735
|
patch.diffRender = value === 'true' || value === '1';
|
|
1403
1736
|
break;
|
|
1737
|
+
case 'scrollMode':
|
|
1738
|
+
patch.scrollMode = clampScrollMode(value, DEFAULT_SCROLL_MODE);
|
|
1739
|
+
break;
|
|
1404
1740
|
case 'syncOutput':
|
|
1405
1741
|
patch.syncOutput = value === 'true' || value === '1';
|
|
1406
1742
|
break;
|
|
@@ -1790,6 +2126,7 @@ function buildProgram() {
|
|
|
1790
2126
|
// one documented escape hatch for a mis-rendering terminal into a no-op.
|
|
1791
2127
|
.option('--diff-render', 'Repaint only changed rows (default, full-screen only)')
|
|
1792
2128
|
.option('--no-diff-render', 'Repaint the whole frame every time (pre-fix behavior)')
|
|
2129
|
+
.option('--scroll-mode <mode>', 'Scrolling: native (terminal scrollback, default) or app (full-screen)')
|
|
1793
2130
|
.option('--sync-output', 'Ask the terminal to present each repaint atomically (the default)')
|
|
1794
2131
|
.option('--no-sync-output', 'Do not emit DEC 2026 begin/end synchronized update')
|
|
1795
2132
|
.option('--no-color', 'Disable ANSI color')
|
|
@@ -1822,14 +2159,14 @@ function buildProgram() {
|
|
|
1822
2159
|
}
|
|
1823
2160
|
return;
|
|
1824
2161
|
}
|
|
1825
|
-
runInteractive(flags, { initialPrompt: prompt });
|
|
2162
|
+
void runInteractive(flags, { initialPrompt: prompt });
|
|
1826
2163
|
});
|
|
1827
2164
|
// aragon config [set <key> <value> | path]
|
|
1828
2165
|
const configCmd = program
|
|
1829
2166
|
.command('config')
|
|
1830
2167
|
.description('Open the settings screen')
|
|
1831
2168
|
.action(() => {
|
|
1832
|
-
runInteractive(toFlags(program.opts()), { initialOverlay: 'settings' });
|
|
2169
|
+
void runInteractive(toFlags(program.opts()), { initialOverlay: 'settings' });
|
|
1833
2170
|
});
|
|
1834
2171
|
configCmd
|
|
1835
2172
|
.command('set <key> <value>')
|