@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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ctrl+K` list, as a pure view model (agent-model-kits §4.6 / D-10).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* WHY THE ROWS ARE COMPUTED HERE AND NOT IN THE COMPONENT: three surfaces print
|
|
7
|
+
* the same three connections (this list, the settings manager, `/kit list`).
|
|
8
|
+
* One function produces them, so the wording of "the fast tier is off" cannot
|
|
9
|
+
* differ between the key the user pressed and the command they typed.
|
|
10
|
+
*/
|
|
11
|
+
import { kitSlotConnections } from '../../config/model-kits.js';
|
|
12
|
+
import { CURRENT_SETUP_NAME } from '../../config/kit-resolution.js';
|
|
13
|
+
function duplicateNames(kits) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
const repeated = new Set();
|
|
16
|
+
for (const kit of kits) {
|
|
17
|
+
if (seen.has(kit.name))
|
|
18
|
+
repeated.add(kit.name);
|
|
19
|
+
seen.add(kit.name);
|
|
20
|
+
}
|
|
21
|
+
return repeated;
|
|
22
|
+
}
|
|
23
|
+
/** `provider:model` plus the endpoint clause shared by every surface. */
|
|
24
|
+
export function connectionText(connection) {
|
|
25
|
+
const endpoint = connection.baseUrl ? hostOf(connection.baseUrl) : 'default URL';
|
|
26
|
+
return `${connection.provider}:${connection.model} ${endpoint}`;
|
|
27
|
+
}
|
|
28
|
+
function hostOf(baseUrl) {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(baseUrl);
|
|
31
|
+
return url.username || url.password || url.search || url.hash ? 'Invalid URL' : url.host;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return 'Invalid URL';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Rows in a FIXED order: `Current setup` first, then the library's own order.
|
|
39
|
+
*
|
|
40
|
+
* The first row never moves (AC-24) - it is the only row that can always be
|
|
41
|
+
* chosen, and a list whose "off" entry wanders with the data is a list a user
|
|
42
|
+
* has to read every single time.
|
|
43
|
+
*/
|
|
44
|
+
export function kitRows(input) {
|
|
45
|
+
const kits = input.library?.kits ?? [];
|
|
46
|
+
const repeated = duplicateNames(kits);
|
|
47
|
+
const rows = [{
|
|
48
|
+
id: null,
|
|
49
|
+
label: `${input.active.source === 'none' ? '[In use] ' : ''}${CURRENT_SETUP_NAME}`,
|
|
50
|
+
markers: { inUse: input.active.source === 'none', isDefault: false, modified: false },
|
|
51
|
+
missing: [],
|
|
52
|
+
}];
|
|
53
|
+
for (const kit of kits) {
|
|
54
|
+
const inUse = input.active.source !== 'none' && input.active.id === kit.id;
|
|
55
|
+
const isDefault = (input.library?.defaultKitId ?? null) === kit.id;
|
|
56
|
+
const modified = inUse && input.active.modified;
|
|
57
|
+
const missing = inUse ? input.active.missingProfiles : [];
|
|
58
|
+
// A DUPLICATE NAME gets its id prefix, exactly as the profile picker does:
|
|
59
|
+
// two rows reading `Cheap` would make Enter a coin flip.
|
|
60
|
+
const identity = repeated.has(kit.name) ? `#${kit.id.slice(0, 8)} ` : '';
|
|
61
|
+
const markers = [inUse ? '[In use]' : '', isDefault ? '[Default]' : '']
|
|
62
|
+
.filter(Boolean).join(' ');
|
|
63
|
+
rows.push({
|
|
64
|
+
id: kit.id,
|
|
65
|
+
label: `${identity}${markers ? `${markers} ` : ''}${kit.name}${modified ? '~' : ''}`,
|
|
66
|
+
markers: { inUse, isDefault, modified },
|
|
67
|
+
missing,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return rows;
|
|
71
|
+
}
|
|
72
|
+
function subagentText(config) {
|
|
73
|
+
const mode = config.team?.model ?? 'main';
|
|
74
|
+
if (mode === 'main')
|
|
75
|
+
return 'same as main';
|
|
76
|
+
if (mode === 'fast')
|
|
77
|
+
return 'fast tier model';
|
|
78
|
+
const id = config.team?.modelProfileId ?? null;
|
|
79
|
+
const entry = id ? config.modelProfiles?.entries.find((item) => item.id === id) : undefined;
|
|
80
|
+
return entry ? `profile ${entry.name}` : 'Profile missing';
|
|
81
|
+
}
|
|
82
|
+
/** The three lines under the focused row (§4.6). */
|
|
83
|
+
export function kitDetail(input) {
|
|
84
|
+
const { row, library, config } = input;
|
|
85
|
+
const kit = row.id ? (library?.kits ?? []).find((item) => item.id === row.id) : undefined;
|
|
86
|
+
if (!kit) {
|
|
87
|
+
// `Current setup` describes the FILE's bindings - what the next process
|
|
88
|
+
// would run - because that is exactly what choosing it restores.
|
|
89
|
+
//
|
|
90
|
+
// THE LIVE CONFIG IS THE WRONG SOURCE WHILE A SESSION KIT IS HELD: it mirrors
|
|
91
|
+
// the kit (I-6), so this block would print the current values and thereby
|
|
92
|
+
// promise that releasing the kit changes nothing. The first row is the only
|
|
93
|
+
// row whose whole meaning is "what the file says", so it is the only one that
|
|
94
|
+
// has to be read from the file.
|
|
95
|
+
const file = input.diskConfig ?? config;
|
|
96
|
+
const fast = file.fast.enabled
|
|
97
|
+
? `${file.fast.provider || file.provider}:${file.fast.model || '(not set)'}`
|
|
98
|
+
: 'off';
|
|
99
|
+
return [`main ${file.provider}:${file.model}`,
|
|
100
|
+
`subagent ${subagentText(file)}`,
|
|
101
|
+
`fast ${fast}`];
|
|
102
|
+
}
|
|
103
|
+
const connections = kitSlotConnections(library, kit);
|
|
104
|
+
const mainLine = connections.main ? connectionText(connections.main) : 'Profile missing';
|
|
105
|
+
const fastLine = kit.fastProfileId === null ? 'off (this kit turns the tier off)'
|
|
106
|
+
: connections.fast ? `${connectionText(connections.fast)} tier on` : 'Profile missing';
|
|
107
|
+
// THE SUBAGENT LINE REUSES THE SLOT'S OWN THREE-STATE VOCABULARY (`same as main`
|
|
108
|
+
// / `fast tier` / a profile), so this block and the settings screen's subagent
|
|
109
|
+
// row cannot say different things about the same kit.
|
|
110
|
+
const subagentLine = kit.subagent.model === 'main' ? 'same as main'
|
|
111
|
+
: kit.subagent.model === 'fast'
|
|
112
|
+
? (connections.fast ? 'fast tier' : 'fast tier (off - falls back to main)')
|
|
113
|
+
: connections.subagent.name
|
|
114
|
+
? `profile ${connections.subagent.name} ${connections.subagent.provider}:${connections.subagent.model}` : 'Profile missing';
|
|
115
|
+
return [`main ${mainLine}`, `subagent ${subagentLine}`, `fast ${fastLine}`];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The footer. `busy` is a warning, never a reason to hide the list (§4.7).
|
|
119
|
+
*
|
|
120
|
+
* THE LAST CLAUSE IS D-7, VERBATIM: this overlay is a session-only surface, so it
|
|
121
|
+
* says where the persistence actually lives. Without it the natural reading of
|
|
122
|
+
* `Enter` is "make this my default", and D-7 exists to keep that action one
|
|
123
|
+
* deliberate trip to `/settings` instead of one keystroke next to a browse key.
|
|
124
|
+
* It is LAST so that a 40-column terminal clips the pointer, never the action.
|
|
125
|
+
*/
|
|
126
|
+
export function kitPickHint(input) {
|
|
127
|
+
if (!input.hasKits)
|
|
128
|
+
return 'Esc close | Create a kit in /settings';
|
|
129
|
+
if (input.busy) {
|
|
130
|
+
return 'Enter use for this session (blocked while running) | Esc close'
|
|
131
|
+
+ ' | Set a default in /settings';
|
|
132
|
+
}
|
|
133
|
+
return 'Enter use for this session | Esc close | Set a default in /settings';
|
|
134
|
+
}
|
|
135
|
+
/** The one guidance row an empty library renders (AC-26). */
|
|
136
|
+
export const NO_KITS_HINT = 'No kits yet. Create one in /settings (Settings > Kit pack).';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings screen's kit pages: manager rows, the editor draft, and the ONE
|
|
3
|
+
* reducer that turns intents into draft mutations (agent-model-kits §4.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* TWO REDUCERS, ONE PAGE AXIS. `modelProfileReducer` owns the profile pages and
|
|
8
|
+
* `kitReducer` owns kit data; both return the same `ModelProfileState`, so the
|
|
9
|
+
* screen still has exactly one state object and one Save button. The alternative
|
|
10
|
+
* - a second state machine inside `SettingsScreen.tsx`, which is already over the
|
|
11
|
+
* 1000-line cap - is what this module exists to avoid.
|
|
12
|
+
*
|
|
13
|
+
* NOTHING HERE WRITES ANYTHING: every action mutates the transaction draft and
|
|
14
|
+
* sets `dirty`. `Save` stays the only writer (F-7/F-10).
|
|
15
|
+
*/
|
|
16
|
+
import type { KitSlot, AgentKit } from '../../config/model-kits.js';
|
|
17
|
+
import type { ModelProfilesConfig } from '../../config/model-profiles.js';
|
|
18
|
+
import type { SubagentModelMode } from '../../team/model.js';
|
|
19
|
+
import type { ModelProfileState } from './model-profile-state.js';
|
|
20
|
+
export interface KitDraft {
|
|
21
|
+
/** `null` while creating; the delete confirmation reuses this field as its id. */
|
|
22
|
+
id: string | null;
|
|
23
|
+
name: string;
|
|
24
|
+
mainProfileId: string;
|
|
25
|
+
/** `null` = this kit turns the fast tier off (D-8). */
|
|
26
|
+
fastProfileId: string | null;
|
|
27
|
+
subagent: {
|
|
28
|
+
model: SubagentModelMode;
|
|
29
|
+
modelProfileId: string | null;
|
|
30
|
+
};
|
|
31
|
+
/** Which slot the profile picker is choosing for; `null` on the editor page. */
|
|
32
|
+
slot: KitSlot | null;
|
|
33
|
+
error?: string;
|
|
34
|
+
/** The row the cursor jumps to when `error` is set (focus-lands-on-error). */
|
|
35
|
+
field?: KitSlot | 'name';
|
|
36
|
+
}
|
|
37
|
+
export type KitAction = {
|
|
38
|
+
type: 'kit-open';
|
|
39
|
+
id: string | null;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'kit-name';
|
|
42
|
+
value: string;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'kit-slot';
|
|
45
|
+
slot: KitSlot;
|
|
46
|
+
} | {
|
|
47
|
+
type: 'kit-main';
|
|
48
|
+
id: string | null;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'kit-fast';
|
|
51
|
+
id: string | null;
|
|
52
|
+
} | {
|
|
53
|
+
type: 'kit-subagent';
|
|
54
|
+
model: SubagentModelMode;
|
|
55
|
+
modelProfileId: string | null;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'kit-save';
|
|
58
|
+
} | {
|
|
59
|
+
type: 'kit-cancel';
|
|
60
|
+
} | {
|
|
61
|
+
type: 'kit-duplicate';
|
|
62
|
+
id: string;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'kit-delete';
|
|
65
|
+
id: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'kit-confirm-delete';
|
|
68
|
+
} | {
|
|
69
|
+
type: 'kit-apply';
|
|
70
|
+
id: string;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'kit-default';
|
|
73
|
+
id: string;
|
|
74
|
+
};
|
|
75
|
+
/** The library the transaction would save, with a read error falling back to empty. */
|
|
76
|
+
export declare function draftLibrary(state: ModelProfileState): ModelProfilesConfig;
|
|
77
|
+
export declare function findKitInState(state: ModelProfileState, id: string | null): AgentKit | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Open the editor on an existing kit, or on a PREFILLED new one (§4.10).
|
|
80
|
+
*
|
|
81
|
+
* THE FIRST KIT IS NEVER A BLANK FORM (D-13): it starts from the three bindings
|
|
82
|
+
* this machine already runs, so "create a kit" is one rename and one Save. When
|
|
83
|
+
* no Main profile is bound the kit starts with an empty main slot and Save
|
|
84
|
+
* refuses - the picker's own `Create profile` action is where a profile is born,
|
|
85
|
+
* which keeps this module free of the profile-creating code path.
|
|
86
|
+
*/
|
|
87
|
+
export declare function openKitDraft(state: ModelProfileState, id: string | null): KitDraft;
|
|
88
|
+
/** Every kit-page intent, applied to the ONE draft. */
|
|
89
|
+
export declare function kitReducer(state: ModelProfileState, action: KitAction): ModelProfileState;
|
|
90
|
+
/** One manager row: the label with its markers, plus the three-slot summary. */
|
|
91
|
+
export interface KitManagerRow {
|
|
92
|
+
id: string;
|
|
93
|
+
label: string;
|
|
94
|
+
summary: string;
|
|
95
|
+
isDefault: boolean;
|
|
96
|
+
inUse: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** Rows in library order, each with the markers the picker also uses (§4.5). */
|
|
99
|
+
export declare function kitManagerRows(state: ModelProfileState): KitManagerRow[];
|
|
100
|
+
/**
|
|
101
|
+
* The `Kit pack` root row's five states (§4.5 table, AC-27).
|
|
102
|
+
*
|
|
103
|
+
* READ FROM THE DRAFT, not from the live config: the row sits inside a
|
|
104
|
+
* transaction, and showing the saved state under a pending selection is how a
|
|
105
|
+
* settings screen lies to the user about what `Save` is going to do.
|
|
106
|
+
*/
|
|
107
|
+
export declare function kitPackRow(state: ModelProfileState): string;
|
|
108
|
+
/** Which of the three root rows must show the KIT's value, and under which name. */
|
|
109
|
+
export interface HeldSlots {
|
|
110
|
+
kitName: string | null;
|
|
111
|
+
/** The slots the session kit still owns; `mainId`/`fastId` are only meaningful
|
|
112
|
+
* for a slot in this list (`null` means "this kit turns the tier off"). */
|
|
113
|
+
held: KitSlot[];
|
|
114
|
+
mainId: string | null;
|
|
115
|
+
fastId: string | null;
|
|
116
|
+
subagent: {
|
|
117
|
+
model: SubagentModelMode;
|
|
118
|
+
modelProfileId: string | null;
|
|
119
|
+
} | null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The held slots' EFFECTIVE values (RV-P0-2).
|
|
123
|
+
*
|
|
124
|
+
* WITHOUT THIS the root page shows the file's bindings while the header shows the
|
|
125
|
+
* kit's, and the only conclusion available to the user is that one of them is
|
|
126
|
+
* broken. `Enter` on any of these rows writes an explicit choice, which releases
|
|
127
|
+
* that slot - so "what you see is what you change" stays true.
|
|
128
|
+
*/
|
|
129
|
+
export declare function heldSlots(state: ModelProfileState): HeldSlots;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings screen's kit pages: manager rows, the editor draft, and the ONE
|
|
3
|
+
* reducer that turns intents into draft mutations (agent-model-kits §4.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* TWO REDUCERS, ONE PAGE AXIS. `modelProfileReducer` owns the profile pages and
|
|
8
|
+
* `kitReducer` owns kit data; both return the same `ModelProfileState`, so the
|
|
9
|
+
* screen still has exactly one state object and one Save button. The alternative
|
|
10
|
+
* - a second state machine inside `SettingsScreen.tsx`, which is already over the
|
|
11
|
+
* 1000-line cap - is what this module exists to avoid.
|
|
12
|
+
*
|
|
13
|
+
* NOTHING HERE WRITES ANYTHING: every action mutates the transaction draft and
|
|
14
|
+
* sets `dirty`. `Save` stays the only writer (F-7/F-10).
|
|
15
|
+
*/
|
|
16
|
+
import { KIT_LIMITS, createAgentKit, duplicateAgentKit, findKit, kitCandidate, kitSlotConnections } from '../../config/model-kits.js';
|
|
17
|
+
import { resolveActiveKit } from '../../config/kit-resolution.js';
|
|
18
|
+
import { emptyModelProfiles } from '../../config/model-profiles.js';
|
|
19
|
+
/** The library the transaction would save, with a read error falling back to empty. */
|
|
20
|
+
export function draftLibrary(state) {
|
|
21
|
+
return state.draft.profiles ?? emptyModelProfiles();
|
|
22
|
+
}
|
|
23
|
+
function kitsOf(state) { return draftLibrary(state).kits ?? []; }
|
|
24
|
+
export function findKitInState(state, id) {
|
|
25
|
+
return id ? findKit(draftLibrary(state), id) : undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Open the editor on an existing kit, or on a PREFILLED new one (§4.10).
|
|
29
|
+
*
|
|
30
|
+
* THE FIRST KIT IS NEVER A BLANK FORM (D-13): it starts from the three bindings
|
|
31
|
+
* this machine already runs, so "create a kit" is one rename and one Save. When
|
|
32
|
+
* no Main profile is bound the kit starts with an empty main slot and Save
|
|
33
|
+
* refuses - the picker's own `Create profile` action is where a profile is born,
|
|
34
|
+
* which keeps this module free of the profile-creating code path.
|
|
35
|
+
*/
|
|
36
|
+
export function openKitDraft(state, id) {
|
|
37
|
+
const existing = findKitInState(state, id);
|
|
38
|
+
if (existing) {
|
|
39
|
+
return { id: existing.id, name: existing.name, mainProfileId: existing.mainProfileId,
|
|
40
|
+
fastProfileId: existing.fastProfileId,
|
|
41
|
+
subagent: { ...existing.subagent }, slot: null };
|
|
42
|
+
}
|
|
43
|
+
const library = draftLibrary(state);
|
|
44
|
+
const team = state.draft.baseline.team;
|
|
45
|
+
return {
|
|
46
|
+
id: null,
|
|
47
|
+
name: `Kit ${kitsOf(state).length + 1}`,
|
|
48
|
+
mainProfileId: library.mainId ?? '',
|
|
49
|
+
// `fastId` alone is not enough: a tier that is switched OFF is off, and
|
|
50
|
+
// prefilling its endpoint would silently turn it back on at the first Save.
|
|
51
|
+
fastProfileId: state.draft.baseline.fast.enabled ? library.fastId ?? null : null,
|
|
52
|
+
subagent: { model: team.model, modelProfileId: team.modelProfileId ?? null },
|
|
53
|
+
slot: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function withDraft(state, kitDraft) {
|
|
57
|
+
return { ...state, kitDraft, error: undefined };
|
|
58
|
+
}
|
|
59
|
+
function saveKitDraft(state) {
|
|
60
|
+
const kitDraft = state.kitDraft;
|
|
61
|
+
if (!kitDraft)
|
|
62
|
+
return state;
|
|
63
|
+
const name = [...kitDraft.name.trim()];
|
|
64
|
+
if (!name.length) {
|
|
65
|
+
return withDraft(state, { ...kitDraft, error: 'Name: enter a name for this kit.',
|
|
66
|
+
field: 'name' });
|
|
67
|
+
}
|
|
68
|
+
if (!kitDraft.mainProfileId) {
|
|
69
|
+
return withDraft(state, { ...kitDraft, error: 'Main profile: choose a profile.',
|
|
70
|
+
field: 'main' });
|
|
71
|
+
}
|
|
72
|
+
if (kitDraft.subagent.model === 'profile' && !kitDraft.subagent.modelProfileId) {
|
|
73
|
+
return withDraft(state, { ...kitDraft, error: 'Subagent: choose a profile.',
|
|
74
|
+
field: 'subagent' });
|
|
75
|
+
}
|
|
76
|
+
const library = draftLibrary(state);
|
|
77
|
+
const kits = library.kits ?? [];
|
|
78
|
+
const fields = { name: name.slice(0, KIT_LIMITS.nameMaxChars).join(''),
|
|
79
|
+
mainProfileId: kitDraft.mainProfileId, fastProfileId: kitDraft.fastProfileId,
|
|
80
|
+
subagent: { ...kitDraft.subagent } };
|
|
81
|
+
let merged;
|
|
82
|
+
if (kitDraft.id) {
|
|
83
|
+
const existing = kits.find((kit) => kit.id === kitDraft.id);
|
|
84
|
+
if (!existing)
|
|
85
|
+
return { ...state, page: 'kits', kitDraft: undefined };
|
|
86
|
+
merged = kits.map((kit) => (kit.id === kitDraft.id ? { ...existing, ...fields } : kit));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (kits.length >= KIT_LIMITS.maxKits) {
|
|
90
|
+
return withDraft(state, { ...kitDraft,
|
|
91
|
+
error: `Maximum ${KIT_LIMITS.maxKits} kits. Delete an unused kit first.` });
|
|
92
|
+
}
|
|
93
|
+
merged = [...kits, createAgentKit(fields)];
|
|
94
|
+
}
|
|
95
|
+
return { ...state, page: 'kits', kitDraft: undefined, dirty: true,
|
|
96
|
+
draft: { ...state.draft, profiles: { ...library, kits: merged } } };
|
|
97
|
+
}
|
|
98
|
+
/** Exit A: the kit's three slots become the draft's bindings (§4.2). */
|
|
99
|
+
function applyKitToDraft(state, id, asDefault) {
|
|
100
|
+
const library = draftLibrary(state);
|
|
101
|
+
const kit = findKit(library, id);
|
|
102
|
+
if (!kit)
|
|
103
|
+
return state;
|
|
104
|
+
const patch = kitCandidate({ library, kit, ...(asDefault ? { defaultKitId: kit.id } : {}) });
|
|
105
|
+
return { ...state, dirty: true, page: 'kits', kitDraft: undefined,
|
|
106
|
+
draft: { ...state.draft,
|
|
107
|
+
profiles: patch.modelProfiles,
|
|
108
|
+
patch: { ...state.draft.patch, team: patch.team,
|
|
109
|
+
fast: { ...state.draft.patch.fast, ...patch.fast } },
|
|
110
|
+
// I-13: a kit that turns the tier off must NOT project the fast role.
|
|
111
|
+
activateRoles: [...new Set([...state.draft.activateRoles,
|
|
112
|
+
'main', ...(kit.fastProfileId === null ? [] : ['fast'])])],
|
|
113
|
+
// The transaction now decides all three slots, so the session kit is done
|
|
114
|
+
// (§4.3 row 4). Leaving it held would make the save ambiguous.
|
|
115
|
+
sessionKit: null } };
|
|
116
|
+
}
|
|
117
|
+
function removeKit(state, id) {
|
|
118
|
+
const library = draftLibrary(state);
|
|
119
|
+
const remaining = (library.kits ?? []).filter((kit) => kit.id !== id);
|
|
120
|
+
const released = state.draft.sessionKit?.id === id ? null : state.draft.sessionKit;
|
|
121
|
+
return { ...state, page: 'kits', kitDraft: undefined, dirty: true,
|
|
122
|
+
draft: { ...state.draft, sessionKit: released,
|
|
123
|
+
profiles: { ...library, kits: remaining,
|
|
124
|
+
defaultKitId: (library.defaultKitId ?? null) === id ? null : library.defaultKitId } } };
|
|
125
|
+
}
|
|
126
|
+
/** Every kit-page intent, applied to the ONE draft. */
|
|
127
|
+
export function kitReducer(state, action) {
|
|
128
|
+
const kitDraft = state.kitDraft;
|
|
129
|
+
switch (action.type) {
|
|
130
|
+
case 'kit-open':
|
|
131
|
+
return { ...state, page: 'kit-edit', error: undefined,
|
|
132
|
+
kitDraft: openKitDraft(state, action.id) };
|
|
133
|
+
case 'kit-name':
|
|
134
|
+
if (!kitDraft)
|
|
135
|
+
return state;
|
|
136
|
+
// Control characters are stripped at the boundary rather than refused: a
|
|
137
|
+
// pasted name with a stray newline should become a name, not an error.
|
|
138
|
+
return withDraft(state, { ...kitDraft,
|
|
139
|
+
name: [...action.value.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')]
|
|
140
|
+
.slice(0, KIT_LIMITS.nameMaxChars).join(''), error: undefined });
|
|
141
|
+
case 'kit-slot':
|
|
142
|
+
return kitDraft ? { ...state, page: 'kit-slot',
|
|
143
|
+
kitDraft: { ...kitDraft, slot: action.slot } } : state;
|
|
144
|
+
case 'kit-main':
|
|
145
|
+
return kitDraft ? { ...state, page: 'kit-edit',
|
|
146
|
+
kitDraft: { ...kitDraft, mainProfileId: action.id ?? '', slot: null, error: undefined } }
|
|
147
|
+
: state;
|
|
148
|
+
case 'kit-fast':
|
|
149
|
+
return kitDraft ? { ...state, page: 'kit-edit',
|
|
150
|
+
kitDraft: { ...kitDraft, fastProfileId: action.id, slot: null, error: undefined } } : state;
|
|
151
|
+
case 'kit-subagent':
|
|
152
|
+
return kitDraft ? { ...state, page: 'kit-edit', kitDraft: { ...kitDraft,
|
|
153
|
+
subagent: { model: action.model, modelProfileId: action.modelProfileId },
|
|
154
|
+
slot: null, error: undefined } } : state;
|
|
155
|
+
case 'kit-save': return saveKitDraft(state);
|
|
156
|
+
case 'kit-cancel': return { ...state, page: 'kits', kitDraft: undefined, error: undefined };
|
|
157
|
+
case 'kit-duplicate': {
|
|
158
|
+
const library = draftLibrary(state);
|
|
159
|
+
const original = findKit(library, action.id);
|
|
160
|
+
if (!original || (library.kits ?? []).length >= KIT_LIMITS.maxKits)
|
|
161
|
+
return state;
|
|
162
|
+
return { ...state, page: 'kits', dirty: true, draft: { ...state.draft,
|
|
163
|
+
profiles: { ...library,
|
|
164
|
+
kits: [...(library.kits ?? []), duplicateAgentKit(original, library.kits ?? [])] } } };
|
|
165
|
+
}
|
|
166
|
+
case 'kit-delete': return { ...state, page: 'kit-delete',
|
|
167
|
+
kitDraft: { ...(kitDraft ?? openKitDraft(state, action.id)), id: action.id } };
|
|
168
|
+
case 'kit-confirm-delete': return kitDraft?.id ? removeKit(state, kitDraft.id) : state;
|
|
169
|
+
case 'kit-apply': return applyKitToDraft(state, action.id, false);
|
|
170
|
+
case 'kit-default': return applyKitToDraft(state, action.id, true);
|
|
171
|
+
default: return state;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function shortConnection(connection) {
|
|
175
|
+
return connection ? `${connection.provider}:${connection.model}` : 'Profile missing';
|
|
176
|
+
}
|
|
177
|
+
function subagentSummary(connections, kit) {
|
|
178
|
+
if (kit.subagent.model === 'main')
|
|
179
|
+
return 'same as main';
|
|
180
|
+
if (kit.subagent.model === 'fast')
|
|
181
|
+
return 'fast tier';
|
|
182
|
+
return connections.subagent.name ? `profile ${connections.subagent.name}` : 'Profile missing';
|
|
183
|
+
}
|
|
184
|
+
/** Rows in library order, each with the markers the picker also uses (§4.5). */
|
|
185
|
+
export function kitManagerRows(state) {
|
|
186
|
+
const library = draftLibrary(state);
|
|
187
|
+
return (library.kits ?? []).map((kit) => {
|
|
188
|
+
const connections = kitSlotConnections(library, kit);
|
|
189
|
+
const isDefault = (library.defaultKitId ?? null) === kit.id;
|
|
190
|
+
const inUse = state.draft.sessionKit?.id === kit.id;
|
|
191
|
+
const markers = [isDefault ? '[Default]' : '', inUse ? '[In use]' : '']
|
|
192
|
+
.filter(Boolean).join(' ');
|
|
193
|
+
const fast = kit.fastProfileId === null ? 'off' : shortConnection(connections.fast);
|
|
194
|
+
return {
|
|
195
|
+
id: kit.id,
|
|
196
|
+
label: `${markers ? `${markers} ` : ''}${kit.name}`,
|
|
197
|
+
summary: [shortConnection(connections.main), fast, subagentSummary(connections, kit)]
|
|
198
|
+
.join(' | '),
|
|
199
|
+
isDefault,
|
|
200
|
+
inUse,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Whether the kit this transaction opened on had already drifted (§4.8).
|
|
206
|
+
*
|
|
207
|
+
* READ FROM `liveBaseline`, i.e. the config the screen was opened over, because
|
|
208
|
+
* drift is a fact about the SESSION - the draft cannot change a connection
|
|
209
|
+
* without releasing the slot, so there is nothing newer to look at.
|
|
210
|
+
*/
|
|
211
|
+
function liveDrift(state, session) {
|
|
212
|
+
// THE DRAFT'S `held`, NOT `liveBaseline.sessionKit`: the transaction may already
|
|
213
|
+
// have given slots back, and the row has to describe THIS transaction.
|
|
214
|
+
const live = resolveActiveKit({ ...state.draft.liveBaseline, sessionKit: session });
|
|
215
|
+
return live.source === 'session' && live.modified;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The `Kit pack` root row's five states (§4.5 table, AC-27).
|
|
219
|
+
*
|
|
220
|
+
* READ FROM THE DRAFT, not from the live config: the row sits inside a
|
|
221
|
+
* transaction, and showing the saved state under a pending selection is how a
|
|
222
|
+
* settings screen lies to the user about what `Save` is going to do.
|
|
223
|
+
*/
|
|
224
|
+
export function kitPackRow(state) {
|
|
225
|
+
const library = draftLibrary(state);
|
|
226
|
+
const kits = library.kits ?? [];
|
|
227
|
+
const session = state.draft.sessionKit;
|
|
228
|
+
const sessionKit = session ? findKit(library, session.id) : undefined;
|
|
229
|
+
const defaultKit = findKit(library, library.defaultKitId ?? null);
|
|
230
|
+
const defaultClause = defaultKit ? ` (default: ${defaultKit.name})` : '';
|
|
231
|
+
if (sessionKit && session) {
|
|
232
|
+
const partial = session.held.length < 3;
|
|
233
|
+
const held = partial ? `: ${session.held.join('+')}` : '';
|
|
234
|
+
// THE SAME `~` THE CHIP SHOWS (D-12): dropping a slot is only ONE way to
|
|
235
|
+
// drift. The other is a connection that `/model`, `/fast` or `/team model`
|
|
236
|
+
// rewrote after the kit was applied, and a row that stayed silent about that
|
|
237
|
+
// would disagree with the status bar about the same session.
|
|
238
|
+
const drift = liveDrift(state, session) ? '~' : '';
|
|
239
|
+
return `${sessionKit.name} [session${held}]${drift}${defaultClause}`;
|
|
240
|
+
}
|
|
241
|
+
if (defaultKit)
|
|
242
|
+
return `${defaultKit.name} [default]`;
|
|
243
|
+
return kits.length ? 'Current setup' : 'Current setup (no kit yet)';
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The held slots' EFFECTIVE values (RV-P0-2).
|
|
247
|
+
*
|
|
248
|
+
* WITHOUT THIS the root page shows the file's bindings while the header shows the
|
|
249
|
+
* kit's, and the only conclusion available to the user is that one of them is
|
|
250
|
+
* broken. `Enter` on any of these rows writes an explicit choice, which releases
|
|
251
|
+
* that slot - so "what you see is what you change" stays true.
|
|
252
|
+
*/
|
|
253
|
+
export function heldSlots(state) {
|
|
254
|
+
const session = state.draft.sessionKit;
|
|
255
|
+
const kit = session ? findKitInState(state, session.id) : undefined;
|
|
256
|
+
if (!session || !kit) {
|
|
257
|
+
return { kitName: null, held: [], mainId: null, fastId: null, subagent: null };
|
|
258
|
+
}
|
|
259
|
+
const held = session.held;
|
|
260
|
+
return {
|
|
261
|
+
kitName: kit.name,
|
|
262
|
+
held,
|
|
263
|
+
mainId: held.includes('main') ? kit.mainProfileId : null,
|
|
264
|
+
fastId: held.includes('fast') ? kit.fastProfileId : null,
|
|
265
|
+
subagent: held.includes('subagent') ? { ...kit.subagent } : null,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type ModelProfile, type ModelProfilesConfig, type ModelRole } from '../../config/model-profiles.js';
|
|
2
|
+
import type { AgentKit } from '../../config/model-kits.js';
|
|
2
3
|
import type { ModelSettingsDraft } from '../../config/model-profile-store.js';
|
|
3
4
|
import type { SubagentModelMode } from '../../team/model.js';
|
|
4
|
-
|
|
5
|
+
import type { KitDraft } from './kit-settings-state.js';
|
|
6
|
+
export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' | 'delete-confirm' | 'discard-confirm' | 'kits' | 'kit-edit' | 'kit-slot' | 'kit-delete';
|
|
5
7
|
/**
|
|
6
8
|
* Which role a PROFILE PICKER page is choosing for.
|
|
7
9
|
*
|
|
@@ -9,8 +11,13 @@ export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' |
|
|
|
9
11
|
* `'main' | 'fast'` because it is the type basis of the config layer's
|
|
10
12
|
* `${role}Id` template keys - widening it would invent a `subagentId` library
|
|
11
13
|
* key nothing reads. The subagent picker writes `draft.patch.team` instead.
|
|
14
|
+
*
|
|
15
|
+
* `'binding'` IS A KIT SLOT CHOOSER (agent-model-kits §4.5): a plain profile
|
|
16
|
+
* list with no `Current custom` row, no built-in rows and one `Create profile`
|
|
17
|
+
* action. A kit slot can only ever name a profile, so offering "no profile" as a
|
|
18
|
+
* choice there would be offering an invalid kit.
|
|
12
19
|
*/
|
|
13
|
-
export type ProfilePickerRole = ModelRole | 'subagent';
|
|
20
|
+
export type ProfilePickerRole = ModelRole | 'subagent' | 'binding';
|
|
14
21
|
/** The picker's built-in `Fast tier model` row; never a profile id on disk. */
|
|
15
22
|
export declare const SUBAGENT_FAST_ROW_ID = "__fast__";
|
|
16
23
|
export interface ProfileEditorDraft {
|
|
@@ -30,13 +37,27 @@ export interface ModelProfileState {
|
|
|
30
37
|
page: ProfilePage;
|
|
31
38
|
role: ProfilePickerRole;
|
|
32
39
|
editor?: ProfileEditorDraft;
|
|
33
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Where the editor and the delete confirmation hand control back.
|
|
42
|
+
*
|
|
43
|
+
* `'kit-slot'` JOINS `'picker'` (agent-model-kits §4.5): the kit slot chooser
|
|
44
|
+
* is a profile picker too, and its `Create profile` action must return to the
|
|
45
|
+
* slot it was creating for rather than to the profile manager.
|
|
46
|
+
*/
|
|
47
|
+
returnPage: 'picker' | 'manager' | 'kit-slot';
|
|
34
48
|
bindNew: boolean;
|
|
35
49
|
dirty: boolean;
|
|
36
50
|
error?: string;
|
|
37
51
|
deleteId?: string;
|
|
38
52
|
/** Who referenced the entry being deleted ('Main + Subagent', or ''). */
|
|
39
53
|
deleteUsers?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The kit editor's own draft (agent-model-kits §4.5), owned by `kitReducer`.
|
|
56
|
+
*
|
|
57
|
+
* ONE FIELD, NOT A SECOND STATE OBJECT: the page axis stays shared, so `Save`
|
|
58
|
+
* and `Esc` mean the same thing on every page of this screen.
|
|
59
|
+
*/
|
|
60
|
+
kitDraft?: KitDraft;
|
|
40
61
|
}
|
|
41
62
|
export type ModelProfileAction = {
|
|
42
63
|
type: 'page';
|
|
@@ -90,6 +111,8 @@ export declare function finishProfileEditor(editor: ProfileEditorDraft): {
|
|
|
90
111
|
};
|
|
91
112
|
/** Make an isolated root draft; opening the screen never mutates live config. */
|
|
92
113
|
export declare function createModelProfileState(draft: ModelSettingsDraft): ModelProfileState;
|
|
114
|
+
/** The kits whose three slots name `id`, for the delete confirmation (D-9). */
|
|
115
|
+
export declare function kitsUsingProfile(profiles: ModelProfilesConfig | undefined, id: string): AgentKit[];
|
|
93
116
|
/** Describe references using text so ASCII and no-color modes remain usable. */
|
|
94
117
|
export declare function profileUsers(profiles: ModelProfilesConfig | undefined, id: string, subagentProfileId?: string | null): string;
|
|
95
118
|
/**
|