@aragon-agent/cli 0.6.23 → 0.6.24
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/README.md +20 -2
- package/dist/agent/controller.d.ts +11 -0
- package/dist/agent/controller.js +33 -3
- package/dist/agent/model-profile-settings.js +29 -2
- package/dist/commands/builtins.js +87 -3
- 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 +13 -0
- package/dist/config/model-profiles.js +18 -2
- package/dist/config/schema.d.ts +61 -0
- package/dist/config/schema.js +36 -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/ui/App.js +5 -0
- package/dist/ui/overlays/ModelProfileEditor.js +18 -4
- 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 +9 -0
- package/dist/ui/overlays/SettingsScreen.js +119 -17
- package/dist/ui/overlays/model-profile-state.d.ts +53 -4
- package/dist/ui/overlays/model-profile-state.js +111 -14
- package/package.json +2 -2
package/dist/team/subagent.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createBuiltinTools } from '../tools/index.js';
|
|
|
18
18
|
import { pickActivityArgs, sanitizeActivity } from './activity.js';
|
|
19
19
|
import { TEAM_LIMITS, TEAM_SUBAGENT_TOOL_NAMES } from './limits.js';
|
|
20
20
|
import { isRetryableStreamError } from './retry.js';
|
|
21
|
+
import { resolveSubagentKey, resolveSubagentModel } from './model.js';
|
|
21
22
|
import { makeTeamSend, makeTeamWait, withMailboxTail } from './comm-tools.js';
|
|
22
23
|
import { buildSubagentBlock } from './prompt.js';
|
|
23
24
|
/** The default factory: a real `Agent` from `@aragon-agent/core`. */
|
|
@@ -162,25 +163,34 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
162
163
|
let agentRef = null;
|
|
163
164
|
const getAgent = () => agentRef;
|
|
164
165
|
const tools = buildSubagentTools(spec, deps, getAgent);
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
166
|
+
// WHICH MODEL A CHILD RUNS IS A USER SETTING, RESOLVED FROM THE
|
|
167
|
+
// DISPATCH-TIME CONFIG through the one authority (subagent-model §3.4).
|
|
168
|
+
// `team.model: 'main'` - the default - is the historical main-agent parity
|
|
169
|
+
// exactly: same provider, model, base URL and thinking level as the lead.
|
|
170
|
+
// `'fast'` / `'profile'` are the user's explicit, visible, reversible cost
|
|
171
|
+
// switches, and EVERY failure mode (tier off, profile deleted, profile key
|
|
172
|
+
// missing) degrades back to the main connection with a disclosed reason
|
|
173
|
+
// rather than failing the dispatch (D-4). The MODEL itself still cannot
|
|
174
|
+
// choose: `normalizeSubagentSpecs` drops a stale `model` key (D-1).
|
|
170
175
|
//
|
|
171
176
|
// `maxTokens` inherits `deps.config.maxTokens` below - the same ambition the
|
|
172
|
-
// lead sends, clamped by
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
// lead sends, clamped by Core's `resolveOutputTokens` against the CHILD
|
|
178
|
+
// model's own ceiling, so an inherited large value degrades to the right
|
|
179
|
+
// number rather than to an HTTP 400.
|
|
180
|
+
const resolution = resolveSubagentModel(deps.config, (id, role) => Boolean(deps.getApiKey(id, role)));
|
|
181
|
+
const model = resolution.ref;
|
|
182
|
+
// THE CHILD'S OWN KEY CLOSURE, PROFILE-AWARE (§3.3 / I-4). A bound profile's
|
|
183
|
+
// dedicated key is visible ONLY inside this closure - never in env, config
|
|
184
|
+
// `apiKeys` or a log line - while every other provider resolves through the
|
|
185
|
+
// lead's resolver at `resolution.keyRole`, which is what keeps a fast child
|
|
186
|
+
// entitled to the fast-role key and a main child to the main one.
|
|
187
|
+
const childGetApiKey = (id) => resolveSubagentKey(deps.config, resolution, id, (providerId, role) => deps.getApiKey(providerId, role));
|
|
188
|
+
// The boolean twin, for the child's compaction summarizer (V-4): the child's
|
|
189
|
+
// own resolution first, the lead layer by role second. Forwarding only
|
|
190
|
+
// `deps.getApiKey` would hide a profile-dedicated key from `resolveSummarizer`
|
|
191
|
+
// and silently disable the child's compaction - the exact gap the req-level
|
|
192
|
+
// resolvers on `ChildContextManagerRequest` exist to close.
|
|
193
|
+
const childHasKey = (id, role) => Boolean(childGetApiKey(id) ?? deps.getApiKey(id, role));
|
|
184
194
|
// HOISTED TO A LOCAL, AND THAT IS WHAT MAKES W3 POSSIBLE AT ALL. This was built
|
|
185
195
|
// INLINE inside the `agentFactory({...})` argument, so there was nothing for the
|
|
186
196
|
// child manager's `getSystemPrompt` accessor to close over - and the accessor
|
|
@@ -216,7 +226,12 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
216
226
|
const contextManager = deps.contextManagerFor?.({
|
|
217
227
|
label: spec.label,
|
|
218
228
|
model,
|
|
219
|
-
|
|
229
|
+
// keyRole (V-4): a fast child's summarizer resolves its main choice with
|
|
230
|
+
// role='fast', so a fast-only key is visible to it; profile keys reach it
|
|
231
|
+
// through the req-level resolvers below instead.
|
|
232
|
+
role: resolution.keyRole,
|
|
233
|
+
getApiKey: childGetApiKey,
|
|
234
|
+
hasKey: childHasKey,
|
|
220
235
|
getMessages: () => agentRef?.state.messages ?? [],
|
|
221
236
|
getSystemPrompt: () => systemPrompt,
|
|
222
237
|
onCompacted: () => {
|
|
@@ -227,12 +242,13 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
227
242
|
systemPrompt,
|
|
228
243
|
model,
|
|
229
244
|
tools,
|
|
230
|
-
thinkingLevel:
|
|
245
|
+
thinkingLevel: resolution.thinkingLevel,
|
|
231
246
|
// The LEAD's registry instance, REUSED (P2-2). It holds adapter state, and
|
|
232
247
|
// constructing five of them per dispatch is pure waste.
|
|
233
248
|
providerRegistry: deps.providerRegistry,
|
|
234
|
-
// The
|
|
235
|
-
|
|
249
|
+
// The child's own profile-aware closure, so a settings-screen key edit
|
|
250
|
+
// reaches children too and a bound profile's key reaches ONLY them.
|
|
251
|
+
getApiKey: childGetApiKey,
|
|
236
252
|
...(deps.config.maxTokens !== undefined ? { maxTokens: deps.config.maxTokens } : {}),
|
|
237
253
|
timeouts: {
|
|
238
254
|
toolTimeout: deps.config.toolTimeoutMs,
|
package/dist/team/task-tool.d.ts
CHANGED
|
@@ -20,14 +20,17 @@
|
|
|
20
20
|
* in the schema would behave differently depending on whether `ajv`, an OPTIONAL
|
|
21
21
|
* dependency of core, happened to install.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
* model
|
|
25
|
-
*
|
|
26
|
-
* a delegation is never a downgrade
|
|
27
|
-
*
|
|
23
|
+
* WHICH MODEL A CHILD RUNS IS THE USER'S SETTING, NOT THE MODEL'S (D-1):
|
|
24
|
+
* `team.model` defaults to the lead's own connection (main-agent parity) and
|
|
25
|
+
* can name the fast tier or a profile; there is still no per-child `model`
|
|
26
|
+
* property, so a delegation is never a self-downgrade. Resolution lives in
|
|
27
|
+
* `team/model.ts` and is re-read from the LIVE config at every dispatch; the
|
|
28
|
+
* fast tier otherwise reaches children the way it reaches the lead
|
|
29
|
+
* (compaction summaries, per-child reviews).
|
|
28
30
|
*/
|
|
29
31
|
import { type AgentTool } from '@aragon-agent/core';
|
|
30
32
|
import type { ModelCost } from '@aragon-agent/core';
|
|
33
|
+
import type { SubagentModelResolution } from './model.js';
|
|
31
34
|
import type { TeamRuntime } from './runtime.js';
|
|
32
35
|
export interface TaskToolDeps {
|
|
33
36
|
runtime: TeamRuntime;
|
|
@@ -37,8 +40,24 @@ export interface TaskToolDeps {
|
|
|
37
40
|
/** Whether a usable key exists for the active provider right now (P2-3). */
|
|
38
41
|
hasApiKey: () => boolean;
|
|
39
42
|
activeProvider: () => string;
|
|
40
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* The cost table of the model children ACTUALLY run (subagent-model §3.4).
|
|
45
|
+
*
|
|
46
|
+
* The controller resolves `team.model` live and prices `modelCost` at the
|
|
47
|
+
* EFFECTIVE model's table: with the default `main` mode this is exactly the
|
|
48
|
+
* lead's own table, so the report's spend line is byte-identical to the
|
|
49
|
+
* parity behavior; with `fast` / `profile` it is that model's table, and an
|
|
50
|
+
* UNPRICED one yields `undefined` so the header says `pricing unknown`
|
|
51
|
+
* rather than fabricating `$0.00` (C-11 / RV-4).
|
|
52
|
+
*/
|
|
41
53
|
modelCost: () => ModelCost | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* The dispatch-time subagent model resolution, for the report's disclosure
|
|
56
|
+
* line (D-4): rendered ONLY when children did not run the main model, and
|
|
57
|
+
* carrying the fallback reason when they could not. OPTIONAL so existing
|
|
58
|
+
* construction sites compile; absent simply omits the line.
|
|
59
|
+
*/
|
|
60
|
+
subagentModel?: () => SubagentModelResolution;
|
|
42
61
|
/**
|
|
43
62
|
* The FAST tier's cost table, so the report's supervisor spend line states
|
|
44
63
|
* real spend (subagent-overseer-v2 D-7 / R-P1-2). OPTIONAL and returning
|
package/dist/team/task-tool.js
CHANGED
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
* in the schema would behave differently depending on whether `ajv`, an OPTIONAL
|
|
21
21
|
* dependency of core, happened to install.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
* model
|
|
25
|
-
*
|
|
26
|
-
* a delegation is never a downgrade
|
|
27
|
-
*
|
|
23
|
+
* WHICH MODEL A CHILD RUNS IS THE USER'S SETTING, NOT THE MODEL'S (D-1):
|
|
24
|
+
* `team.model` defaults to the lead's own connection (main-agent parity) and
|
|
25
|
+
* can name the fast tier or a profile; there is still no per-child `model`
|
|
26
|
+
* property, so a delegation is never a self-downgrade. Resolution lives in
|
|
27
|
+
* `team/model.ts` and is re-read from the LIVE config at every dispatch; the
|
|
28
|
+
* fast tier otherwise reaches children the way it reaches the lead
|
|
29
|
+
* (compaction summaries, per-child reviews).
|
|
28
30
|
*/
|
|
29
31
|
import { errorResult, textResult } from '@aragon-agent/core';
|
|
30
32
|
import { TEAM_LIMITS } from './limits.js';
|
|
@@ -39,8 +41,9 @@ const TEAM_OFF_REFUSAL = 'Team mode is off for this session. Do this work yourse
|
|
|
39
41
|
*/
|
|
40
42
|
const TASK_DESCRIPTION = 'Run several independent subagents in parallel and get one combined report. Each ' +
|
|
41
43
|
'subagent is a fresh agent with the same tools and working directory but NO memory of ' +
|
|
42
|
-
'this conversation - its prompt must be self-contained. Each runs on the
|
|
43
|
-
'
|
|
44
|
+
'this conversation - its prompt must be self-contained. Each runs on the model the ' +
|
|
45
|
+
'user configured for subagents (the main model by default). Use this when a task ' +
|
|
46
|
+
'splits ' +
|
|
44
47
|
'into 2 or more parts that do not depend on each other\'s output, especially reading or ' +
|
|
45
48
|
'searching several areas at once. Do NOT use it when one part needs another part\'s ' +
|
|
46
49
|
'result (do those yourself, in order), when two parts would edit the same file, or for ' +
|
|
@@ -120,13 +123,16 @@ export function createTaskTool(deps) {
|
|
|
120
123
|
// A cancellation or a partial result rendered as a tool FAILURE invites
|
|
121
124
|
// the model to retry the whole fan-out, which is the most expensive
|
|
122
125
|
// possible reaction (the same reasoning as `ask_user`'s cancelled
|
|
123
|
-
// shape, and D-14 one level up).
|
|
124
|
-
//
|
|
126
|
+
// shape, and D-14 one level up). `modelCost` is priced at the model
|
|
127
|
+
// children ACTUALLY ran - the lead's own table under the default
|
|
128
|
+
// `main` mode - so the total stays one honest price.
|
|
125
129
|
const cost = deps.modelCost();
|
|
126
130
|
const fastCost = deps.fastModelCost?.();
|
|
131
|
+
const subagentModel = deps.subagentModel?.();
|
|
127
132
|
return textResult(buildDispatchReport(outcome, {
|
|
128
133
|
...(cost ? { cost } : {}),
|
|
129
134
|
...(fastCost ? { fastCost } : {}),
|
|
135
|
+
...(subagentModel ? { subagentModel } : {}),
|
|
130
136
|
}));
|
|
131
137
|
},
|
|
132
138
|
};
|
package/dist/ui/App.js
CHANGED
|
@@ -2144,6 +2144,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2144
2144
|
// `auto`, not an empty string: an empty field reads as "unset", which
|
|
2145
2145
|
// is indistinguishable from AUTO and is not a state this can be in.
|
|
2146
2146
|
maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : 'auto',
|
|
2147
|
+
// BOTH HALVES, seeded AND read back (§6.3): the sibling comment above
|
|
2148
|
+
// records what omitting either one costs. `auto` mirrors the row's
|
|
2149
|
+
// empty-field reading; a bound Main profile hides the row entirely
|
|
2150
|
+
// and the profile editor seeds from the entry instead.
|
|
2151
|
+
contextWindow: cfg.contextWindow === null ? 'auto' : String(cfg.contextWindow),
|
|
2147
2152
|
apiKey: '',
|
|
2148
2153
|
logLevel: cfg.log.level,
|
|
2149
2154
|
// The four fast rows, seeded through the pure mapper so the screen and
|
|
@@ -9,8 +9,13 @@ import { stripEnterFrames } from '../enter-frames.js';
|
|
|
9
9
|
import { pickGlyphs } from '../glyphs.js';
|
|
10
10
|
import { profileEndpointSummary, useProfileScroll } from './ModelProfilePicker.js';
|
|
11
11
|
import { finishProfileEditor } from './model-profile-state.js';
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// `Context window` sits right after Model: the window is an attribute of the
|
|
13
|
+
// model+endpoint the two rows above name. 14 characters, inside the 18-column
|
|
14
|
+
// label budget `LABELS.map(...padEnd(18))` enforces below.
|
|
15
|
+
const FIELDS = ['name', 'provider', 'model', 'contextWindow', 'baseUrl', 'credentialMode',
|
|
16
|
+
'apiKey'];
|
|
17
|
+
const LABELS = ['Name', 'Provider', 'Model', 'Context window', 'Base URL', 'Credential mode',
|
|
18
|
+
'API key'];
|
|
14
19
|
function useEditorInput(props, navigation) {
|
|
15
20
|
const { editor, dispatch } = props;
|
|
16
21
|
const { index, setIndex, retain, count } = navigation;
|
|
@@ -83,8 +88,11 @@ function buildEditorRows(props, navigation) {
|
|
|
83
88
|
: editor.credentialMode === 'keep' ? 'Configured (unchanged)' : 'Shared / environment';
|
|
84
89
|
if (field === 'credentialMode')
|
|
85
90
|
return editor.credentialMode;
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
// `(auto)` EXPLICITLY, or an empty window field would fall through to
|
|
92
|
+
// `(required)` - and AUTO is this field's natural state, never a gap.
|
|
93
|
+
return editor[field] || (field === 'name' ? '(defaults to model)'
|
|
94
|
+
: field === 'contextWindow' ? '(auto)' : field === 'baseUrl'
|
|
95
|
+
? '(provider default)' : '(required)');
|
|
88
96
|
});
|
|
89
97
|
const lines = LABELS.map((label, at) => `${label.padEnd(18)}${values[at]}`);
|
|
90
98
|
if (retain)
|
|
@@ -92,6 +100,12 @@ function buildEditorRows(props, navigation) {
|
|
|
92
100
|
lines.push('Apply to settings draft', 'Cancel editing');
|
|
93
101
|
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));
|
|
94
102
|
rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: ["Target: ", editor.provider, " ", profileEndpointSummary(editor.baseUrl)] }, "target"));
|
|
103
|
+
// HONESTY FOR THE FAST-ONLY ENTRY (§6.5): a window saved on a profile that
|
|
104
|
+
// only ever serves the Fast role does not take effect today, and saying so
|
|
105
|
+
// beats letting the user believe it does.
|
|
106
|
+
if (props.users === 'Fast') {
|
|
107
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: "Window applies when this profile is bound as Main." }, "window-scope"));
|
|
108
|
+
}
|
|
95
109
|
return rows;
|
|
96
110
|
}
|
|
97
111
|
/** Replacement key input starts empty; retention across targets is explicit. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { ModelProfile, ModelProfilesConfig
|
|
2
|
+
import type { ModelProfile, ModelProfilesConfig } from '../../config/model-profiles.js';
|
|
3
3
|
import type { Theme } from '../theme.js';
|
|
4
4
|
import type { TermCapabilities } from '../capabilities.js';
|
|
5
|
+
import { type ProfilePickerRole } from './model-profile-state.js';
|
|
5
6
|
export interface ProfileFrameProps {
|
|
6
7
|
isActive?: boolean;
|
|
7
8
|
theme: Theme;
|
|
@@ -19,11 +20,22 @@ export declare function profileEndpointSummary(baseUrl: string | null): string;
|
|
|
19
20
|
interface ModelProfilePickerProps extends ProfileFrameProps {
|
|
20
21
|
profiles: ModelProfilesConfig;
|
|
21
22
|
savedProfiles?: ModelProfilesConfig;
|
|
22
|
-
role
|
|
23
|
+
/** Which role this page selects for; `'subagent'` swaps the built-in rows (D-5). */
|
|
24
|
+
role: ProfilePickerRole;
|
|
23
25
|
manager?: boolean;
|
|
24
26
|
error?: string;
|
|
25
27
|
apiKeys: Record<string, string | undefined>;
|
|
26
28
|
customProfile?: ModelProfile;
|
|
29
|
+
/**
|
|
30
|
+
* The selection id for a role the library has no `${role}Id` key for: the
|
|
31
|
+
* subagent projection (`Same as main` = null, fast = the sentinel, profile
|
|
32
|
+
* = its id). Takes precedence over the `${role}Id` lookup when present.
|
|
33
|
+
*/
|
|
34
|
+
selectedIdOverride?: string | null;
|
|
35
|
+
/** The subagent binding, for the reference suffix on profile labels (3.7). */
|
|
36
|
+
subagentProfileId?: string | null;
|
|
37
|
+
/** Whether to annotate the built-in `Fast tier model` row with `(fast tier off)`. */
|
|
38
|
+
fastTierOff?: boolean;
|
|
27
39
|
onSelect: (id: string | null) => void;
|
|
28
40
|
onCreate: () => void;
|
|
29
41
|
onEdit: (profile: ModelProfile) => void;
|
|
@@ -6,7 +6,7 @@ import { pickGlyphs } from '../glyphs.js';
|
|
|
6
6
|
import { OverlayFrame, overlayBodyRows } from '../layout/OverlayFrame.js';
|
|
7
7
|
import { stripPasteFrames } from '../paste-frames.js';
|
|
8
8
|
import { stripEnterFrames } from '../enter-frames.js';
|
|
9
|
-
import { profileUsers } from './model-profile-state.js';
|
|
9
|
+
import { profileUsers, SUBAGENT_FAST_ROW_ID } from './model-profile-state.js';
|
|
10
10
|
/** Move focus into view while accepting App's independent wheel/page scrolling. */
|
|
11
11
|
export function useProfileScroll(props, focus) {
|
|
12
12
|
const [offset, setOffset] = useState(0);
|
|
@@ -62,7 +62,9 @@ function usePickerState(props) {
|
|
|
62
62
|
const entries = props.profiles.entries.filter((entry) => `${entry.name} ${entry.provider} ${entry.model}`.toLocaleLowerCase()
|
|
63
63
|
.includes(query.toLocaleLowerCase()));
|
|
64
64
|
const actions = props.manager ? ['New', 'Edit', 'Duplicate', 'Delete'] : ['Create profile'];
|
|
65
|
-
|
|
65
|
+
// THE SUBAGENT PICKER swaps the `Current custom` row for TWO built-in rows:
|
|
66
|
+
// `Same as main` (selection id null) and `Fast tier model` (the sentinel).
|
|
67
|
+
const prefix = props.role === 'subagent' ? 2 : props.manager ? 0 : 1;
|
|
66
68
|
const count = entries.length + prefix + actions.length;
|
|
67
69
|
const cursor = Math.min(index, count - 1);
|
|
68
70
|
const focused = entries[cursor - prefix];
|
|
@@ -76,7 +78,18 @@ function usePickerState(props) {
|
|
|
76
78
|
function usePickerInput(props, state) {
|
|
77
79
|
const { cursor, focused, selected, actions, prefix, entries, count, searching, searchable, setIndex, setQuery, setSearching } = state;
|
|
78
80
|
function activate() {
|
|
79
|
-
if (
|
|
81
|
+
if (props.role === 'subagent') {
|
|
82
|
+
// Built-in rows first: `Same as main`, then `Fast tier model`.
|
|
83
|
+
if (cursor === 0) {
|
|
84
|
+
props.onSelect(null);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (cursor === 1) {
|
|
88
|
+
props.onSelect(SUBAGENT_FAST_ROW_ID);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (!props.manager && cursor === 0) {
|
|
80
93
|
props.onSelect(null);
|
|
81
94
|
return;
|
|
82
95
|
}
|
|
@@ -138,11 +151,26 @@ function usePickerInput(props, state) {
|
|
|
138
151
|
else if (!key.ctrl && !key.meta && input.toLowerCase() === 'e' && focused) {
|
|
139
152
|
props.onEdit(focused);
|
|
140
153
|
}
|
|
154
|
+
// `d` deletes the focused entry (D-7), symmetric with `e`: same guards,
|
|
155
|
+
// same no-ops on the built-in and action rows (`focused` is undefined
|
|
156
|
+
// there), and the `searching` early-return above keeps `d` a query
|
|
157
|
+
// character while typing a search.
|
|
158
|
+
else if (!key.ctrl && !key.meta && input.toLowerCase() === 'd' && focused) {
|
|
159
|
+
props.onDelete(focused.id);
|
|
160
|
+
}
|
|
141
161
|
});
|
|
142
162
|
}
|
|
163
|
+
/** The selection id this page marks with `[Selected]`, by role (6.2). */
|
|
164
|
+
function currentSelectionId(props) {
|
|
165
|
+
if (props.selectedIdOverride !== undefined)
|
|
166
|
+
return props.selectedIdOverride;
|
|
167
|
+
if (props.role === 'subagent')
|
|
168
|
+
return undefined;
|
|
169
|
+
return props.profiles[`${props.role}Id`];
|
|
170
|
+
}
|
|
143
171
|
function profilePickerLabel(props, entry) {
|
|
144
|
-
const saved = profileUsers(props.savedProfiles, entry.id);
|
|
145
|
-
const checked = props
|
|
172
|
+
const saved = profileUsers(props.savedProfiles, entry.id, props.subagentProfileId);
|
|
173
|
+
const checked = currentSelectionId(props) === entry.id ? '[Selected] ' : '';
|
|
146
174
|
const duplicate = props.profiles.entries.some((other) => other.id !== entry.id
|
|
147
175
|
&& other.name === entry.name);
|
|
148
176
|
const identity = duplicate ? `#${uniqueId(entry, props.profiles.entries)} ` : '';
|
|
@@ -154,18 +182,30 @@ function buildPickerRows(props, state) {
|
|
|
154
182
|
const { entries, actions, cursor, searching, searchable, query, focused, selected } = state;
|
|
155
183
|
const glyphs = pickGlyphs(props.caps);
|
|
156
184
|
const lines = [];
|
|
157
|
-
if (
|
|
185
|
+
if (props.role === 'subagent') {
|
|
186
|
+
// TWO BUILT-IN ROWS (D-5), marked with the same `[Selected]` prefix the
|
|
187
|
+
// profile rows use; the fast row annotates an unusable tier honestly.
|
|
188
|
+
const current = props.selectedIdOverride ?? null;
|
|
189
|
+
lines.push(`${current === null ? '[Selected] ' : ''}Same as main`);
|
|
190
|
+
lines.push(`${current === SUBAGENT_FAST_ROW_ID ? '[Selected] ' : ''}Fast tier model`
|
|
191
|
+
+ `${props.fastTierOff === true ? ' (fast tier off)' : ''}`);
|
|
192
|
+
}
|
|
193
|
+
else if (!props.manager)
|
|
158
194
|
lines.push('Current custom');
|
|
159
195
|
lines.push(...entries.map((entry) => profilePickerLabel(props, entry)));
|
|
160
196
|
lines.push(...actions);
|
|
161
197
|
const rows = lines.map((line, row) => _jsxs(Text, { wrap: "truncate", color: row === cursor ? props.theme.accent : props.theme.muted, children: [`${row === cursor && !searching ? glyphs.caret : ' '} `, clipProfileText(line, props.cols - 8)] }, row));
|
|
162
198
|
if (searchable)
|
|
163
199
|
rows.unshift(_jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [searching ? `${glyphs.caret} ` : ' ', "Search: ", query || '(/ to search)'] }, "search"));
|
|
164
|
-
|
|
200
|
+
// The subagent picker has no `Current custom` row to describe; its
|
|
201
|
+
// built-in rows already say everything there is to say.
|
|
202
|
+
const detail = props.role === 'subagent' ? focused
|
|
203
|
+
: !props.manager && cursor === 0 ? props.customProfile : focused ?? selected;
|
|
165
204
|
if (detail) {
|
|
166
205
|
rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: [detail.provider, ":", detail.model || '(not set)', " ", host(detail), detail === props.customProfile ? '' : ` #${detail.id}`] }, "detail"));
|
|
167
206
|
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: detail.apiKey ? 'Profile key: Configured'
|
|
168
207
|
: props.apiKeys[detail.provider] ? 'Shared / environment: Configured' : 'Missing API key' }, "key"));
|
|
208
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: `Window: ${detail.contextWindow ?? 'auto'}` }, "window"));
|
|
169
209
|
}
|
|
170
210
|
return rows;
|
|
171
211
|
}
|
|
@@ -174,7 +214,8 @@ export function ModelProfilePicker(props) {
|
|
|
174
214
|
const state = usePickerState(props);
|
|
175
215
|
usePickerInput(props, state);
|
|
176
216
|
return _jsx(OverlayFrame, { ...props, title: props.error ?? (props.manager ? 'Manage profiles'
|
|
177
|
-
:
|
|
178
|
-
|
|
179
|
-
|
|
217
|
+
: props.role === 'subagent' ? 'Subagent model'
|
|
218
|
+
: `${props.role === 'main' ? 'Main' : 'Fast'} profile`), rows: buildPickerRows(props, state), scrollOffset: state.searching ? 0 : state.offset, hint: state.searching ? 'Type search | Enter done | Esc back'
|
|
219
|
+
: props.manager ? 'Enter action | e edit | d delete | Esc back'
|
|
220
|
+
: 'Enter select | e edit | d delete | Esc back' });
|
|
180
221
|
}
|
|
@@ -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
|
}
|
|
@@ -36,6 +36,15 @@ export interface SettingsValues {
|
|
|
36
36
|
*/
|
|
37
37
|
liveToolOutput: 'on' | 'off';
|
|
38
38
|
maxTokens: string;
|
|
39
|
+
/**
|
|
40
|
+
* The top-level context-window override, as text (profile-context-window
|
|
41
|
+
* §6.3). EDITED ONLY WHILE MAIN IS UNBOUND: once a profile is bound as Main
|
|
42
|
+
* the profile owns the denominator, this row hides, and the editor becomes
|
|
43
|
+
* the only surface that writes the key - two entries on one key can only
|
|
44
|
+
* drift. Empty renders as `auto` because an empty field IS AUTO here, the
|
|
45
|
+
* reading `maxTokens` established.
|
|
46
|
+
*/
|
|
47
|
+
contextWindow: string;
|
|
39
48
|
apiKey: string;
|
|
40
49
|
/**
|
|
41
50
|
* Only the LEVEL is editable here. The rest of `LogConfig` — and above all
|