@aragon-agent/cli 0.5.6 → 0.5.9
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 +356 -0
- package/README.md +1429 -1105
- 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/entry-limits.d.ts +65 -0
- package/dist/agent/entry-limits.js +133 -0
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +253 -3
- package/dist/agent/reducer.js +553 -28
- 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/cli.js +122 -4
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +313 -2
- package/dist/commands/perf.d.ts +45 -0
- package/dist/commands/perf.js +78 -0
- package/dist/config/cli-commands.d.ts +53 -2
- package/dist/config/cli-commands.js +136 -3
- package/dist/config/env.d.ts +6 -1
- package/dist/config/env.js +131 -1
- package/dist/config/load.d.ts +63 -0
- package/dist/config/load.js +125 -2
- package/dist/config/schema.d.ts +273 -10
- package/dist/config/schema.js +250 -7
- package/dist/config/store.js +20 -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/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 +1 -4
- package/dist/ui/App.js +357 -44
- package/dist/ui/BottomStatusRow.d.ts +43 -0
- package/dist/ui/BottomStatusRow.js +14 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Markdown.d.ts +7 -1
- package/dist/ui/Markdown.js +15 -48
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +50 -0
- package/dist/ui/StatusBar.js +7 -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 +80 -5
- package/dist/ui/Transcript.js +179 -11
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +38 -1
- package/dist/ui/entries/AssistantEntry.js +34 -3
- 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.d.ts +9 -1
- package/dist/ui/entries/TeamCard.js +28 -2
- package/dist/ui/entries/TodoCard.d.ts +9 -1
- package/dist/ui/entries/TodoCard.js +23 -1
- package/dist/ui/entries/ToolCard.d.ts +50 -1
- package/dist/ui/entries/ToolCard.js +104 -7
- package/dist/ui/entries/ToolPreview.d.ts +2 -2
- package/dist/ui/entries/UserEntry.d.ts +9 -1
- package/dist/ui/entries/UserEntry.js +17 -1
- package/dist/ui/exit-snapshot.d.ts +7 -0
- 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/MeasuredEntry.d.ts +21 -0
- package/dist/ui/layout/MeasuredEntry.js +30 -0
- package/dist/ui/layout/ScrollViewport.js +8 -2
- package/dist/ui/layout/viewport-geometry.d.ts +28 -0
- package/dist/ui/layout/viewport-geometry.js +27 -0
- package/dist/ui/layout/virtual-window.d.ts +100 -0
- package/dist/ui/layout/virtual-window.js +308 -0
- package/dist/ui/live-clamp.d.ts +42 -0
- package/dist/ui/live-clamp.js +74 -0
- package/dist/ui/markdown-blocks.d.ts +25 -1
- package/dist/ui/markdown-blocks.js +43 -0
- 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/render-cache.d.ts +81 -0
- package/dist/ui/render-cache.js +195 -0
- package/dist/ui/render-governor.d.ts +54 -0
- package/dist/ui/render-governor.js +78 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +32 -0
- package/dist/ui/use-height-store.d.ts +31 -0
- package/dist/ui/use-height-store.js +76 -0
- package/dist/ui/use-render-governor.d.ts +39 -0
- package/dist/ui/use-render-governor.js +57 -0
- package/dist/ui/use-startup-notices.d.ts +9 -1
- package/dist/ui/use-startup-notices.js +35 -1
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- package/package.json +2 -2
- 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
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render caches (tui-render-performance L2 / R2 + R3).
|
|
3
|
+
*
|
|
4
|
+
* Ink has no output caching: every frame re-walks the whole mounted tree,
|
|
5
|
+
* re-measures every text node and re-serialises the frame. The two most
|
|
6
|
+
* expensive PURE computations in that walk were being redone from scratch on
|
|
7
|
+
* every one of them:
|
|
8
|
+
*
|
|
9
|
+
* - `cli-highlight`'s `highlight()` ran INSIDE `CodeBlock`'s render body, so a
|
|
10
|
+
* 200-line fenced block was a complete highlight.js tokenise, 30 times a
|
|
11
|
+
* second, for as long as it stayed mounted;
|
|
12
|
+
* - `Markdown` re-split and re-regexed the whole answer into one React element
|
|
13
|
+
* per line on every render.
|
|
14
|
+
*
|
|
15
|
+
* Everything cached here is a function of its INPUT STRING ALONE. Theme and
|
|
16
|
+
* glyphs are applied at render time and are never cached, because a themed
|
|
17
|
+
* React element in a cache makes `/theme` a no-op until the entry evicts (K-5) —
|
|
18
|
+
* exactly the class of silent bug this package writes comments to avoid.
|
|
19
|
+
*
|
|
20
|
+
* Caches are MODULE-LEVEL on purpose: settled transcript text is identical
|
|
21
|
+
* across component instances and across mount/unmount cycles, so per-instance
|
|
22
|
+
* caches would miss precisely when a scroll brings an old entry back.
|
|
23
|
+
*/
|
|
24
|
+
import { type MdBlock } from './markdown-blocks.js';
|
|
25
|
+
import { type Glyphs } from './glyphs.js';
|
|
26
|
+
import type { TermCapabilities } from './capabilities.js';
|
|
27
|
+
export declare const RENDER_CACHE_LIMITS: {
|
|
28
|
+
readonly highlightEntries: 128;
|
|
29
|
+
readonly highlightBytes: number;
|
|
30
|
+
/**
|
|
31
|
+
* Above this, skip highlighting entirely.
|
|
32
|
+
*
|
|
33
|
+
* highlight.js is superlinear in the length of a single block, and a
|
|
34
|
+
* multi-megabyte "code block" is almost always a pasted log rather than
|
|
35
|
+
* source. Rendering it plain costs the user nothing they would have been able
|
|
36
|
+
* to read; tokenising it costs the whole frame budget.
|
|
37
|
+
*/
|
|
38
|
+
readonly highlightMaxChars: 20000;
|
|
39
|
+
readonly markdownEntries: 256;
|
|
40
|
+
readonly markdownBytes: number;
|
|
41
|
+
readonly linesEntries: 512;
|
|
42
|
+
};
|
|
43
|
+
export interface RenderCacheStats {
|
|
44
|
+
markdownEntries: number;
|
|
45
|
+
markdownBytes: number;
|
|
46
|
+
highlightEntries: number;
|
|
47
|
+
highlightBytes: number;
|
|
48
|
+
lineEntries: number;
|
|
49
|
+
hits: number;
|
|
50
|
+
misses: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Syntax-highlight `code`, memoised on `(lang, code)`.
|
|
54
|
+
*
|
|
55
|
+
* `cli-highlight` emits chalk-level ANSI that does not depend on `theme.*`, so
|
|
56
|
+
* unlike a rendered element this string IS safe to cache across a `/theme`
|
|
57
|
+
* switch — the same property it has had since before this cache existed.
|
|
58
|
+
*/
|
|
59
|
+
export declare function highlightCached(code: string, lang: string): string;
|
|
60
|
+
/** Parse a markdown document into blocks, memoised on the whole text. */
|
|
61
|
+
export declare function parseMarkdownCached(text: string): MdBlock[];
|
|
62
|
+
/**
|
|
63
|
+
* `text.split('\n')`, memoised.
|
|
64
|
+
*
|
|
65
|
+
* READ-ONLY BY CONTRACT. The returned array is shared with every other holder of
|
|
66
|
+
* the same string, so a caller that sorts or splices it in place corrupts an
|
|
67
|
+
* unrelated card. The `readonly string[]` return type is the enforcement.
|
|
68
|
+
*/
|
|
69
|
+
export declare function splitLinesCached(text: string): readonly string[];
|
|
70
|
+
/**
|
|
71
|
+
* The glyph set for a terminal.
|
|
72
|
+
*
|
|
73
|
+
* `pickGlyphs` already returns one of two module constants, so this adds no
|
|
74
|
+
* caching of its own — it exists so that every render-layer memoisation the
|
|
75
|
+
* transcript depends on is reachable from one module, and so the components can
|
|
76
|
+
* stop calling `pickGlyphs` once per entry per frame through six import paths.
|
|
77
|
+
*/
|
|
78
|
+
export declare function glyphsFor(caps: TermCapabilities): Glyphs;
|
|
79
|
+
/** Drop everything. `/perf reset`, and the test suite between cases. */
|
|
80
|
+
export declare function clearRenderCaches(): void;
|
|
81
|
+
export declare function renderCacheStats(): RenderCacheStats;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render caches (tui-render-performance L2 / R2 + R3).
|
|
3
|
+
*
|
|
4
|
+
* Ink has no output caching: every frame re-walks the whole mounted tree,
|
|
5
|
+
* re-measures every text node and re-serialises the frame. The two most
|
|
6
|
+
* expensive PURE computations in that walk were being redone from scratch on
|
|
7
|
+
* every one of them:
|
|
8
|
+
*
|
|
9
|
+
* - `cli-highlight`'s `highlight()` ran INSIDE `CodeBlock`'s render body, so a
|
|
10
|
+
* 200-line fenced block was a complete highlight.js tokenise, 30 times a
|
|
11
|
+
* second, for as long as it stayed mounted;
|
|
12
|
+
* - `Markdown` re-split and re-regexed the whole answer into one React element
|
|
13
|
+
* per line on every render.
|
|
14
|
+
*
|
|
15
|
+
* Everything cached here is a function of its INPUT STRING ALONE. Theme and
|
|
16
|
+
* glyphs are applied at render time and are never cached, because a themed
|
|
17
|
+
* React element in a cache makes `/theme` a no-op until the entry evicts (K-5) —
|
|
18
|
+
* exactly the class of silent bug this package writes comments to avoid.
|
|
19
|
+
*
|
|
20
|
+
* Caches are MODULE-LEVEL on purpose: settled transcript text is identical
|
|
21
|
+
* across component instances and across mount/unmount cycles, so per-instance
|
|
22
|
+
* caches would miss precisely when a scroll brings an old entry back.
|
|
23
|
+
*/
|
|
24
|
+
import { highlight } from 'cli-highlight';
|
|
25
|
+
import { parseMarkdownBlocks } from './markdown-blocks.js';
|
|
26
|
+
import { pickGlyphs } from './glyphs.js';
|
|
27
|
+
export const RENDER_CACHE_LIMITS = {
|
|
28
|
+
highlightEntries: 128,
|
|
29
|
+
highlightBytes: 2 * 1024 * 1024,
|
|
30
|
+
/**
|
|
31
|
+
* Above this, skip highlighting entirely.
|
|
32
|
+
*
|
|
33
|
+
* highlight.js is superlinear in the length of a single block, and a
|
|
34
|
+
* multi-megabyte "code block" is almost always a pasted log rather than
|
|
35
|
+
* source. Rendering it plain costs the user nothing they would have been able
|
|
36
|
+
* to read; tokenising it costs the whole frame budget.
|
|
37
|
+
*/
|
|
38
|
+
highlightMaxChars: 20_000,
|
|
39
|
+
markdownEntries: 256,
|
|
40
|
+
markdownBytes: 4 * 1024 * 1024,
|
|
41
|
+
linesEntries: 512,
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Insertion-ordered LRU with both an entry-count and an approximate byte budget.
|
|
45
|
+
*
|
|
46
|
+
* TWO BUDGETS, NOT ONE. An entry count alone is unbounded in memory (one 5 MB
|
|
47
|
+
* answer), and a byte budget alone is unbounded in Map overhead (a hundred
|
|
48
|
+
* thousand one-character keys). Every cache here can be fed by the model, so
|
|
49
|
+
* both directions have to be closed.
|
|
50
|
+
*/
|
|
51
|
+
class LruCache {
|
|
52
|
+
maxEntries;
|
|
53
|
+
maxBytes;
|
|
54
|
+
map = new Map();
|
|
55
|
+
total = 0;
|
|
56
|
+
hits = 0;
|
|
57
|
+
misses = 0;
|
|
58
|
+
constructor(maxEntries, maxBytes) {
|
|
59
|
+
this.maxEntries = maxEntries;
|
|
60
|
+
this.maxBytes = maxBytes;
|
|
61
|
+
}
|
|
62
|
+
get(key) {
|
|
63
|
+
const hit = this.map.get(key);
|
|
64
|
+
if (hit === undefined) {
|
|
65
|
+
this.misses += 1;
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
// Re-insert to move the key to the young end of the insertion order.
|
|
69
|
+
this.map.delete(key);
|
|
70
|
+
this.map.set(key, hit);
|
|
71
|
+
this.hits += 1;
|
|
72
|
+
return hit.value;
|
|
73
|
+
}
|
|
74
|
+
set(key, value, bytes) {
|
|
75
|
+
const existing = this.map.get(key);
|
|
76
|
+
if (existing)
|
|
77
|
+
this.total -= existing.bytes;
|
|
78
|
+
this.map.delete(key);
|
|
79
|
+
this.map.set(key, { value, bytes });
|
|
80
|
+
this.total += bytes;
|
|
81
|
+
while (this.map.size > this.maxEntries || this.total > this.maxBytes) {
|
|
82
|
+
const oldest = this.map.keys().next();
|
|
83
|
+
if (oldest.done)
|
|
84
|
+
break;
|
|
85
|
+
const entry = this.map.get(oldest.value);
|
|
86
|
+
if (entry)
|
|
87
|
+
this.total -= entry.bytes;
|
|
88
|
+
this.map.delete(oldest.value);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
clear() {
|
|
93
|
+
this.map.clear();
|
|
94
|
+
this.total = 0;
|
|
95
|
+
this.hits = 0;
|
|
96
|
+
this.misses = 0;
|
|
97
|
+
}
|
|
98
|
+
get size() {
|
|
99
|
+
return this.map.size;
|
|
100
|
+
}
|
|
101
|
+
get bytes() {
|
|
102
|
+
return this.total;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const highlightCache = new LruCache(RENDER_CACHE_LIMITS.highlightEntries, RENDER_CACHE_LIMITS.highlightBytes);
|
|
106
|
+
const markdownCache = new LruCache(RENDER_CACHE_LIMITS.markdownEntries, RENDER_CACHE_LIMITS.markdownBytes);
|
|
107
|
+
/**
|
|
108
|
+
* `text.split('\n')` results.
|
|
109
|
+
*
|
|
110
|
+
* Bounded by ENTRY COUNT ONLY, and the byte budget is set to the markdown one
|
|
111
|
+
* because the arrays are views over strings the transcript already holds — they
|
|
112
|
+
* add pointer overhead, not a second copy of the text.
|
|
113
|
+
*/
|
|
114
|
+
const lineCache = new LruCache(RENDER_CACHE_LIMITS.linesEntries, RENDER_CACHE_LIMITS.markdownBytes);
|
|
115
|
+
/**
|
|
116
|
+
* Syntax-highlight `code`, memoised on `(lang, code)`.
|
|
117
|
+
*
|
|
118
|
+
* `cli-highlight` emits chalk-level ANSI that does not depend on `theme.*`, so
|
|
119
|
+
* unlike a rendered element this string IS safe to cache across a `/theme`
|
|
120
|
+
* switch — the same property it has had since before this cache existed.
|
|
121
|
+
*/
|
|
122
|
+
export function highlightCached(code, lang) {
|
|
123
|
+
if (code.length === 0)
|
|
124
|
+
return code;
|
|
125
|
+
if (code.length > RENDER_CACHE_LIMITS.highlightMaxChars)
|
|
126
|
+
return code;
|
|
127
|
+
// The delimiter is written as an ESCAPE and never as a literal control
|
|
128
|
+
// character: a raw NUL byte makes the whole file "binary" to grep and
|
|
129
|
+
// ripgrep, which silently hides every definition in it from the tool this
|
|
130
|
+
// codebase navigates itself with. NUL rather than a space because a space
|
|
131
|
+
// still lets ("a b", "ts") and ("b", "ts a") collide on one key.
|
|
132
|
+
const key = `${lang}\u0000${code}`;
|
|
133
|
+
const hit = highlightCache.get(key);
|
|
134
|
+
if (hit !== undefined)
|
|
135
|
+
return hit;
|
|
136
|
+
let rendered = code;
|
|
137
|
+
try {
|
|
138
|
+
rendered = highlight(code, { language: lang || undefined, ignoreIllegals: true });
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
rendered = code;
|
|
142
|
+
}
|
|
143
|
+
return highlightCache.set(key, rendered, key.length + rendered.length);
|
|
144
|
+
}
|
|
145
|
+
/** Parse a markdown document into blocks, memoised on the whole text. */
|
|
146
|
+
export function parseMarkdownCached(text) {
|
|
147
|
+
const hit = markdownCache.get(text);
|
|
148
|
+
if (hit !== undefined)
|
|
149
|
+
return hit;
|
|
150
|
+
return markdownCache.set(text, parseMarkdownBlocks(text), text.length * 2);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* `text.split('\n')`, memoised.
|
|
154
|
+
*
|
|
155
|
+
* READ-ONLY BY CONTRACT. The returned array is shared with every other holder of
|
|
156
|
+
* the same string, so a caller that sorts or splices it in place corrupts an
|
|
157
|
+
* unrelated card. The `readonly string[]` return type is the enforcement.
|
|
158
|
+
*/
|
|
159
|
+
export function splitLinesCached(text) {
|
|
160
|
+
if (text.length === 0)
|
|
161
|
+
return EMPTY_LINES;
|
|
162
|
+
const hit = lineCache.get(text);
|
|
163
|
+
if (hit !== undefined)
|
|
164
|
+
return hit;
|
|
165
|
+
return lineCache.set(text, text.split('\n'), text.length);
|
|
166
|
+
}
|
|
167
|
+
const EMPTY_LINES = [];
|
|
168
|
+
/**
|
|
169
|
+
* The glyph set for a terminal.
|
|
170
|
+
*
|
|
171
|
+
* `pickGlyphs` already returns one of two module constants, so this adds no
|
|
172
|
+
* caching of its own — it exists so that every render-layer memoisation the
|
|
173
|
+
* transcript depends on is reachable from one module, and so the components can
|
|
174
|
+
* stop calling `pickGlyphs` once per entry per frame through six import paths.
|
|
175
|
+
*/
|
|
176
|
+
export function glyphsFor(caps) {
|
|
177
|
+
return pickGlyphs(caps);
|
|
178
|
+
}
|
|
179
|
+
/** Drop everything. `/perf reset`, and the test suite between cases. */
|
|
180
|
+
export function clearRenderCaches() {
|
|
181
|
+
highlightCache.clear();
|
|
182
|
+
markdownCache.clear();
|
|
183
|
+
lineCache.clear();
|
|
184
|
+
}
|
|
185
|
+
export function renderCacheStats() {
|
|
186
|
+
return {
|
|
187
|
+
markdownEntries: markdownCache.size,
|
|
188
|
+
markdownBytes: markdownCache.bytes,
|
|
189
|
+
highlightEntries: highlightCache.size,
|
|
190
|
+
highlightBytes: highlightCache.bytes,
|
|
191
|
+
lineEntries: lineCache.size,
|
|
192
|
+
hits: markdownCache.hits + highlightCache.hits + lineCache.hits,
|
|
193
|
+
misses: markdownCache.misses + highlightCache.misses + lineCache.misses,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive render governor (tui-render-performance L4 / R7). Pure, React-free.
|
|
3
|
+
*
|
|
4
|
+
* Neither the streaming coalescer nor Ink adapts when a frame overruns its
|
|
5
|
+
* budget: the coalescer keeps its 33 ms timer, Ink keeps its 32 ms throttle, and
|
|
6
|
+
* the event loop saturates. Renders then queue behind each other on Node's
|
|
7
|
+
* single thread and KEYSTROKES QUEUE BEHIND THE RENDERS — which is what turns a
|
|
8
|
+
* slow frame into an unresponsive terminal.
|
|
9
|
+
*
|
|
10
|
+
* The ladder is the safety net under L1/L2/L3, not a substitute for them: it
|
|
11
|
+
* cannot make a frame cheaper, only rarer. Its job is to turn any residual
|
|
12
|
+
* pathology from a freeze into a visibly slower but still interactive UI.
|
|
13
|
+
*
|
|
14
|
+
* I-L4-1 — the governor changes only HOW OFTEN the view updates. Never what is
|
|
15
|
+
* dispatched, never the order, never the final state. `mergeDeltas` already
|
|
16
|
+
* guarantees that a longer window is simply a larger merge and that any
|
|
17
|
+
* non-delta action flushes the pending run first, so the same event sequence
|
|
18
|
+
* must produce the same final `ViewState` at every rung
|
|
19
|
+
* (`render-governor.test.ts`).
|
|
20
|
+
*/
|
|
21
|
+
/** Coalescing intervals, in ms. Rung 0 is the pre-feature `COALESCE_MS`. */
|
|
22
|
+
export declare const GOVERNOR_LADDER: readonly [33, 50, 80, 125, 200, 320];
|
|
23
|
+
/** Step up when a commit costs more than this fraction of the current interval. */
|
|
24
|
+
export declare const GOVERNOR_UP_RATIO = 0.7;
|
|
25
|
+
/**
|
|
26
|
+
* Consecutive cheap frames required before stepping back down.
|
|
27
|
+
*
|
|
28
|
+
* Hysteresis, and it has to be asymmetric: stepping up on one expensive frame
|
|
29
|
+
* and down on one cheap one would oscillate through the whole ladder while a
|
|
30
|
+
* long answer streams past a couple of code blocks.
|
|
31
|
+
*/
|
|
32
|
+
export declare const GOVERNOR_DOWN_STREAK = 8;
|
|
33
|
+
export interface GovernorState {
|
|
34
|
+
rung: number;
|
|
35
|
+
cheapStreak: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function initialGovernor(): GovernorState;
|
|
38
|
+
/**
|
|
39
|
+
* The coalescing interval for a state.
|
|
40
|
+
*
|
|
41
|
+
* Clamped to `maxIntervalMs` in BOTH directions: `maxRenderIntervalMs = 33`
|
|
42
|
+
* flattens the ladder to one rung, which is the documented way to get the
|
|
43
|
+
* pre-feature cadence without reaching for the kill switch.
|
|
44
|
+
*/
|
|
45
|
+
export declare function intervalOf(state: GovernorState, maxIntervalMs: number): number;
|
|
46
|
+
/**
|
|
47
|
+
* Fold one commit's cost into the ladder.
|
|
48
|
+
*
|
|
49
|
+
* `lastCommitMs` covers React reconciliation AND Ink's yoga layout, because Ink
|
|
50
|
+
* computes layout in `onRender`, which React runs before layout effects. It does
|
|
51
|
+
* NOT cover Ink's serialisation and the stdout write, which happen after. In
|
|
52
|
+
* practice layout dominates, and the ladder's job is DIRECTION, not accuracy.
|
|
53
|
+
*/
|
|
54
|
+
export declare function stepGovernor(state: GovernorState, lastCommitMs: number, maxIntervalMs: number): GovernorState;
|