@aragon-agent/cli 0.6.34 → 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.
Files changed (64) hide show
  1. package/README.md +3071 -3007
  2. package/dist/agent/controller.d.ts +29 -0
  3. package/dist/agent/controller.js +42 -1
  4. package/dist/agent/kit-apply.d.ts +56 -0
  5. package/dist/agent/kit-apply.js +209 -0
  6. package/dist/agent/model-profile-settings.d.ts +28 -0
  7. package/dist/agent/model-profile-settings.js +142 -29
  8. package/dist/agent/reducer.d.ts +1 -1
  9. package/dist/commands/builtins.js +4 -0
  10. package/dist/commands/kit-command.d.ts +20 -0
  11. package/dist/commands/kit-command.js +131 -0
  12. package/dist/config/kit-resolution.d.ts +93 -0
  13. package/dist/config/kit-resolution.js +226 -0
  14. package/dist/config/model-kits.d.ts +224 -0
  15. package/dist/config/model-kits.js +302 -0
  16. package/dist/config/model-profile-store.d.ts +25 -0
  17. package/dist/config/model-profiles.d.ts +41 -3
  18. package/dist/config/model-profiles.js +46 -5
  19. package/dist/config/schema.d.ts +16 -0
  20. package/dist/input/keymap.js +18 -4
  21. package/dist/ui/App.js +108 -12
  22. package/dist/ui/Header.d.ts +9 -1
  23. package/dist/ui/Header.js +8 -2
  24. package/dist/ui/PromptInput.js +1 -1
  25. package/dist/ui/ScrollbackShell.d.ts +18 -3
  26. package/dist/ui/ScrollbackShell.js +1 -1
  27. package/dist/ui/StatusBar.d.ts +13 -0
  28. package/dist/ui/StatusBar.js +10 -3
  29. package/dist/ui/TeamPanel.d.ts +12 -0
  30. package/dist/ui/TeamPanel.js +10 -31
  31. package/dist/ui/Transcript.js +22 -2
  32. package/dist/ui/entries/TeamCard.d.ts +18 -2
  33. package/dist/ui/entries/TeamCard.js +47 -13
  34. package/dist/ui/layout/header-hints.d.ts +11 -1
  35. package/dist/ui/layout/header-hints.js +35 -3
  36. package/dist/ui/layout/kit-chip.d.ts +50 -0
  37. package/dist/ui/layout/kit-chip.js +81 -0
  38. package/dist/ui/layout/virtual-window.js +9 -0
  39. package/dist/ui/overlays/HelpOverlay.js +7 -0
  40. package/dist/ui/overlays/KitEditor.d.ts +23 -0
  41. package/dist/ui/overlays/KitEditor.js +101 -0
  42. package/dist/ui/overlays/KitManager.d.ts +23 -0
  43. package/dist/ui/overlays/KitManager.js +104 -0
  44. package/dist/ui/overlays/KitPicker.d.ts +49 -0
  45. package/dist/ui/overlays/KitPicker.js +58 -0
  46. package/dist/ui/overlays/ModelProfilePicker.js +16 -6
  47. package/dist/ui/overlays/SettingsScreen.js +139 -16
  48. package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
  49. package/dist/ui/overlays/kit-picker-state.js +136 -0
  50. package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
  51. package/dist/ui/overlays/kit-settings-state.js +267 -0
  52. package/dist/ui/overlays/model-profile-state.d.ts +26 -3
  53. package/dist/ui/overlays/model-profile-state.js +59 -6
  54. package/dist/ui/scrollback/LiveTail.d.ts +2 -2
  55. package/dist/ui/scrollback/LiveTail.js +7 -2
  56. package/dist/ui/scrollback/live-preview.js +10 -3
  57. package/dist/ui/scrollback/tail-window.d.ts +12 -0
  58. package/dist/ui/scrollback/tail-window.js +17 -2
  59. package/dist/ui/scrollback/use-scrollback-view.js +7 -0
  60. package/dist/ui/team-status.d.ts +141 -0
  61. package/dist/ui/team-status.js +224 -0
  62. package/dist/ui/transcript-text.js +13 -4
  63. package/dist/ui/use-wheel-routing.js +2 -2
  64. package/package.json +2 -2
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Settings > Kit pack > editor (agent-model-kits §4.5).
4
+ *
5
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * FOUR ROWS AND TWO ACTIONS, and every slot is an explicit decision (D-8): the
8
+ * Main slot is required, the Fast slot is a two-state choice (`off` is a
9
+ * decision, not an omission), and the Subagent slot reuses the existing
10
+ * `Same as main` / `Fast tier model` vocabulary so the summary sentence the root
11
+ * page already prints (`Fast tier (off - falls back to main)`) needs no second
12
+ * wording. Nothing is written until the ROOT page's Save.
13
+ */
14
+ import { useState } from 'react';
15
+ import { Text, useInput } from 'ink';
16
+ import { pickGlyphs } from '../glyphs.js';
17
+ import { OverlayFrame } from '../layout/OverlayFrame.js';
18
+ import { stripPasteFrames } from '../paste-frames.js';
19
+ import { stripEnterFrames } from '../enter-frames.js';
20
+ import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
21
+ const ROWS = ['name', 'main', 'subagent', 'fast', 'save', 'cancel'];
22
+ function slotLabel(draft, state) {
23
+ const entries = state.draft.profiles?.entries ?? [];
24
+ const name = (id) => {
25
+ const entry = id ? entries.find((item) => item.id === id) : undefined;
26
+ // `Profile missing` rather than a blank: a kit can be pointed at a profile
27
+ // that was removed on disk, and the editor is where that gets fixed.
28
+ return entry ? `${entry.provider}:${entry.model} (${entry.name})` : id ? 'Profile missing'
29
+ : '(not set)';
30
+ };
31
+ const subagent = draft.subagent.model === 'main' ? 'Same as main'
32
+ : draft.subagent.model === 'fast' ? 'Fast tier model'
33
+ : name(draft.subagent.modelProfileId);
34
+ return { main: name(draft.mainProfileId), fast: name(draft.fastProfileId),
35
+ subagent };
36
+ }
37
+ export function KitEditor(props) {
38
+ const glyphs = pickGlyphs(props.caps);
39
+ const { draft, dispatch } = props;
40
+ const [index, setIndex] = useState(0);
41
+ const row = ROWS[index];
42
+ const offset = useProfileScroll(props, index);
43
+ const labels = slotLabel(draft, props.state);
44
+ const filled = [draft.mainProfileId, draft.fastProfileId,
45
+ draft.subagent.model !== 'profile' || draft.subagent.modelProfileId]
46
+ .filter(Boolean).length;
47
+ function activate(target) {
48
+ if (target === 'main')
49
+ dispatch({ type: 'kit-slot', slot: 'main' });
50
+ if (target === 'fast')
51
+ dispatch({ type: 'kit-slot', slot: 'fast' });
52
+ if (target === 'subagent')
53
+ dispatch({ type: 'kit-slot', slot: 'subagent' });
54
+ if (target === 'save')
55
+ dispatch({ type: 'kit-save' });
56
+ if (target === 'cancel')
57
+ dispatch({ type: 'kit-cancel' });
58
+ }
59
+ useInput((input, key) => {
60
+ if (props.isActive === false)
61
+ return;
62
+ if (key.escape) {
63
+ dispatch({ type: 'kit-cancel' });
64
+ return;
65
+ }
66
+ if (key.upArrow || (key.tab && key.shift)) {
67
+ setIndex((index - 1 + ROWS.length) % ROWS.length);
68
+ return;
69
+ }
70
+ if (key.downArrow || key.tab) {
71
+ setIndex((index + 1) % ROWS.length);
72
+ return;
73
+ }
74
+ if (key.return) {
75
+ activate(row);
76
+ return;
77
+ }
78
+ if (row !== 'name' || key.ctrl || key.meta)
79
+ return;
80
+ // THE NAME IS THE ONLY TEXT FIELD, and control characters are stripped by the
81
+ // reducer rather than refused here - a pasted name with a newline should
82
+ // become a name, not an error the user cannot see.
83
+ const text = stripPasteFrames(stripEnterFrames(input));
84
+ if (key.backspace || key.delete) {
85
+ dispatch({ type: 'kit-name', value: [...draft.name].slice(0, -1).join('') });
86
+ return;
87
+ }
88
+ if (text)
89
+ dispatch({ type: 'kit-name', value: `${draft.name}${text}` });
90
+ });
91
+ const lines = [`Name ${draft.name}`,
92
+ `Main profile ${labels.main}`,
93
+ `Subagent ${labels.subagent}`,
94
+ `Fast tier ${draft.fastProfileId ? labels.fast : 'off (this kit turns it off)'}`,
95
+ `Save (${filled} slots set)`, 'Cancel'];
96
+ const rows = lines.map((line, at) => (_jsx(Text, { wrap: "truncate", color: at === index ? props.theme.accent : props.theme.muted, children: `${at === index ? glyphs.caret : ' '} ${clipProfileText(line, props.cols - 8)}` }, at)));
97
+ if (draft.error) {
98
+ rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.noticeError, children: clipProfileText(draft.error, props.cols - 8) }, "error"));
99
+ }
100
+ return _jsx(OverlayFrame, { ...props, title: draft.id ? 'Edit kit' : 'New kit', hint: "Enter choose | type to rename | Esc back", rows: rows, scrollOffset: offset });
101
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Settings > Kit pack: the manager page (agent-model-kits §4.5 / D-9).
3
+ *
4
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * CRUD LIVES HERE AND NOWHERE ELSE: `New kit` and `e` open the editor, `d` asks
7
+ * a confirmation that NAMES the consequences, `s` runs exit A (three slots plus
8
+ * the persisted pointer, because a default that only moved a pointer would not
9
+ * be in effect at the next launch - RV-P0-3), and `Enter` applies a kit to the
10
+ * transaction draft. Nothing on this page writes the config file.
11
+ */
12
+ import React from 'react';
13
+ import { type ProfileFrameProps } from './ModelProfilePicker.js';
14
+ import type { KitAction, KitManagerRow } from './kit-settings-state.js';
15
+ interface KitManagerProps extends ProfileFrameProps {
16
+ rows: KitManagerRow[];
17
+ error?: string;
18
+ dispatch: React.Dispatch<KitAction>;
19
+ /** Leave the kit pages entirely: `Esc` here means "back to Settings". */
20
+ onBack: () => void;
21
+ }
22
+ export declare function KitManager(props: KitManagerProps): React.ReactElement;
23
+ export {};
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Settings > Kit pack: the manager page (agent-model-kits §4.5 / D-9).
4
+ *
5
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * CRUD LIVES HERE AND NOWHERE ELSE: `New kit` and `e` open the editor, `d` asks
8
+ * a confirmation that NAMES the consequences, `s` runs exit A (three slots plus
9
+ * the persisted pointer, because a default that only moved a pointer would not
10
+ * be in effect at the next launch - RV-P0-3), and `Enter` applies a kit to the
11
+ * transaction draft. Nothing on this page writes the config file.
12
+ */
13
+ import { useState } from 'react';
14
+ import { Text, useInput } from 'ink';
15
+ import { pickGlyphs } from '../glyphs.js';
16
+ import { OverlayFrame } from '../layout/OverlayFrame.js';
17
+ import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
18
+ /** `New kit` first, then one row per kit, then the three actions. */
19
+ const NEW_KIT = 'New kit';
20
+ const ACTIONS = ['Duplicate', 'Delete', 'Set default'];
21
+ export function KitManager(props) {
22
+ const glyphs = pickGlyphs(props.caps);
23
+ const [index, setIndex] = useState(0);
24
+ // NULL, NOT THE FIRST KIT: `selected` is "the last kit the cursor was on", and
25
+ // starting it at `rows[0]` would let `d` on the `New kit` row delete a kit the
26
+ // user never pointed at.
27
+ const [selected, setSelected] = useState(null);
28
+ const count = props.rows.length + 1 + ACTIONS.length;
29
+ const cursor = Math.min(index, count - 1);
30
+ const focused = props.rows[cursor - 1];
31
+ const offset = useProfileScroll(props, cursor);
32
+ const current = focused?.id ?? selected;
33
+ function action(name) {
34
+ if (!current)
35
+ return;
36
+ if (name === 'Duplicate')
37
+ props.dispatch({ type: 'kit-duplicate', id: current });
38
+ if (name === 'Delete')
39
+ props.dispatch({ type: 'kit-delete', id: current });
40
+ // EXIT A, NOT A POINTER WRITE (RV-P0-3): the three slots and the pointer move
41
+ // together, because "the default" IS the disk binding (D-13).
42
+ if (name === 'Set default')
43
+ props.dispatch({ type: 'kit-default', id: current });
44
+ }
45
+ useInput((input, key) => {
46
+ if (props.isActive === false)
47
+ return;
48
+ if (key.escape) {
49
+ props.onBack();
50
+ return;
51
+ }
52
+ if (key.upArrow || (key.tab && key.shift)) {
53
+ const next = (cursor - 1 + count) % count;
54
+ setIndex(next);
55
+ if (props.rows[next - 1])
56
+ setSelected(props.rows[next - 1].id);
57
+ return;
58
+ }
59
+ if (key.downArrow || key.tab) {
60
+ const next = (cursor + 1) % count;
61
+ setIndex(next);
62
+ if (props.rows[next - 1])
63
+ setSelected(props.rows[next - 1].id);
64
+ return;
65
+ }
66
+ if (key.return) {
67
+ if (cursor === 0) {
68
+ props.dispatch({ type: 'kit-open', id: null });
69
+ return;
70
+ }
71
+ if (focused) {
72
+ props.dispatch({ type: 'kit-apply', id: focused.id });
73
+ return;
74
+ }
75
+ action(ACTIONS[cursor - 1 - props.rows.length]);
76
+ return;
77
+ }
78
+ if (key.ctrl || key.meta)
79
+ return;
80
+ const letter = input.toLowerCase();
81
+ // `e` opens the focused kit; `d` and `s` are the two destructive/intent-bearing
82
+ // single keys. ALL THREE require a focused KIT ROW, so none of them can fire
83
+ // from `New kit` or from the action rows at the bottom.
84
+ if (!focused)
85
+ return;
86
+ if (letter === 'e') {
87
+ props.dispatch({ type: 'kit-open', id: focused.id });
88
+ return;
89
+ }
90
+ if (letter === 'd') {
91
+ props.dispatch({ type: 'kit-delete', id: focused.id });
92
+ return;
93
+ }
94
+ if (letter === 's')
95
+ action('Set default');
96
+ });
97
+ const lines = [NEW_KIT, ...props.rows.map((row) => `${row.label} ${row.summary}`),
98
+ ...ACTIONS];
99
+ const rows = lines.map((line, at) => (_jsx(Text, { wrap: "truncate", color: at === cursor ? props.theme.accent : props.theme.muted, children: `${at === cursor ? glyphs.caret : ' '} ${clipProfileText(line, props.cols - 8)}` }, at)));
100
+ if (!props.rows.length) {
101
+ rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: clipProfileText('No kits yet. Press Enter on New kit to build one from this setup.', props.cols - 8) }, "empty"));
102
+ }
103
+ return _jsx(OverlayFrame, { ...props, title: props.error ?? 'Agent kits', hint: "Enter apply | e edit | d delete | s set default | Esc back", rows: rows, scrollOffset: offset });
104
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The Agent-kit picker: `ctrl+K` (agent-model-kits §4.6 / D-10 / D-11).
3
+ *
4
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * A LIST PLUS A DETAIL BLOCK, not one row per kit with three columns: three
7
+ * connections never fit on a 72-column terminal, and the `OverlayFrame` detail
8
+ * idiom is how this package already says everything that does not fit in a row
9
+ * (`ModelProfilePicker`, the compaction card). The list carries names and
10
+ * markers only, so the choice stays scannable at any width.
11
+ *
12
+ * THE OVERLAY IS READ-ONLY (R-11): Enter applies for this session, Esc leaves
13
+ * everything as it was. That is what makes moving `killToEnd` off `Ctrl+K`
14
+ * (D-15) a one-time surprise instead of a risk - a user who presses the old
15
+ * key gets a reversible screen, not a deleted line.
16
+ */
17
+ import React from 'react';
18
+ import type { ModelProfilesConfig } from '../../config/model-profiles.js';
19
+ import type { CliConfig } from '../../config/schema.js';
20
+ import type { ActiveKit } from '../../config/kit-resolution.js';
21
+ import type { Theme } from '../theme.js';
22
+ import type { TermCapabilities } from '../capabilities.js';
23
+ import { type KitConnectionsSource } from './kit-picker-state.js';
24
+ export interface KitPickerProps {
25
+ isActive?: boolean;
26
+ theme: Theme;
27
+ caps: TermCapabilities;
28
+ cols: number;
29
+ maxRows: number;
30
+ scrollOffset: number;
31
+ onScrollClamp?: (offset: number) => void;
32
+ library?: ModelProfilesConfig | undefined;
33
+ config: CliConfig;
34
+ /**
35
+ * The config file's bindings, for the `Current setup` detail block (§4.6).
36
+ *
37
+ * OMITTED BY EMBEDDED HOSTS AND TESTS, in which case the block falls back to
38
+ * `config` - which is exactly right when no kit is in effect, since the live
39
+ * config then IS the file.
40
+ */
41
+ diskConfig?: KitConnectionsSource | undefined;
42
+ active: ActiveKit;
43
+ /** Only the APPLY is refused while work is in flight; browsing still works. */
44
+ busy: boolean;
45
+ /** `null` = `Current setup`, i.e. release the session kit. */
46
+ onChoose: (kitId: string | null) => void;
47
+ onClose: () => void;
48
+ }
49
+ export declare function KitPicker(props: KitPickerProps): React.ReactElement;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * The Agent-kit picker: `ctrl+K` (agent-model-kits §4.6 / D-10 / D-11).
4
+ *
5
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * A LIST PLUS A DETAIL BLOCK, not one row per kit with three columns: three
8
+ * connections never fit on a 72-column terminal, and the `OverlayFrame` detail
9
+ * idiom is how this package already says everything that does not fit in a row
10
+ * (`ModelProfilePicker`, the compaction card). The list carries names and
11
+ * markers only, so the choice stays scannable at any width.
12
+ *
13
+ * THE OVERLAY IS READ-ONLY (R-11): Enter applies for this session, Esc leaves
14
+ * everything as it was. That is what makes moving `killToEnd` off `Ctrl+K`
15
+ * (D-15) a one-time surprise instead of a risk - a user who presses the old
16
+ * key gets a reversible screen, not a deleted line.
17
+ */
18
+ import { useState } from 'react';
19
+ import { Text, useInput } from 'ink';
20
+ import { pickGlyphs } from '../glyphs.js';
21
+ import { OverlayFrame } from '../layout/OverlayFrame.js';
22
+ import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
23
+ import { kitDetail, kitPickHint, kitRows, NO_KITS_HINT } from './kit-picker-state.js';
24
+ export function KitPicker(props) {
25
+ const rows = kitRows({ library: props.library, active: props.active });
26
+ const glyphs = pickGlyphs(props.caps);
27
+ const [index, setIndex] = useState(0);
28
+ const count = rows.length;
29
+ const cursor = Math.min(index, count - 1);
30
+ const focused = rows[cursor];
31
+ const offset = useProfileScroll(props, cursor);
32
+ const hasKits = (props.library?.kits ?? []).length > 0;
33
+ const body = rows.map((row, at) => (_jsx(Text, { wrap: "truncate", color: at === cursor ? props.theme.accent : props.theme.muted, children: `${at === cursor ? glyphs.caret : ' '} ${clipProfileText(row.label, props.cols - 8)}` }, row.id ?? 'current')));
34
+ if (!hasKits) {
35
+ body.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: clipProfileText(NO_KITS_HINT, props.cols - 8) }, "empty"));
36
+ }
37
+ // THE DETAIL BLOCK DESCRIBES THE FOCUSED ROW ONLY. Shown last so a narrow
38
+ // terminal clips the detail, never the choice.
39
+ for (const [at, line] of kitDetail({ row: focused, library: props.library,
40
+ config: props.config, diskConfig: props.diskConfig }).entries()) {
41
+ body.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: clipProfileText(` ${line}`, props.cols - 8) }, `detail-${at}`));
42
+ }
43
+ useInput((_input, key) => {
44
+ if (props.isActive === false)
45
+ return;
46
+ if (key.escape) {
47
+ props.onClose();
48
+ return;
49
+ }
50
+ if (key.upArrow || (key.tab && key.shift))
51
+ setIndex((cursor - 1 + count) % count);
52
+ else if (key.downArrow || key.tab)
53
+ setIndex((cursor + 1) % count);
54
+ else if (key.return)
55
+ props.onChoose(focused.id);
56
+ });
57
+ return _jsx(OverlayFrame, { title: props.busy ? 'Agent kits (running)' : 'Agent kits', hint: kitPickHint({ busy: props.busy, hasKits }), rows: body, scrollOffset: offset, onScrollClamp: props.onScrollClamp, maxRows: props.maxRows, cols: props.cols, theme: props.theme, caps: props.caps });
58
+ }
@@ -64,7 +64,12 @@ function usePickerState(props) {
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
66
  // `Same as main` (selection id null) and `Fast tier model` (the sentinel).
67
- const prefix = props.role === 'subagent' ? 2 : props.manager ? 0 : 1;
67
+ //
68
+ // THE BINDING PICKER HAS NEITHER (agent-model-kits §4.5): a kit slot can only
69
+ // name a profile, so `prefix` is 0 there even though it is a selection page -
70
+ // "no profile" is not a choice a kit may make.
71
+ const prefix = props.role === 'subagent' ? 2 : props.role === 'binding' ? 0
72
+ : props.manager ? 0 : 1;
68
73
  const count = entries.length + prefix + actions.length;
69
74
  const cursor = Math.min(index, count - 1);
70
75
  const focused = entries[cursor - prefix];
@@ -89,7 +94,7 @@ function usePickerInput(props, state) {
89
94
  return;
90
95
  }
91
96
  }
92
- else if (!props.manager && cursor === 0) {
97
+ else if (!props.manager && props.role !== 'binding' && cursor === 0) {
93
98
  props.onSelect(null);
94
99
  return;
95
100
  }
@@ -164,7 +169,10 @@ function usePickerInput(props, state) {
164
169
  function currentSelectionId(props) {
165
170
  if (props.selectedIdOverride !== undefined)
166
171
  return props.selectedIdOverride;
167
- if (props.role === 'subagent')
172
+ // `subagent` and `binding` have no `${role}Id` key: their selection arrives
173
+ // through `selectedIdOverride`, so looking one up would read a key that does
174
+ // not exist on the library.
175
+ if (props.role === 'subagent' || props.role === 'binding')
168
176
  return undefined;
169
177
  return props.profiles[`${props.role}Id`];
170
178
  }
@@ -190,7 +198,7 @@ function buildPickerRows(props, state) {
190
198
  lines.push(`${current === SUBAGENT_FAST_ROW_ID ? '[Selected] ' : ''}Fast tier model`
191
199
  + `${props.fastTierOff === true ? ' (fast tier off)' : ''}`);
192
200
  }
193
- else if (!props.manager)
201
+ else if (!props.manager && props.role !== 'binding')
194
202
  lines.push('Current custom');
195
203
  lines.push(...entries.map((entry) => profilePickerLabel(props, entry)));
196
204
  lines.push(...actions);
@@ -200,7 +208,8 @@ function buildPickerRows(props, state) {
200
208
  // The subagent picker has no `Current custom` row to describe; its
201
209
  // built-in rows already say everything there is to say.
202
210
  const detail = props.role === 'subagent' ? focused
203
- : !props.manager && cursor === 0 ? props.customProfile : focused ?? selected;
211
+ : !props.manager && props.role !== 'binding' && cursor === 0 ? props.customProfile
212
+ : focused ?? selected;
204
213
  if (detail) {
205
214
  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"));
206
215
  rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: detail.apiKey ? 'Profile key: Configured'
@@ -215,7 +224,8 @@ export function ModelProfilePicker(props) {
215
224
  usePickerInput(props, state);
216
225
  return _jsx(OverlayFrame, { ...props, title: props.error ?? (props.manager ? 'Manage profiles'
217
226
  : 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'
227
+ : props.role === 'binding' ? 'Choose profile'
228
+ : `${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
229
  : props.manager ? 'Enter action | e edit | d delete | Esc back'
220
230
  : 'Enter select | e edit | d delete | Esc back' });
221
231
  }
@@ -24,7 +24,10 @@ import { ModelProfileEditor } from './ModelProfileEditor.js';
24
24
  import { ModelDiscoveryList } from './ModelDiscoveryList.js';
25
25
  import { useModelProbe } from './use-model-probe.js';
26
26
  import { canProbe, editorProbeTarget, visibleProbeViews } from './model-probe-view.js';
27
- import { createModelProfileState, createCustomProfile, modelProfileReducer, profileUsers, draftTeam, draftSubagentProfileId, subagentModelSummary, subagentSelectedId } from './model-profile-state.js';
27
+ import { createModelProfileState, createCustomProfile, modelProfileReducer, profileUsers, draftTeam, draftSubagentProfileId, subagentModelSummary, subagentSelectedId, SUBAGENT_FAST_ROW_ID } from './model-profile-state.js';
28
+ import { KitEditor } from './KitEditor.js';
29
+ import { KitManager } from './KitManager.js';
30
+ import { heldSlots, kitManagerRows, kitPackRow, kitReducer } from './kit-settings-state.js';
28
31
  /**
29
32
  * THE ROOT PAGE'S ROW INDEXES, AS NAMES (subagent-model V-5).
30
33
  *
@@ -32,12 +35,18 @@ import { createModelProfileState, createCustomProfile, modelProfileReducer, prof
32
35
  * model row moves every one of them. Each site below now cites its constant,
33
36
  * so adding a row later is a one-line change instead of a silent regression
34
37
  * that focuses an error message on the wrong field.
38
+ *
39
+ * DERIVED FROM ONE LIST (agent-model-kits §4.5), because the kit row is the
40
+ * fifth of these and the "five offsets agreed by accident" defect does not need
41
+ * a sequel. Order here IS the render order.
35
42
  */
36
- const ROOT_MAIN = 0;
37
- const ROOT_FAST = 1;
38
- const ROOT_SUBAGENT = 2;
39
- const ROOT_MANAGE = 3;
40
- const ROOT_FIRST_FIELD = 4;
43
+ const ROOT_ROWS = ['main', 'fast', 'subagent', 'kit', 'manage'];
44
+ const ROOT_MAIN = ROOT_ROWS.indexOf('main');
45
+ const ROOT_FAST = ROOT_ROWS.indexOf('fast');
46
+ const ROOT_SUBAGENT = ROOT_ROWS.indexOf('subagent');
47
+ const ROOT_KIT = ROOT_ROWS.indexOf('kit');
48
+ const ROOT_MANAGE = ROOT_ROWS.indexOf('manage');
49
+ const ROOT_FIRST_FIELD = ROOT_ROWS.length;
41
50
  /** Stand-in target while no editor exists; no probe row is rendered then. */
42
51
  const NO_PROFILE_TARGET = { provider: '', model: '', baseUrl: null,
43
52
  apiKey: null };
@@ -523,8 +532,23 @@ function saveError(result) {
523
532
  };
524
533
  return errors[result.code ?? ''] ?? 'Could not save. Review settings and retry.';
525
534
  }
535
+ function isKitAction(action) {
536
+ return action.type.startsWith('kit-');
537
+ }
538
+ /**
539
+ * ONE REDUCER FOR THE WHOLE SCREEN, TWO OWNERS INSIDE IT (agent-model-kits §4.5).
540
+ *
541
+ * The kit pages need their own data mutations (create, duplicate, delete, apply)
542
+ * but must not introduce a second state object: `Save`, `Esc` and the dirty
543
+ * badge are shared, and a second store is how a settings screen starts
544
+ * disagreeing with itself. `kitReducer` returns the SAME state shape, so the
545
+ * split is invisible from the outside.
546
+ */
547
+ function settingsReducer(state, action) {
548
+ return isKitAction(action) ? kitReducer(state, action) : modelProfileReducer(state, action);
549
+ }
526
550
  function useProfileSettingsState(props) {
527
- const [state, dispatch] = useReducer(modelProfileReducer, props.draft, createModelProfileState);
551
+ const [state, dispatch] = useReducer(settingsReducer, props.draft, createModelProfileState);
528
552
  const [initial, setInitial] = useState(() => profileSettingsValues(props.initial, props.draft));
529
553
  const [values, setValues] = useState(initial);
530
554
  const [index, setIndex] = useState(0);
@@ -559,12 +583,16 @@ function useProfileSettingsState(props) {
559
583
  && ['provider', 'model', 'baseUrl', 'contextWindow', 'apiKey'].includes(item.key)),
560
584
  ...FIELDS.filter((item) => library.fastId === null
561
585
  && ['fastModel', 'fastProvider'].includes(item.key)), ...PROFILE_FIELDS];
562
- // SIX FIXED ROWS now (Main, Fast, SUBAGENT, Manage, Save, Reload) around
563
- // the `fields` block - see the ROOT_* constants for the five offset sites.
564
- const count = fields.length + 6;
586
+ // SEVEN FIXED ROWS now (Main, Fast, SUBAGENT, Kit pack, Manage, Save, Reload)
587
+ // around the `fields` block - derived from ROOT_ROWS so the count and the
588
+ // offsets cannot drift apart (see those constants).
589
+ const count = fields.length + ROOT_ROWS.length + 2;
565
590
  const field = fields[index - ROOT_FIRST_FIELD];
566
591
  const readError = state.draft.readError;
567
- const confirming = state.page === 'delete-confirm' || state.page === 'discard-confirm';
592
+ // THREE CONFIRMATIONS SHARE ONE PAGE, which is why the page name - not a
593
+ // boolean - is what the renderer and the input hook both branch on.
594
+ const confirming = state.page === 'delete-confirm' || state.page === 'discard-confirm'
595
+ || state.page === 'kit-delete';
568
596
  return { state, dispatch, initial, setInitial, values, setValues, index, setIndex,
569
597
  error, setError, persisted, setPersisted, confirmIndex, setConfirmIndex,
570
598
  reloadPending, setReloadPending, offset, library, glyphs, dirty, fields, count,
@@ -651,6 +679,11 @@ function activateRootSetting(props, view) {
651
679
  // `draft.patch.team` rather than a `${role}Id` library key.
652
680
  dispatch({ type: 'page', page: 'picker', role: 'subagent' });
653
681
  }
682
+ else if (index === ROOT_KIT) {
683
+ // The fourth root row opens the manager, not a picker: kits are data the user
684
+ // owns here, and `ctrl+K` is where they are CONSUMED (agent-model-kits §4.5).
685
+ dispatch({ type: 'page', page: 'kits' });
686
+ }
654
687
  else if (index === ROOT_MANAGE)
655
688
  dispatch({ type: 'page', page: 'manager' });
656
689
  else
@@ -717,6 +750,11 @@ function useSettingsConfirmationInput(props, view) {
717
750
  }
718
751
  if (!key.return)
719
752
  return;
753
+ if (state.page === 'kit-delete') {
754
+ dispatch(confirmIndex === 1 ? { type: 'kit-confirm-delete' }
755
+ : { type: 'page', page: 'kits' });
756
+ return;
757
+ }
720
758
  if (state.page === 'delete-confirm') {
721
759
  dispatch(confirmIndex === 1 ? { type: 'confirm-delete' }
722
760
  : { type: 'page', page: 'manager' });
@@ -729,8 +767,26 @@ function useSettingsConfirmationInput(props, view) {
729
767
  props.onClose?.();
730
768
  }, { isActive: confirming });
731
769
  }
770
+ /**
771
+ * The kit deletion confirmation (agent-model-kits D-9 / AC-30).
772
+ *
773
+ * `Cancel` IS THE DEFAULT FOCUS, like the profile confirmation next door: the
774
+ * `Enter` key that opened this page must not be the one that deletes. The second
775
+ * line names what the user loses - a kit, and with it the default pointer and any
776
+ * session binding that named it.
777
+ */
778
+ function renderKitDeleteConfirmation(props, view) {
779
+ const { state, confirmIndex, glyphs } = view;
780
+ const kit = (state.draft.profiles?.kits ?? []).find((item) => item.id === state.kitDraft?.id);
781
+ const rows = ['Cancel', 'Delete'].map((label, at) => (_jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [confirmIndex === at ? `${glyphs.caret} ` : ' ', label] }, label)));
782
+ rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: `Deletes kit "${kit?.name ?? ''}". Its three slots stay as they are; only the preset goes.` }, "consequence"));
783
+ return _jsx(OverlayFrame, { ...props, title: `Delete kit ${kit?.name ?? ''}?`, hint: "Enter choose | Esc continue", rows: rows, scrollOffset: confirmIndex });
784
+ }
732
785
  function renderSettingsConfirmation(props, view) {
733
786
  const { state, library, confirmIndex, glyphs } = view;
787
+ const deletingKit = state.page === 'kit-delete';
788
+ if (deletingKit)
789
+ return renderKitDeleteConfirmation(props, view);
734
790
  const deleting = state.page === 'delete-confirm';
735
791
  const name = library.entries.find((entry) => entry.id === state.deleteId)?.name ?? '';
736
792
  // UNBIND-AND-DELETE (D-6): a referenced entry stays deletable in ONE flow,
@@ -780,6 +836,16 @@ function fastTierUnavailable(props, view) {
780
836
  }
781
837
  function renderProfileSubpage(props, view) {
782
838
  const { state, library, dispatch, setConfirmIndex } = view;
839
+ // THE KIT PAGES (agent-model-kits §4.5). They are dispatched BEFORE the profile
840
+ // pages because `'kit-slot'` RENDERS a profile picker while belonging to the
841
+ // kit editor - the page name, not the component, decides where the user is.
842
+ if (state.page === 'kits')
843
+ return _jsx(KitManager, { ...props, rows: kitManagerRows(state), error: state.error, dispatch: dispatch, onBack: () => dispatch({ type: 'page', page: 'root' }) });
844
+ if (state.page === 'kit-edit' && state.kitDraft)
845
+ return _jsx(KitEditor, { ...props, draft: state.kitDraft, state: state, dispatch: dispatch });
846
+ if (state.page === 'kit-slot' && state.kitDraft) {
847
+ return renderKitSlotPicker(props, view, state.kitDraft);
848
+ }
783
849
  if (state.page === 'editor' && state.editor)
784
850
  return _jsx(ModelProfileEditor, { ...props, editor: state.editor, users: profileUsers(library, state.editor.original.id, draftSubagentProfileId(state.draft)), error: state.error, dispatch: dispatch, probeView: view.probeViews.test, probeBusy: view.probe.busy, probeTarget: view.probeTarget, onTest: () => startTest(view), onDiscover: () => startDiscovery(view, dispatch), onCancelProbe: view.probe.cancel });
785
851
  // A DISCOVERY IS A PAGE, NOT A POPUP: the list is where the answer belongs, and
@@ -788,13 +854,50 @@ function renderProfileSubpage(props, view) {
788
854
  return _jsx(ModelDiscoveryList, { ...props, provider: state.editor.provider, currentModel: state.editor.model, view: view.probeViews.discovery, busy: view.probe.discovery.phase === 'discovering', onChoose: (model) => dispatch({ type: 'choose-model', model }), onRetry: () => startDiscovery(view, dispatch), onBack: () => dispatch({ type: 'page', page: 'editor' }), onCancelProbe: view.probe.cancel });
789
855
  // A NEW PROFILE SEEDED FROM THE MAIN CONNECTION while the subagent picker is
790
856
  // open: 'subagent' is picker-only (6.2), so the seed role falls back to
791
- // 'main' while `state.role` keeps driving the select that binds it.
792
- const createRole = state.role === 'subagent' ? 'main' : state.role;
857
+ // 'main' while `state.role` keeps driving the select that binds it. 'binding'
858
+ // is the kit slot's chooser and seeds the same way.
859
+ const createRole = state.role === 'subagent' || state.role === 'binding'
860
+ ? 'main' : state.role;
793
861
  if (state.page === 'picker' || state.page === 'manager')
794
862
  return _jsx(ModelProfilePicker, { ...props, profiles: library, savedProfiles: state.draft.baseline.modelProfiles, role: state.role, customProfile: createCustomProfile(state.draft, createRole), manager: state.page === 'manager', error: state.error, selectedIdOverride: state.role === 'subagent' ? subagentSelectedId(state.draft) : undefined, subagentProfileId: draftSubagentProfileId(state.draft), fastTierOff: state.role === 'subagent' && fastTierUnavailable(props, view), onSelect: (id) => dispatch({ type: 'select', role: state.role, id }), onCreate: () => dispatch({ type: 'edit', profile: createCustomProfile(state.draft, createRole),
795
863
  bindNew: state.page === 'picker' }), onEdit: (profile) => dispatch({ type: 'edit', profile }), onDuplicate: (id) => dispatch({ type: 'duplicate', id }), onDelete: (id) => { setConfirmIndex(0); dispatch({ type: 'delete', id }); }, onCancel: () => dispatch({ type: 'page', page: 'root' }) });
796
864
  return null;
797
865
  }
866
+ /** Which slot value the kit picker should mark `[Selected]`. */
867
+ function kitSlotSelection(kitDraft) {
868
+ if (kitDraft.slot === 'main')
869
+ return kitDraft.mainProfileId || null;
870
+ if (kitDraft.slot === 'fast')
871
+ return kitDraft.fastProfileId;
872
+ if (kitDraft.subagent.model === 'profile')
873
+ return kitDraft.subagent.modelProfileId;
874
+ return kitDraft.subagent.model === 'fast' ? SUBAGENT_FAST_ROW_ID : null;
875
+ }
876
+ /**
877
+ * The slot chooser: the SAME profile picker, in its `binding` role.
878
+ *
879
+ * A kit slot can only name a profile, so there is no `Current custom` row and no
880
+ * `Same as main` (those are the subagent slot's own three-state vocabulary). The
881
+ * subagent slot keeps the `subagent` role, which is why `Fast tier (off - falls
882
+ * back to main)` needs no new wording anywhere (RV-P2-6).
883
+ */
884
+ function renderKitSlotPicker(props, view, kitDraft) {
885
+ const { state, library, dispatch } = view;
886
+ const role = kitDraft.slot === 'subagent' ? 'subagent' : 'binding';
887
+ return _jsx(ModelProfilePicker, { ...props, profiles: library, savedProfiles: state.draft.baseline.modelProfiles, role: role, customProfile: createCustomProfile(state.draft, 'main'), error: state.error, selectedIdOverride: kitSlotSelection(kitDraft), subagentProfileId: draftSubagentProfileId(state.draft), fastTierOff: kitDraft.slot === 'subagent' && fastTierUnavailable(props, view), onSelect: (id) => {
888
+ if (kitDraft.slot === 'main') {
889
+ dispatch({ type: 'kit-main', id });
890
+ return;
891
+ }
892
+ if (kitDraft.slot === 'fast') {
893
+ dispatch({ type: 'kit-fast', id });
894
+ return;
895
+ }
896
+ dispatch({ type: 'kit-subagent',
897
+ model: id === null ? 'main' : id === SUBAGENT_FAST_ROW_ID ? 'fast' : 'profile',
898
+ modelProfileId: id === null || id === SUBAGENT_FAST_ROW_ID ? null : id });
899
+ }, onCreate: () => dispatch({ type: 'edit', profile: createCustomProfile(state.draft, 'main') }), onEdit: (profile) => dispatch({ type: 'edit', profile }), onDuplicate: (id) => dispatch({ type: 'duplicate', id }), onDelete: (id) => dispatch({ type: 'delete', id }), onCancel: () => dispatch({ type: 'kit-cancel' }) });
900
+ }
798
901
  function buildRootSettingsRows(props, view) {
799
902
  const { library, fields, values, persisted, readError, index, glyphs, state } = view;
800
903
  const name = (id) => {
@@ -823,9 +926,29 @@ function buildRootSettingsRows(props, view) {
823
926
  modelProfileId: team.modelProfileId, mainConnection, fastConnection,
824
927
  profiles: state.draft.profiles,
825
928
  hasKey: (id) => (props.apiKeys[id] ?? '').length > 0 });
826
- const lines = [`Main profile ${name(library.mainId)}`,
827
- `Fast profile ${name(library.fastId)}`,
828
- `Subagent model ${subagentLine}`,
929
+ // HELD SLOTS SHOW THE KIT'S EFFECTIVE VALUES (RV-P0-2 / AC-38). Three rows on
930
+ // this page read the FILE's bindings while the header reads the live ones, so
931
+ // without this the screen says `Main profile Opus` over a header that says
932
+ // `haiku` - and the only available conclusion is that the app is broken.
933
+ // `Enter` on any of these rows writes an explicit choice, which releases that
934
+ // slot (§4.3), so what the user sees is what they are about to change.
935
+ const held = heldSlots(state);
936
+ const kitTag = held.kitName ? ` (kit ${held.kitName})` : '';
937
+ const mainValue = `${name(held.mainId ?? library.mainId)}${held.held.includes('main')
938
+ ? kitTag : ''}`;
939
+ const fastValue = held.held.includes('fast')
940
+ ? `${held.fastId ? name(held.fastId) : 'Off (this kit turns the tier off)'}${kitTag}`
941
+ : name(library.fastId);
942
+ const subagentValue = held.subagent
943
+ ? `${subagentModelSummary({ mode: held.subagent.model,
944
+ modelProfileId: held.subagent.modelProfileId, mainConnection, fastConnection,
945
+ profiles: state.draft.profiles,
946
+ hasKey: (id) => (props.apiKeys[id] ?? '').length > 0 })}${kitTag}`
947
+ : subagentLine;
948
+ const lines = [`Main profile ${mainValue}`,
949
+ `Fast profile ${fastValue}`,
950
+ `Subagent model ${subagentValue}`,
951
+ `Kit pack ${kitPackRow(state)}`,
829
952
  `Manage profiles ${library.entries.length} saved`,
830
953
  ...fields.map((item) => `${item.label.padEnd(18)}${item.key === 'apiKey'
831
954
  ? values.apiKey ? 'Configured (replacement)' : props.apiKeys[values.provider]