@aragon-agent/cli 0.5.8 → 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 +279 -0
- package/README.md +1429 -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/cli.js +86 -2
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +308 -2
- package/dist/config/cli-commands.d.ts +53 -2
- package/dist/config/cli-commands.js +136 -3
- package/dist/config/env.d.ts +6 -3
- package/dist/config/env.js +103 -3
- package/dist/config/load.d.ts +49 -0
- package/dist/config/load.js +83 -1
- package/dist/config/schema.d.ts +212 -2
- package/dist/config/schema.js +207 -1
- 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 +273 -37
- 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/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/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/package.json +2 -2
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { type RenderMode } from './frame.js';
|
|
14
|
-
import { type FrameGeometry } from './regions.js';
|
|
15
14
|
export interface AppShellProps {
|
|
16
15
|
mode: RenderMode;
|
|
17
16
|
rows: number;
|
|
@@ -21,11 +20,10 @@ export interface AppShellProps {
|
|
|
21
20
|
/**
|
|
22
21
|
* The live subagent roster (team-subagents §6.1), or nothing.
|
|
23
22
|
*
|
|
24
|
-
* IT RENDERS
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* composer band boundary stale the instant a dispatch started.
|
|
23
|
+
* IT RENDERS IN THE BOTTOM CHROME OF THE FULL-SCREEN BRANCH, above `toast` and
|
|
24
|
+
* below the viewport: the roster is transient status about the run in progress,
|
|
25
|
+
* so it belongs with the other transient rows rather than inside the scrolling
|
|
26
|
+
* transcript, where it would be interleaved with history and scroll away.
|
|
29
27
|
*/
|
|
30
28
|
team?: React.ReactNode;
|
|
31
29
|
/**
|
|
@@ -36,10 +34,11 @@ export interface AppShellProps {
|
|
|
36
34
|
* primitive and has no business knowing what a todo is. `team` above already
|
|
37
35
|
* leaks content vocabulary in here; there is no reason to repeat it.
|
|
38
36
|
*
|
|
39
|
-
* IT MUST
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
37
|
+
* IT MUST BE RENDERED IN THE MIDDLE BAND, BESIDE THE VIEWPORT (C-6 / I-4) —
|
|
38
|
+
* that is what makes it a rail rather than another row of bottom chrome. Moved
|
|
39
|
+
* down into the bottom box it would stop being a column beside the transcript
|
|
40
|
+
* and start eating viewport height, growing and shrinking the transcript every
|
|
41
|
+
* time the list changed length.
|
|
43
42
|
*
|
|
44
43
|
* The INLINE branch does not include it at all — a structural guarantee rather
|
|
45
44
|
* than a promise that the caller passes `null` (non-goal 4).
|
|
@@ -56,21 +55,13 @@ export interface AppShellProps {
|
|
|
56
55
|
* both on screen showing the same list (I-6). Fullscreen has the rail; that is
|
|
57
56
|
* the whole design.
|
|
58
57
|
*
|
|
59
|
-
* It sits between `viewport` and `team`,
|
|
60
|
-
* other branch
|
|
61
|
-
*
|
|
58
|
+
* It sits between `viewport` and `team`, mirroring where the rail sits in the
|
|
59
|
+
* other branch: last thing above the bottom chrome, so the strip and the
|
|
60
|
+
* roster read top-to-bottom in the same order in both modes.
|
|
62
61
|
*/
|
|
63
62
|
strip?: React.ReactNode;
|
|
64
63
|
toast: React.ReactNode;
|
|
65
64
|
composer: React.ReactNode;
|
|
66
65
|
status: React.ReactNode;
|
|
67
|
-
/**
|
|
68
|
-
* Where the wheel router reads the composer band boundary from (mouse-wheel
|
|
69
|
-
* §4.5). Written in a LAYOUT EFFECT to a MUTABLE REF, never to state: the
|
|
70
|
-
* measurement happens on every commit, and routing it through `setState`
|
|
71
|
-
* would add a re-render to the streaming path for a value nothing renders
|
|
72
|
-
* (R-7).
|
|
73
|
-
*/
|
|
74
|
-
geometryRef?: React.MutableRefObject<FrameGeometry>;
|
|
75
66
|
}
|
|
76
|
-
export declare function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status,
|
|
67
|
+
export declare function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, }: AppShellProps): React.ReactElement;
|
|
@@ -1,44 +1,9 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
* AppShell (spec §4.3) — the frame that makes R2 true.
|
|
4
|
-
*
|
|
5
|
-
* In `fullscreen` the root box has a FIXED height of `frameHeight(rows)` and the
|
|
6
|
-
* middle viewport is the only `flexGrow` child, so the bottom chrome (toast /
|
|
7
|
-
* composer / status bar) is physically the last thing on the screen from the
|
|
8
|
-
* very first frame — empty session, long session, mid-scroll, overlay open.
|
|
9
|
-
*
|
|
10
|
-
* In `inline` the same children are emitted in the same order with no height
|
|
11
|
-
* constraint, i.e. byte-for-byte the v0.2.0 document-flow behavior.
|
|
12
|
-
*/
|
|
13
|
-
import { useLayoutEffect, useRef } from 'react';
|
|
14
|
-
import { Box, measureElement } from 'ink';
|
|
2
|
+
import { Box } from 'ink';
|
|
15
3
|
import { frameHeight } from './frame.js';
|
|
16
|
-
|
|
17
|
-
export function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, geometryRef, }) {
|
|
18
|
-
const bottomRef = useRef(null);
|
|
19
|
-
// DECLARED ABOVE THE INLINE EARLY RETURN, and it has to stay there: React
|
|
20
|
-
// keys hooks by call order, and a hook that only runs in one mode changes
|
|
21
|
-
// that order between modes.
|
|
22
|
-
//
|
|
23
|
-
// Ink computes yoga layout in `onRender`, which React runs BEFORE layout
|
|
24
|
-
// effects, so this reflects the commit that just happened — the same
|
|
25
|
-
// mechanism `ScrollViewport` already relies on.
|
|
26
|
-
useLayoutEffect(() => {
|
|
27
|
-
if (!geometryRef)
|
|
28
|
-
return;
|
|
29
|
-
if (mode === 'inline' || !bottomRef.current) {
|
|
30
|
-
geometryRef.current = UNMEASURED_GEOMETRY;
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
geometryRef.current = {
|
|
34
|
-
// 1 in the alternate screen — see the note on `FrameGeometry.top`.
|
|
35
|
-
top: 1,
|
|
36
|
-
height: frameHeight(rows),
|
|
37
|
-
bottom: measureElement(bottomRef.current).height,
|
|
38
|
-
};
|
|
39
|
-
});
|
|
4
|
+
export function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, }) {
|
|
40
5
|
if (mode === 'inline') {
|
|
41
6
|
return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, strip, team, toast, composer, status] }));
|
|
42
7
|
}
|
|
43
|
-
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), rail] }), _jsxs(Box, {
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), rail] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [team, toast, composer, status] })] }));
|
|
44
9
|
}
|
|
@@ -73,7 +73,7 @@ export declare function heightKey(entry: Entry, cols: number, expanded: boolean,
|
|
|
73
73
|
* after the entry is measured, while an under-estimate can put the newest output
|
|
74
74
|
* off the bottom of a viewport the user believes is pinned.
|
|
75
75
|
*/
|
|
76
|
-
export declare function estimateEntryRows(entry: Entry, cols: number, density: DensityMode, expanded: boolean): number;
|
|
76
|
+
export declare function estimateEntryRows(entry: Entry, cols: number, density: DensityMode, expanded: boolean, thinkingVisible?: boolean): number;
|
|
77
77
|
export interface WindowSelection {
|
|
78
78
|
/** First mounted entry, inclusive. */
|
|
79
79
|
startIndex: number;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* unchanged. No scroll-anchoring compensation pass is required. Do NOT
|
|
22
22
|
* "simplify" `offset` to count from the top.
|
|
23
23
|
*/
|
|
24
|
+
import { FAST_LIMITS } from '../../fast/limits.js';
|
|
24
25
|
import { GUTTER_WIDTH } from './Gutter.js';
|
|
25
26
|
export const VIRTUAL_LIMITS = {
|
|
26
27
|
/**
|
|
@@ -54,16 +55,52 @@ export function entryRevision(entry) {
|
|
|
54
55
|
return 'u';
|
|
55
56
|
case 'notice':
|
|
56
57
|
return 'n';
|
|
58
|
+
// `thinkingMs` is O(1) and it CHANGES THE ROW: sealing the clock turns
|
|
59
|
+
// `* thought` into `* thought for 12s`, and with thinking hidden that row is
|
|
60
|
+
// the entry's whole rendered height.
|
|
57
61
|
case 'assistant':
|
|
58
|
-
return `a${entry.text.length}.${entry.thinking?.length ?? 0}.${entry.streaming ? 1 : 0}.${entry.aborted ? 1 : 0}`;
|
|
62
|
+
return `a${entry.text.length}.${entry.thinking?.length ?? 0}.${entry.streaming ? 1 : 0}.${entry.aborted ? 1 : 0}.${entry.thinkingMs ?? -1}`;
|
|
63
|
+
// `patch.lineCount` is PRECOMPUTED for exactly this term: summing
|
|
64
|
+
// `hunks[].lines.length` per entry per frame is the cost the height cache
|
|
65
|
+
// exists to remove, and I-L3-1 forbids hashing.
|
|
66
|
+
// `liveSeq` IS A COUNTER AND NOT `live.length`, and that is precisely the
|
|
67
|
+
// NON-APPEND mutation I-L3-1 above says must extend the revision: a
|
|
68
|
+
// fixed-size tail evicting its oldest row while appending a new one of the
|
|
69
|
+
// same width leaves the joined length unchanged, so a length term would go
|
|
70
|
+
// on matching while the card's content changed under it.
|
|
71
|
+
//
|
|
72
|
+
// SIX TERMS NOW, the longest revision in this file (P2-12). Still O(1) per
|
|
73
|
+
// entry per frame, which is the property I-L3-1 requires -- but the next
|
|
74
|
+
// field makes it seven, and a term that is not O(1) would put the cost this
|
|
75
|
+
// module exists to remove back on every frame.
|
|
59
76
|
case 'tool':
|
|
60
|
-
return `t${entry.status}.${entry.argsRaw.length}.${entry.preview?.length ?? 0}.${entry.durationMs ?? -1}`;
|
|
77
|
+
return `t${entry.status}.${entry.argsRaw.length}.${entry.preview?.length ?? 0}.${entry.durationMs ?? -1}.${entry.patch?.lineCount ?? -1}.${entry.liveSeq ?? -1}`;
|
|
61
78
|
case 'team':
|
|
62
79
|
return `m${entry.active ? 1 : 0}.${entry.runs.length}.${entry.runs
|
|
63
80
|
.map((r) => r.phase[0])
|
|
64
81
|
.join('')}.${entry.runs.reduce((acc, r) => acc + r.toolCalls, 0)}`;
|
|
65
82
|
case 'todo':
|
|
66
83
|
return `d${entry.live ? 1 : 0}.${entry.doneCount}.${entry.total}.${entry.items.length}`;
|
|
84
|
+
/**
|
|
85
|
+
* The retry card is ONE ROW IN EVERY PHASE, so its revision carries the phase
|
|
86
|
+
* and the counter but deliberately NOT the countdown: `resumeAt` changes the
|
|
87
|
+
* rendered SECONDS once a second while the height stays 1, and folding it in
|
|
88
|
+
* here would invalidate the height cache 30 times per backoff to re-measure a
|
|
89
|
+
* number that cannot change the answer.
|
|
90
|
+
*
|
|
91
|
+
* `phase` and `attempt` ARE in, because they change the row's text and a stale
|
|
92
|
+
* measurement would be wrong in a way nothing reports (I-L3-1).
|
|
93
|
+
*/
|
|
94
|
+
case 'retry':
|
|
95
|
+
return `r${entry.phase}.${entry.attempt}.${entry.maxRetries}`;
|
|
96
|
+
/**
|
|
97
|
+
* `status`, `live` and the TEXT LENGTH, because all three change the rows the
|
|
98
|
+
* card draws: `advice` renders a body, everything else renders one line, and
|
|
99
|
+
* the body's length decides how many rows it wraps to. `durationMs` is in the
|
|
100
|
+
* header, which is `wrap="truncate"`, so it can never change the height.
|
|
101
|
+
*/
|
|
102
|
+
case 'fast':
|
|
103
|
+
return `f${entry.status}.${entry.live ? 1 : 0}.${entry.text?.length ?? 0}.${entry.detail?.length ?? 0}`;
|
|
67
104
|
default:
|
|
68
105
|
return 'x';
|
|
69
106
|
}
|
|
@@ -101,6 +138,10 @@ function wrappedRows(text, usable) {
|
|
|
101
138
|
}
|
|
102
139
|
/** Collapsed preview rows in `ToolCard`; duplicated as a NUMBER, not imported. */
|
|
103
140
|
const TOOL_COLLAPSED_LINES = 8;
|
|
141
|
+
/** Collapsed diff rows in `DiffView`; duplicated for the same reason as above. */
|
|
142
|
+
const DIFF_COLLAPSED_LINES = 12;
|
|
143
|
+
/** Live tail rows in `ToolCard`; duplicated as a NUMBER, for the same reason. */
|
|
144
|
+
const TOOL_LIVE_TAIL_ROWS = 8;
|
|
104
145
|
/**
|
|
105
146
|
* Pure UPPER-BOUND estimate of an entry's height, used until a real measurement
|
|
106
147
|
* exists.
|
|
@@ -110,7 +151,7 @@ const TOOL_COLLAPSED_LINES = 8;
|
|
|
110
151
|
* after the entry is measured, while an under-estimate can put the newest output
|
|
111
152
|
* off the bottom of a viewport the user believes is pinned.
|
|
112
153
|
*/
|
|
113
|
-
export function estimateEntryRows(entry, cols, density, expanded) {
|
|
154
|
+
export function estimateEntryRows(entry, cols, density, expanded, thinkingVisible = true) {
|
|
114
155
|
const usable = Math.max(1, cols - GUTTER_WIDTH);
|
|
115
156
|
// `separationRows` needs the PRECEDING entry, which a per-entry estimate does
|
|
116
157
|
// not have. One row is its maximum, so charging it unconditionally keeps this
|
|
@@ -122,12 +163,60 @@ export function estimateEntryRows(entry, cols, density, expanded) {
|
|
|
122
163
|
case 'notice':
|
|
123
164
|
return separation + wrappedRows(entry.text, usable);
|
|
124
165
|
case 'assistant': {
|
|
125
|
-
|
|
166
|
+
// THE PARAMETER'S `true` DEFAULT IS A FAIL-SAFE FOR A FORGOTTEN CALLER,
|
|
167
|
+
// NOT THE PLAN FOR THE ONE THERE IS (P1-6a). `Transcript.tsx` holds
|
|
168
|
+
// `thinkingVisible` as a prop and MUST pass it: with thinking hidden by
|
|
169
|
+
// default, an unpassed flag inflates every assistant entry that thought by
|
|
170
|
+
// `1 + wrappedRows(thinking)` — hundreds of rows at `thinkingLevel: high`
|
|
171
|
+
// against a real height of one. The self-correction argument does not
|
|
172
|
+
// rescue that, because an entry is only measured once `selectWindow`
|
|
173
|
+
// mounts it, and an entry estimated at 400 rows is exactly the one that
|
|
174
|
+
// never gets mounted.
|
|
175
|
+
const thinking = !entry.thinking
|
|
176
|
+
? 0
|
|
177
|
+
: thinkingVisible
|
|
178
|
+
? 1 + wrappedRows(entry.thinking, usable)
|
|
179
|
+
: // The collapsed `thought for Ns` marker is exactly one row, and it is
|
|
180
|
+
// suppressed while the entry is still streaming (the activity line is
|
|
181
|
+
// the live surface then).
|
|
182
|
+
entry.streaming
|
|
183
|
+
? 0
|
|
184
|
+
: 1;
|
|
126
185
|
const body = entry.text.length > 0 ? wrappedRows(entry.text, usable) : 1;
|
|
127
186
|
return separation + thinking + body + (entry.aborted ? 1 : 0);
|
|
128
187
|
}
|
|
129
188
|
case 'tool': {
|
|
130
|
-
|
|
189
|
+
const settled = entry.status === 'done' || entry.status === 'error';
|
|
190
|
+
// WHEN A PATCH IS PRESENT IT, NOT THE PREVIEW, IS WHAT DRAWS (§3.4): the
|
|
191
|
+
// stored preview is one line by then, so estimating from it would report a
|
|
192
|
+
// 3-row card for a 12-row diff — an UNDER-estimate, the direction this
|
|
193
|
+
// file names as unsafe.
|
|
194
|
+
if (settled && entry.patch) {
|
|
195
|
+
const rows = entry.patch.lineCount + Math.max(0, entry.patch.hunks.length - 1);
|
|
196
|
+
const shown = expanded ? rows : Math.min(rows, DIFF_COLLAPSED_LINES);
|
|
197
|
+
// card header + `+N -M` summary + rows + footer
|
|
198
|
+
return separation + 1 + 1 + shown + 1;
|
|
199
|
+
}
|
|
200
|
+
// THE ONE-LINE `if (!entry.preview || !settled)` THIS REPLACES COVERED TWO
|
|
201
|
+
// CASES, and both must survive the split (P1-4): not settled, and settled
|
|
202
|
+
// with nothing to draw (an aborted call, a tool that returned an empty
|
|
203
|
+
// string). A `!settled` block alone would drop the second into
|
|
204
|
+
// `wrappedRows(entry.preview, usable)` with `undefined`.
|
|
205
|
+
if (!settled) {
|
|
206
|
+
const live = entry.live?.length ?? 0;
|
|
207
|
+
if (live === 0)
|
|
208
|
+
return separation + 1;
|
|
209
|
+
// Header + tail rows + the unconditional stall/`(running)` footer row.
|
|
210
|
+
//
|
|
211
|
+
// ROWS ARE COUNTED, NOT WRAPPED, which is legitimate only because every
|
|
212
|
+
// live row is rendered `wrap="truncate"` AND clipped at
|
|
213
|
+
// `LIVE_ROW_MAX_CHARS` on the way into the store -- the same argument
|
|
214
|
+
// `DiffView.tsx:6-11` makes for itself, and the same coupling: if a
|
|
215
|
+
// future change lets a live row wrap, this silently UNDER-counts, the
|
|
216
|
+
// direction this file names as the one the layout cannot absorb.
|
|
217
|
+
return separation + 1 + Math.min(live, TOOL_LIVE_TAIL_ROWS) + 1;
|
|
218
|
+
}
|
|
219
|
+
if (!entry.preview) {
|
|
131
220
|
return separation + 1;
|
|
132
221
|
}
|
|
133
222
|
const lines = wrappedRows(entry.preview, usable);
|
|
@@ -139,6 +228,21 @@ export function estimateEntryRows(entry, cols, density, expanded) {
|
|
|
139
228
|
return separation + 1 + entry.runs.length + (expanded ? entry.runs.length * 3 + 1 : 1);
|
|
140
229
|
case 'todo':
|
|
141
230
|
return separation + 1 + entry.items.length;
|
|
231
|
+
// ONE ROW, ALWAYS. The card is `wrap="truncate"` in every phase, so this is an
|
|
232
|
+
// exact height rather than an upper bound.
|
|
233
|
+
case 'retry':
|
|
234
|
+
return separation + 1;
|
|
235
|
+
// ADDING THE CASE TO ONLY ONE OF THIS FILE'S TWO SWITCHES YIELDS A
|
|
236
|
+
// TRANSCRIPT WHOSE SCROLL ARITHMETIC DISAGREES WITH WHAT IT DREW, which
|
|
237
|
+
// manifests as drift rather than as an error (RV-13). Header row, then
|
|
238
|
+
// either the wrapped critique (clamped to `cardTextRows`) or one status
|
|
239
|
+
// line — an upper bound, which is what this function owes its caller.
|
|
240
|
+
case 'fast': {
|
|
241
|
+
const body = entry.status === 'advice' && entry.text
|
|
242
|
+
? Math.min(FAST_LIMITS.cardTextRows, wrappedRows(entry.text, usable))
|
|
243
|
+
: 1;
|
|
244
|
+
return separation + 1 + body;
|
|
245
|
+
}
|
|
142
246
|
default:
|
|
143
247
|
return separation + 1;
|
|
144
248
|
}
|
|
@@ -10,10 +10,10 @@ function keyRows(times) {
|
|
|
10
10
|
['Shift+Tab', 'Toggle plan mode (same as /plan)'],
|
|
11
11
|
[`Ctrl+C ${times}2`, 'Exit'],
|
|
12
12
|
['Ctrl+L', 'Redraw the frame'],
|
|
13
|
-
['Ctrl+T', '
|
|
13
|
+
['Ctrl+T', 'Show/hide thinking (off by default)'],
|
|
14
14
|
['PgUp / PgDn', 'Scroll a page (transcript, or this overlay)'],
|
|
15
15
|
['Shift+Up / Shift+Down', 'Scroll the transcript a line (full-screen mode)'],
|
|
16
|
-
['Wheel', 'Scroll the transcript (
|
|
16
|
+
['Wheel', 'Scroll the transcript (or the open overlay)'],
|
|
17
17
|
['Shift+Wheel', 'Scroll the transcript a page'],
|
|
18
18
|
['Up / Down', 'Prompt history (empty input)'],
|
|
19
19
|
];
|
|
@@ -27,6 +27,7 @@ const COMMANDS = [
|
|
|
27
27
|
['/thinking <level>', 'Set thinking level'],
|
|
28
28
|
['/max-tokens [n|auto]', 'Output token cap; no argument reports the effective one'],
|
|
29
29
|
['/team [on|off|max n]', 'Team subagents: status, switch, fan-out width'],
|
|
30
|
+
['/fast [on|off|model|review]', 'Fast model tier: status, switch, model, review cadence'],
|
|
30
31
|
[
|
|
31
32
|
'/todo [on|off|panel|follow|clear|continue]',
|
|
32
33
|
'Todo planning: status, switches, the rail, follow-through',
|
|
@@ -67,6 +68,16 @@ const TEAM_ROWS = [
|
|
|
67
68
|
['--team / --no-team', 'Start a session with team mode on or off (also ARAGON_TEAM=0)'],
|
|
68
69
|
['team_send / team_wait', 'How subagents message each other; 6 each, one every 15s'],
|
|
69
70
|
];
|
|
71
|
+
const FAST_ROWS = [
|
|
72
|
+
['/fast', 'Status: on/off, the model, the cadence, and this session\'s totals'],
|
|
73
|
+
['/fast on | off', 'Switch the tier for this session (and save it)'],
|
|
74
|
+
['/fast model <id>', 'Set the fast model; accepts provider:model'],
|
|
75
|
+
['/fast same', 'Run the fast tier on the main model'],
|
|
76
|
+
['/fast review <n> | off', 'Turns between automatic reviews, or turn them off'],
|
|
77
|
+
['/fast delegate on | off', 'Allow model:"fast" on task subagents'],
|
|
78
|
+
['--fast / --no-fast', 'Start a session with the tier on or off (also ARAGON_FAST=1)'],
|
|
79
|
+
['<fast_review>', 'An automated second opinion from the fast model - advice, not the user'],
|
|
80
|
+
];
|
|
70
81
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
71
82
|
export function helpRows(theme, caps) {
|
|
72
83
|
const glyphs = pickGlyphs(caps);
|
|
@@ -85,6 +96,9 @@ export function helpRows(theme, caps) {
|
|
|
85
96
|
rows.push(section('Team subagents'));
|
|
86
97
|
for (const [c, d] of TEAM_ROWS)
|
|
87
98
|
rows.push(pair(c, d));
|
|
99
|
+
rows.push(section('Fast model tier'));
|
|
100
|
+
for (const [c, d] of FAST_ROWS)
|
|
101
|
+
rows.push(pair(c, d));
|
|
88
102
|
rows.push(section('Skills'));
|
|
89
103
|
for (const [c, d] of SKILL_ROWS)
|
|
90
104
|
rows.push(pair(c, d));
|
|
@@ -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
|
}
|