@aragon-agent/cli 0.5.9 → 0.5.11
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 +1157 -1026
- package/README.md +123 -0
- package/dist/boot/guard.d.ts +135 -0
- package/dist/boot/guard.js +218 -0
- package/dist/boot/rollback.d.ts +86 -0
- package/dist/boot/rollback.js +185 -0
- package/dist/cli.js +376 -2
- package/dist/commands/builtins.js +118 -0
- package/dist/commands/perf.d.ts +5 -1
- package/dist/commands/perf.js +43 -1
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +31 -0
- package/dist/config/app-paths.js +37 -0
- package/dist/config/cli-commands.d.ts +27 -1
- package/dist/config/cli-commands.js +51 -2
- package/dist/config/env.d.ts +11 -1
- package/dist/config/env.js +58 -1
- package/dist/config/load.d.ts +30 -0
- package/dist/config/load.js +56 -1
- package/dist/config/schema.d.ts +91 -0
- package/dist/config/schema.js +107 -0
- package/dist/config/store.js +13 -1
- package/dist/launcher.d.ts +14 -0
- package/dist/launcher.js +43 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +88 -2
- package/dist/ui/BottomStatusRow.d.ts +25 -3
- package/dist/ui/BottomStatusRow.js +10 -7
- package/dist/ui/PromptInput.d.ts +10 -0
- package/dist/ui/PromptInput.js +41 -48
- package/dist/ui/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- package/dist/ui/editor-reducer.d.ts +62 -0
- package/dist/ui/editor-reducer.js +90 -0
- package/dist/ui/frame-differ.d.ts +95 -0
- package/dist/ui/frame-differ.js +230 -0
- package/dist/ui/stdout-frame-writer.d.ts +41 -0
- package/dist/ui/stdout-frame-writer.js +102 -0
- package/dist/update/classify-failure.d.ts +32 -0
- package/dist/update/classify-failure.js +60 -0
- package/dist/update/install-lock.d.ts +55 -0
- package/dist/update/install-lock.js +184 -0
- package/dist/update/install-source.d.ts +97 -0
- package/dist/update/install-source.js +218 -0
- package/dist/update/installer.d.ts +108 -0
- package/dist/update/installer.js +242 -0
- package/dist/update/limits.d.ts +111 -0
- package/dist/update/limits.js +111 -0
- package/dist/update/npm-view.d.ts +62 -0
- package/dist/update/npm-view.js +133 -0
- package/dist/update/registry.d.ts +52 -0
- package/dist/update/registry.js +161 -0
- package/dist/update/semver.d.ts +89 -0
- package/dist/update/semver.js +295 -0
- package/dist/update/service.d.ts +193 -0
- package/dist/update/service.js +636 -0
- package/dist/update/state.d.ts +103 -0
- package/dist/update/state.js +142 -0
- package/dist/update/types.d.ts +169 -0
- package/dist/update/types.js +68 -0
- package/package.json +3 -3
package/dist/config/schema.d.ts
CHANGED
|
@@ -98,6 +98,23 @@ export declare const DEFAULT_RENDER_GOVERNOR = true;
|
|
|
98
98
|
*/
|
|
99
99
|
export declare const DEFAULT_MAX_RENDER_INTERVAL_MS = 320;
|
|
100
100
|
export declare function clampMaxRenderInterval(v: unknown, fallback: number): number;
|
|
101
|
+
/**
|
|
102
|
+
* Repaint only the frame lines that changed (full-screen only).
|
|
103
|
+
*
|
|
104
|
+
* ON by default, and `--no-diff-render` restores today's byte stream EXACTLY —
|
|
105
|
+
* the interposer is not constructed at all, so a regression on an exotic
|
|
106
|
+
* terminal is one flag away from being neutralised (§3.5 rung 2).
|
|
107
|
+
*/
|
|
108
|
+
export declare const DEFAULT_DIFF_RENDER = true;
|
|
109
|
+
/**
|
|
110
|
+
* Wrap each repaint batch in DEC 2026 begin/end synchronized update.
|
|
111
|
+
*
|
|
112
|
+
* ON by default with NO capability probe (§3.3): terminals that do not implement
|
|
113
|
+
* mode 2026 parse the CSI sequence, find an unknown private mode and discard it,
|
|
114
|
+
* so a false positive costs nothing — and a false negative costs nothing either,
|
|
115
|
+
* because the frame differ already removes the flicker on its own.
|
|
116
|
+
*/
|
|
117
|
+
export declare const DEFAULT_SYNC_OUTPUT = true;
|
|
101
118
|
/** `ask_user` calls allowed per user turn. */
|
|
102
119
|
export declare const DEFAULT_PLAN_MAX_ASK_ROUNDS = 4;
|
|
103
120
|
/**
|
|
@@ -434,6 +451,44 @@ export declare const DEFAULT_FAST_CONFIG: FastConfig;
|
|
|
434
451
|
* user really did configure a dead vendor as the MAIN one.
|
|
435
452
|
*/
|
|
436
453
|
export declare function clampFastConfig(raw: unknown): FastConfig;
|
|
454
|
+
/** `auto` installs; `notify` only reports; `off` disables the subsystem. */
|
|
455
|
+
export declare const UPDATE_MODES: readonly ["auto", "notify", "off"];
|
|
456
|
+
export type UpdateMode = (typeof UPDATE_MODES)[number];
|
|
457
|
+
export declare function isUpdateMode(v: unknown): v is UpdateMode;
|
|
458
|
+
export interface UpdateConfig {
|
|
459
|
+
/**
|
|
460
|
+
* Whether the updater installs, merely reports, or does nothing at all.
|
|
461
|
+
*
|
|
462
|
+
* DEFAULTS TO `'auto'`, and that is a PRODUCT decision rather than a technical
|
|
463
|
+
* one: this CLI's users are overwhelmingly on a stale version they did not
|
|
464
|
+
* choose. The trade it makes — a wider supply-chain window in exchange for
|
|
465
|
+
* users who are actually on the current release — is stated plainly in the
|
|
466
|
+
* README (R-1), and `'off'` plus `ARAGON_UPDATE=0` are the documented kill
|
|
467
|
+
* switches.
|
|
468
|
+
*/
|
|
469
|
+
mode: UpdateMode;
|
|
470
|
+
/** Minimum wall-clock between registry checks, MACHINE-WIDE (§3.7). */
|
|
471
|
+
checkIntervalMs: number;
|
|
472
|
+
/** `''` = derive: `ARAGON_UPDATE_REGISTRY` › `npm_config_registry` › npmjs. */
|
|
473
|
+
registry: string;
|
|
474
|
+
/** The dist-tag to track. */
|
|
475
|
+
distTag: string;
|
|
476
|
+
}
|
|
477
|
+
export declare const DEFAULT_UPDATE_CONFIG: UpdateConfig;
|
|
478
|
+
/**
|
|
479
|
+
* THE single gate for every update-config read AND write, mirroring
|
|
480
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` / `clampTodoConfig`
|
|
481
|
+
* / `clampRetryConfig` / `clampFastConfig` for the reason all six record:
|
|
482
|
+
* hardening only the read path leaves a bad value on disk that reverts to the
|
|
483
|
+
* default on every launch, which presents to the user as "my setting won't
|
|
484
|
+
* stick".
|
|
485
|
+
*
|
|
486
|
+
* `registry` is CLAMPED TO `''` rather than rejected when it is not an
|
|
487
|
+
* `http(s):` URL. `''` means "derive it" (§3.3), which is the safe reading of a
|
|
488
|
+
* typo — the alternative is a background timer that throws on a value the user
|
|
489
|
+
* cannot see it reading.
|
|
490
|
+
*/
|
|
491
|
+
export declare function clampUpdateConfig(raw: unknown): UpdateConfig;
|
|
437
492
|
export interface PersistedConfig {
|
|
438
493
|
version: number;
|
|
439
494
|
provider: string;
|
|
@@ -492,6 +547,10 @@ export interface PersistedConfig {
|
|
|
492
547
|
renderGovernor: boolean;
|
|
493
548
|
/** Ceiling of the governor ladder in ms; `33` flattens it (L4). */
|
|
494
549
|
maxRenderIntervalMs: number;
|
|
550
|
+
/** Repaint only the frame rows that changed (tui-input-flicker-fix §5.1). */
|
|
551
|
+
diffRender: boolean;
|
|
552
|
+
/** Wrap each repaint batch in DEC 2026 synchronized update (§5.1). */
|
|
553
|
+
syncOutput: boolean;
|
|
495
554
|
confirmTools: boolean;
|
|
496
555
|
toolTimeoutMs: number;
|
|
497
556
|
idleTimeoutMs: number;
|
|
@@ -537,6 +596,8 @@ export interface PersistedConfig {
|
|
|
537
596
|
retry: RetryConfig;
|
|
538
597
|
/** Fast model tier. The SEVENTH nested object; scalars only (see FastConfig). */
|
|
539
598
|
fast: FastConfig;
|
|
599
|
+
/** Auto-update. The EIGHTH nested object; scalars only (see UpdateConfig). */
|
|
600
|
+
update: UpdateConfig;
|
|
540
601
|
}
|
|
541
602
|
export declare const DEFAULT_CONFIG: PersistedConfig;
|
|
542
603
|
/**
|
|
@@ -631,6 +692,20 @@ export interface CliConfig {
|
|
|
631
692
|
renderGovernor: boolean;
|
|
632
693
|
/** Ceiling of the governor ladder in ms; `33` flattens it (L4). */
|
|
633
694
|
maxRenderIntervalMs: number;
|
|
695
|
+
/**
|
|
696
|
+
* Repaint only the frame rows that changed (tui-input-flicker-fix §5.1).
|
|
697
|
+
*
|
|
698
|
+
* IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
|
|
699
|
+
* `showThinking` / `liveToolOutput` / `update` each record: `runInteractive`
|
|
700
|
+
* reads `config.diffRender`, where `config` is a `CliConfig`, so a key added to
|
|
701
|
+
* the persisted shape alone would round-trip through `config set`, appear in
|
|
702
|
+
* `config list` — and never reach the one gate that constructs the writer.
|
|
703
|
+
* `--no-diff-render` and `ARAGON_DIFF_RENDER=0` would parse and be discarded,
|
|
704
|
+
* with no compile error and no runtime error.
|
|
705
|
+
*/
|
|
706
|
+
diffRender: boolean;
|
|
707
|
+
/** Wrap each repaint batch in DEC 2026 synchronized update (§5.1). */
|
|
708
|
+
syncOutput: boolean;
|
|
634
709
|
confirmTools: boolean;
|
|
635
710
|
toolTimeoutMs: number;
|
|
636
711
|
idleTimeoutMs: number;
|
|
@@ -709,6 +784,22 @@ export interface CliConfig {
|
|
|
709
784
|
* `/provider`, `/reload` and the settings screen.
|
|
710
785
|
*/
|
|
711
786
|
fast: FastConfig;
|
|
787
|
+
/**
|
|
788
|
+
* Resolved auto-update settings (cli-auto-update §4.1).
|
|
789
|
+
*
|
|
790
|
+
* `mode` here decides whether `runInteractive` CONSTRUCTS the service at all
|
|
791
|
+
* (§3.8), which is what makes `'off'` byte-identical to a pre-feature build:
|
|
792
|
+
* no timer, no file read, no socket. The other three keys are read by the
|
|
793
|
+
* service on every check, so a `/update off` mid-session stops the next one.
|
|
794
|
+
*
|
|
795
|
+
* IT MUST EXIST HERE AND NOT ONLY IN `PersistedConfig`, for the reason
|
|
796
|
+
* `showThinking` and `liveToolOutput` each record above: the gate reads
|
|
797
|
+
* `cfg.update`, where `cfg` is a `CliConfig`, so a key added to the persisted
|
|
798
|
+
* shape alone would round-trip through `config set`, appear in `config list` —
|
|
799
|
+
* and never reach the gate. `--no-update` and `ARAGON_UPDATE=0` would parse
|
|
800
|
+
* and be discarded, with no compile error and no runtime error.
|
|
801
|
+
*/
|
|
802
|
+
update: UpdateConfig;
|
|
712
803
|
/**
|
|
713
804
|
* Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
|
|
714
805
|
*
|
package/dist/config/schema.js
CHANGED
|
@@ -145,6 +145,30 @@ export function clampMaxRenderInterval(v, fallback) {
|
|
|
145
145
|
return clampInt(v, fallback, RENDER_INTERVAL_RANGE);
|
|
146
146
|
}
|
|
147
147
|
// ---------------------------------------------------------------------------
|
|
148
|
+
// Frame diffing (tui-input-flicker-fix §5.1)
|
|
149
|
+
//
|
|
150
|
+
// TWO TROUBLESHOOTING SWITCHES, in the same class as `historyEnabled`: neither
|
|
151
|
+
// appears on the settings overlay, because a user who needs them is debugging a
|
|
152
|
+
// terminal, not adjusting a preference.
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
/**
|
|
155
|
+
* Repaint only the frame lines that changed (full-screen only).
|
|
156
|
+
*
|
|
157
|
+
* ON by default, and `--no-diff-render` restores today's byte stream EXACTLY —
|
|
158
|
+
* the interposer is not constructed at all, so a regression on an exotic
|
|
159
|
+
* terminal is one flag away from being neutralised (§3.5 rung 2).
|
|
160
|
+
*/
|
|
161
|
+
export const DEFAULT_DIFF_RENDER = true;
|
|
162
|
+
/**
|
|
163
|
+
* Wrap each repaint batch in DEC 2026 begin/end synchronized update.
|
|
164
|
+
*
|
|
165
|
+
* ON by default with NO capability probe (§3.3): terminals that do not implement
|
|
166
|
+
* mode 2026 parse the CSI sequence, find an unknown private mode and discard it,
|
|
167
|
+
* so a false positive costs nothing — and a false negative costs nothing either,
|
|
168
|
+
* because the frame differ already removes the flicker on its own.
|
|
169
|
+
*/
|
|
170
|
+
export const DEFAULT_SYNC_OUTPUT = true;
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
148
172
|
// Plan mode (plan-mode §4.4)
|
|
149
173
|
//
|
|
150
174
|
// THREE FLAT KEYS, not a nested `planMode` object. `store.ts` documents that its
|
|
@@ -570,6 +594,81 @@ export function clampFastConfig(raw) {
|
|
|
570
594
|
reviewMaxPerSession: clampInt(src.reviewMaxPerSession, DEFAULT_FAST_CONFIG.reviewMaxPerSession, REVIEW_SESSION_RANGE),
|
|
571
595
|
};
|
|
572
596
|
}
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
// Auto-update (cli-auto-update §4.1)
|
|
599
|
+
//
|
|
600
|
+
// The EIGHTH nested section in the persisted config, after `apiKeys`, `skills`,
|
|
601
|
+
// `log`, `team`, `todo`, `retry` and `fast` — and, like all of them, SCALARS
|
|
602
|
+
// ONLY, exactly one level deep. `store.ts` merges these sections BY HAND and
|
|
603
|
+
// that merge is only correct while nesting stays flat (C-5).
|
|
604
|
+
//
|
|
605
|
+
// BOTH OF `store.ts`'s MERGES MUST LEARN ABOUT THIS SECTION, AND THE OMISSION IS
|
|
606
|
+
// SILENT (C-4 / R-10). `config set update.mode notify` sends
|
|
607
|
+
// `{ update: { mode: 'notify' } }`; without the write merge the shallow
|
|
608
|
+
// top-level spread REPLACES the whole section and `checkIntervalMs`, `registry`
|
|
609
|
+
// and `distTag` silently revert on disk. This package has paid for that twice
|
|
610
|
+
// already — `todo` P0-1 and `fast` R-10 — which is why it is written down a
|
|
611
|
+
// third time rather than assumed to be common knowledge.
|
|
612
|
+
//
|
|
613
|
+
// FOUR KEYS AND NO MORE (D-16). `mode` covers on / notify / off; a fifth key
|
|
614
|
+
// (`autoInstall`) would be a second way to say the same thing, and two ways to
|
|
615
|
+
// express one setting is how they end up disagreeing.
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
/** `auto` installs; `notify` only reports; `off` disables the subsystem. */
|
|
618
|
+
export const UPDATE_MODES = ['auto', 'notify', 'off'];
|
|
619
|
+
export function isUpdateMode(v) {
|
|
620
|
+
return typeof v === 'string' && UPDATE_MODES.includes(v);
|
|
621
|
+
}
|
|
622
|
+
export const DEFAULT_UPDATE_CONFIG = {
|
|
623
|
+
mode: 'auto',
|
|
624
|
+
// FOUR HOURS, in ms. `checkIntervalMs` and not `checkIntervalHours` (D-17):
|
|
625
|
+
// every other duration in this config is in ms (`toolTimeoutMs`,
|
|
626
|
+
// `idleTimeoutMs`, `team.dispatchTimeoutMs`, `retry.initialDelayMs`), and one
|
|
627
|
+
// unit per file is worth more than one shorter number.
|
|
628
|
+
checkIntervalMs: 14_400_000,
|
|
629
|
+
registry: '',
|
|
630
|
+
distTag: 'latest',
|
|
631
|
+
};
|
|
632
|
+
/** 15 minutes to 7 days. The floor is a politeness bound on the registry. */
|
|
633
|
+
const UPDATE_INTERVAL_RANGE = { min: 900_000, max: 604_800_000 };
|
|
634
|
+
/** npm's own dist-tag grammar, loosely: no slashes, no spaces, no `@`. */
|
|
635
|
+
const DIST_TAG_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
636
|
+
/**
|
|
637
|
+
* THE single gate for every update-config read AND write, mirroring
|
|
638
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` / `clampTodoConfig`
|
|
639
|
+
* / `clampRetryConfig` / `clampFastConfig` for the reason all six record:
|
|
640
|
+
* hardening only the read path leaves a bad value on disk that reverts to the
|
|
641
|
+
* default on every launch, which presents to the user as "my setting won't
|
|
642
|
+
* stick".
|
|
643
|
+
*
|
|
644
|
+
* `registry` is CLAMPED TO `''` rather than rejected when it is not an
|
|
645
|
+
* `http(s):` URL. `''` means "derive it" (§3.3), which is the safe reading of a
|
|
646
|
+
* typo — the alternative is a background timer that throws on a value the user
|
|
647
|
+
* cannot see it reading.
|
|
648
|
+
*/
|
|
649
|
+
export function clampUpdateConfig(raw) {
|
|
650
|
+
const src = (raw && typeof raw === 'object' ? raw : {});
|
|
651
|
+
const registry = text(src.registry, DEFAULT_UPDATE_CONFIG.registry);
|
|
652
|
+
const distTag = text(src.distTag, DEFAULT_UPDATE_CONFIG.distTag);
|
|
653
|
+
return {
|
|
654
|
+
mode: isUpdateMode(src.mode) ? src.mode : DEFAULT_UPDATE_CONFIG.mode,
|
|
655
|
+
checkIntervalMs: clampInt(src.checkIntervalMs, DEFAULT_UPDATE_CONFIG.checkIntervalMs, UPDATE_INTERVAL_RANGE),
|
|
656
|
+
registry: isRegistryUrl(registry) ? registry : DEFAULT_UPDATE_CONFIG.registry,
|
|
657
|
+
distTag: DIST_TAG_RE.test(distTag) ? distTag : DEFAULT_UPDATE_CONFIG.distTag,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function isRegistryUrl(raw) {
|
|
661
|
+
if (raw.length === 0)
|
|
662
|
+
return false;
|
|
663
|
+
try {
|
|
664
|
+
const url = new URL(raw);
|
|
665
|
+
// `http:` is accepted for a LAN mirror; nothing else is a registry.
|
|
666
|
+
return url.protocol === 'https:' || url.protocol === 'http:';
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
573
672
|
export const DEFAULT_CONFIG = {
|
|
574
673
|
version: CONFIG_VERSION,
|
|
575
674
|
provider: 'anthropic',
|
|
@@ -594,6 +693,8 @@ export const DEFAULT_CONFIG = {
|
|
|
594
693
|
transcriptRetain: DEFAULT_TRANSCRIPT_RETAIN,
|
|
595
694
|
renderGovernor: DEFAULT_RENDER_GOVERNOR,
|
|
596
695
|
maxRenderIntervalMs: DEFAULT_MAX_RENDER_INTERVAL_MS,
|
|
696
|
+
diffRender: DEFAULT_DIFF_RENDER,
|
|
697
|
+
syncOutput: DEFAULT_SYNC_OUTPUT,
|
|
597
698
|
confirmTools: false,
|
|
598
699
|
toolTimeoutMs: DEFAULT_TOOL_TIMEOUT_MS,
|
|
599
700
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
@@ -621,6 +722,12 @@ export const DEFAULT_CONFIG = {
|
|
|
621
722
|
// falsy and so LOOKS correct — right up until `/fast on` writes a section that
|
|
622
723
|
// has no other keys in it (P0-1's shape, one feature later).
|
|
623
724
|
fast: DEFAULT_FAST_CONFIG,
|
|
725
|
+
// And one section later again, for the third time. Without this line a user
|
|
726
|
+
// whose `config.json` predates auto-update resolves `config.update.mode` to
|
|
727
|
+
// `undefined`, which is neither `'auto'` nor `'off'` — so the §3.8 gate
|
|
728
|
+
// (`cfg.update.mode !== 'off'`) would read TRUE and the feature would be on
|
|
729
|
+
// for a reason nobody could point at.
|
|
730
|
+
update: DEFAULT_UPDATE_CONFIG,
|
|
624
731
|
};
|
|
625
732
|
/**
|
|
626
733
|
* Runtime state that used to be stored in `config.json` by mistake
|
package/dist/config/store.js
CHANGED
|
@@ -14,7 +14,7 @@ import { dirname } from 'node:path';
|
|
|
14
14
|
import process from 'node:process';
|
|
15
15
|
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
16
|
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
-
import { CONFIG_VERSION, DEFAULT_CONFIG, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
17
|
+
import { CONFIG_VERSION, DEFAULT_CONFIG, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
18
18
|
/**
|
|
19
19
|
* Normalize `maxTokens` for both directions of the config file.
|
|
20
20
|
*
|
|
@@ -134,6 +134,11 @@ export function loadPersistedConfig() {
|
|
|
134
134
|
// READ half of the pair that keeps `/fast review 8` from silently switching
|
|
135
135
|
// the whole tier off on the next launch (C-3 / R-10).
|
|
136
136
|
fast: clampFastConfig({ ...DEFAULT_CONFIG.fast, ...(partial.fast ?? {}) }),
|
|
137
|
+
// `update` is the eighth. Same one-level rule, same merge — and the READ
|
|
138
|
+
// half is what supplies a whole section to every user whose `config.json`
|
|
139
|
+
// predates auto-update, which is all of them. Without it `config.update.mode`
|
|
140
|
+
// reads `undefined`, and the §3.8 gate tests `!== 'off'`.
|
|
141
|
+
update: clampUpdateConfig({ ...DEFAULT_CONFIG.update, ...(partial.update ?? {}) }),
|
|
137
142
|
};
|
|
138
143
|
}
|
|
139
144
|
/**
|
|
@@ -196,6 +201,13 @@ export function updatePersistedConfig(patch) {
|
|
|
196
201
|
// tomorrow with no error anywhere. This package has paid for that once
|
|
197
202
|
// already, in `todo` (P0-1).
|
|
198
203
|
fast: clampFastConfig({ ...current.fast, ...(patch.fast ?? {}) }),
|
|
204
|
+
// The eighth section, and the third time this file records the same trap.
|
|
205
|
+
// `config set update.mode notify` sends `{ update: { mode: 'notify' } }`;
|
|
206
|
+
// without this line the shallow top-level spread REPLACES the section,
|
|
207
|
+
// `checkIntervalMs` / `registry` / `distTag` are written absent, and an
|
|
208
|
+
// enterprise mirror the user configured last month is gone with no error
|
|
209
|
+
// anywhere. AC-20 is the assertion that proves both halves exist.
|
|
210
|
+
update: clampUpdateConfig({ ...current.update, ...(patch.update ?? {}) }),
|
|
199
211
|
};
|
|
200
212
|
writeConfigFile(merged);
|
|
201
213
|
return merged;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bin entry (cli-auto-update-hardening section 5.1.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - this file is inside the glyph scanner's scope (C-19).
|
|
5
|
+
*
|
|
6
|
+
* KEEP IT THIS SMALL. Every import here is a module that can crash before the
|
|
7
|
+
* guard that exists to survive crashes. `node:process` is the only static one,
|
|
8
|
+
* and `__tests__/update-wiring.test.ts` asserts that on the source text.
|
|
9
|
+
*
|
|
10
|
+
* `dist/cli.js` IS UNCHANGED AND KEEPS ITS SHEBANG (R-17 / AC-50). Only the
|
|
11
|
+
* `bin` mapping moves; `npm start` (`node dist/cli.js`) and any user script that
|
|
12
|
+
* invokes `dist/cli.js` directly keep working byte for byte.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
package/dist/launcher.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The bin entry (cli-auto-update-hardening section 5.1.1).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - this file is inside the glyph scanner's scope (C-19).
|
|
6
|
+
*
|
|
7
|
+
* KEEP IT THIS SMALL. Every import here is a module that can crash before the
|
|
8
|
+
* guard that exists to survive crashes. `node:process` is the only static one,
|
|
9
|
+
* and `__tests__/update-wiring.test.ts` asserts that on the source text.
|
|
10
|
+
*
|
|
11
|
+
* `dist/cli.js` IS UNCHANGED AND KEEPS ITS SHEBANG (R-17 / AC-50). Only the
|
|
12
|
+
* `bin` mapping moves; `npm start` (`node dist/cli.js`) and any user script that
|
|
13
|
+
* invokes `dist/cli.js` directly keep working byte for byte.
|
|
14
|
+
*/
|
|
15
|
+
import process from 'node:process';
|
|
16
|
+
try {
|
|
17
|
+
// DYNAMIC, AND THAT IS THE WHOLE POINT (P1-2 / R-22). A STATIC import of the
|
|
18
|
+
// guard would put `boot/guard -> update/state -> config/app-paths ->
|
|
19
|
+
// env-paths` in the bin entry's OWN module graph, and a resolution failure
|
|
20
|
+
// anywhere in that chain throws during THIS module's evaluation - outside the
|
|
21
|
+
// `try`, before any line of it runs. "An undeclared dependency" is the first
|
|
22
|
+
// way section 1.1 says a release bricks itself; a guard whose own import fails
|
|
23
|
+
// in that exact case has to degrade to "no guard", never to "no CLI".
|
|
24
|
+
const { runBootGuard } = await import('./boot/guard.js');
|
|
25
|
+
runBootGuard();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// A guard that throws must never be the reason the CLI will not start. This is
|
|
29
|
+
// the one catch in the package that is allowed to be empty: there is no logger
|
|
30
|
+
// yet, and there is nothing the user could do with the message.
|
|
31
|
+
}
|
|
32
|
+
// The `.catch` is for the IMPORT rejecting, which is precisely the import-time
|
|
33
|
+
// crash H1 exists for and which today produces an unhandled rejection with a
|
|
34
|
+
// stack the user cannot act on. `cli.js` runs `main()` at module scope and
|
|
35
|
+
// installs its own `.catch` for everything after that point.
|
|
36
|
+
//
|
|
37
|
+
// EXITING NON-ZERO HERE IS WHAT FEEDS THE GUARD'S COUNTER (D-30a): the exit hook
|
|
38
|
+
// the guard installed counts non-zero exits, and a module that will not import
|
|
39
|
+
// raises, so it arrives here.
|
|
40
|
+
await import('./cli.js').catch((err) => {
|
|
41
|
+
process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
});
|
package/dist/logging/logger.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import type { LogConfig } from '../config/schema.js';
|
|
24
24
|
import { type LogLevelName } from './levels.js';
|
|
25
25
|
/** Closed set, not free text: a typo'd scope is a record nobody can filter on. */
|
|
26
|
-
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history';
|
|
26
|
+
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update';
|
|
27
27
|
export interface LogRecord {
|
|
28
28
|
ts: string;
|
|
29
29
|
lv: LogLevelName;
|
package/dist/ui/App.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import React from 'react';
|
|
|
8
8
|
import type { AgentController } from '../agent/controller.js';
|
|
9
9
|
import type { ConfirmRequest } from '../tools/index.js';
|
|
10
10
|
import { type Overlay, type ToastLevel } from '../agent/reducer.js';
|
|
11
|
+
import type { UpdateBridge } from '../update/types.js';
|
|
11
12
|
import { type RenderMode } from './layout/frame.js';
|
|
12
13
|
import type { MouseSource } from '../input/stdin-mouse-filter.js';
|
|
13
14
|
import type { HumanInputBridge } from '../tools/human-input.js';
|
|
@@ -49,5 +50,14 @@ export interface AppProps {
|
|
|
49
50
|
* is off: inline mode, `--no-mouse`, a non-TTY, or a filter that failed to
|
|
50
51
|
* build (mouse-wheel-region-routing §4.2). */
|
|
51
52
|
mouseSource?: MouseSource;
|
|
53
|
+
/**
|
|
54
|
+
* The auto-updater's late-arrival channel (cli-auto-update §3.8).
|
|
55
|
+
*
|
|
56
|
+
* A BRIDGE AND NOT THE SERVICE ITSELF, for the reason `confirmBridge` above is
|
|
57
|
+
* one: the service is built by a DYNAMIC `import()` fired after `render()`, so
|
|
58
|
+
* it does not exist when this component mounts. Absent under `update.mode:
|
|
59
|
+
* 'off'`, on a non-TTY, in CI, and in every test that does not exercise it.
|
|
60
|
+
*/
|
|
61
|
+
updateBridge?: UpdateBridge;
|
|
52
62
|
}
|
|
53
|
-
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, }: AppProps): React.ReactElement;
|
|
63
|
+
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, updateBridge, }: AppProps): React.ReactElement;
|
package/dist/ui/App.js
CHANGED
|
@@ -34,6 +34,15 @@ import { TodoPanel } from './TodoPanel.js';
|
|
|
34
34
|
import { TodoStrip } from './TodoStrip.js';
|
|
35
35
|
import { ActivityLine } from './ActivityLine.js';
|
|
36
36
|
import { BottomStatusRow } from './BottomStatusRow.js';
|
|
37
|
+
import { UpdateLine } from './UpdateLine.js';
|
|
38
|
+
// The updater's two PURE modules. `shouldRenderUpdateLine` decides the row's
|
|
39
|
+
// presence AT THE CALL SITE (C-15), and `UPDATE_LIMITS` owns the width
|
|
40
|
+
// threshold - both are values, and both are resolved here rather than inside
|
|
41
|
+
// `UpdateLine` so that component's only edge into `update/` stays erasable by
|
|
42
|
+
// tsc (cli-auto-update section 3.1 rule 1 / IF-1). Neither module opens a
|
|
43
|
+
// socket, spawns a process or creates a timer.
|
|
44
|
+
import { shouldRenderUpdateLine } from '../update/types.js';
|
|
45
|
+
import { UPDATE_LIMITS } from '../update/limits.js';
|
|
37
46
|
import { AppShell } from './layout/AppShell.js';
|
|
38
47
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
39
48
|
import { todoRailRows, todoRailWidth } from './layout/rail.js';
|
|
@@ -101,7 +110,7 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
101
110
|
function seedViewState(thinkingVisible) {
|
|
102
111
|
return initialViewState({ thinkingVisible });
|
|
103
112
|
}
|
|
104
|
-
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, }) {
|
|
113
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, updateBridge, }) {
|
|
105
114
|
const { exit } = useApp();
|
|
106
115
|
const { stdout } = useStdout();
|
|
107
116
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -125,6 +134,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
125
134
|
const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
|
|
126
135
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
127
136
|
const [confirmState, setConfirmState] = useState(null);
|
|
137
|
+
/**
|
|
138
|
+
* The updater's latest snapshot, or `null` while there is no service
|
|
139
|
+
* (cli-auto-update section 6.1).
|
|
140
|
+
*
|
|
141
|
+
* `UpdateSnapshot` is a VALUE OBJECT the service replaces wholesale on every
|
|
142
|
+
* transition, so this state can use referential equality and never diff fields.
|
|
143
|
+
*/
|
|
144
|
+
const [updateSnapshot, setUpdateSnapshot] = useState(null);
|
|
128
145
|
/** The request the question / plan overlay is currently rendering. */
|
|
129
146
|
const [humanRequest, setHumanRequest] = useState(null);
|
|
130
147
|
// Seeded from the history file, not from `CliConfig`: the list is no longer
|
|
@@ -200,6 +217,15 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
200
217
|
// `ViewState` is a field `restoreEntries`, `/clear`, `/reset` and the session
|
|
201
218
|
// round-trip each have to have an opinion about, and none of them has an
|
|
202
219
|
// opinion about a 3-second timer.
|
|
220
|
+
/**
|
|
221
|
+
* Whether the one `installing -> ready` toast has already been pushed
|
|
222
|
+
* (cli-auto-update section 6.3).
|
|
223
|
+
*
|
|
224
|
+
* A REF AND NOT STATE: this must survive every re-render without causing one,
|
|
225
|
+
* and it is the whole of AC-19 - ten re-renders while the phase sits at
|
|
226
|
+
* `ready` must still produce exactly one toast.
|
|
227
|
+
*/
|
|
228
|
+
const updateToastPushed = useRef(false);
|
|
203
229
|
const budgetRef = useRef(emptyBudget());
|
|
204
230
|
const followTimer = useRef(null);
|
|
205
231
|
/** True while the run in flight was started by the grace timer, not the user. */
|
|
@@ -801,6 +827,48 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
801
827
|
// --- Helpers -----------------------------------------------------------
|
|
802
828
|
const notify = (level, text) => dispatch({ type: 'notice', level, text });
|
|
803
829
|
const toast = (level, text) => dispatch({ type: 'pushToast', level, text });
|
|
830
|
+
// --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
|
|
831
|
+
//
|
|
832
|
+
// TWO ARRIVAL ORDERS, AND BOTH HAPPEN. The service is built by a dynamic
|
|
833
|
+
// `import()` fired after `render()`, so on a warm module cache it can be
|
|
834
|
+
// attached BEFORE this effect runs, and on a cold one long after. Reading
|
|
835
|
+
// `bridge.service` first and installing `onAttach` second covers both without
|
|
836
|
+
// a race - the same shape `confirmBridge` uses one screen up.
|
|
837
|
+
useEffect(() => {
|
|
838
|
+
if (!updateBridge)
|
|
839
|
+
return undefined;
|
|
840
|
+
const bridge = updateBridge;
|
|
841
|
+
let unsubscribe = null;
|
|
842
|
+
const attach = (service) => {
|
|
843
|
+
setUpdateSnapshot(service.snapshot());
|
|
844
|
+
unsubscribe = service.subscribe(setUpdateSnapshot);
|
|
845
|
+
};
|
|
846
|
+
if (bridge.service)
|
|
847
|
+
attach(bridge.service);
|
|
848
|
+
else
|
|
849
|
+
bridge.onAttach = attach;
|
|
850
|
+
return () => {
|
|
851
|
+
bridge.onAttach = null;
|
|
852
|
+
unsubscribe?.();
|
|
853
|
+
};
|
|
854
|
+
}, [updateBridge]);
|
|
855
|
+
// ONE TOAST, ON THE `installing -> ready` EDGE ONLY (section 6.3 / AC-19).
|
|
856
|
+
//
|
|
857
|
+
// It exists because the moment of completion may arrive while the user is
|
|
858
|
+
// reading the transcript with the row occupied by a run: the toast wins the row
|
|
859
|
+
// for its TTL and then hands it back to the persistent line. The ref is what
|
|
860
|
+
// makes ten re-renders at `ready` push exactly one.
|
|
861
|
+
useEffect(() => {
|
|
862
|
+
if (updateSnapshot?.phase !== 'ready')
|
|
863
|
+
return;
|
|
864
|
+
if (updateToastPushed.current)
|
|
865
|
+
return;
|
|
866
|
+
updateToastPushed.current = true;
|
|
867
|
+
toast('info', `${updateSnapshot.latestVersion ?? 'Update'} installed - restart aragon to apply.`);
|
|
868
|
+
// `toast` is a stable dispatch wrapper; depending on it would re-run this on
|
|
869
|
+
// every render and defeat the ref guard's purpose rather than its mechanism.
|
|
870
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
871
|
+
}, [updateSnapshot?.phase, updateSnapshot?.latestVersion]);
|
|
804
872
|
const persistConfig = (patch) => {
|
|
805
873
|
try {
|
|
806
874
|
updatePersistedConfig(patch);
|
|
@@ -858,6 +926,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
858
926
|
// Read at command time, so `/todo status` reports the live count rather than
|
|
859
927
|
// whatever it was when this context was built.
|
|
860
928
|
followBudget: budgetRef.current,
|
|
929
|
+
// Read at command time for the same reason, and additionally because the
|
|
930
|
+
// service ATTACHES LATE (section 3.8): a value captured when this closure was
|
|
931
|
+
// first created would be `undefined` for the whole session on a cold module
|
|
932
|
+
// cache, and `/update` would report "disabled" on a session where it is on.
|
|
933
|
+
// `?? undefined` because the bridge holds `null` and the port is optional.
|
|
934
|
+
update: updateBridge?.service ?? undefined,
|
|
861
935
|
});
|
|
862
936
|
/**
|
|
863
937
|
* Submitting a prompt no longer touches `config.json` at all — it appends one
|
|
@@ -1379,7 +1453,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1379
1453
|
// surface the run is not visible on. (Inline overlays are part of the
|
|
1380
1454
|
// document flow, so there the suppression is conservative rather than
|
|
1381
1455
|
// necessary — P2-5.)
|
|
1382
|
-
activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null,
|
|
1456
|
+
activity: running && !overlayNode ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null,
|
|
1457
|
+
// PRESENCE IS DECIDED HERE, NOT BY `UpdateLine` RETURNING `null`
|
|
1458
|
+
// (C-15 / cli-auto-update P0-1 / D-19). `BottomStatusRow` tests this
|
|
1459
|
+
// prop for truthiness, and a React element is truthy however it
|
|
1460
|
+
// renders - so a component that rendered nothing would give the row
|
|
1461
|
+
// ZERO rows and silently unbalance the frame budget. Same rule the
|
|
1462
|
+
// activity line above follows with `running && !overlayNode`.
|
|
1463
|
+
//
|
|
1464
|
+
// `!overlayNode` for the same reason it appears there: the full-screen
|
|
1465
|
+
// branch replaces the whole viewport with the overlay, and an update
|
|
1466
|
+
// notice under a settings screen is noise attached to a surface it is
|
|
1467
|
+
// not visible on.
|
|
1468
|
+
update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
|
|
1383
1469
|
? {
|
|
1384
1470
|
teamActive: {
|
|
1385
1471
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -26,8 +26,13 @@
|
|
|
26
26
|
* Sharing the row means `budget.ts` and `AppShell.tsx` come out of this round
|
|
27
27
|
* UNMODIFIED — and `budget.test.ts` asserts that they did (AC-8a, DoD #8).
|
|
28
28
|
*
|
|
29
|
-
* PRECEDENCE:
|
|
30
|
-
* row nearest the input belongs
|
|
29
|
+
* PRECEDENCE: toast > activity > update > blank. A toast wins because a
|
|
30
|
+
* transient ack is a RESPONSE TO THE USER, and the row nearest the input belongs
|
|
31
|
+
* to it. The update line is LAST because it is the only one of the three that is
|
|
32
|
+
* PERSISTENT (cli-auto-update D-2): it loses nothing by waiting for the run to
|
|
33
|
+
* end, whereas an activity line deferred is an activity line that never renders.
|
|
34
|
+
* That also means a user who is actively working never sees the updater at all
|
|
35
|
+
* until they stop, which is the whole of the silence requirement.
|
|
31
36
|
*/
|
|
32
37
|
import React from 'react';
|
|
33
38
|
import type { Theme } from './theme.js';
|
|
@@ -38,6 +43,23 @@ export interface BottomStatusRowProps {
|
|
|
38
43
|
toasts: Toast[];
|
|
39
44
|
/** `<ActivityLine>` while a run owns the screen, else `null`. */
|
|
40
45
|
activity: React.ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* `<UpdateLine>` when the updater has something to say, else `null` — AND THE
|
|
48
|
+
* CALLER DECIDES WHICH (C-15 / cli-auto-update P0-1).
|
|
49
|
+
*
|
|
50
|
+
* `if (update)` below tests a REACT ELEMENT, and an element whose own render
|
|
51
|
+
* returns `null` is still truthy: that branch would emit a box of ZERO rows,
|
|
52
|
+
* the frame would keep its fixed height, Yoga would hand the row to the
|
|
53
|
+
* transcript, and `viewportRows()` would go on reporting the old number to
|
|
54
|
+
* five consumers. So `UpdateLine` is typed `React.ReactElement` and `App`
|
|
55
|
+
* gates it with `shouldRenderUpdateLine(snapshot)`.
|
|
56
|
+
*
|
|
57
|
+
* OPTIONAL, because `bottom-status-row.test.tsx` and the `App.tsx` fixtures
|
|
58
|
+
* construct this component without it and the test tree is inside
|
|
59
|
+
* `tsconfig.test.json` (`typecheck-scope.test.ts`); a required prop turns
|
|
60
|
+
* `npm run typecheck` red for a reason unrelated to this feature (P2-1).
|
|
61
|
+
*/
|
|
62
|
+
update?: React.ReactNode;
|
|
41
63
|
theme: Theme;
|
|
42
64
|
}
|
|
43
|
-
export declare function BottomStatusRow({ mode, toasts, activity, theme, }: BottomStatusRowProps): React.ReactElement | null;
|
|
65
|
+
export declare function BottomStatusRow({ mode, toasts, activity, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box } from 'ink';
|
|
3
3
|
import { ToastStack } from './ToastStack.js';
|
|
4
|
-
export function BottomStatusRow({ mode, toasts, activity, theme, }) {
|
|
5
|
-
// A toast wins the row
|
|
6
|
-
|
|
7
|
-
if (toasts.length > 0 || !activity) {
|
|
4
|
+
export function BottomStatusRow({ mode, toasts, activity, update, theme, }) {
|
|
5
|
+
// A toast wins the row outright.
|
|
6
|
+
if (toasts.length > 0)
|
|
8
7
|
return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
|
|
9
|
-
}
|
|
10
8
|
// Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
|
|
11
|
-
// branch is already a conditional 0-3 row stack, and
|
|
9
|
+
// branch is already a conditional 0-3 row stack, and these are simply one more
|
|
12
10
|
// conditional row above it.
|
|
13
|
-
|
|
11
|
+
if (activity)
|
|
12
|
+
return _jsx(Box, { flexShrink: 0, children: activity });
|
|
13
|
+
if (update)
|
|
14
|
+
return _jsx(Box, { flexShrink: 0, children: update });
|
|
15
|
+
// The blank full-screen row that IS the budget must keep being emitted.
|
|
16
|
+
return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
|
|
14
17
|
}
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
* The buffer/cursor transforms (`applyEdit`, `moveVertical`) and the suggestion
|
|
8
8
|
* computations (`slashSuggestions`, `fileTokenAt`) are pure module functions,
|
|
9
9
|
* exported for `input.test.ts`, so the component itself stays thin.
|
|
10
|
+
*
|
|
11
|
+
* THE EDITOR'S FIVE PIECES OF STATE LIVE IN ONE REDUCER, and every branch of the
|
|
12
|
+
* input handler performs AT MOST ONE `dispatch` (tui-input-flicker-fix §3.4).
|
|
13
|
+
* Ink mounts a LEGACY React root, where `setState` outside a React event handler
|
|
14
|
+
* is not batched — and `useInput` runs from a stdin `'data'` listener, which is
|
|
15
|
+
* outside React entirely. The five `setState` calls one keystroke used to make
|
|
16
|
+
* were five commits, five whole-tree Yoga layouts and, through Ink's 32 ms
|
|
17
|
+
* leading+trailing throttle, two full-frame repaints per character. `fileMatches`
|
|
18
|
+
* stays a plain `useState` because it is written from the debounced glob effect
|
|
19
|
+
* rather than from a key.
|
|
10
20
|
*/
|
|
11
21
|
import React from 'react';
|
|
12
22
|
import type { Theme } from './theme.js';
|