@aragon-agent/cli 0.6.33 → 0.6.35
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 +2305 -2242
- package/README.md +119 -18
- package/dist/agent/controller.d.ts +29 -0
- package/dist/agent/controller.js +42 -1
- package/dist/agent/kit-apply.d.ts +56 -0
- package/dist/agent/kit-apply.js +209 -0
- package/dist/agent/model-profile-settings.d.ts +28 -0
- package/dist/agent/model-profile-settings.js +142 -29
- package/dist/agent/reducer.d.ts +1 -1
- package/dist/commands/builtins.js +4 -0
- package/dist/commands/kit-command.d.ts +20 -0
- package/dist/commands/kit-command.js +131 -0
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +1 -1
- package/dist/config/kit-resolution.d.ts +93 -0
- package/dist/config/kit-resolution.js +226 -0
- package/dist/config/model-kits.d.ts +224 -0
- package/dist/config/model-kits.js +302 -0
- package/dist/config/model-profile-store.d.ts +25 -0
- package/dist/config/model-profiles.d.ts +41 -3
- package/dist/config/model-profiles.js +46 -5
- package/dist/config/schema.d.ts +16 -0
- package/dist/input/keymap.js +18 -4
- package/dist/ui/App.js +135 -15
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +8 -2
- package/dist/ui/PromptInput.js +1 -1
- package/dist/ui/ScrollbackShell.d.ts +38 -13
- package/dist/ui/ScrollbackShell.js +4 -3
- package/dist/ui/StatusBar.d.ts +13 -0
- package/dist/ui/StatusBar.js +10 -3
- package/dist/ui/TeamPanel.d.ts +12 -0
- package/dist/ui/TeamPanel.js +10 -31
- package/dist/ui/Transcript.js +22 -2
- package/dist/ui/entries/TeamCard.d.ts +18 -2
- package/dist/ui/entries/TeamCard.js +47 -13
- package/dist/ui/layout/header-hints.d.ts +11 -1
- package/dist/ui/layout/header-hints.js +35 -3
- package/dist/ui/layout/kit-chip.d.ts +50 -0
- package/dist/ui/layout/kit-chip.js +81 -0
- package/dist/ui/layout/virtual-window.js +9 -0
- package/dist/ui/overlays/HelpOverlay.js +7 -0
- package/dist/ui/overlays/KitEditor.d.ts +23 -0
- package/dist/ui/overlays/KitEditor.js +101 -0
- package/dist/ui/overlays/KitManager.d.ts +23 -0
- package/dist/ui/overlays/KitManager.js +104 -0
- package/dist/ui/overlays/KitPicker.d.ts +49 -0
- package/dist/ui/overlays/KitPicker.js +58 -0
- package/dist/ui/overlays/ModelProfilePicker.js +16 -6
- package/dist/ui/overlays/SettingsScreen.js +139 -16
- package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
- package/dist/ui/overlays/kit-picker-state.js +136 -0
- package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
- package/dist/ui/overlays/kit-settings-state.js +267 -0
- package/dist/ui/overlays/model-profile-state.d.ts +26 -3
- package/dist/ui/overlays/model-profile-state.js +59 -6
- package/dist/ui/scrollback/LiveTail.d.ts +44 -10
- package/dist/ui/scrollback/LiveTail.js +20 -4
- package/dist/ui/scrollback/limits.d.ts +16 -0
- package/dist/ui/scrollback/limits.js +16 -0
- package/dist/ui/scrollback/live-preview.js +10 -3
- package/dist/ui/scrollback/overflow-print.d.ts +108 -0
- package/dist/ui/scrollback/overflow-print.js +120 -0
- package/dist/ui/scrollback/renderer.d.ts +11 -1
- package/dist/ui/scrollback/renderer.js +53 -25
- package/dist/ui/scrollback/tail-anchor.d.ts +68 -0
- package/dist/ui/scrollback/tail-anchor.js +77 -0
- package/dist/ui/scrollback/tail-window.d.ts +21 -8
- package/dist/ui/scrollback/tail-window.js +26 -10
- package/dist/ui/scrollback/use-scrollback-view.d.ts +38 -10
- package/dist/ui/scrollback/use-scrollback-view.js +372 -85
- package/dist/ui/team-status.d.ts +141 -0
- package/dist/ui/team-status.js +224 -0
- package/dist/ui/transcript-text.js +13 -4
- package/dist/ui/use-wheel-routing.js +2 -2
- package/package.json +2 -2
|
@@ -4,7 +4,10 @@ import { mergePersistedConfig } from '../config/store.js';
|
|
|
4
4
|
import { adaptLegacyConnectionPatch, assertLegacyFastTarget, commitModelSettings, readModelSettingsDisk, } from '../config/model-profile-store.js';
|
|
5
5
|
import { projectModelProfiles, resolveModelProfileState } from '../config/model-profile-resolution.js';
|
|
6
6
|
import { validateModelProfiles } from '../config/model-profiles.js';
|
|
7
|
+
import { applyHeldToLive, carrySessionKit } from '../config/kit-resolution.js';
|
|
8
|
+
import { findKit } from '../config/model-kits.js';
|
|
7
9
|
import { registerProfileSecrets } from '../logging/secret-registry.js';
|
|
10
|
+
import { getLogger } from '../logging/logger.js';
|
|
8
11
|
/** Capture both baselines when the editor opens, without migrating or writing anything. */
|
|
9
12
|
export function createModelSettingsDraft(live, liveRevision) {
|
|
10
13
|
const disk = readModelSettingsDisk();
|
|
@@ -15,6 +18,11 @@ export function createModelSettingsDraft(live, liveRevision) {
|
|
|
15
18
|
profiles: disk.config?.modelProfiles
|
|
16
19
|
? structuredClone(disk.config.modelProfiles) : undefined,
|
|
17
20
|
activateRoles: [], patch: {},
|
|
21
|
+
// SEEDED FROM THE LIVE CONFIG (RV-P0-2). Without this line `draft.sessionKit`
|
|
22
|
+
// is always `undefined`, the `undefined` case below means "release", and any
|
|
23
|
+
// save made from the settings screen would silently drop a session kit the
|
|
24
|
+
// user just applied - the exact opposite of "this session only".
|
|
25
|
+
sessionKit: live.sessionKit ? structuredClone(live.sessionKit) : undefined,
|
|
18
26
|
...(!disk.ok ? { readError: 'Cannot read config. Repair it with aragon config edit.' } : {}),
|
|
19
27
|
};
|
|
20
28
|
}
|
|
@@ -61,6 +69,47 @@ function hasConnectionPatch(patch, role) {
|
|
|
61
69
|
const section = role === 'main' ? patch : patch.fast;
|
|
62
70
|
return !!section && ['provider', 'model', 'baseUrl'].some((key) => key in section);
|
|
63
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The slots THIS transaction explicitly decided, and that therefore leave the
|
|
74
|
+
* session kit (§4.3 / D-6).
|
|
75
|
+
*
|
|
76
|
+
* Released BY SLOT, never wholesale: a user who only retargeted the subagent
|
|
77
|
+
* must not lose the main and fast choices the kit made for this session, and a
|
|
78
|
+
* user who retargeted main must not have it silently ignored. Per-slot is the
|
|
79
|
+
* only rule shaped like the data (three independent slots).
|
|
80
|
+
*/
|
|
81
|
+
export function releasedSlots(draft) {
|
|
82
|
+
const held = draft.sessionKit?.held ?? [];
|
|
83
|
+
if (!held.length)
|
|
84
|
+
return [];
|
|
85
|
+
// THE APPLY ITSELF RELEASES NOTHING (`kit-apply.ts` sets this marker). Without
|
|
86
|
+
// it the apply's own writes - `activateRoles`, `patch.team`, the re-bound
|
|
87
|
+
// library - read as "the user decided every slot", and the save would drop the
|
|
88
|
+
// three slots it was just handed.
|
|
89
|
+
if (draft.sessionKitFresh)
|
|
90
|
+
return [];
|
|
91
|
+
const kit = findKit(draft.profiles ?? draft.baseline.modelProfiles, draft.sessionKit?.id);
|
|
92
|
+
if (!kit)
|
|
93
|
+
return [...held];
|
|
94
|
+
const released = new Set();
|
|
95
|
+
for (const role of ['main', 'fast']) {
|
|
96
|
+
if (draft.activateRoles.includes(role) || connectionChanged(draft, role)
|
|
97
|
+
|| hasConnectionPatch(draft.patch, role))
|
|
98
|
+
released.add(role);
|
|
99
|
+
}
|
|
100
|
+
if (draft.patch.team !== undefined)
|
|
101
|
+
released.add('subagent');
|
|
102
|
+
return [...released].filter((slot) => held.includes(slot));
|
|
103
|
+
}
|
|
104
|
+
/** The session kit as it stands AFTER this transaction's explicit decisions. */
|
|
105
|
+
function narrowedSessionKit(draft) {
|
|
106
|
+
const current = draft.sessionKit;
|
|
107
|
+
if (!current)
|
|
108
|
+
return null;
|
|
109
|
+
const released = releasedSlots(draft);
|
|
110
|
+
const held = current.held.filter((slot) => !released.includes(slot));
|
|
111
|
+
return held.length ? { id: current.id, held } : null;
|
|
112
|
+
}
|
|
64
113
|
function liveCandidate(live, candidate, draft) {
|
|
65
114
|
const patch = draft.patch;
|
|
66
115
|
const normalizedPatch = {};
|
|
@@ -128,8 +177,16 @@ function liveCandidate(live, candidate, draft) {
|
|
|
128
177
|
next.apiKeyOverrideTarget = undefined;
|
|
129
178
|
}
|
|
130
179
|
else {
|
|
131
|
-
|
|
132
|
-
|
|
180
|
+
// I-13 (RV-P1-7): `projectModelProfiles` projects fast ONLY when the
|
|
181
|
+
// library binds one. With `fastId: null` the projection's `fast` is just the
|
|
182
|
+
// file's untouched section (`projected` starts as `{...file}`), so copying it
|
|
183
|
+
// here would resurrect the disk's fast endpoint underneath a kit that
|
|
184
|
+
// deliberately turned the tier off - the live config would then say "off"
|
|
185
|
+
// while naming a model nobody chose.
|
|
186
|
+
if (projected.modelProfiles?.fastId) {
|
|
187
|
+
next.fast = { ...next.fast, provider: projected.fast.provider,
|
|
188
|
+
model: projected.fast.model, baseUrl: projected.fast.baseUrl };
|
|
189
|
+
}
|
|
133
190
|
}
|
|
134
191
|
}
|
|
135
192
|
if (next.apiKeyOverride && !next.apiKeyOverrideTarget
|
|
@@ -139,49 +196,100 @@ function liveCandidate(live, candidate, draft) {
|
|
|
139
196
|
next.modelProfileState = resolveModelProfileState(next);
|
|
140
197
|
return next;
|
|
141
198
|
}
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const issues = validateModelProfiles(draft.profiles ??
|
|
199
|
+
/**
|
|
200
|
+
* Build both candidates and the snapshot.
|
|
201
|
+
*
|
|
202
|
+
* TWO CANDIDATES, NAMED DIFFERENTLY ON PURPOSE (RV-P0-1 / I-14). `diskCandidate`
|
|
203
|
+
* is the commit side and never learns about a held slot; `liveCandidateInput`
|
|
204
|
+
* is what the session sees and is never committed. Collapsing them into one
|
|
205
|
+
* variable is the defect this split exists to prevent: one unrelated save would
|
|
206
|
+
* then write the session's kit into the user's permanent defaults, and nothing
|
|
207
|
+
* visible would change.
|
|
208
|
+
*/
|
|
209
|
+
function prepareModelSettings(input) {
|
|
210
|
+
const { draft, controller, current } = input;
|
|
211
|
+
const issues = validateModelProfiles(draft.profiles ?? current.modelProfiles);
|
|
155
212
|
if (issues.length)
|
|
156
|
-
return { ...rejected('invalid'), issues };
|
|
157
|
-
let snapshot;
|
|
158
|
-
let candidate;
|
|
213
|
+
return { failure: { ...rejected('invalid'), issues } };
|
|
159
214
|
try {
|
|
160
|
-
const current = disk.config ?? structuredClone(DEFAULT_CONFIG);
|
|
161
215
|
const profilesChanged = draft.activateRoles.length > 0
|
|
162
216
|
|| JSON.stringify(draft.profiles) !== JSON.stringify(draft.baseline.modelProfiles);
|
|
163
217
|
const implicitLegacy = draft.profiles === undefined || !profilesChanged;
|
|
164
218
|
const patch = adaptLegacyConnectionPatch({ current, patch: implicitLegacy
|
|
165
219
|
? draft.patch : { ...draft.patch, modelProfiles: draft.profiles } });
|
|
166
|
-
|
|
167
|
-
registerProfileSecrets(
|
|
168
|
-
const
|
|
220
|
+
const diskCandidate = mergePersistedConfig(current, patch);
|
|
221
|
+
registerProfileSecrets(diskCandidate);
|
|
222
|
+
const sessionKit = narrowedSessionKit(draft);
|
|
223
|
+
const applied = applyHeldToLive(diskCandidate, sessionKit);
|
|
224
|
+
if (applied.dropped.length) {
|
|
225
|
+
getLogger().warn('agent', 'session_kit_slots_dropped', { slots: applied.dropped });
|
|
226
|
+
}
|
|
227
|
+
const next = liveCandidate(controller.getConfig(), applied.candidate, draft);
|
|
228
|
+
next.sessionKit = sessionKit ?? undefined;
|
|
169
229
|
// Disk preferences cannot reveal startup/session gateways retained by the live main role.
|
|
170
230
|
if (implicitLegacy && current.modelProfiles?.fastId && hasConnectionPatch(draft.patch, 'fast')) {
|
|
171
231
|
assertLegacyFastTarget(next.fast, next);
|
|
172
232
|
}
|
|
173
|
-
snapshot = controller.prepareModelSettingsSnapshot(next);
|
|
233
|
+
const snapshot = controller.prepareModelSettingsSnapshot(next);
|
|
234
|
+
return { prepared: { diskCandidate, liveCandidateInput: applied.candidate, snapshot } };
|
|
174
235
|
}
|
|
175
236
|
catch (error) {
|
|
176
237
|
const code = error.code ?? 'invalid';
|
|
177
|
-
return rejected(code);
|
|
238
|
+
return { failure: rejected(code) };
|
|
178
239
|
}
|
|
179
|
-
|
|
240
|
+
}
|
|
241
|
+
/** Validate and prepare everything before the one synchronous, atomic disk commit. */
|
|
242
|
+
export function saveModelSettings(input) {
|
|
243
|
+
const { draft, controller } = input;
|
|
244
|
+
if (controller.isModelSettingsBusy())
|
|
245
|
+
return rejected('busy');
|
|
246
|
+
if (draft.liveRevision !== controller.getSettingsRevision())
|
|
247
|
+
return rejected('conflict');
|
|
248
|
+
const disk = readModelSettingsDisk();
|
|
249
|
+
if (!disk.ok || draft.readError)
|
|
250
|
+
return rejected('read_failed');
|
|
251
|
+
if (disk.revision !== draft.diskRevision)
|
|
252
|
+
return rejected('conflict');
|
|
253
|
+
const prepared = prepareModelSettings({ draft, controller,
|
|
254
|
+
current: disk.config ?? structuredClone(DEFAULT_CONFIG) });
|
|
255
|
+
if (!prepared.prepared)
|
|
256
|
+
return prepared.failure;
|
|
257
|
+
const commit = commitModelSettings({ candidate: prepared.prepared.diskCandidate,
|
|
258
|
+
diskRevision: draft.diskRevision });
|
|
180
259
|
if (!commit.ok)
|
|
181
260
|
return rejected(commit.code ?? 'write_failed');
|
|
182
|
-
return applyPreparedSnapshot(controller, snapshot, true);
|
|
261
|
+
return applyPreparedSnapshot(controller, prepared.prepared.snapshot, true);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The SAME prepare chain, without the commit: a session kit (§4.2 exit B).
|
|
265
|
+
*
|
|
266
|
+
* NO `diskRevision` GATE: nothing is written, so a save that landed elsewhere is
|
|
267
|
+
* not this operation's business. It still refuses while work is in flight
|
|
268
|
+
* (`busy`) and still refuses a draft whose disk could not be read, so "nothing
|
|
269
|
+
* changed" is guaranteed for every other failure too (I-10).
|
|
270
|
+
*/
|
|
271
|
+
export function previewModelSettings(draft, controller) {
|
|
272
|
+
if (controller.isModelSettingsBusy())
|
|
273
|
+
return rejected('busy');
|
|
274
|
+
if (draft.readError)
|
|
275
|
+
return rejected('read_failed');
|
|
276
|
+
const disk = readModelSettingsDisk();
|
|
277
|
+
if (!disk.ok)
|
|
278
|
+
return rejected('read_failed');
|
|
279
|
+
const prepared = prepareModelSettings({ draft, controller,
|
|
280
|
+
current: disk.config ?? structuredClone(DEFAULT_CONFIG) });
|
|
281
|
+
if (!prepared.prepared)
|
|
282
|
+
return prepared.failure;
|
|
283
|
+
return applyPreparedSnapshot(controller, prepared.prepared.snapshot, false);
|
|
183
284
|
}
|
|
184
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Apply a prepared snapshot.
|
|
287
|
+
*
|
|
288
|
+
* `persisted` HAS NO DEFAULT (RV-P1-4): the one attribute that distinguishes
|
|
289
|
+
* "the file now says this" from "only this process says this" is not something
|
|
290
|
+
* a call site should be able to forget.
|
|
291
|
+
*/
|
|
292
|
+
export function applyPreparedSnapshot(controller, snapshot, persisted) {
|
|
185
293
|
try {
|
|
186
294
|
controller.applyModelSettingsSnapshot(snapshot);
|
|
187
295
|
}
|
|
@@ -220,14 +328,19 @@ export function reloadModelSettings(controller) {
|
|
|
220
328
|
if (live.unicode !== undefined)
|
|
221
329
|
resolved.unicode = live.unicode;
|
|
222
330
|
resolved.reducedMotion ||= !live.color;
|
|
331
|
+
// THE SESSION KIT SURVIVES A RELOAD (D-6): the three connections it holds
|
|
332
|
+
// must come from the kit, not from the file we just re-read, and the
|
|
333
|
+
// projection has to be recomputed so the live config stays equivalent to
|
|
334
|
+
// what its own bindings say (I-3).
|
|
335
|
+
const carried = carrySessionKit({ resolved, previous: live });
|
|
223
336
|
const after = readModelSettingsDisk();
|
|
224
337
|
if (!after.ok)
|
|
225
338
|
return rejected('read_failed');
|
|
226
339
|
if (after.revision !== disk.revision)
|
|
227
340
|
return rejected('conflict');
|
|
228
|
-
if (
|
|
341
|
+
if (carried.modelProfileState?.main.invalid)
|
|
229
342
|
return rejected('invalid');
|
|
230
|
-
return applyPreparedSnapshot(controller, controller.prepareModelSettingsSnapshot(
|
|
343
|
+
return applyPreparedSnapshot(controller, controller.prepareModelSettingsSnapshot(carried), false);
|
|
231
344
|
}
|
|
232
345
|
catch {
|
|
233
346
|
return rejected('invalid');
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { RetryPhase } from './retry-view.js';
|
|
|
22
22
|
import type { FilePatch } from '../tools/patch.js';
|
|
23
23
|
export type NoticeLevel = 'info' | 'warn' | 'error';
|
|
24
24
|
export type ToastLevel = NoticeLevel | 'success';
|
|
25
|
-
export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan';
|
|
25
|
+
export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan' | 'kit';
|
|
26
26
|
export type ToolStatus = 'pending' | 'running' | 'done' | 'error';
|
|
27
27
|
/** Ephemeral, auto-dismissed acknowledgement (spec §3.9). */
|
|
28
28
|
export interface Toast {
|
|
@@ -23,6 +23,7 @@ import { getLogger } from '../logging/logger.js';
|
|
|
23
23
|
import { resolveFastTier } from '../fast/resolve.js';
|
|
24
24
|
import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
|
|
25
25
|
import { registerQueueCommand } from './queue-command.js';
|
|
26
|
+
import { registerKitCommand } from './kit-command.js';
|
|
26
27
|
import { perfCommand } from './perf.js';
|
|
27
28
|
import { runCompactCommand } from '../compaction/command.js';
|
|
28
29
|
import { runClearCommand } from './clear-command.js';
|
|
@@ -1371,6 +1372,9 @@ export function registerBuiltinCommands(registry) {
|
|
|
1371
1372
|
for (const command of COMMANDS)
|
|
1372
1373
|
registry.register(command);
|
|
1373
1374
|
registerQueueCommand(registry);
|
|
1375
|
+
// The command BODY lives in its own module (builtins.ts is already 1.5k lines);
|
|
1376
|
+
// this line is the whole registration (agent-model-kits §4.9).
|
|
1377
|
+
registerKitCommand(registry);
|
|
1374
1378
|
}
|
|
1375
1379
|
/**
|
|
1376
1380
|
* Every built-in command name INCLUDING aliases (`quit`), for documentation and
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/kit` - the slash surface for agent model kits (agent-model-kits §4.9).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* WHY A COMMAND AT ALL, WHEN `ctrl+K` EXISTS: a key that a terminal, tmux, an
|
|
7
|
+
* input method or a IME can swallow needs an equivalent that cannot be swallowed
|
|
8
|
+
* (`R-4`), and the same reading is what a user pastes into an issue. The two
|
|
9
|
+
* surfaces call the SAME controller methods, so they can never diverge in
|
|
10
|
+
* behaviour - only in how the answer is rendered.
|
|
11
|
+
*
|
|
12
|
+
* THERE IS NO `/kit default` (D-7): the session path is forbidden from writing
|
|
13
|
+
* the file, so a "make this the default" verb here would be the one hole in that
|
|
14
|
+
* promise. Setting a default is a settings action, with a Save the user sees.
|
|
15
|
+
*/
|
|
16
|
+
import type { CommandRegistry, CommandContext } from './registry.js';
|
|
17
|
+
/** The command body; `registerKitCommand` is the only registration site. */
|
|
18
|
+
export declare function runKitCommand(ctx: CommandContext): void;
|
|
19
|
+
/** Registered from `builtins.ts` with one line, next to every other subsystem. */
|
|
20
|
+
export declare function registerKitCommand(registry: CommandRegistry): void;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/kit` - the slash surface for agent model kits (agent-model-kits §4.9).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* WHY A COMMAND AT ALL, WHEN `ctrl+K` EXISTS: a key that a terminal, tmux, an
|
|
7
|
+
* input method or a IME can swallow needs an equivalent that cannot be swallowed
|
|
8
|
+
* (`R-4`), and the same reading is what a user pastes into an issue. The two
|
|
9
|
+
* surfaces call the SAME controller methods, so they can never diverge in
|
|
10
|
+
* behaviour - only in how the answer is rendered.
|
|
11
|
+
*
|
|
12
|
+
* THERE IS NO `/kit default` (D-7): the session path is forbidden from writing
|
|
13
|
+
* the file, so a "make this the default" verb here would be the one hole in that
|
|
14
|
+
* promise. Setting a default is a settings action, with a Save the user sees.
|
|
15
|
+
*/
|
|
16
|
+
import { kitMatches, findKit, kitSlotConnections } from '../config/model-kits.js';
|
|
17
|
+
import { kitApplyMessage, kitNoteMessages } from '../agent/kit-apply.js';
|
|
18
|
+
/** `main a:b / sub same as main / fast off`, shared by every branch below. */
|
|
19
|
+
function describeKit(config, kit) {
|
|
20
|
+
const connections = kitSlotConnections(config.modelProfiles, kit);
|
|
21
|
+
const main = connections.main
|
|
22
|
+
? `${connections.main.provider}:${connections.main.model}` : 'Profile missing';
|
|
23
|
+
const fast = kit.fastProfileId === null ? 'off'
|
|
24
|
+
: connections.fast ? `${connections.fast.provider}:${connections.fast.model}`
|
|
25
|
+
: 'Profile missing';
|
|
26
|
+
const subagent = kit.subagent.model === 'main' ? 'same as main'
|
|
27
|
+
: kit.subagent.model === 'fast' ? 'fast tier'
|
|
28
|
+
: connections.subagent.name ? `profile ${connections.subagent.name}` : 'Profile missing';
|
|
29
|
+
return `main ${main} / sub ${subagent} / fast ${fast}`;
|
|
30
|
+
}
|
|
31
|
+
function kitNames(config) {
|
|
32
|
+
return (config.modelProfiles?.kits ?? []).map((kit) => kit.name);
|
|
33
|
+
}
|
|
34
|
+
/** `/kit` with no argument: what is in effect, and what it resolves to. */
|
|
35
|
+
function reportActive(ctx) {
|
|
36
|
+
const config = ctx.controller.getConfig();
|
|
37
|
+
const active = ctx.controller.getActiveKit();
|
|
38
|
+
const kits = config.modelProfiles?.kits ?? [];
|
|
39
|
+
if (!kits.length) {
|
|
40
|
+
ctx.notify('info', 'No kits yet. Create one in /settings (Settings > Kit pack).');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const session = active.source === 'session'
|
|
44
|
+
? `session: ${active.name}${active.held.length < 3
|
|
45
|
+
? `, holds ${active.held.join('+')}` : ''}`
|
|
46
|
+
: active.source === 'default' ? 'default' : 'current setup';
|
|
47
|
+
const kit = findKit(config.modelProfiles, active.id);
|
|
48
|
+
const detail = kit ? ` ${describeKit(config, kit)}.` : '';
|
|
49
|
+
const drift = active.modified && active.driftReasons.length
|
|
50
|
+
? ` Drifted: ${active.driftReasons.join('; ')}.` : '';
|
|
51
|
+
ctx.notify('info', `Kit pack: ${active.name} (${session}).${detail}${drift}`);
|
|
52
|
+
}
|
|
53
|
+
/** `/kit list`: every kit, its markers and its three connections. */
|
|
54
|
+
function listKits(ctx) {
|
|
55
|
+
const config = ctx.controller.getConfig();
|
|
56
|
+
const kits = config.modelProfiles?.kits ?? [];
|
|
57
|
+
const active = ctx.controller.getActiveKit();
|
|
58
|
+
const fallback = config.modelProfiles?.defaultKitId ?? null;
|
|
59
|
+
if (!kits.length) {
|
|
60
|
+
ctx.notify('info', 'No kits yet. Create one in /settings.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const lines = kits.map((kit) => {
|
|
64
|
+
const markers = [kit.id === fallback ? '[Default]' : '',
|
|
65
|
+
active.source !== 'none' && active.id === kit.id ? '[In use]' : '',
|
|
66
|
+
active.source !== 'none' && active.id === kit.id && active.modified ? '~' : '']
|
|
67
|
+
.filter(Boolean).join(' ');
|
|
68
|
+
return `${markers ? `${markers} ` : ''}${kit.name}: ${describeKit(config, kit)}`;
|
|
69
|
+
});
|
|
70
|
+
ctx.notify('info', `Kits (${kits.length}):\n${lines.join('\n')}`);
|
|
71
|
+
}
|
|
72
|
+
/** `/kit use current` and `/kit use <name|#prefix>`. */
|
|
73
|
+
function useKit(ctx, ref) {
|
|
74
|
+
const config = ctx.controller.getConfig();
|
|
75
|
+
if (ref.toLowerCase() === 'current') {
|
|
76
|
+
const released = ctx.controller.clearSessionKit();
|
|
77
|
+
ctx.toast(released.ok ? 'success' : 'warn', kitApplyMessage(released));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const matches = kitMatches(config.modelProfiles, ref);
|
|
81
|
+
if (!matches.length) {
|
|
82
|
+
const names = kitNames(config);
|
|
83
|
+
ctx.notify('warn', `No kit named "${ref}".`
|
|
84
|
+
+ (names.length ? ` Available: ${names.join(', ')}.` : ' Create one in /settings first.'));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (matches.length > 1) {
|
|
88
|
+
const ids = matches.map((kit) => `#${kit.id.slice(0, 8)}`).join(', ');
|
|
89
|
+
ctx.notify('warn', `Two or more kits are named "${ref}": ${ids}. Use one of those ids.`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const kit = matches[0];
|
|
93
|
+
const result = ctx.controller.applySessionKit(kit.id);
|
|
94
|
+
if (!result.ok) {
|
|
95
|
+
// `kit_references_missing_profile` names the SLOT in its issues; without them
|
|
96
|
+
// the user would have to open three screens to find out which one is dead.
|
|
97
|
+
const detail = (result.issues ?? []).map((issue) => issue.message).join(' ');
|
|
98
|
+
ctx.notify('warn', [kitApplyMessage(result), detail].filter(Boolean).join(' '));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const notes = kitNoteMessages(result.notes);
|
|
102
|
+
ctx.toast(notes.length ? 'warn' : 'success', [`Kit ${kit.name} applied for this session.`, ...notes].join(' '));
|
|
103
|
+
}
|
|
104
|
+
/** The command body; `registerKitCommand` is the only registration site. */
|
|
105
|
+
export function runKitCommand(ctx) {
|
|
106
|
+
const args = ctx.args.trim();
|
|
107
|
+
if (!args) {
|
|
108
|
+
reportActive(ctx);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const [verb = '', ...rest] = args.split(/\s+/);
|
|
112
|
+
if (verb.toLowerCase() === 'list') {
|
|
113
|
+
listKits(ctx);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (verb.toLowerCase() === 'use') {
|
|
117
|
+
const ref = rest.join(' ').trim();
|
|
118
|
+
if (!ref) {
|
|
119
|
+
ctx.notify('warn', 'Which kit? Use /kit use <name|#id prefix>, or /kit use current.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
useKit(ctx, ref);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
ctx.notify('warn', 'Unknown /kit subcommand. Try /kit, /kit list, /kit use <name>.');
|
|
126
|
+
}
|
|
127
|
+
/** Registered from `builtins.ts` with one line, next to every other subsystem. */
|
|
128
|
+
export function registerKitCommand(registry) {
|
|
129
|
+
registry.register({ name: 'kit', description: 'Agent kits: status, list, use for this session',
|
|
130
|
+
run: (ctx) => { runKitCommand(ctx); } });
|
|
131
|
+
}
|
package/dist/commands/perf.d.ts
CHANGED
|
@@ -84,6 +84,16 @@ export interface BandPerf {
|
|
|
84
84
|
*/
|
|
85
85
|
windowStart: number;
|
|
86
86
|
windowEntries: number;
|
|
87
|
+
/**
|
|
88
|
+
* Rows the TOP GIVE-WAY has printed for entries that are still inside the window.
|
|
89
|
+
*
|
|
90
|
+
* `window=` says what the region holds; `head=` says how much of it the region could
|
|
91
|
+
* not show and therefore gave back to the terminal scrollback
|
|
92
|
+
* (tui-final-reply-truncated-unscrollable). Zero is the healthy answer for a
|
|
93
|
+
* conversation that always fits; a growing number means the box is smaller than the
|
|
94
|
+
* reply, which is exactly the state that used to be unrecoverable.
|
|
95
|
+
*/
|
|
96
|
+
headRows: number;
|
|
87
97
|
}
|
|
88
98
|
/**
|
|
89
99
|
* Why the right-edge scrollbar is or is not usable, in one record
|
package/dist/commands/perf.js
CHANGED
|
@@ -139,7 +139,7 @@ function formatBandLine(band) {
|
|
|
139
139
|
`tail=${band.previewRows} pool=${band.bandRows} ` +
|
|
140
140
|
`cols=${band.contentCols}/${band.previewCols} ` +
|
|
141
141
|
`reason=${band.reason} held=${band.heldRailRows}/${band.heldTeamRows} ` +
|
|
142
|
-
`window=${band.windowStart}/${band.windowEntries}`;
|
|
142
|
+
`window=${band.windowStart}/${band.windowEntries} head=${band.headRows}`;
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
145
|
* Format a snapshot as the seven-line block documented in §5.4 (plus the scrollbar
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the ACTIVE kit comes from, and the two places a session kit has to be
|
|
3
|
+
* re-applied (agent-model-kits §4.3 / D-5 / D-6).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/config/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* ONE TRUTH, PURELY DERIVED. `CliConfig` carries only `sessionKit: {id, held}`;
|
|
8
|
+
* everything a surface prints about "which kit is running" comes out of
|
|
9
|
+
* `resolveActiveKit(config)`. There is no `config.activeKit` field, because a
|
|
10
|
+
* second copy of that answer is a copy that can disagree with the first.
|
|
11
|
+
*/
|
|
12
|
+
import type { CliConfig, FastConfig, TeamConfig } from './schema.js';
|
|
13
|
+
import type { ModelProfilesConfig } from './model-profiles.js';
|
|
14
|
+
import { type KitSlot, type SessionKitState } from './model-kits.js';
|
|
15
|
+
/** The three slots, in the order every surface prints them. */
|
|
16
|
+
export declare const ALL_SLOTS: readonly KitSlot[];
|
|
17
|
+
/** The name the UI gives "no kit is in effect" (D-13). */
|
|
18
|
+
export declare const CURRENT_SETUP_NAME = "Current setup";
|
|
19
|
+
/**
|
|
20
|
+
* The answer when there is no kit, or when the host has no kit support at all.
|
|
21
|
+
*
|
|
22
|
+
* A CONSTANT SO CALLERS CANNOT INVENT A SECOND SHAPE: `App` needs one value that
|
|
23
|
+
* renders "no chip, `Current setup` row" for a controller that predates this
|
|
24
|
+
* port (a test double, an embedded host), and re-deriving it there would be a
|
|
25
|
+
* second definition of "nothing".
|
|
26
|
+
*/
|
|
27
|
+
export declare const INACTIVE_KIT: ActiveKit;
|
|
28
|
+
export interface ActiveKit {
|
|
29
|
+
source: 'session' | 'default' | 'none';
|
|
30
|
+
id: string | null;
|
|
31
|
+
/** `Current setup` when `source === 'none'`. */
|
|
32
|
+
name: string;
|
|
33
|
+
/** `[]` when `source === 'none'`; all three for a default kit. */
|
|
34
|
+
held: KitSlot[];
|
|
35
|
+
/** The kit's definition and the live connections no longer agree (§4.8). */
|
|
36
|
+
modified: boolean;
|
|
37
|
+
/** Human-readable ASCII reasons, for `/kit` and the root row. */
|
|
38
|
+
driftReasons: string[];
|
|
39
|
+
/** Slots whose referenced profile is gone (RV-P1-1 / I-8). */
|
|
40
|
+
missingProfiles: KitSlot[];
|
|
41
|
+
}
|
|
42
|
+
/** The session-kit runtime shape, owned by `model-kits.ts` and re-exported here. */
|
|
43
|
+
export type { SessionKitState } from './model-kits.js';
|
|
44
|
+
/**
|
|
45
|
+
* Which kit is in effect, and whether it has drifted (§4.8).
|
|
46
|
+
*
|
|
47
|
+
* Precedence is D-6's: session kit, then the persisted default, then the disk
|
|
48
|
+
* bindings (which the UI calls `Current setup`).
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveActiveKit(config: CliConfig): ActiveKit;
|
|
51
|
+
/**
|
|
52
|
+
* The minimum a candidate has to offer for the held slots to be re-applied.
|
|
53
|
+
*
|
|
54
|
+
* STRUCTURAL, NOT `PersistedConfig`: the same routine serves a disk candidate on
|
|
55
|
+
* its way to `liveCandidate` and a freshly reloaded `CliConfig`, and forcing
|
|
56
|
+
* either through a cast at the call site would hide the fact that only these
|
|
57
|
+
* three sections are ever written.
|
|
58
|
+
*/
|
|
59
|
+
export interface HeldTarget {
|
|
60
|
+
modelProfiles?: ModelProfilesConfig;
|
|
61
|
+
fast?: Partial<FastConfig>;
|
|
62
|
+
team?: Partial<TeamConfig>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Re-apply the still-`held` slots of a session kit onto a candidate.
|
|
66
|
+
*
|
|
67
|
+
* THE RETURN VALUE MAY ONLY EVER FEED `liveCandidate`. It must NEVER be handed
|
|
68
|
+
* to `commitModelSettings` (I-14 / RV-P0-1): "this session only" is a claim
|
|
69
|
+
* about the FILE, and a held slot that reaches the commit turns one unrelated
|
|
70
|
+
* save into a permanent, invisible change of the user's defaults. The one
|
|
71
|
+
* caller is `prepareModelSettings` in `agent/model-profile-settings.ts`, which
|
|
72
|
+
* keeps the two candidates in separate variables for exactly this reason.
|
|
73
|
+
*
|
|
74
|
+
* Slots whose kit, or whose referenced profile, disappeared are DROPPED and
|
|
75
|
+
* reported, so the caller can log the release instead of silently forgetting it.
|
|
76
|
+
*/
|
|
77
|
+
export declare function applyHeldToLive<T extends HeldTarget>(candidate: T, sessionKit: SessionKitState | null | undefined): {
|
|
78
|
+
candidate: T;
|
|
79
|
+
dropped: KitSlot[];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Keep a session kit alive across `Reload settings` (D-6).
|
|
83
|
+
*
|
|
84
|
+
* THE PROJECTION HAS TO BE RECOMPUTED, not just the bindings: `I-3` says a
|
|
85
|
+
* library's projections never depend on whether a kit exists, so a live config
|
|
86
|
+
* whose `mainId` says "kit" while `provider`/`model` still name the reloaded
|
|
87
|
+
* file would break the equivalence every other surface relies on. Hence the
|
|
88
|
+
* round trip through `projectModelProfiles` + `resolveModelProfileState`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function carrySessionKit(input: {
|
|
91
|
+
resolved: CliConfig;
|
|
92
|
+
previous: CliConfig;
|
|
93
|
+
}): CliConfig;
|