@aragon-agent/cli 0.6.23 → 0.6.25
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 +1835 -1786
- package/README.md +2578 -2544
- package/dist/agent/controller.d.ts +60 -3
- package/dist/agent/controller.js +111 -6
- package/dist/agent/conversation-reset.d.ts +91 -0
- package/dist/agent/conversation-reset.js +66 -0
- package/dist/agent/model-probe-limits.d.ts +39 -0
- package/dist/agent/model-probe-limits.js +25 -0
- package/dist/agent/model-probe.d.ts +104 -0
- package/dist/agent/model-probe.js +391 -0
- package/dist/agent/model-profile-settings.js +29 -2
- package/dist/agent/reducer.js +45 -13
- package/dist/commands/builtins.js +101 -37
- package/dist/commands/clear-command.d.ts +27 -0
- package/dist/commands/clear-command.js +107 -0
- package/dist/commands/registry.d.ts +20 -1
- package/dist/compaction/child.d.ts +15 -0
- package/dist/compaction/child.js +14 -3
- package/dist/compaction/compactor.js +1 -0
- package/dist/compaction/context-command.js +10 -1
- package/dist/compaction/meter.d.ts +9 -0
- package/dist/compaction/meter.js +16 -1
- package/dist/compaction/pressure.d.ts +7 -0
- package/dist/compaction/pressure.js +5 -0
- package/dist/compaction/types.d.ts +11 -0
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +12 -0
- package/dist/config/load.js +24 -6
- package/dist/config/model-profile-resolution.d.ts +8 -0
- package/dist/config/model-profile-resolution.js +13 -6
- package/dist/config/model-profile-store.d.ts +4 -2
- package/dist/config/model-profiles.d.ts +21 -0
- package/dist/config/model-profiles.js +28 -2
- package/dist/config/schema.d.ts +61 -0
- package/dist/config/schema.js +36 -0
- package/dist/session/clear-archive.d.ts +51 -0
- package/dist/session/clear-archive.js +160 -0
- package/dist/team/model.d.ts +88 -0
- package/dist/team/model.js +136 -0
- package/dist/team/report.d.ts +11 -0
- package/dist/team/report.js +24 -1
- package/dist/team/subagent.js +37 -21
- package/dist/team/task-tool.d.ts +25 -6
- package/dist/team/task-tool.js +15 -9
- package/dist/todo/types.d.ts +5 -3
- package/dist/ui/App.js +44 -7
- package/dist/ui/entries/CompactionCard.d.ts +5 -4
- package/dist/ui/entries/CompactionCard.js +5 -4
- package/dist/ui/overlays/HelpOverlay.js +8 -2
- package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
- package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
- package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
- package/dist/ui/overlays/ModelProfileEditor.js +117 -54
- package/dist/ui/overlays/ModelProfilePicker.d.ts +14 -2
- package/dist/ui/overlays/ModelProfilePicker.js +51 -10
- package/dist/ui/overlays/QuestionOverlay.js +47 -6
- package/dist/ui/overlays/SettingsScreen.d.ts +17 -0
- package/dist/ui/overlays/SettingsScreen.js +152 -18
- package/dist/ui/overlays/model-probe-view.d.ts +128 -0
- package/dist/ui/overlays/model-probe-view.js +225 -0
- package/dist/ui/overlays/model-profile-state.d.ts +62 -5
- package/dist/ui/overlays/model-profile-state.js +121 -14
- package/dist/ui/overlays/use-model-probe.d.ts +30 -0
- package/dist/ui/overlays/use-model-probe.js +109 -0
- package/package.json +2 -2
|
@@ -38,6 +38,21 @@ export function QuestionOverlay({ isActive = true, questions, maxRows, cols, the
|
|
|
38
38
|
const [phase, setPhase] = useState('choosing');
|
|
39
39
|
const [cursors, setCursors] = useState(() => initialCursors(questions));
|
|
40
40
|
const [selections, setSelections] = useState(() => initialSelections(questions));
|
|
41
|
+
/**
|
|
42
|
+
* THE PER-QUESTION EXPLICIT-MARK BIT (V-1 / D-8).
|
|
43
|
+
*
|
|
44
|
+
* `initialSelections` PRE-SELECTS the recommended option on EVERY question
|
|
45
|
+
* (or the first option when there is none), so "the selection set is empty"
|
|
46
|
+
* is unreachable and can never be the test for "did the user choose?". The
|
|
47
|
+
* only reliable discriminator is the fact itself: the user pressed SPACE on
|
|
48
|
+
* this question. Without it, a user who only MOVES the cursor and presses
|
|
49
|
+
* Enter would submit the RECOMMENDED option instead of the highlighted one -
|
|
50
|
+
* a silent regression of the wizard's most common path. The bit survives
|
|
51
|
+
* Left/Right navigation exactly like `selections` and `cursors` do, and a
|
|
52
|
+
* single-choice mark is idempotent (no un-mark; Enter always has at least
|
|
53
|
+
* the marked item to submit).
|
|
54
|
+
*/
|
|
55
|
+
const [markedSteps, setMarkedSteps] = useState(() => questions.map(() => false));
|
|
41
56
|
const [customs, setCustoms] = useState(() => questions.map(() => ''));
|
|
42
57
|
const [draft, setDraft] = useState('');
|
|
43
58
|
const [offset, setOffset] = useState(0);
|
|
@@ -56,6 +71,7 @@ export function QuestionOverlay({ isActive = true, questions, maxRows, cols, the
|
|
|
56
71
|
return next;
|
|
57
72
|
}));
|
|
58
73
|
const chooseAt = (index, option) => setSelections((prev) => prev.map((set, i) => (i === index ? new Set([option]) : set)));
|
|
74
|
+
const markAt = (index) => setMarkedSteps((prev) => prev.map((marked, i) => (i === index ? true : marked)));
|
|
59
75
|
const buildAnswers = () => questions.map((q, i) => {
|
|
60
76
|
const chosen = [...(selections[i] ?? new Set())].sort((a, b) => a - b);
|
|
61
77
|
const labels = chosen.map((index) => q.options[index]?.label ?? '').filter((l) => l.length > 0);
|
|
@@ -159,8 +175,21 @@ export function QuestionOverlay({ isActive = true, questions, maxRows, cols, the
|
|
|
159
175
|
goBack();
|
|
160
176
|
return;
|
|
161
177
|
}
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
// SPACE, BY QUESTION SHAPE (D-8). The strict `input === ' '` keeps a
|
|
179
|
+
// multi-character paste frame from ever matching (R-8); the Other row is
|
|
180
|
+
// a no-op in both shapes because it needs TEXT, and Enter is its key.
|
|
181
|
+
if (input === ' ' && !question.options[cursor]?.isOther) {
|
|
182
|
+
if (question.allowMultiple) {
|
|
183
|
+
toggleAt(step, cursor);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// SINGLE CHOICE: mark the highlighted item and STAY - the terminal
|
|
187
|
+
// convention of "space selects, enter confirms". The mark is recorded
|
|
188
|
+
// both in the selection set (so `[x]` renders) and in the explicit
|
|
189
|
+
// mark bit (so a later Enter submits THIS item even after the cursor
|
|
190
|
+
// has moved on).
|
|
191
|
+
chooseAt(step, cursor);
|
|
192
|
+
markAt(step);
|
|
164
193
|
return;
|
|
165
194
|
}
|
|
166
195
|
if (key.return) {
|
|
@@ -170,8 +199,15 @@ export function QuestionOverlay({ isActive = true, questions, maxRows, cols, the
|
|
|
170
199
|
setPhase('typingOther');
|
|
171
200
|
return;
|
|
172
201
|
}
|
|
173
|
-
|
|
174
|
-
|
|
202
|
+
// A single-choice Enter submits the CURSOR item unless the user has
|
|
203
|
+
// explicitly space-marked this question, in which case the marked item
|
|
204
|
+
// wins even if the cursor has since moved (V-1's regression guard:
|
|
205
|
+
// cursor + Enter alone keeps today's behavior, recommended preselect
|
|
206
|
+
// and all).
|
|
207
|
+
if (!question.allowMultiple) {
|
|
208
|
+
if (!markedSteps[step])
|
|
209
|
+
chooseAt(step, cursor);
|
|
210
|
+
}
|
|
175
211
|
else if (!(selections[step] ?? new Set()).has(cursor))
|
|
176
212
|
toggleAt(step, cursor);
|
|
177
213
|
advance();
|
|
@@ -183,7 +219,9 @@ export function QuestionOverlay({ isActive = true, questions, maxRows, cols, the
|
|
|
183
219
|
? `type your answer ${glyphs.midDot} ${glyphs.enterKey} commit ${glyphs.midDot} ${glyphs.arrowLeft} back ${glyphs.midDot} esc cancel`
|
|
184
220
|
: phase === 'review'
|
|
185
221
|
? `${glyphs.enterKey} submit ${glyphs.midDot} ${glyphs.arrowLeft} back ${glyphs.midDot} esc cancel`
|
|
186
|
-
:
|
|
222
|
+
: question.options[cursor]?.isOther
|
|
223
|
+
? `${glyphs.arrowUp}${glyphs.arrowDown} choose ${glyphs.midDot} ${glyphs.enterKey} to type ${glyphs.midDot} ${glyphs.arrowLeft} back ${glyphs.midDot} esc cancel`
|
|
224
|
+
: `${glyphs.arrowUp}${glyphs.arrowDown} choose ${glyphs.midDot} space select ${glyphs.midDot} ${glyphs.enterKey} next ${glyphs.midDot} ${glyphs.arrowLeft} back ${glyphs.midDot} esc cancel`;
|
|
187
225
|
const rowTexts = [];
|
|
188
226
|
const optionStarts = [];
|
|
189
227
|
const push = (text) => { rowTexts.push(...wrapToRows(text, Math.max(1, cols - 4))); };
|
|
@@ -208,8 +246,11 @@ export function QuestionOverlay({ isActive = true, questions, maxRows, cols, the
|
|
|
208
246
|
setOffset(previous => focus < previous ? focus : focus >= previous + height
|
|
209
247
|
? focus - height + 1 : previous);
|
|
210
248
|
}, [focus, maxRows, cols, step, phase]);
|
|
249
|
+
// `space select` shows for BOTH shapes (AC-10) - 42 characters on the
|
|
250
|
+
// single-choice shape without `cancel`, a deliberate 5-column growth inside
|
|
251
|
+
// the `cols <= 48` budget this hint exists for (V-12).
|
|
211
252
|
const shortHint = phase === 'typingOther' ? 'Enter commit | Left back | Esc cancel'
|
|
212
253
|
: phase === 'review' ? 'Enter submit | Left back | Esc cancel'
|
|
213
|
-
:
|
|
254
|
+
: '^v choose Space select Enter next Esc';
|
|
214
255
|
return _jsx(OverlayFrame, { title: `${title}${position}`, hint: cols <= 48 ? shortHint : hint, maxRows: maxRows, cols: cols, theme: theme, caps: caps, rows: rowTexts.map((line, index) => _jsx(Text, { wrap: "truncate", children: line || ' ' }, index)), scrollOffset: offset, onScrollClamp: setOffset });
|
|
215
256
|
}
|
|
@@ -14,6 +14,7 @@ import { type LogLevelName } from '../../logging/levels.js';
|
|
|
14
14
|
import type { Theme } from '../theme.js';
|
|
15
15
|
import type { TermCapabilities } from '../capabilities.js';
|
|
16
16
|
import type { ModelSettingsDraft, ModelSettingsPatch, ModelSettingsSaveResult } from '../../config/model-profile-store.js';
|
|
17
|
+
import type { ModelProbe } from '../../agent/model-probe.js';
|
|
17
18
|
export interface SettingsValues {
|
|
18
19
|
provider: string;
|
|
19
20
|
model: string;
|
|
@@ -36,6 +37,15 @@ export interface SettingsValues {
|
|
|
36
37
|
*/
|
|
37
38
|
liveToolOutput: 'on' | 'off';
|
|
38
39
|
maxTokens: string;
|
|
40
|
+
/**
|
|
41
|
+
* The top-level context-window override, as text (profile-context-window
|
|
42
|
+
* §6.3). EDITED ONLY WHILE MAIN IS UNBOUND: once a profile is bound as Main
|
|
43
|
+
* the profile owns the denominator, this row hides, and the editor becomes
|
|
44
|
+
* the only surface that writes the key - two entries on one key can only
|
|
45
|
+
* drift. Empty renders as `auto` because an empty field IS AUTO here, the
|
|
46
|
+
* reading `maxTokens` established.
|
|
47
|
+
*/
|
|
48
|
+
contextWindow: string;
|
|
39
49
|
apiKey: string;
|
|
40
50
|
/**
|
|
41
51
|
* Only the LEVEL is editable here. The rest of `LogConfig` — and above all
|
|
@@ -104,6 +114,13 @@ interface SettingsScreenProps {
|
|
|
104
114
|
onProfileSave?: (draft: ModelSettingsDraft) => ModelSettingsSaveResult;
|
|
105
115
|
onClose?: () => void;
|
|
106
116
|
onReload?: () => ModelSettingsDraft;
|
|
117
|
+
/**
|
|
118
|
+
* The connection probe port (model-profile-connection-test 6.1). Absent in an
|
|
119
|
+
* embedded or test mount, where the two probe rows read `(unavailable)` - an
|
|
120
|
+
* EXPLICIT degradation this feature adds, not the silent no-op of the
|
|
121
|
+
* optional callbacks above.
|
|
122
|
+
*/
|
|
123
|
+
probe?: ModelProbe;
|
|
107
124
|
/**
|
|
108
125
|
* `API retries` READ-ONLY row (llm-api-retry-backoff §6.9), e.g. `10` or `off`.
|
|
109
126
|
*
|
|
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
12
12
|
import { Text, useInput } from 'ink';
|
|
13
13
|
import { DEFAULT_MAX_OUTPUT_TOKENS, resolveOutputTokens, } from '@aragon-agent/core';
|
|
14
|
-
import { ADAPTER_PROVIDERS, THINKING_LEVELS, maskSecret, parseMaxTokensInput, parseThresholdInput, } from '../../config/schema.js';
|
|
14
|
+
import { ADAPTER_PROVIDERS, THINKING_LEVELS, maskSecret, parseContextWindowInput, parseMaxTokensInput, parseThresholdInput, } from '../../config/schema.js';
|
|
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';
|
|
@@ -21,7 +21,26 @@ import { emptyModelProfiles } from '../../config/model-profiles.js';
|
|
|
21
21
|
import { registerSecret } from '../../logging/secret-registry.js';
|
|
22
22
|
import { ModelProfilePicker, useProfileScroll } from './ModelProfilePicker.js';
|
|
23
23
|
import { ModelProfileEditor } from './ModelProfileEditor.js';
|
|
24
|
-
import {
|
|
24
|
+
import { ModelDiscoveryList } from './ModelDiscoveryList.js';
|
|
25
|
+
import { useModelProbe } from './use-model-probe.js';
|
|
26
|
+
import { canProbe, editorProbeTarget, visibleProbeViews } from './model-probe-view.js';
|
|
27
|
+
import { createModelProfileState, createCustomProfile, modelProfileReducer, profileUsers, draftTeam, draftSubagentProfileId, subagentModelSummary, subagentSelectedId } from './model-profile-state.js';
|
|
28
|
+
/**
|
|
29
|
+
* THE ROOT PAGE'S ROW INDEXES, AS NAMES (subagent-model V-5).
|
|
30
|
+
*
|
|
31
|
+
* Five offset sites used to agree by accident on the number 3; the Subagent
|
|
32
|
+
* model row moves every one of them. Each site below now cites its constant,
|
|
33
|
+
* so adding a row later is a one-line change instead of a silent regression
|
|
34
|
+
* that focuses an error message on the wrong field.
|
|
35
|
+
*/
|
|
36
|
+
const ROOT_MAIN = 0;
|
|
37
|
+
const ROOT_FAST = 1;
|
|
38
|
+
const ROOT_SUBAGENT = 2;
|
|
39
|
+
const ROOT_MANAGE = 3;
|
|
40
|
+
const ROOT_FIRST_FIELD = 4;
|
|
41
|
+
/** Stand-in target while no editor exists; no probe row is rendered then. */
|
|
42
|
+
const NO_PROFILE_TARGET = { provider: '', model: '', baseUrl: null,
|
|
43
|
+
apiKey: null };
|
|
25
44
|
const SHOW_THINKING_OPTIONS = ['off', 'on'];
|
|
26
45
|
const LIVE_OUTPUT_OPTIONS = ['off', 'on'];
|
|
27
46
|
const FAST_ENABLED_OPTIONS = ['off', 'on'];
|
|
@@ -172,6 +191,11 @@ const FIELDS = [
|
|
|
172
191
|
// `.padEnd(18)` enforces below and that IF-H7 records as silently overflowing.
|
|
173
192
|
{ key: 'liveToolOutput', label: 'Live output', kind: 'enum', options: LIVE_OUTPUT_OPTIONS },
|
|
174
193
|
{ key: 'maxTokens', label: 'Max tokens', kind: 'text' },
|
|
194
|
+
// DIRECTLY UNDER `Max tokens`: the two token rows read together. `Context
|
|
195
|
+
// window` is 14 characters, inside the 18-column label budget `.padEnd(18)`
|
|
196
|
+
// enforces below (IF-H7). Only ever RENDERED while Main is unbound - see
|
|
197
|
+
// `SettingsValues.contextWindow` and the unbound-group filter below.
|
|
198
|
+
{ key: 'contextWindow', label: 'Context window', kind: 'text' },
|
|
175
199
|
// The fast tier sits AFTER `Max tokens` and BEFORE `API key`, because the key
|
|
176
200
|
// row is the one a user scrolls to deliberately and putting five rows past it
|
|
177
201
|
// would bury it (fast-model-tier §4.5). The rationale survives a fifth row;
|
|
@@ -348,6 +372,11 @@ function LegacySettingsScreen({ isActive = true, initial, apiKeys, maxRows, cols
|
|
|
348
372
|
// setting, which is the one thing this field can never be.
|
|
349
373
|
if (f.key === 'maxTokens' && raw.length === 0)
|
|
350
374
|
return 'auto';
|
|
375
|
+
// An empty window field is AUTO, not "unset" - the same reading, and the
|
|
376
|
+
// second of the TWO render points that must agree (P2-1: the profile
|
|
377
|
+
// screen's row fallback below is the other).
|
|
378
|
+
if (f.key === 'contextWindow' && raw.length === 0)
|
|
379
|
+
return 'auto';
|
|
351
380
|
// `(not set)` and NOT `(same as main)`, which is the honest reading of §3.2
|
|
352
381
|
// rule 3: an empty `fast.model` does not inherit, it means the tier is
|
|
353
382
|
// unconfigured. `(same as main)` is a fact the derived line below states
|
|
@@ -385,15 +414,23 @@ function LegacySettingsScreen({ isActive = true, initial, apiKeys, maxRows, cols
|
|
|
385
414
|
rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: "Enter saves. Keys persist to the config file (0600)." }, "__save"));
|
|
386
415
|
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: focusedOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
|
|
387
416
|
}
|
|
417
|
+
// `contextWindow` joins the exclusion list so a bound Main can never leak a
|
|
418
|
+
// SECOND copy of the row through PROFILE_FIELDS - the root row belongs to the
|
|
419
|
+
// unbound group below, exclusively (AC-11).
|
|
388
420
|
const PROFILE_FIELDS = FIELDS.filter((field) => ![
|
|
389
421
|
'provider', 'model', 'baseUrl', 'apiKey', 'fastProvider', 'fastModel', 'fastEnabled',
|
|
422
|
+
'contextWindow',
|
|
390
423
|
].includes(field.key));
|
|
391
424
|
function profileSettingsValues(initial, draft) {
|
|
392
425
|
const file = draft.baseline;
|
|
393
426
|
return { ...initial, provider: file.provider ?? initial.provider,
|
|
394
427
|
model: file.model ?? initial.model,
|
|
395
428
|
baseUrl: file.baseUrl ?? '', apiKey: '', fastModel: file.fast?.model ?? '',
|
|
396
|
-
fastProvider: file.fast?.provider || FAST_INHERIT_PROVIDER
|
|
429
|
+
fastProvider: file.fast?.provider || FAST_INHERIT_PROVIDER,
|
|
430
|
+
// SEEDED FROM THE FILE VALUE, like its provider/model/baseUrl siblings
|
|
431
|
+
// (AC-11 / P2-3): when env holds an override the file value is not the
|
|
432
|
+
// effective one, and this screen edits the FILE.
|
|
433
|
+
contextWindow: file.contextWindow == null ? 'auto' : String(file.contextWindow) };
|
|
397
434
|
}
|
|
398
435
|
/** Only fields changed by the user belong in a settings transaction. */
|
|
399
436
|
export function readDirtySettings(initial, values) {
|
|
@@ -422,6 +459,15 @@ export function readDirtySettings(initial, values) {
|
|
|
422
459
|
throw new Error('Max tokens: use a number or auto.');
|
|
423
460
|
patch.maxTokens = parsed.kind === 'auto' ? null : parsed.value;
|
|
424
461
|
}
|
|
462
|
+
if (dirty('contextWindow')) {
|
|
463
|
+
// TYPO CHANGES NOTHING: the message's `Context window:` prefix is what
|
|
464
|
+
// `saveProfileSettings`'s focus logic matches to jump to this row. The
|
|
465
|
+
// silent clamp on a value mirrors `config set contextWindow`.
|
|
466
|
+
const parsed = parseContextWindowInput(values.contextWindow);
|
|
467
|
+
if (parsed.kind === 'invalid')
|
|
468
|
+
throw new Error('Context window: use a number or auto.');
|
|
469
|
+
patch.contextWindow = parsed.kind === 'auto' ? null : parsed.value;
|
|
470
|
+
}
|
|
425
471
|
const fast = {};
|
|
426
472
|
const parsedFast = readFastSettings(values);
|
|
427
473
|
if (dirty('fastModel'))
|
|
@@ -487,6 +533,15 @@ function useProfileSettingsState(props) {
|
|
|
487
533
|
const [confirmIndex, setConfirmIndex] = useState(0);
|
|
488
534
|
const [reloadPending, setReloadPending] = useState(false);
|
|
489
535
|
const offset = useProfileScroll(props, index);
|
|
536
|
+
// THE PROBE LIVES HERE, NOT IN THE EDITOR (RV-P0-1): this component survives a
|
|
537
|
+
// page switch, the editor does not, and a hook inside the editor would abort its
|
|
538
|
+
// own discovery the moment `Discover models` opens the list page.
|
|
539
|
+
const probe = useModelProbe(props.probe);
|
|
540
|
+
const probeTarget = state.editor
|
|
541
|
+
? editorProbeTarget(state.editor, props.apiKeys) : undefined;
|
|
542
|
+
// TWO SLOTS: a verdict and a discovered list answer different questions, and
|
|
543
|
+
// opening the list must not erase a verdict the user still wants to read.
|
|
544
|
+
const probeViews = visibleProbeViews(probe.test, probe.discovery, probeTarget ?? NO_PROFILE_TARGET, props.probe);
|
|
490
545
|
const previousPage = useRef(state.page);
|
|
491
546
|
useEffect(() => {
|
|
492
547
|
const previous = previousPage.current;
|
|
@@ -501,17 +556,19 @@ function useProfileSettingsState(props) {
|
|
|
501
556
|
const dirty = state.dirty || Object.keys(values).some((key) => values[key] !== initial[key]);
|
|
502
557
|
const fields = [FIELDS.find((item) => item.key === 'fastEnabled'),
|
|
503
558
|
...FIELDS.filter((item) => library.mainId === null
|
|
504
|
-
&& ['provider', 'model', 'baseUrl', 'apiKey'].includes(item.key)),
|
|
559
|
+
&& ['provider', 'model', 'baseUrl', 'contextWindow', 'apiKey'].includes(item.key)),
|
|
505
560
|
...FIELDS.filter((item) => library.fastId === null
|
|
506
561
|
&& ['fastModel', 'fastProvider'].includes(item.key)), ...PROFILE_FIELDS];
|
|
507
|
-
|
|
508
|
-
|
|
562
|
+
// SIX FIXED ROWS now (Main, Fast, SUBAGENT, Manage, Save, Reload) around
|
|
563
|
+
// the `fields` block - see the ROOT_* constants for the five offset sites.
|
|
564
|
+
const count = fields.length + 6;
|
|
565
|
+
const field = fields[index - ROOT_FIRST_FIELD];
|
|
509
566
|
const readError = state.draft.readError;
|
|
510
567
|
const confirming = state.page === 'delete-confirm' || state.page === 'discard-confirm';
|
|
511
568
|
return { state, dispatch, initial, setInitial, values, setValues, index, setIndex,
|
|
512
569
|
error, setError, persisted, setPersisted, confirmIndex, setConfirmIndex,
|
|
513
570
|
reloadPending, setReloadPending, offset, library, glyphs, dirty, fields, count,
|
|
514
|
-
field, readError, confirming };
|
|
571
|
+
field, readError, confirming, probe, probeTarget, probeViews };
|
|
515
572
|
}
|
|
516
573
|
function saveProfileSettings(props, view) {
|
|
517
574
|
const { persisted, readError, initial, values, fields, state, setError, setIndex, setPersisted } = view;
|
|
@@ -526,7 +583,7 @@ function saveProfileSettings(props, view) {
|
|
|
526
583
|
setError(message);
|
|
527
584
|
const at = fields.findIndex((item) => message.startsWith(`${item.label}:`));
|
|
528
585
|
if (at >= 0)
|
|
529
|
-
setIndex(at +
|
|
586
|
+
setIndex(at + ROOT_FIRST_FIELD);
|
|
530
587
|
return;
|
|
531
588
|
}
|
|
532
589
|
const result = props.onProfileSave({
|
|
@@ -549,6 +606,7 @@ function reloadProfileSettings(props, view) {
|
|
|
549
606
|
showThinking: live.showThinking ? 'on' : 'off',
|
|
550
607
|
liveToolOutput: live.liveToolOutput ? 'on' : 'off',
|
|
551
608
|
maxTokens: live.maxTokens === undefined ? '' : String(live.maxTokens),
|
|
609
|
+
contextWindow: live.contextWindow == null ? 'auto' : String(live.contextWindow),
|
|
552
610
|
logLevel: live.log?.level ?? props.initial.logLevel,
|
|
553
611
|
...(live.fast ? fastSettingsFrom(live.fast) : {}),
|
|
554
612
|
...(live.compaction ? compactionSettingsFrom(live.compaction) : {}),
|
|
@@ -585,9 +643,15 @@ function activateRootSetting(props, view) {
|
|
|
585
643
|
}
|
|
586
644
|
if (readError || persisted)
|
|
587
645
|
return;
|
|
588
|
-
if (index
|
|
589
|
-
dispatch({ type: 'page', page: 'picker', role: index ===
|
|
590
|
-
|
|
646
|
+
if (index === ROOT_MAIN || index === ROOT_FAST) {
|
|
647
|
+
dispatch({ type: 'page', page: 'picker', role: index === ROOT_MAIN ? 'main' : 'fast' });
|
|
648
|
+
}
|
|
649
|
+
else if (index === ROOT_SUBAGENT) {
|
|
650
|
+
// The user-owned cost switch (D-5): its own picker mode, writing
|
|
651
|
+
// `draft.patch.team` rather than a `${role}Id` library key.
|
|
652
|
+
dispatch({ type: 'page', page: 'picker', role: 'subagent' });
|
|
653
|
+
}
|
|
654
|
+
else if (index === ROOT_MANAGE)
|
|
591
655
|
dispatch({ type: 'page', page: 'manager' });
|
|
592
656
|
else
|
|
593
657
|
saveProfileSettings(props, view);
|
|
@@ -669,16 +733,65 @@ function renderSettingsConfirmation(props, view) {
|
|
|
669
733
|
const { state, library, confirmIndex, glyphs } = view;
|
|
670
734
|
const deleting = state.page === 'delete-confirm';
|
|
671
735
|
const name = library.entries.find((entry) => entry.id === state.deleteId)?.name ?? '';
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
736
|
+
// UNBIND-AND-DELETE (D-6): a referenced entry stays deletable in ONE flow,
|
|
737
|
+
// and the button names what will happen.
|
|
738
|
+
const labels = deleting
|
|
739
|
+
? ['Cancel', state.deleteUsers ? 'Unbind and delete' : 'Delete']
|
|
740
|
+
: ['Continue', 'Discard'];
|
|
741
|
+
const rows = labels
|
|
742
|
+
.map((label, at) => _jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [confirmIndex === at ? `${glyphs.caret} ` : ' ', label] }, label));
|
|
743
|
+
// V-8: A RENDER-ONLY ROW, pushed AFTER the buttons. `confirmIndex` stays a
|
|
744
|
+
// 0..1 index over the two buttons and `scrollOffset` follows it, so this
|
|
745
|
+
// line must never sit between them or take an array slot of its own.
|
|
746
|
+
if (deleting && state.deleteUsers) {
|
|
747
|
+
rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: ["Used by ", state.deleteUsers, ". The binding(s) will be cleared."] }, "users"));
|
|
748
|
+
}
|
|
749
|
+
return _jsx(OverlayFrame, { ...props, title: deleting ? `Delete ${name}?` : 'Discard unsaved changes?', hint: "Enter choose | Esc continue", rows: rows, scrollOffset: confirmIndex });
|
|
750
|
+
}
|
|
751
|
+
/** Zero-network guard: no key (or no probe port) means no request at all. */
|
|
752
|
+
function startTest(view) {
|
|
753
|
+
if (view.probeTarget && canProbe(view.probeViews.test)) {
|
|
754
|
+
view.probe.startTest(view.probeTarget);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/** Discover from the draft, then show the list page: the state stays here. */
|
|
758
|
+
function startDiscovery(view, dispatch) {
|
|
759
|
+
if (!view.probeTarget || !canProbe(view.probeViews.test))
|
|
760
|
+
return;
|
|
761
|
+
view.probe.startDiscovery(view.probeTarget);
|
|
762
|
+
dispatch({ type: 'page', page: 'models' });
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Whether the fast tier resolves FOR DISPLAY while the subagent picker is
|
|
766
|
+
* open (D-5): the built-in `Fast tier model` row then annotates itself with
|
|
767
|
+
* `(fast tier off)`, so choosing it is never a surprise fallback. The same
|
|
768
|
+
* enabled + model + config-file-key approximation `fastTierLine` uses - and
|
|
769
|
+
* the same env-key blindness (V-10), accepted for consistency with it.
|
|
770
|
+
*/
|
|
771
|
+
function fastTierUnavailable(props, view) {
|
|
772
|
+
const { values } = view;
|
|
773
|
+
if (values.fastEnabled !== 'on')
|
|
774
|
+
return true;
|
|
775
|
+
if (values.fastModel.trim().length === 0)
|
|
776
|
+
return true;
|
|
777
|
+
const provider = values.fastProvider === FAST_INHERIT_PROVIDER
|
|
778
|
+
|| values.fastProvider.trim().length === 0 ? values.provider : values.fastProvider;
|
|
779
|
+
return (props.apiKeys[provider] ?? '').length === 0;
|
|
675
780
|
}
|
|
676
781
|
function renderProfileSubpage(props, view) {
|
|
677
782
|
const { state, library, dispatch, setConfirmIndex } = view;
|
|
678
783
|
if (state.page === 'editor' && state.editor)
|
|
679
|
-
return _jsx(ModelProfileEditor, { ...props, editor: state.editor, users: profileUsers(library, state.editor.original.id), error: state.error, dispatch: dispatch });
|
|
784
|
+
return _jsx(ModelProfileEditor, { ...props, editor: state.editor, users: profileUsers(library, state.editor.original.id, draftSubagentProfileId(state.draft)), error: state.error, dispatch: dispatch, probeView: view.probeViews.test, probeBusy: view.probe.busy, probeTarget: view.probeTarget, onTest: () => startTest(view), onDiscover: () => startDiscovery(view, dispatch), onCancelProbe: view.probe.cancel });
|
|
785
|
+
// A DISCOVERY IS A PAGE, NOT A POPUP: the list is where the answer belongs, and
|
|
786
|
+
// the probe state it renders keeps running because it lives one level up.
|
|
787
|
+
if (state.page === 'models' && state.editor)
|
|
788
|
+
return _jsx(ModelDiscoveryList, { ...props, provider: state.editor.provider, currentModel: state.editor.model, view: view.probeViews.discovery, busy: view.probe.discovery.phase === 'discovering', onChoose: (model) => dispatch({ type: 'choose-model', model }), onRetry: () => startDiscovery(view, dispatch), onBack: () => dispatch({ type: 'page', page: 'editor' }), onCancelProbe: view.probe.cancel });
|
|
789
|
+
// A NEW PROFILE SEEDED FROM THE MAIN CONNECTION while the subagent picker is
|
|
790
|
+
// open: 'subagent' is picker-only (6.2), so the seed role falls back to
|
|
791
|
+
// 'main' while `state.role` keeps driving the select that binds it.
|
|
792
|
+
const createRole = state.role === 'subagent' ? 'main' : state.role;
|
|
680
793
|
if (state.page === 'picker' || state.page === 'manager')
|
|
681
|
-
return _jsx(ModelProfilePicker, { ...props, profiles: library, savedProfiles: state.draft.baseline.modelProfiles, role: state.role, customProfile: createCustomProfile(state.draft,
|
|
794
|
+
return _jsx(ModelProfilePicker, { ...props, profiles: library, savedProfiles: state.draft.baseline.modelProfiles, role: state.role, customProfile: createCustomProfile(state.draft, createRole), manager: state.page === 'manager', error: state.error, selectedIdOverride: state.role === 'subagent' ? subagentSelectedId(state.draft) : undefined, subagentProfileId: draftSubagentProfileId(state.draft), fastTierOff: state.role === 'subagent' && fastTierUnavailable(props, view), onSelect: (id) => dispatch({ type: 'select', role: state.role, id }), onCreate: () => dispatch({ type: 'edit', profile: createCustomProfile(state.draft, createRole),
|
|
682
795
|
bindNew: state.page === 'picker' }), onEdit: (profile) => dispatch({ type: 'edit', profile }), onDuplicate: (id) => dispatch({ type: 'duplicate', id }), onDelete: (id) => { setConfirmIndex(0); dispatch({ type: 'delete', id }); }, onCancel: () => dispatch({ type: 'page', page: 'root' }) });
|
|
683
796
|
return null;
|
|
684
797
|
}
|
|
@@ -691,13 +804,34 @@ function buildRootSettingsRows(props, view) {
|
|
|
691
804
|
const missing = !entry.apiKey && !props.apiKeys[entry.provider];
|
|
692
805
|
return `${entry.name} ${entry.provider}:${entry.model}${missing ? ' [Missing API key]' : ''}`;
|
|
693
806
|
};
|
|
807
|
+
// THE SUBAGENT MODEL ROW (D-5), third on the page, summarized from the
|
|
808
|
+
// same draft/baseline sources every other derived row here reads. The fast
|
|
809
|
+
// tier check mirrors `fastTierLine`'s approximation (enabled + model + a
|
|
810
|
+
// config-file key) so the two derived lines can never disagree.
|
|
811
|
+
const team = draftTeam(state.draft);
|
|
812
|
+
const mainBound = library.entries.find((entry) => entry.id === library.mainId);
|
|
813
|
+
const mainConnection = mainBound
|
|
814
|
+
? `${mainBound.provider}:${mainBound.model}`
|
|
815
|
+
: `${values.provider}:${values.model}`;
|
|
816
|
+
const fastModel = values.fastModel.trim();
|
|
817
|
+
const fastProvider = values.fastProvider === FAST_INHERIT_PROVIDER
|
|
818
|
+
|| values.fastProvider.trim().length === 0 ? values.provider : values.fastProvider;
|
|
819
|
+
const fastConnection = values.fastEnabled === 'on' && fastModel.length > 0
|
|
820
|
+
&& (props.apiKeys[fastProvider] ?? '').length > 0
|
|
821
|
+
? `${fastProvider}:${fastModel}` : null;
|
|
822
|
+
const subagentLine = subagentModelSummary({ mode: team.model,
|
|
823
|
+
modelProfileId: team.modelProfileId, mainConnection, fastConnection,
|
|
824
|
+
profiles: state.draft.profiles,
|
|
825
|
+
hasKey: (id) => (props.apiKeys[id] ?? '').length > 0 });
|
|
694
826
|
const lines = [`Main profile ${name(library.mainId)}`,
|
|
695
827
|
`Fast profile ${name(library.fastId)}`,
|
|
828
|
+
`Subagent model ${subagentLine}`,
|
|
696
829
|
`Manage profiles ${library.entries.length} saved`,
|
|
697
830
|
...fields.map((item) => `${item.label.padEnd(18)}${item.key === 'apiKey'
|
|
698
831
|
? values.apiKey ? 'Configured (replacement)' : props.apiKeys[values.provider]
|
|
699
832
|
? 'Configured (type to replace)' : '(type to set)'
|
|
700
|
-
: values[item.key] || (item.key === 'maxTokens'
|
|
833
|
+
: values[item.key] || (item.key === 'maxTokens' || item.key === 'contextWindow'
|
|
834
|
+
? 'auto' : '(not set)')}`),
|
|
701
835
|
persisted || readError ? 'Save (disabled)' : 'Save settings', 'Reload settings'];
|
|
702
836
|
const rows = lines.map((line, at) => _jsxs(Text, { wrap: "truncate", color: at === index ? props.theme.accent : props.theme.muted, children: [at === index ? `${glyphs.caret} ` : ' ', line] }, at));
|
|
703
837
|
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: effectiveCapLine(values) }, "effective"));
|
|
@@ -720,7 +854,7 @@ function ProfileSettingsScreen(props) {
|
|
|
720
854
|
return subpage;
|
|
721
855
|
const { error, readError, dirty, persisted, index, offset } = view;
|
|
722
856
|
return _jsx(OverlayFrame, { ...props, rows: buildRootSettingsRows(props, view), scrollOffset: offset, title: error ?? (readError ? 'Cannot read config. Repair with aragon config edit; Reload.'
|
|
723
|
-
: dirty ? 'Settings | Unsaved changes' : 'Settings'), hint: persisted ? 'Esc close; restart to apply' : index <
|
|
857
|
+
: dirty ? 'Settings | Unsaved changes' : 'Settings'), hint: persisted ? 'Esc close; restart to apply' : index < ROOT_FIRST_FIELD
|
|
724
858
|
? 'Enter select/edit | Ctrl+S save | Esc cancel'
|
|
725
859
|
: 'Enter save/action | Ctrl+S save | Esc cancel' });
|
|
726
860
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The probe's VIEW MODEL (model-profile-connection-test §3.2 / §6.3).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only: the editor's row list, the result sentences, the candidate
|
|
5
|
+
* ordering and the two number formatters. Rendering and input both read
|
|
6
|
+
* `editorRows()`, so "which row am I on" has exactly one definition and inserting a
|
|
7
|
+
* row can no longer shift `apiKey` off index 6 by arithmetic.
|
|
8
|
+
*
|
|
9
|
+
* ASCII ONLY (`ui/` is inside `glyphs.test.ts::inScope`).
|
|
10
|
+
*/
|
|
11
|
+
import type { ProbeOutcome, ProbeTarget, ModelCandidate, DiscoveryOutcome, ModelProbe } from '../../agent/model-probe.js';
|
|
12
|
+
import type { ProfileEditorDraft } from './model-profile-state.js';
|
|
13
|
+
export declare const EDITOR_FIELDS: readonly ["name", "provider", "model", "contextWindow", "baseUrl", "credentialMode", "apiKey"];
|
|
14
|
+
export type EditorField = typeof EDITOR_FIELDS[number];
|
|
15
|
+
export declare const EDITOR_LABELS: string[];
|
|
16
|
+
export type EditorAction = 'retain' | 'test' | 'discover' | 'apply' | 'cancel';
|
|
17
|
+
export type EditorRow = {
|
|
18
|
+
kind: 'field';
|
|
19
|
+
field: EditorField;
|
|
20
|
+
label: string;
|
|
21
|
+
value: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'action';
|
|
24
|
+
action: EditorAction;
|
|
25
|
+
label: string;
|
|
26
|
+
value: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'note';
|
|
29
|
+
label: string;
|
|
30
|
+
muted: true;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'result';
|
|
33
|
+
label: string;
|
|
34
|
+
tone: 'ok' | 'fail' | 'muted';
|
|
35
|
+
};
|
|
36
|
+
/** What the current target's probe looks like to a renderer (editor surface). */
|
|
37
|
+
export type ProbeView = {
|
|
38
|
+
kind: 'idle';
|
|
39
|
+
reason: 'no_result' | 'no_key' | 'unavailable';
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'testing';
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'result';
|
|
44
|
+
outcome: ProbeOutcome;
|
|
45
|
+
};
|
|
46
|
+
/** The list page's own surface: a discovery is a page, not a result row. */
|
|
47
|
+
export type DiscoveryView = {
|
|
48
|
+
kind: 'idle';
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'discovering';
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'discovery';
|
|
53
|
+
outcome: DiscoveryOutcome;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The hook's two INDEPENDENT slots, declared here so this module stays React-free.
|
|
57
|
+
*
|
|
58
|
+
* They are separate on purpose: a test outcome and a discovered list describe the
|
|
59
|
+
* same connection but answer different questions, and a single slot would make
|
|
60
|
+
* "run a test, then open the list, then come back" lose the verdict (AC-27).
|
|
61
|
+
*/
|
|
62
|
+
export type ProbeTestPhase = {
|
|
63
|
+
phase: 'idle';
|
|
64
|
+
} | {
|
|
65
|
+
phase: 'testing';
|
|
66
|
+
targetId: string;
|
|
67
|
+
} | {
|
|
68
|
+
phase: 'tested';
|
|
69
|
+
outcome: ProbeOutcome;
|
|
70
|
+
};
|
|
71
|
+
export type ProbeDiscoveryPhase = {
|
|
72
|
+
phase: 'idle';
|
|
73
|
+
} | {
|
|
74
|
+
phase: 'discovering';
|
|
75
|
+
connectionId: string;
|
|
76
|
+
} | {
|
|
77
|
+
phase: 'discovered';
|
|
78
|
+
outcome: DiscoveryOutcome;
|
|
79
|
+
};
|
|
80
|
+
export interface ProbeViews {
|
|
81
|
+
test: ProbeView;
|
|
82
|
+
discovery: DiscoveryView;
|
|
83
|
+
}
|
|
84
|
+
/** Whether pressing a probe action can produce anything at all (AC-12 / AC-15). */
|
|
85
|
+
export declare function canProbe(view: ProbeView): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Filter both slots down to what the CURRENT target may honestly claim (§4.6).
|
|
88
|
+
*
|
|
89
|
+
* A `OK 812 ms` that describes a model the user has since edited is worse than no
|
|
90
|
+
* result at all, so a fingerprint mismatch degrades to `idle` - no effect, no
|
|
91
|
+
* reset call, no cross-page synchronisation.
|
|
92
|
+
*/
|
|
93
|
+
export declare function visibleProbeViews(test: ProbeTestPhase, discovery: ProbeDiscoveryPhase, target: ProbeTarget, probe?: ModelProbe): ProbeViews;
|
|
94
|
+
/** The exact target a draft would use - the resolved key included (§4.2.1). */
|
|
95
|
+
export declare function editorProbeTarget(editor: ProfileEditorDraft, apiKeys: Record<string, string | undefined>): ProbeTarget;
|
|
96
|
+
/** `812 ms` / `1.4 s` - one decimal only once a second has passed. */
|
|
97
|
+
export declare function latencyLabel(ms: number): string;
|
|
98
|
+
/** `128K` / `1M` / `auto` - `auto` when the vendor did not report a window. */
|
|
99
|
+
export declare function windowLabel(window?: number): string;
|
|
100
|
+
/**
|
|
101
|
+
* Clean and order a discovered list: one row per id, and the model the draft
|
|
102
|
+
* already names first, so "which one am I on" is answerable without scrolling.
|
|
103
|
+
*/
|
|
104
|
+
export declare function rankCandidates(models: readonly ModelCandidate[], currentModel: string): ModelCandidate[];
|
|
105
|
+
/** At most TWO rows: the verdict, then what to do about it (§3.1 / D-11). */
|
|
106
|
+
export declare function probeLines(view: ProbeView, target: ProbeTarget): string[];
|
|
107
|
+
/** Colour for the result rows; the status WORDS carry the meaning without colour. */
|
|
108
|
+
export declare function probeTone(view: ProbeView): 'ok' | 'fail' | 'muted';
|
|
109
|
+
export interface EditorRowsInput {
|
|
110
|
+
editor: ProfileEditorDraft;
|
|
111
|
+
retain: boolean;
|
|
112
|
+
probe: ProbeView;
|
|
113
|
+
target: ProbeTarget;
|
|
114
|
+
/** The `Window applies ...` honesty line for a Fast-only entry (§3.2 / RV-P1-7). */
|
|
115
|
+
fastOnly?: boolean;
|
|
116
|
+
}
|
|
117
|
+
/** THE single source of the editor's rows - fields, actions, notes AND results. */
|
|
118
|
+
export declare function editorRows(input: EditorRowsInput): EditorRow[];
|
|
119
|
+
/** Rows the cursor can land on; index arithmetic now derives from the row model. */
|
|
120
|
+
export declare function isFocusable(row: EditorRow): boolean;
|
|
121
|
+
/** The focusable rows, in order - the index space `useProfileScroll` follows. */
|
|
122
|
+
export declare function focusableRows(rows: readonly EditorRow[]): EditorRow[];
|
|
123
|
+
/** Result rows currently on screen, so the viewport can be widened to include them. */
|
|
124
|
+
export declare function resultRowCount(rows: readonly EditorRow[]): number;
|
|
125
|
+
/** Longest id shown in one list row, inside the 40-column budget. */
|
|
126
|
+
export declare const LIST_ID_CHARS = 28;
|
|
127
|
+
/** A model's one-line name, bounded by `maxNameChars`. */
|
|
128
|
+
export declare function clipName(name: string): string;
|