@aragon-agent/cli 0.5.8 → 0.5.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 +1157 -747
- package/README.md +1552 -1163
- package/dist/agent/coalesce.d.ts +13 -0
- package/dist/agent/coalesce.js +50 -11
- package/dist/agent/controller.d.ts +141 -2
- package/dist/agent/controller.js +396 -4
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +245 -3
- package/dist/agent/reducer.js +490 -17
- package/dist/agent/retry-view.d.ts +68 -0
- package/dist/agent/retry-view.js +95 -0
- package/dist/agent/system-prompt.d.ts +11 -0
- package/dist/agent/system-prompt.js +2 -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 +368 -3
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +426 -2
- 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 +79 -2
- package/dist/config/cli-commands.js +185 -3
- package/dist/config/env.d.ts +15 -3
- package/dist/config/env.js +143 -3
- package/dist/config/load.d.ts +66 -0
- package/dist/config/load.js +107 -1
- package/dist/config/schema.d.ts +268 -2
- package/dist/config/schema.js +288 -1
- package/dist/config/store.js +32 -1
- package/dist/fast/critique.d.ts +43 -0
- package/dist/fast/critique.js +62 -0
- package/dist/fast/digest.d.ts +42 -0
- package/dist/fast/digest.js +85 -0
- package/dist/fast/frames.d.ts +99 -0
- package/dist/fast/frames.js +180 -0
- package/dist/fast/limits.d.ts +98 -0
- package/dist/fast/limits.js +98 -0
- package/dist/fast/prompt.d.ts +72 -0
- package/dist/fast/prompt.js +94 -0
- package/dist/fast/resolve.d.ts +43 -0
- package/dist/fast/resolve.js +105 -0
- package/dist/fast/review-call.d.ts +99 -0
- package/dist/fast/review-call.js +130 -0
- package/dist/fast/reviewer.d.ts +248 -0
- package/dist/fast/reviewer.js +664 -0
- package/dist/fast/types.d.ts +128 -0
- package/dist/fast/types.js +14 -0
- package/dist/fast/wiring.d.ts +170 -0
- package/dist/fast/wiring.js +316 -0
- package/dist/launcher.d.ts +14 -0
- package/dist/launcher.js +43 -0
- package/dist/logging/install.d.ts +15 -1
- package/dist/logging/install.js +37 -4
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +57 -1
- package/dist/team/normalize.d.ts +26 -1
- package/dist/team/normalize.js +23 -3
- package/dist/team/prompt.d.ts +11 -0
- package/dist/team/prompt.js +10 -0
- package/dist/team/report.d.ts +18 -0
- package/dist/team/report.js +27 -1
- package/dist/team/runtime.d.ts +13 -2
- package/dist/team/runtime.js +33 -6
- package/dist/team/subagent.d.ts +19 -1
- package/dist/team/subagent.js +72 -10
- package/dist/team/task-tool.d.ts +26 -0
- package/dist/team/task-tool.js +48 -4
- package/dist/team/types.d.ts +80 -1
- package/dist/tools/bash-tool.js +19 -3
- package/dist/tools/diff.d.ts +25 -5
- package/dist/tools/diff.js +72 -41
- package/dist/tools/file-change-store.d.ts +34 -0
- package/dist/tools/file-change-store.js +57 -0
- package/dist/tools/fs-tools.d.ts +26 -0
- package/dist/tools/fs-tools.js +62 -5
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch.d.ts +115 -0
- package/dist/tools/patch.js +453 -0
- package/dist/tools/terminal-output.d.ts +107 -0
- package/dist/tools/terminal-output.js +198 -0
- package/dist/tools/tool-output-store.d.ts +68 -0
- package/dist/tools/tool-output-store.js +102 -0
- package/dist/ui/ActivityLine.d.ts +48 -0
- package/dist/ui/ActivityLine.js +22 -0
- package/dist/ui/App.d.ts +11 -4
- package/dist/ui/App.js +359 -37
- package/dist/ui/BottomStatusRow.d.ts +65 -0
- package/dist/ui/BottomStatusRow.js +17 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +37 -0
- package/dist/ui/StatusBar.js +5 -3
- package/dist/ui/TeamPanel.d.ts +5 -4
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/TodoPanel.d.ts +5 -4
- package/dist/ui/Transcript.d.ts +26 -3
- package/dist/ui/Transcript.js +76 -10
- package/dist/ui/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +20 -0
- package/dist/ui/entries/AssistantEntry.js +3 -2
- package/dist/ui/entries/DiffView.d.ts +67 -0
- package/dist/ui/entries/DiffView.js +103 -0
- package/dist/ui/entries/FastCard.d.ts +53 -0
- package/dist/ui/entries/FastCard.js +87 -0
- package/dist/ui/entries/RetryCard.d.ts +56 -0
- package/dist/ui/entries/RetryCard.js +60 -0
- package/dist/ui/entries/TeamCard.js +6 -1
- package/dist/ui/entries/ToolCard.d.ts +38 -1
- package/dist/ui/entries/ToolCard.js +68 -5
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/AppShell.d.ts +13 -22
- package/dist/ui/layout/AppShell.js +3 -38
- package/dist/ui/layout/virtual-window.d.ts +1 -1
- package/dist/ui/layout/virtual-window.js +109 -5
- package/dist/ui/overlays/HelpOverlay.js +16 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
- package/dist/ui/overlays/SettingsScreen.js +147 -1
- package/dist/ui/transcript-text.js +27 -0
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- 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/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { type ThinkingLevel } from '@aragon-agent/core';
|
|
12
|
+
import { type FastConfig } from '../../config/schema.js';
|
|
12
13
|
import { type LogLevelName } from '../../logging/levels.js';
|
|
13
14
|
import type { Theme } from '../theme.js';
|
|
14
15
|
import type { TermCapabilities } from '../capabilities.js';
|
|
@@ -17,6 +18,22 @@ export interface SettingsValues {
|
|
|
17
18
|
model: string;
|
|
18
19
|
baseUrl: string;
|
|
19
20
|
thinkingLevel: ThinkingLevel;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the transcript draws the reasoning (agent-activity-presentation
|
|
23
|
+
* §4.3). `'on' | 'off'` rather than a boolean because this screen edits an
|
|
24
|
+
* enum row and reads it back as one; `App` converts at both ends.
|
|
25
|
+
*/
|
|
26
|
+
showThinking: 'on' | 'off';
|
|
27
|
+
/**
|
|
28
|
+
* Whether a running tool card draws a live output tail
|
|
29
|
+
* (agent-activity-presentation-live §4.1). `'on' | 'off'` for the reason its
|
|
30
|
+
* neighbour records: this screen edits an enum row and reads it back as one.
|
|
31
|
+
*
|
|
32
|
+
* PERSIST-ONLY, unlike `showThinking`. The store is allocated in
|
|
33
|
+
* `AgentController`'s constructor and there is no setter, so this row takes
|
|
34
|
+
* effect on the next launch -- which is also what manual row 8 asks for.
|
|
35
|
+
*/
|
|
36
|
+
liveToolOutput: 'on' | 'off';
|
|
20
37
|
maxTokens: string;
|
|
21
38
|
apiKey: string;
|
|
22
39
|
/**
|
|
@@ -25,6 +42,21 @@ export interface SettingsValues {
|
|
|
25
42
|
* credentials on disk cannot be flipped by a stray arrow key.
|
|
26
43
|
*/
|
|
27
44
|
logLevel: LogLevelName;
|
|
45
|
+
/**
|
|
46
|
+
* The fast tier (fast-model-tier §4.5, extended by the hardening round §4.4).
|
|
47
|
+
*
|
|
48
|
+
* FIVE ROWS, and only the first is always meaningful — the other four
|
|
49
|
+
* describe a tier that may not be on. They are `string` rather than their
|
|
50
|
+
* config types because this screen edits TEXT: `fastReview` accepts a number
|
|
51
|
+
* or `off`, and a typo must change nothing rather than resolve to a default
|
|
52
|
+
* (the discipline `maxTokens` already follows here).
|
|
53
|
+
*/
|
|
54
|
+
fastEnabled: 'on' | 'off';
|
|
55
|
+
fastModel: string;
|
|
56
|
+
fastProvider: string;
|
|
57
|
+
fastReview: string;
|
|
58
|
+
/** The session review budget. `fastReview` is the PACE, this is the TOTAL. */
|
|
59
|
+
fastReviewBudget: string;
|
|
28
60
|
}
|
|
29
61
|
interface SettingsScreenProps {
|
|
30
62
|
initial: SettingsValues;
|
|
@@ -39,7 +71,56 @@ interface SettingsScreenProps {
|
|
|
39
71
|
theme: Theme;
|
|
40
72
|
caps: TermCapabilities;
|
|
41
73
|
onSave: (values: SettingsValues) => void;
|
|
74
|
+
/**
|
|
75
|
+
* `API retries` READ-ONLY row (llm-api-retry-backoff §6.9), e.g. `10` or `off`.
|
|
76
|
+
*
|
|
77
|
+
* A STRING FROM `App`, NOT A `SettingsValues` FIELD, and that distinction is the
|
|
78
|
+
* whole design: `SettingsValues` is what Enter WRITES, and routing retry through
|
|
79
|
+
* it would mean nine more editable rows (or one row whose Enter silently
|
|
80
|
+
* rewrote a section it does not own). `/retry` is the discoverable control;
|
|
81
|
+
* this row exists so "is it on, and how many" is answerable without leaving the
|
|
82
|
+
* screen. Absent = omit the row entirely.
|
|
83
|
+
*/
|
|
84
|
+
retrySummary?: string;
|
|
42
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The derived, non-editable line under the five fast rows — the sibling of
|
|
88
|
+
* `effectiveCapLine`, and it exists for the same reason (§4.5).
|
|
89
|
+
*
|
|
90
|
+
* A SETTINGS SCREEN THAT LETS A USER SWITCH A TIER ON AND GIVES NO SIGNAL THAT
|
|
91
|
+
* IT CANNOT RESOLVE is the failure `effectiveCapLine` was added to fix for
|
|
92
|
+
* `maxTokens`, one screen later. `keyFor` is the same resolver the controller
|
|
93
|
+
* uses, so this line and the runtime cannot disagree.
|
|
94
|
+
*
|
|
95
|
+
* IT DOES NOT TAKE `fastReviewBudget` AND MUST NOT (§4.4 part 5): this line
|
|
96
|
+
* describes what the tier IS, not what it is allowed to spend.
|
|
97
|
+
*/
|
|
98
|
+
export declare function fastTierLine(values: Pick<SettingsValues, 'provider' | 'fastEnabled' | 'fastModel' | 'fastProvider' | 'fastReview'>, hasKey: (providerId: string) => boolean): string;
|
|
99
|
+
/**
|
|
100
|
+
* The five editable fast rows, as a config patch (§4.5 / hardening §4.4).
|
|
101
|
+
*
|
|
102
|
+
* PURE AND EXPORTED so `App`'s save handler holds no parsing of its own, and so
|
|
103
|
+
* the round trip `fastSettingsFrom(config)` -> edit -> `readFastSettings` can be
|
|
104
|
+
* tested without a terminal.
|
|
105
|
+
*
|
|
106
|
+
* A TYPO IN `fastReview` CHANGES NOTHING — the `review` / `reviewEveryTurns`
|
|
107
|
+
* keys are simply omitted from the patch, so `clampFastConfig`'s merge keeps
|
|
108
|
+
* whatever was there. That is the `Max tokens` discipline this screen already
|
|
109
|
+
* follows, and the alternative (resolving a typo to the default) silently
|
|
110
|
+
* discards the cadence the user actually set. `fastReviewBudget` follows the
|
|
111
|
+
* same rule; an out-of-range NUMBER is passed through to `clampFastConfig`'s
|
|
112
|
+
* `REVIEW_SESSION_RANGE` rather than being validated a second time here, so a
|
|
113
|
+
* row edit and a hand-edited `config.json` land on the same value.
|
|
114
|
+
*/
|
|
115
|
+
export declare function readFastSettings(values: Pick<SettingsValues, 'fastEnabled' | 'fastModel' | 'fastProvider' | 'fastReview' | 'fastReviewBudget'>): Partial<FastConfig>;
|
|
116
|
+
/**
|
|
117
|
+
* The inverse: seed the five rows from a resolved config.
|
|
118
|
+
*
|
|
119
|
+
* OMITTING A KEY HERE IS THE SILENT HALF OF ADDING A ROW (§4.4 part 4): the row
|
|
120
|
+
* renders empty, `readFastSettings` sees an unparseable value, and an untouched
|
|
121
|
+
* save writes the clamp's default over whatever the user had.
|
|
122
|
+
*/
|
|
123
|
+
export declare function fastSettingsFrom(fast: FastConfig): Pick<SettingsValues, 'fastEnabled' | 'fastModel' | 'fastProvider' | 'fastReview' | 'fastReviewBudget'>;
|
|
43
124
|
/**
|
|
44
125
|
* The cap this setting will ACTUALLY produce against the selected model.
|
|
45
126
|
*
|
|
@@ -51,5 +132,5 @@ interface SettingsScreenProps {
|
|
|
51
132
|
* "I set 64000 but answers still stop early" has no answer anywhere in the UI.
|
|
52
133
|
*/
|
|
53
134
|
export declare function effectiveCapLine(values: Pick<SettingsValues, 'provider' | 'model' | 'maxTokens'>): string;
|
|
54
|
-
export declare function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, }: SettingsScreenProps): React.ReactElement;
|
|
135
|
+
export declare function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, retrySummary, }: SettingsScreenProps): React.ReactElement;
|
|
55
136
|
export {};
|
|
@@ -15,12 +15,137 @@ import { ADAPTER_PROVIDERS, THINKING_LEVELS, maskSecret, parseMaxTokensInput, }
|
|
|
15
15
|
import { LOG_LEVEL_NAMES } from '../../logging/levels.js';
|
|
16
16
|
import { pickGlyphs } from '../glyphs.js';
|
|
17
17
|
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
18
|
+
const SHOW_THINKING_OPTIONS = ['off', 'on'];
|
|
19
|
+
const LIVE_OUTPUT_OPTIONS = ['off', 'on'];
|
|
20
|
+
const FAST_ENABLED_OPTIONS = ['off', 'on'];
|
|
21
|
+
/**
|
|
22
|
+
* `(inherit)` IS THE EMPTY STRING, rendered as a word.
|
|
23
|
+
*
|
|
24
|
+
* An empty enum cell would read as "unset and therefore broken"; the truth is
|
|
25
|
+
* that an empty `fast.provider` INHERITS the main provider, which is the setting
|
|
26
|
+
* most users want and the one they should not have to type (§3.2 rule 2).
|
|
27
|
+
*/
|
|
28
|
+
const FAST_INHERIT_PROVIDER = '(inherit)';
|
|
29
|
+
const FAST_PROVIDER_OPTIONS = [FAST_INHERIT_PROVIDER, ...ADAPTER_PROVIDERS];
|
|
30
|
+
/**
|
|
31
|
+
* The derived, non-editable line under the five fast rows — the sibling of
|
|
32
|
+
* `effectiveCapLine`, and it exists for the same reason (§4.5).
|
|
33
|
+
*
|
|
34
|
+
* A SETTINGS SCREEN THAT LETS A USER SWITCH A TIER ON AND GIVES NO SIGNAL THAT
|
|
35
|
+
* IT CANNOT RESOLVE is the failure `effectiveCapLine` was added to fix for
|
|
36
|
+
* `maxTokens`, one screen later. `keyFor` is the same resolver the controller
|
|
37
|
+
* uses, so this line and the runtime cannot disagree.
|
|
38
|
+
*
|
|
39
|
+
* IT DOES NOT TAKE `fastReviewBudget` AND MUST NOT (§4.4 part 5): this line
|
|
40
|
+
* describes what the tier IS, not what it is allowed to spend.
|
|
41
|
+
*/
|
|
42
|
+
export function fastTierLine(values, hasKey) {
|
|
43
|
+
if (values.fastEnabled !== 'on')
|
|
44
|
+
return 'Fast tier: off';
|
|
45
|
+
const model = values.fastModel.trim();
|
|
46
|
+
if (model.length === 0)
|
|
47
|
+
return 'Fast tier: off (fast.model is not set)';
|
|
48
|
+
const provider = values.fastProvider === FAST_INHERIT_PROVIDER || values.fastProvider.trim().length === 0
|
|
49
|
+
? values.provider
|
|
50
|
+
: values.fastProvider;
|
|
51
|
+
if (!hasKey(provider)) {
|
|
52
|
+
return `Fast tier: ${provider}:${model} (no API key for ${provider} - the tier will stay off)`;
|
|
53
|
+
}
|
|
54
|
+
const review = values.fastReview.trim().toLowerCase();
|
|
55
|
+
const cadence = review === 'off'
|
|
56
|
+
? 'review off'
|
|
57
|
+
: /^\d+$/.test(review)
|
|
58
|
+
? `review every ${review} turns`
|
|
59
|
+
: 'review every 5 turns';
|
|
60
|
+
return `Fast tier: ${provider}:${model} (key set) ${cadence}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The five editable fast rows, as a config patch (§4.5 / hardening §4.4).
|
|
64
|
+
*
|
|
65
|
+
* PURE AND EXPORTED so `App`'s save handler holds no parsing of its own, and so
|
|
66
|
+
* the round trip `fastSettingsFrom(config)` -> edit -> `readFastSettings` can be
|
|
67
|
+
* tested without a terminal.
|
|
68
|
+
*
|
|
69
|
+
* A TYPO IN `fastReview` CHANGES NOTHING — the `review` / `reviewEveryTurns`
|
|
70
|
+
* keys are simply omitted from the patch, so `clampFastConfig`'s merge keeps
|
|
71
|
+
* whatever was there. That is the `Max tokens` discipline this screen already
|
|
72
|
+
* follows, and the alternative (resolving a typo to the default) silently
|
|
73
|
+
* discards the cadence the user actually set. `fastReviewBudget` follows the
|
|
74
|
+
* same rule; an out-of-range NUMBER is passed through to `clampFastConfig`'s
|
|
75
|
+
* `REVIEW_SESSION_RANGE` rather than being validated a second time here, so a
|
|
76
|
+
* row edit and a hand-edited `config.json` land on the same value.
|
|
77
|
+
*/
|
|
78
|
+
export function readFastSettings(values) {
|
|
79
|
+
const review = values.fastReview.trim().toLowerCase();
|
|
80
|
+
const parsed = /^\d+$/.test(review) ? Number.parseInt(review, 10) : Number.NaN;
|
|
81
|
+
const budget = values.fastReviewBudget.trim();
|
|
82
|
+
const parsedBudget = /^\d+$/.test(budget) ? Number.parseInt(budget, 10) : Number.NaN;
|
|
83
|
+
return {
|
|
84
|
+
enabled: values.fastEnabled === 'on',
|
|
85
|
+
model: values.fastModel.trim(),
|
|
86
|
+
provider: values.fastProvider === FAST_INHERIT_PROVIDER ? '' : values.fastProvider.trim(),
|
|
87
|
+
...(review === 'off' ? { review: false } : {}),
|
|
88
|
+
...(Number.isFinite(parsed) && parsed > 0
|
|
89
|
+
? { review: true, reviewEveryTurns: parsed }
|
|
90
|
+
: {}),
|
|
91
|
+
...(Number.isFinite(parsedBudget) ? { reviewMaxPerSession: parsedBudget } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The inverse: seed the five rows from a resolved config.
|
|
96
|
+
*
|
|
97
|
+
* OMITTING A KEY HERE IS THE SILENT HALF OF ADDING A ROW (§4.4 part 4): the row
|
|
98
|
+
* renders empty, `readFastSettings` sees an unparseable value, and an untouched
|
|
99
|
+
* save writes the clamp's default over whatever the user had.
|
|
100
|
+
*/
|
|
101
|
+
export function fastSettingsFrom(fast) {
|
|
102
|
+
return {
|
|
103
|
+
fastEnabled: fast.enabled ? 'on' : 'off',
|
|
104
|
+
fastModel: fast.model,
|
|
105
|
+
fastProvider: fast.provider.trim().length > 0 ? fast.provider : FAST_INHERIT_PROVIDER,
|
|
106
|
+
fastReview: fast.review ? String(fast.reviewEveryTurns) : 'off',
|
|
107
|
+
fastReviewBudget: String(fast.reviewMaxPerSession),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
18
110
|
const FIELDS = [
|
|
19
111
|
{ key: 'provider', label: 'Provider', kind: 'enum', options: ADAPTER_PROVIDERS },
|
|
20
112
|
{ key: 'model', label: 'Model', kind: 'text' },
|
|
21
113
|
{ key: 'baseUrl', label: 'Base URL', kind: 'text' },
|
|
22
114
|
{ key: 'thinkingLevel', label: 'Thinking', kind: 'enum', options: THINKING_LEVELS },
|
|
115
|
+
// Directly under `Thinking`, so the effort setting and the display setting are
|
|
116
|
+
// read together. `Show thinking` is 13 characters, inside the 18-column label
|
|
117
|
+
// budget `.padEnd(18)` enforces below and that IF-H7 records as silently
|
|
118
|
+
// overflowing.
|
|
119
|
+
{ key: 'showThinking', label: 'Show thinking', kind: 'enum', options: SHOW_THINKING_OPTIONS },
|
|
120
|
+
// Beside `Show thinking`, because the two are the transcript's only display
|
|
121
|
+
// switches. `Live output` is 11 characters, inside the 18-column label budget
|
|
122
|
+
// `.padEnd(18)` enforces below and that IF-H7 records as silently overflowing.
|
|
123
|
+
{ key: 'liveToolOutput', label: 'Live output', kind: 'enum', options: LIVE_OUTPUT_OPTIONS },
|
|
23
124
|
{ key: 'maxTokens', label: 'Max tokens', kind: 'text' },
|
|
125
|
+
// The fast tier sits AFTER `Max tokens` and BEFORE `API key`, because the key
|
|
126
|
+
// row is the one a user scrolls to deliberately and putting five rows past it
|
|
127
|
+
// would bury it (fast-model-tier §4.5). The rationale survives a fifth row;
|
|
128
|
+
// the count in it did not.
|
|
129
|
+
//
|
|
130
|
+
// `Fast review budget` is a `'text'` row, NOT a new numeric kind (D-H12):
|
|
131
|
+
// `SettingsRow.kind` is `'enum' | 'text' | 'secret'`, and `Max tokens` and
|
|
132
|
+
// `Fast review` are both bounded numbers already living as text plus an
|
|
133
|
+
// adapter parse. A fourth kind for one row would be a new interaction pattern
|
|
134
|
+
// in a screen whose whole design is that it edits text.
|
|
135
|
+
//
|
|
136
|
+
// ITS LABEL IS SHORT BECAUSE THE LABEL COLUMN IS A BUDGET, NOT A HINT SLOT.
|
|
137
|
+
// Labels render `.padEnd(18)` below, so one that reaches 18 does not truncate
|
|
138
|
+
// and does not wrap - it pushes ITS OWN value right (or butts against it at
|
|
139
|
+
// exactly 18) while every other row's value stays put, and the column stops
|
|
140
|
+
// being a column. So no `(1-500)` here: no other bounded row carries its range
|
|
141
|
+
// either (`Max tokens`, `Fast review`), `clampFastConfig` is the enforcement,
|
|
142
|
+
// and `/fast budget` states the range in the one place a user meets it - when
|
|
143
|
+
// it refuses a bad one. The name matches that command for the same reason.
|
|
144
|
+
{ key: 'fastEnabled', label: 'Fast tier', kind: 'enum', options: FAST_ENABLED_OPTIONS },
|
|
145
|
+
{ key: 'fastModel', label: 'Fast model', kind: 'text' },
|
|
146
|
+
{ key: 'fastProvider', label: 'Fast provider', kind: 'enum', options: FAST_PROVIDER_OPTIONS },
|
|
147
|
+
{ key: 'fastReview', label: 'Fast review', kind: 'text' },
|
|
148
|
+
{ key: 'fastReviewBudget', label: 'Fast budget', kind: 'text' },
|
|
24
149
|
{ key: 'apiKey', label: 'API key', kind: 'secret' },
|
|
25
150
|
{ key: 'logLevel', label: 'Log level', kind: 'enum', options: LOG_LEVEL_NAMES },
|
|
26
151
|
];
|
|
@@ -52,7 +177,7 @@ export function effectiveCapLine(values) {
|
|
|
52
177
|
return `Effective: ${resolution.value} (auto)`;
|
|
53
178
|
return `Effective: ${resolution.value}`;
|
|
54
179
|
}
|
|
55
|
-
export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, }) {
|
|
180
|
+
export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, retrySummary, }) {
|
|
56
181
|
const [values, setValues] = useState(initial);
|
|
57
182
|
const [index, setIndex] = useState(0);
|
|
58
183
|
const glyphs = pickGlyphs(caps);
|
|
@@ -132,6 +257,15 @@ export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset,
|
|
|
132
257
|
// setting, which is the one thing this field can never be.
|
|
133
258
|
if (f.key === 'maxTokens' && raw.length === 0)
|
|
134
259
|
return 'auto';
|
|
260
|
+
// `(not set)` and NOT `(same as main)`, which is the honest reading of §3.2
|
|
261
|
+
// rule 3: an empty `fast.model` does not inherit, it means the tier is
|
|
262
|
+
// unconfigured. `(same as main)` is a fact the derived line below states
|
|
263
|
+
// when the value literally equals the main model — never a placeholder.
|
|
264
|
+
if (f.key === 'fastModel' && raw.length === 0) {
|
|
265
|
+
return selected ? '(type a model id)' : '(not set)';
|
|
266
|
+
}
|
|
267
|
+
if (f.key === 'fastReview' && raw.length === 0)
|
|
268
|
+
return 'every 5 turns';
|
|
135
269
|
return raw.length > 0 ? raw : selected ? '(type to set)' : '(none)';
|
|
136
270
|
};
|
|
137
271
|
// One element per row, each `wrap="truncate"` — the contract for OverlayFrame's
|
|
@@ -145,6 +279,18 @@ export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset,
|
|
|
145
279
|
// controlled mode slices by ELEMENT, so a row that wraps desynchronises the
|
|
146
280
|
// position indicator.
|
|
147
281
|
rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: effectiveCapLine(values) }, "__effective"));
|
|
282
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: fastTierLine(values, (id) => (apiKeys[id] ?? '').length > 0) }, "__fast"));
|
|
283
|
+
/**
|
|
284
|
+
* ONE ROW, NOT NINE (llm-api-retry-backoff §6.9).
|
|
285
|
+
*
|
|
286
|
+
* READ-ONLY, and deliberately so: the nine `retry.*` keys belong to `config set`
|
|
287
|
+
* and `config.json`, and a settings screen that exposed a jitter checkbox is a
|
|
288
|
+
* settings screen nobody reads. What a user needs HERE is the answer to "is it
|
|
289
|
+
* on, and how many" — the discoverable control is `/retry`, which this row names.
|
|
290
|
+
*/
|
|
291
|
+
if (retrySummary) {
|
|
292
|
+
rows.push(_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', 'API retries'.padEnd(18), retrySummary] }, "__retry"));
|
|
293
|
+
}
|
|
148
294
|
rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: "Enter saves. Keys persist to the config file (0600)." }, "__save"));
|
|
149
295
|
return (_jsx(OverlayFrame, { title: "Settings", hint: `${glyphs.arrowUp}${glyphs.arrowDown} field ${glyphs.midDot} ${glyphs.arrowLeft}${glyphs.arrowRight} change ${glyphs.midDot} Enter save ${glyphs.midDot} Esc close`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: scrollOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
|
|
150
296
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* simply print everything twice.
|
|
8
8
|
*/
|
|
9
9
|
import { formatCost, formatDuration, formatTokens } from '../agent/usage.js';
|
|
10
|
+
import { formatRetryLine } from '../agent/retry-view.js';
|
|
10
11
|
const DEFAULT_MAX_ENTRIES = 200;
|
|
11
12
|
const CONTINUATION_INDENT = ' ';
|
|
12
13
|
const TOOL_INDENT = ' ';
|
|
@@ -84,6 +85,32 @@ function renderEntry(entry, glyphs) {
|
|
|
84
85
|
];
|
|
85
86
|
return lines;
|
|
86
87
|
}
|
|
88
|
+
case 'retry': {
|
|
89
|
+
// A turn that spent three minutes retrying is exactly the kind of cost an
|
|
90
|
+
// exported transcript is supposed to account for — the same argument the
|
|
91
|
+
// `team` case above makes in its own words. This file's `default: return []`
|
|
92
|
+
// means the addition is a CHOICE rather than a compile requirement, and it is
|
|
93
|
+
// worth making: without it a replay of a session that lost two minutes to a
|
|
94
|
+
// 529 shows a gap and no reason for it.
|
|
95
|
+
const glyph = entry.phase === 'exhausted' ? glyphs.error : glyphs.retry;
|
|
96
|
+
return [
|
|
97
|
+
`${TOOL_INDENT}${glyph} ${formatRetryLine(entry, glyphs, Date.now())}`,
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
case 'fast': {
|
|
101
|
+
// Reviews are real spend and real advice, and an exported transcript that
|
|
102
|
+
// omitted them would be unaccountable for both — the argument the `team`
|
|
103
|
+
// and `retry` cases above each make in their own words.
|
|
104
|
+
const glyph = entry.status === 'failed' ? glyphs.toolError : glyphs.toolDone;
|
|
105
|
+
const duration = entry.durationMs === undefined ? '' : ` (${entry.durationMs}ms)`;
|
|
106
|
+
const head = `${TOOL_INDENT}${glyph} fast review #${entry.reviewIndex} ${entry.model} ` +
|
|
107
|
+
`turn ${entry.turn}${duration}`;
|
|
108
|
+
if (entry.status === 'advice' && entry.text) {
|
|
109
|
+
return [head, ...entry.text.split('\n').map((l) => `${TOOL_INDENT}${CONTINUATION_INDENT}${l}`)];
|
|
110
|
+
}
|
|
111
|
+
const detail = entry.detail ? `: ${entry.detail}` : '';
|
|
112
|
+
return [`${head} ${entry.status}${detail}`];
|
|
113
|
+
}
|
|
87
114
|
case 'notice': {
|
|
88
115
|
const glyph = entry.level === 'error'
|
|
89
116
|
? glyphs.error
|
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Wheel routing (
|
|
2
|
+
* Wheel routing (wheel-scrolls-transcript-only §4.1).
|
|
3
3
|
*
|
|
4
|
-
* Subscribes to the mouse source
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 1000-line component (D-13).
|
|
4
|
+
* Subscribes to the mouse source and dispatches every notch to THE ONE
|
|
5
|
+
* SCROLLABLE REGION ON SCREEN — the transcript, or the controlled overlay
|
|
6
|
+
* covering it. Lives outside `App.tsx` because the routing table is the most
|
|
7
|
+
* testable part of this feature and a table wants to be readable on its own,
|
|
8
|
+
* away from a large component (D-13).
|
|
10
9
|
*
|
|
11
|
-
* THE
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* THE POINTER ROW IS NOT CONSULTED, and that is the requirement rather than a
|
|
11
|
+
* simplification (§1). A wheel notch is a VIEWPORT gesture: the pointer in a
|
|
12
|
+
* TUI rests wherever the user last left it — in a chat-shaped app, next to the
|
|
13
|
+
* thing they type into — so routing by row meant the ordinary "scroll back to
|
|
14
|
+
* see what the agent just did" gesture landed on the composer and replaced the
|
|
15
|
+
* draft with an old prompt. Prompt history belongs to `↑` / `↓` alone (G1 / G3),
|
|
16
|
+
* which is the contract Claude Code, Codex and every full-screen terminal app
|
|
17
|
+
* the user brings muscle memory from already follow.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - composer: exactly ONE history step per notch. Multiplying here would make
|
|
18
|
-
* a single flick jump three prompts back — unusable, and destructive when a
|
|
19
|
-
* draft is in play.
|
|
20
|
-
*
|
|
21
|
-
* Bursts are handled differently for the same reason: transcript events are
|
|
22
|
-
* coalesced into one intent carrying `repeat`, composer events never are (they
|
|
23
|
-
* are rate-limited by the human, and each must produce its own visible step).
|
|
19
|
+
* Every notch is coalesced into one intent carrying `repeat`, no matter how fast
|
|
20
|
+
* the wheel spins: one gesture, one `setState`, one render.
|
|
24
21
|
*/
|
|
25
|
-
import { type MutableRefObject } from 'react';
|
|
26
22
|
import type { Overlay } from '../agent/reducer.js';
|
|
27
23
|
import type { MouseSource } from '../input/stdin-mouse-filter.js';
|
|
28
|
-
import { type FrameGeometry } from './layout/regions.js';
|
|
29
24
|
import type { ScrollIntent } from './layout/scroll.js';
|
|
30
25
|
/**
|
|
31
26
|
* Rows a PgUp/PgDn — and a Shift+wheel notch — moves inside a controlled
|
|
@@ -37,22 +32,11 @@ export declare const OVERLAY_PAGE = 8;
|
|
|
37
32
|
export interface WheelRoutingOptions {
|
|
38
33
|
/** Absent when mouse support is off, in inline mode, or on a non-TTY. */
|
|
39
34
|
mouseSource?: MouseSource;
|
|
40
|
-
/** Live frame geometry, written by `AppShell`'s layout effect (§4.5). */
|
|
41
|
-
geometryRef: MutableRefObject<FrameGeometry>;
|
|
42
|
-
/**
|
|
43
|
-
* Test-only seam (§8.2): when set, the router reads this instead of the
|
|
44
|
-
* measured ref, so a routing test never depends on `measureElement` working
|
|
45
|
-
* under `ink-testing-library` — a dependency no other test in this package
|
|
46
|
-
* exercises, and one that would make a routing regression present as a
|
|
47
|
-
* measurement flake.
|
|
48
|
-
*/
|
|
49
|
-
geometryOverride?: FrameGeometry;
|
|
50
35
|
/** Read at EVENT time for the routing branch, and again at FLUSH time (P1-3). */
|
|
51
36
|
getOverlay: () => Overlay | null;
|
|
52
37
|
onScroll: (kind: ScrollIntent, repeat: number) => void;
|
|
53
38
|
/** Signed row delta; the caller clamps at 0 the way the keyboard branch does. */
|
|
54
39
|
onOverlayScroll: (delta: number) => void;
|
|
55
|
-
onHistoryStep: (dir: 'up' | 'down') => void;
|
|
56
40
|
}
|
|
57
41
|
export interface WheelRouting {
|
|
58
42
|
/**
|
|
@@ -1,29 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Wheel routing (
|
|
2
|
+
* Wheel routing (wheel-scrolls-transcript-only §4.1).
|
|
3
3
|
*
|
|
4
|
-
* Subscribes to the mouse source
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 1000-line component (D-13).
|
|
4
|
+
* Subscribes to the mouse source and dispatches every notch to THE ONE
|
|
5
|
+
* SCROLLABLE REGION ON SCREEN — the transcript, or the controlled overlay
|
|
6
|
+
* covering it. Lives outside `App.tsx` because the routing table is the most
|
|
7
|
+
* testable part of this feature and a table wants to be readable on its own,
|
|
8
|
+
* away from a large component (D-13).
|
|
10
9
|
*
|
|
11
|
-
* THE
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* THE POINTER ROW IS NOT CONSULTED, and that is the requirement rather than a
|
|
11
|
+
* simplification (§1). A wheel notch is a VIEWPORT gesture: the pointer in a
|
|
12
|
+
* TUI rests wherever the user last left it — in a chat-shaped app, next to the
|
|
13
|
+
* thing they type into — so routing by row meant the ordinary "scroll back to
|
|
14
|
+
* see what the agent just did" gesture landed on the composer and replaced the
|
|
15
|
+
* draft with an old prompt. Prompt history belongs to `↑` / `↓` alone (G1 / G3),
|
|
16
|
+
* which is the contract Claude Code, Codex and every full-screen terminal app
|
|
17
|
+
* the user brings muscle memory from already follow.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - composer: exactly ONE history step per notch. Multiplying here would make
|
|
18
|
-
* a single flick jump three prompts back — unusable, and destructive when a
|
|
19
|
-
* draft is in play.
|
|
20
|
-
*
|
|
21
|
-
* Bursts are handled differently for the same reason: transcript events are
|
|
22
|
-
* coalesced into one intent carrying `repeat`, composer events never are (they
|
|
23
|
-
* are rate-limited by the human, and each must produce its own visible step).
|
|
19
|
+
* Every notch is coalesced into one intent carrying `repeat`, no matter how fast
|
|
20
|
+
* the wheel spins: one gesture, one `setState`, one render.
|
|
24
21
|
*/
|
|
25
22
|
import { useCallback, useEffect, useRef } from 'react';
|
|
26
|
-
import { hitTestWheel } from './layout/regions.js';
|
|
27
23
|
/**
|
|
28
24
|
* Rows a PgUp/PgDn — and a Shift+wheel notch — moves inside a controlled
|
|
29
25
|
* overlay. Declared here rather than in `App.tsx` because both the keyboard
|
|
@@ -89,42 +85,29 @@ export function useWheelRouting(options) {
|
|
|
89
85
|
}, [flush]);
|
|
90
86
|
const route = useCallback((event) => {
|
|
91
87
|
const current = optionsRef.current;
|
|
92
|
-
const geometry = current.geometryOverride ?? current.geometryRef.current;
|
|
93
|
-
const band = hitTestWheel(event.y, geometry);
|
|
94
88
|
const overlay = current.getOverlay();
|
|
95
|
-
// THE OVERLAY BRANCH IS EVALUATED FIRST AND RETURNS FOR EVERY
|
|
89
|
+
// THE OVERLAY BRANCH IS EVALUATED FIRST AND RETURNS FOR EVERY OVERLAY IT
|
|
96
90
|
// RECOGNIZES — the same two ordering rules the keyboard handler follows,
|
|
97
91
|
// and for the same reason (R-P1-7).
|
|
98
92
|
if (overlay) {
|
|
99
|
-
// The composer is inactive behind an overlay; stepping a hidden draft
|
|
100
|
-
// is worse than doing nothing.
|
|
101
|
-
if (band !== 'content')
|
|
102
|
-
return;
|
|
103
93
|
// Modes A only. `model` / `confirm` / `question` own their own keys and
|
|
104
94
|
// scrolling them would leave an inconsistent cursor (R-11).
|
|
105
95
|
const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan';
|
|
106
96
|
if (!controlled)
|
|
107
97
|
return;
|
|
98
|
+
// D-3: the row is not consulted here either. Behind an overlay the
|
|
99
|
+
// composer is inactive and the overlay is the only scrollable thing on
|
|
100
|
+
// screen, so a notch anywhere scrolls it.
|
|
108
101
|
const step = event.shift ? OVERLAY_PAGE : OVERLAY_LINES_PER_NOTCH;
|
|
109
102
|
current.onOverlayScroll(event.dir === 'up' ? -step : step);
|
|
110
103
|
return;
|
|
111
104
|
}
|
|
112
|
-
if (band === 'composer') {
|
|
113
|
-
// §4.7: the accumulator flushes when the BAND changes, not only when
|
|
114
|
-
// the direction does. The composer path is never coalesced, so without
|
|
115
|
-
// this the transcript scroll that preceded the pointer moving down
|
|
116
|
-
// lands ~16 ms AFTER the history step it happened before — the two
|
|
117
|
-
// halves of one gesture, applied out of order.
|
|
118
|
-
flush();
|
|
119
|
-
current.onHistoryStep(event.dir);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
105
|
if (event.shift) {
|
|
123
106
|
accumulate(event.dir === 'up' ? 'pageUp' : 'pageDown', 1);
|
|
124
107
|
return;
|
|
125
108
|
}
|
|
126
109
|
accumulate(event.dir === 'up' ? 'lineUp' : 'lineDown', CONTENT_LINES_PER_NOTCH);
|
|
127
|
-
}, [accumulate
|
|
110
|
+
}, [accumulate]);
|
|
128
111
|
const { mouseSource } = options;
|
|
129
112
|
useEffect(() => {
|
|
130
113
|
if (!mouseSource)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a tail of npm output into a reason (cli-auto-update-hardening H2 /
|
|
3
|
+
* section 5.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
|
|
6
|
+
*
|
|
7
|
+
* PURE, AND THE ONLY IMPORT IS A TYPE. The interesting half of H2 is not this
|
|
8
|
+
* table, it is the REPORTING rule that decides which of the resulting reasons
|
|
9
|
+
* may break the three-strike silence - and that lives in `types.ts` beside
|
|
10
|
+
* `shouldRenderUpdateLine`, as `IMMEDIATE_NOTICE_REASONS`.
|
|
11
|
+
*
|
|
12
|
+
* WHY THIS EXISTS AT ALL. The one line a user is ever allowed to see on the
|
|
13
|
+
* failure path reads `update failed <dot> npm i -g @aragon-agent/cli` for EVERY
|
|
14
|
+
* cause. For `EPERM` / `EBUSY` - the Windows self-replacement case, R-3 - that
|
|
15
|
+
* advice hits the same held handle and fails identically, so the single visible
|
|
16
|
+
* failure is also a wrong instruction.
|
|
17
|
+
*/
|
|
18
|
+
import type { UpdateReason } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Classify an install failure from the bounded stderr tail `runNpmInstall`
|
|
21
|
+
* already returns.
|
|
22
|
+
*
|
|
23
|
+
* ONE PARAMETER, NOT TWO. An earlier draft took `exitCode` and never read it:
|
|
24
|
+
* npm exits `1` for every one of these, so the code carries no information the
|
|
25
|
+
* tail does not, and a parameter that no branch and no test consumes is a
|
|
26
|
+
* maintenance liability that reads like a promise (P2-2).
|
|
27
|
+
*
|
|
28
|
+
* An absent or unrecognised tail is `install-failed` - the pre-H2 behaviour, so
|
|
29
|
+
* a machine whose npm says something we have never seen degrades to exactly what
|
|
30
|
+
* it did before rather than to a confident wrong answer.
|
|
31
|
+
*/
|
|
32
|
+
export declare function classifyInstallFailure(tail: string | undefined): UpdateReason;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a tail of npm output into a reason (cli-auto-update-hardening H2 /
|
|
3
|
+
* section 5.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope (C-2 / C-3).
|
|
6
|
+
*
|
|
7
|
+
* PURE, AND THE ONLY IMPORT IS A TYPE. The interesting half of H2 is not this
|
|
8
|
+
* table, it is the REPORTING rule that decides which of the resulting reasons
|
|
9
|
+
* may break the three-strike silence - and that lives in `types.ts` beside
|
|
10
|
+
* `shouldRenderUpdateLine`, as `IMMEDIATE_NOTICE_REASONS`.
|
|
11
|
+
*
|
|
12
|
+
* WHY THIS EXISTS AT ALL. The one line a user is ever allowed to see on the
|
|
13
|
+
* failure path reads `update failed <dot> npm i -g @aragon-agent/cli` for EVERY
|
|
14
|
+
* cause. For `EPERM` / `EBUSY` - the Windows self-replacement case, R-3 - that
|
|
15
|
+
* advice hits the same held handle and fails identically, so the single visible
|
|
16
|
+
* failure is also a wrong instruction.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* FIRST MATCH WINS, AND THE ORDER IS NOT ALPHABETICAL.
|
|
20
|
+
*
|
|
21
|
+
* `EPERM` is tested before `EACCES` because npm's Windows output frequently
|
|
22
|
+
* carries BOTH - the `EPERM` on the shim rename plus an `EACCES` from a retry -
|
|
23
|
+
* and the actionable one is the first. `network` is last because "proxy" and
|
|
24
|
+
* "network" appear inside unrelated npm advisory text often enough that an
|
|
25
|
+
* earlier position would swallow the three specific cases above it.
|
|
26
|
+
*/
|
|
27
|
+
const TABLE = [
|
|
28
|
+
[
|
|
29
|
+
/\bEPERM\b|\bEBUSY\b|\bETXTBSY\b|operation not permitted|resource busy or locked/i,
|
|
30
|
+
'blocked-by-os',
|
|
31
|
+
],
|
|
32
|
+
[/\bEACCES\b|permission denied/i, 'not-writable'],
|
|
33
|
+
[/\bENOSPC\b|no space left/i, 'no-space'],
|
|
34
|
+
[
|
|
35
|
+
/\bENOTFOUND\b|\bEAI_AGAIN\b|\bECONNRESET\b|\bETIMEDOUT\b|network|proxy|tunneling socket/i,
|
|
36
|
+
'network',
|
|
37
|
+
],
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Classify an install failure from the bounded stderr tail `runNpmInstall`
|
|
41
|
+
* already returns.
|
|
42
|
+
*
|
|
43
|
+
* ONE PARAMETER, NOT TWO. An earlier draft took `exitCode` and never read it:
|
|
44
|
+
* npm exits `1` for every one of these, so the code carries no information the
|
|
45
|
+
* tail does not, and a parameter that no branch and no test consumes is a
|
|
46
|
+
* maintenance liability that reads like a promise (P2-2).
|
|
47
|
+
*
|
|
48
|
+
* An absent or unrecognised tail is `install-failed` - the pre-H2 behaviour, so
|
|
49
|
+
* a machine whose npm says something we have never seen degrades to exactly what
|
|
50
|
+
* it did before rather than to a confident wrong answer.
|
|
51
|
+
*/
|
|
52
|
+
export function classifyInstallFailure(tail) {
|
|
53
|
+
if (!tail)
|
|
54
|
+
return 'install-failed';
|
|
55
|
+
for (const [re, reason] of TABLE) {
|
|
56
|
+
if (re.test(tail))
|
|
57
|
+
return reason;
|
|
58
|
+
}
|
|
59
|
+
return 'install-failed';
|
|
60
|
+
}
|