@aragon-agent/cli 0.6.34 → 0.6.36

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 (185) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +173 -7
  3. package/dist/agent/agent-mode.d.ts +20 -4
  4. package/dist/agent/agent-mode.js +20 -4
  5. package/dist/agent/controller.d.ts +67 -3
  6. package/dist/agent/controller.js +106 -5
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/kit-apply.d.ts +56 -0
  9. package/dist/agent/kit-apply.js +209 -0
  10. package/dist/agent/model-profile-settings.d.ts +28 -0
  11. package/dist/agent/model-profile-settings.js +142 -29
  12. package/dist/agent/reducer.d.ts +13 -2
  13. package/dist/agent/reducer.js +1 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/agents-command.d.ts +15 -0
  16. package/dist/commands/agents-command.js +27 -0
  17. package/dist/commands/builtins.js +135 -4
  18. package/dist/commands/kit-command.d.ts +20 -0
  19. package/dist/commands/kit-command.js +131 -0
  20. package/dist/commands/perf.d.ts +11 -0
  21. package/dist/commands/perf.js +2 -1
  22. package/dist/commands/registry.d.ts +14 -0
  23. package/dist/config/app-paths.d.ts +15 -0
  24. package/dist/config/app-paths.js +33 -2
  25. package/dist/config/env.d.ts +4 -1
  26. package/dist/config/env.js +28 -2
  27. package/dist/config/kit-resolution.d.ts +93 -0
  28. package/dist/config/kit-resolution.js +226 -0
  29. package/dist/config/load.d.ts +10 -0
  30. package/dist/config/load.js +8 -1
  31. package/dist/config/model-kits.d.ts +224 -0
  32. package/dist/config/model-kits.js +302 -0
  33. package/dist/config/model-profile-store.d.ts +25 -0
  34. package/dist/config/model-profiles.d.ts +41 -3
  35. package/dist/config/model-profiles.js +46 -5
  36. package/dist/config/schema.d.ts +84 -0
  37. package/dist/config/schema.js +39 -0
  38. package/dist/config/store.js +12 -1
  39. package/dist/config/ui-state.d.ts +18 -0
  40. package/dist/config/ui-state.js +9 -0
  41. package/dist/exec/events.d.ts +25 -0
  42. package/dist/exec/events.js +4 -0
  43. package/dist/exec/index.js +16 -1
  44. package/dist/exec/runner.js +10 -0
  45. package/dist/input/keymap.js +18 -4
  46. package/dist/proc/limits.d.ts +16 -0
  47. package/dist/proc/limits.js +16 -0
  48. package/dist/proc/supervisor.d.ts +20 -1
  49. package/dist/proc/supervisor.js +42 -14
  50. package/dist/prompts/command.d.ts +34 -0
  51. package/dist/prompts/command.js +237 -0
  52. package/dist/prompts/copy.d.ts +89 -0
  53. package/dist/prompts/copy.js +131 -0
  54. package/dist/prompts/fill.d.ts +51 -0
  55. package/dist/prompts/fill.js +46 -0
  56. package/dist/prompts/limits.d.ts +53 -0
  57. package/dist/prompts/limits.js +53 -0
  58. package/dist/prompts/normalize.d.ts +53 -0
  59. package/dist/prompts/normalize.js +190 -0
  60. package/dist/prompts/resolve.d.ts +31 -0
  61. package/dist/prompts/resolve.js +60 -0
  62. package/dist/prompts/search.d.ts +58 -0
  63. package/dist/prompts/search.js +146 -0
  64. package/dist/prompts/store.d.ts +86 -0
  65. package/dist/prompts/store.js +453 -0
  66. package/dist/prompts/types.d.ts +204 -0
  67. package/dist/prompts/types.js +13 -0
  68. package/dist/session/checkpoint.d.ts +8 -1
  69. package/dist/session/checkpoint.js +6 -0
  70. package/dist/session/persist.d.ts +19 -1
  71. package/dist/session/persist.js +13 -1
  72. package/dist/session/startup-session.d.ts +7 -2
  73. package/dist/session/startup-session.js +5 -2
  74. package/dist/session/store.js +3 -0
  75. package/dist/session/validate-session.js +64 -1
  76. package/dist/team/limits.d.ts +25 -0
  77. package/dist/team/limits.js +26 -0
  78. package/dist/team/runtime.js +27 -2
  79. package/dist/team/settled.d.ts +57 -0
  80. package/dist/team/settled.js +87 -0
  81. package/dist/team/subagent.js +48 -2
  82. package/dist/team/timeline.d.ts +74 -0
  83. package/dist/team/timeline.js +101 -0
  84. package/dist/team/types.d.ts +24 -1
  85. package/dist/todo/acceptance.d.ts +120 -0
  86. package/dist/todo/acceptance.js +399 -0
  87. package/dist/todo/follow-through.d.ts +48 -23
  88. package/dist/todo/follow-through.js +110 -44
  89. package/dist/todo/limits.d.ts +62 -1
  90. package/dist/todo/limits.js +62 -1
  91. package/dist/todo/panel-rows.d.ts +29 -39
  92. package/dist/todo/panel-rows.js +37 -83
  93. package/dist/todo/plan-sections.d.ts +60 -0
  94. package/dist/todo/plan-sections.js +108 -0
  95. package/dist/todo/prompt.d.ts +17 -12
  96. package/dist/todo/prompt.js +59 -40
  97. package/dist/todo/row-window.d.ts +51 -0
  98. package/dist/todo/row-window.js +107 -0
  99. package/dist/todo/store.d.ts +139 -31
  100. package/dist/todo/store.js +271 -73
  101. package/dist/todo/todo-tool.d.ts +33 -8
  102. package/dist/todo/todo-tool.js +186 -30
  103. package/dist/todo/types.d.ts +58 -0
  104. package/dist/ui/App.js +414 -54
  105. package/dist/ui/Composer.d.ts +6 -2
  106. package/dist/ui/Composer.js +2 -2
  107. package/dist/ui/Header.d.ts +17 -1
  108. package/dist/ui/Header.js +8 -2
  109. package/dist/ui/PromptInput.d.ts +21 -1
  110. package/dist/ui/PromptInput.js +22 -2
  111. package/dist/ui/ScrollbackShell.d.ts +22 -7
  112. package/dist/ui/ScrollbackShell.js +8 -1
  113. package/dist/ui/StatusBar.d.ts +18 -0
  114. package/dist/ui/StatusBar.js +11 -3
  115. package/dist/ui/TeamPanel.d.ts +35 -41
  116. package/dist/ui/TeamPanel.js +53 -163
  117. package/dist/ui/TodoPanel.d.ts +17 -14
  118. package/dist/ui/TodoPanel.js +146 -38
  119. package/dist/ui/Transcript.js +23 -3
  120. package/dist/ui/composer-input.d.ts +10 -0
  121. package/dist/ui/composer-input.js +57 -24
  122. package/dist/ui/entries/TeamCard.d.ts +18 -2
  123. package/dist/ui/entries/TeamCard.js +47 -13
  124. package/dist/ui/entries/TodoCard.d.ts +26 -11
  125. package/dist/ui/entries/TodoCard.js +56 -15
  126. package/dist/ui/glyphs.d.ts +19 -0
  127. package/dist/ui/glyphs.js +4 -0
  128. package/dist/ui/layout/agents-board.d.ts +102 -0
  129. package/dist/ui/layout/agents-board.js +389 -0
  130. package/dist/ui/layout/cells.d.ts +61 -0
  131. package/dist/ui/layout/cells.js +156 -0
  132. package/dist/ui/layout/header-hints.d.ts +23 -3
  133. package/dist/ui/layout/header-hints.js +73 -7
  134. package/dist/ui/layout/kit-chip.d.ts +50 -0
  135. package/dist/ui/layout/kit-chip.js +81 -0
  136. package/dist/ui/layout/status-detail-layout.js +9 -0
  137. package/dist/ui/layout/status-layout.d.ts +13 -0
  138. package/dist/ui/layout/team-row.d.ts +139 -0
  139. package/dist/ui/layout/team-row.js +246 -0
  140. package/dist/ui/layout/todo-layout.d.ts +38 -0
  141. package/dist/ui/layout/todo-layout.js +75 -0
  142. package/dist/ui/layout/virtual-window.js +32 -3
  143. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  144. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  145. package/dist/ui/overlays/HelpOverlay.js +24 -2
  146. package/dist/ui/overlays/KitEditor.d.ts +23 -0
  147. package/dist/ui/overlays/KitEditor.js +101 -0
  148. package/dist/ui/overlays/KitManager.d.ts +23 -0
  149. package/dist/ui/overlays/KitManager.js +104 -0
  150. package/dist/ui/overlays/KitPicker.d.ts +49 -0
  151. package/dist/ui/overlays/KitPicker.js +58 -0
  152. package/dist/ui/overlays/ModelProfilePicker.js +16 -6
  153. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  154. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  155. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  156. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  157. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  158. package/dist/ui/overlays/PromptPicker.js +129 -0
  159. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  160. package/dist/ui/overlays/PromptTextArea.js +141 -0
  161. package/dist/ui/overlays/SettingsScreen.js +139 -16
  162. package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
  163. package/dist/ui/overlays/kit-picker-state.js +136 -0
  164. package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
  165. package/dist/ui/overlays/kit-settings-state.js +267 -0
  166. package/dist/ui/overlays/model-profile-state.d.ts +26 -3
  167. package/dist/ui/overlays/model-profile-state.js +59 -6
  168. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  169. package/dist/ui/overlays/prompt-library-state.js +335 -0
  170. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  171. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  172. package/dist/ui/scrollback/LiveTail.d.ts +2 -2
  173. package/dist/ui/scrollback/LiveTail.js +7 -2
  174. package/dist/ui/scrollback/band.js +14 -1
  175. package/dist/ui/scrollback/live-preview.js +10 -3
  176. package/dist/ui/scrollback/tail-window.d.ts +12 -0
  177. package/dist/ui/scrollback/tail-window.js +17 -2
  178. package/dist/ui/scrollback/use-scrollback-view.js +7 -0
  179. package/dist/ui/team-status.d.ts +166 -0
  180. package/dist/ui/team-status.js +252 -0
  181. package/dist/ui/transcript-text.js +13 -4
  182. package/dist/ui/use-startup-notices.d.ts +35 -1
  183. package/dist/ui/use-startup-notices.js +29 -2
  184. package/dist/ui/use-wheel-routing.js +10 -2
  185. package/package.json +2 -2
@@ -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
- export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' | 'delete-confirm' | 'discard-confirm';
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
- returnPage: 'picker' | 'manager';
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
  /**
@@ -41,10 +41,38 @@ export function createModelProfileState(draft) {
41
41
  return { draft: copy, page: 'root', role: 'main', returnPage: 'manager',
42
42
  bindNew: false, dirty: false };
43
43
  }
44
+ /** The kits whose three slots name `id`, for the delete confirmation (D-9). */
45
+ export function kitsUsingProfile(profiles, id) {
46
+ return (profiles?.kits ?? []).filter((kit) => kit.mainProfileId === id
47
+ || kit.fastProfileId === id
48
+ || (kit.subagent.model === 'profile' && kit.subagent.modelProfileId === id));
49
+ }
50
+ /**
51
+ * Drop `slot` from the session kit (§4.3 row 2 / AC-38).
52
+ *
53
+ * THE DRAFT IS NARROWED HERE, NOT ONLY AT SAVE TIME. `releasedSlots` already
54
+ * strips the same slot from the transaction before it commits, but the ROOT PAGE
55
+ * paints the kit's effective value for every HELD slot - so without this line a
56
+ * user picks a profile and the row keeps showing the one they just replaced,
57
+ * which is the most confusing thing a settings screen can do. Releasing the last
58
+ * slot ends the kit: `held: []` is not a state any surface can render.
59
+ */
60
+ function releaseHeldSlot(draft, slot) {
61
+ const current = draft.sessionKit;
62
+ if (!current || !current.held.includes(slot))
63
+ return draft;
64
+ const held = current.held.filter((item) => item !== slot);
65
+ return { ...draft, sessionKit: held.length ? { id: current.id, held } : null };
66
+ }
44
67
  /** Describe references using text so ASCII and no-color modes remain usable. */
45
68
  export function profileUsers(profiles, id, subagentProfileId) {
69
+ // THE KITS ARE REFERENCES TOO (D-9 / RV-P2-6): "Main + kit "Deep work"" is what
70
+ // makes the confirmation honest, because deleting this entry also deletes that
71
+ // kit. The empty case must stay empty - a leading ` + ` is what a naive
72
+ // `join` after an unconditional push produces.
46
73
  return [profiles?.mainId === id ? 'Main' : '', profiles?.fastId === id ? 'Fast' : '',
47
- subagentProfileId === id ? 'Subagent' : '']
74
+ subagentProfileId === id ? 'Subagent' : '',
75
+ ...kitsUsingProfile(profiles, id).map((kit) => `kit "${kit.name}"`)]
48
76
  .filter(Boolean).join(' + ');
49
77
  }
50
78
  /**
@@ -155,14 +183,27 @@ export function modelProfileReducer(state, action) {
155
183
  ? { model: 'fast', modelProfileId: null }
156
184
  : { model: 'profile', modelProfileId: action.id };
157
185
  return { ...state, page: 'root', dirty: true, error: undefined,
158
- draft: { ...state.draft, patch: { ...state.draft.patch, team } } };
186
+ draft: releaseHeldSlot({ ...state.draft,
187
+ patch: { ...state.draft.patch, team } }, 'subagent') };
159
188
  }
189
+ // `'binding'` NEVER REACHES THIS REDUCER: a kit slot's choice is applied
190
+ // by `kitReducer` (`kit-main` / `kit-fast`), because the value belongs to
191
+ // an editor draft rather than to a `${role}Id` library key. Returning the
192
+ // state unchanged is the honest answer to an impossible action.
193
+ if (action.role === 'binding')
194
+ return state;
160
195
  return { ...state, page: 'root', dirty: true, error: undefined,
161
- draft: { ...state.draft, profiles: { ...library, [`${action.role}Id`]: action.id },
162
- activateRoles: [...new Set([...state.draft.activateRoles, action.role])] } };
196
+ draft: releaseHeldSlot({ ...state.draft,
197
+ profiles: { ...library, [`${action.role}Id`]: action.id },
198
+ activateRoles: [...new Set([...state.draft.activateRoles, action.role])] }, action.role) };
163
199
  }
164
200
  case 'edit': return { ...state, editor: createProfileEditor(action.profile), page: 'editor',
165
- returnPage: state.page === 'picker' ? 'picker' : 'manager',
201
+ // A PROFILE BORN IN A KIT SLOT RETURNS TO THAT SLOT (§4.5): the picker's
202
+ // `Create profile` action is the only way to invent one and it is offered on
203
+ // the kit's slot chooser - landing in the profile MANAGER there would
204
+ // abandon the half-built kit behind a page the user never asked for.
205
+ returnPage: state.page === 'picker' || state.page === 'kit-slot'
206
+ ? state.page : 'manager',
166
207
  bindNew: action.bindNew ?? false, error: undefined };
167
208
  case 'editor-field': return editField(state, action);
168
209
  case 'retain-key': return state.editor?.original.apiKey ? { ...state,
@@ -206,17 +247,29 @@ export function modelProfileReducer(state, action) {
206
247
  // when it named THIS entry. `removeModelProfile`'s throw stays as the
207
248
  // bottom-line defense - the UI has already unbound, so it cannot fire
208
249
  // on this path.
250
+ //
251
+ // AND THE KITS GO WITH IT (agent-model-kits D-9 / I-8): a kit naming a
252
+ // deleted profile can neither be applied nor repaired from any screen, so
253
+ // leaving it behind would be leaving a permanent error. The confirmation
254
+ // page has already named them, so this is the action the user agreed to.
255
+ const doomed = kitsUsingProfile(library, id);
256
+ const defaultKitKept = doomed.some((kit) => kit.id === (library.defaultKitId ?? null))
257
+ ? null : library.defaultKitId;
258
+ const sessionReleased = doomed.some((kit) => kit.id === state.draft.sessionKit?.id)
259
+ ? null : state.draft.sessionKit;
209
260
  const profiles = {
210
261
  ...library,
211
262
  mainId: library.mainId === id ? null : library.mainId,
212
263
  fastId: library.fastId === id ? null : library.fastId,
213
264
  entries: library.entries.filter((entry) => entry.id !== id),
265
+ kits: (library.kits ?? []).filter((kit) => !doomed.includes(kit)),
266
+ defaultKitId: defaultKitKept,
214
267
  };
215
268
  const teamReset = draftSubagentProfileId(state.draft) === id
216
269
  ? { team: { model: 'main', modelProfileId: null } }
217
270
  : {};
218
271
  return { ...state, page: 'manager', dirty: true,
219
- draft: { ...state.draft, profiles,
272
+ draft: { ...state.draft, profiles, sessionKit: sessionReleased,
220
273
  patch: { ...state.draft.patch, ...teamReset } },
221
274
  deleteId: undefined, deleteUsers: undefined,
222
275
  // No orphan editor for a deleted entry (3.7).
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The prompt manager's state machine and view models (quick-prompt-library §4.4).
3
+ *
4
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`. Every sentence the
5
+ * user reads comes from `prompts/copy.ts`; nothing in this file may hold a literal
6
+ * non-ASCII byte.
7
+ *
8
+ * WHY A PURE REDUCER AND NOT `useState` INSIDE THE COMPONENT: the page stack has two
9
+ * transitions that are easy to get wrong and impossible to see on screen - "the
10
+ * second Esc discards, the first does not" and "the intent is consumed exactly
11
+ * once" - and both are assertions, not screenshots. Everything below can be driven
12
+ * from a test with no Ink mounted.
13
+ */
14
+ import type { PromptDraftInput, PromptDraftVerdict, PromptEditorDraft, PromptLibrary, PromptLibraryEntry, PromptWriteError, QuickPrompt } from '../../prompts/types.js';
15
+ export type PromptLibraryPage = 'list' | 'editor' | 'delete';
16
+ /** The six rows `Tab` cycles on the editor page, in visual order. */
17
+ export declare const PROMPT_EDITOR_FIELDS: readonly ["title", "content", "description", "tags", "save", "cancel"];
18
+ export type PromptEditorField = (typeof PROMPT_EDITOR_FIELDS)[number];
19
+ export interface PromptLibraryState {
20
+ page: PromptLibraryPage;
21
+ /** Focus index on the list page (clamped by the view). */
22
+ index: number;
23
+ /**
24
+ * Id the focus should land on when the list is next drawn.
25
+ *
26
+ * BY ID, NEVER BY POSITION: a create appends and an edit does not move the entry,
27
+ * but a delete shifts every row after it, and "focus row 3" would then point at a
28
+ * different prompt than the one the user was working on.
29
+ */
30
+ focusId: string | null;
31
+ draft: PromptEditorDraft;
32
+ /** The entry the draft was seeded from, so `dirty` is a comparison, not a guess. */
33
+ baseline: PromptEditorDraft | null;
34
+ /** The first Esc on a dirty editor arms the discard; any edit disarms it. */
35
+ escArmed: boolean;
36
+ pendingDeleteId: string | null;
37
+ error: PromptWriteError | null;
38
+ /** The row that should own focus after a refusal (D-12). */
39
+ errorField: PromptEditorField | null;
40
+ /** Highest intent nonce this overlay has acted on (I-8). */
41
+ consumedIntentNonce: number;
42
+ }
43
+ export type PromptLibraryAction = {
44
+ type: 'init';
45
+ entry: PromptLibraryEntry;
46
+ library: PromptLibrary;
47
+ nonce: number;
48
+ } | {
49
+ type: 'list';
50
+ focusId?: string;
51
+ } | {
52
+ type: 'focus';
53
+ index: number;
54
+ } | {
55
+ type: 'create';
56
+ seed?: {
57
+ title?: string;
58
+ content?: string;
59
+ };
60
+ } | {
61
+ type: 'edit';
62
+ id: string;
63
+ library: PromptLibrary;
64
+ } | {
65
+ type: 'delete';
66
+ id: string;
67
+ } | {
68
+ type: 'draft';
69
+ patch: Partial<PromptEditorDraft>;
70
+ } | {
71
+ type: 'commit';
72
+ id: string | null;
73
+ } | {
74
+ type: 'failed';
75
+ code: PromptWriteError;
76
+ field?: PromptEditorField;
77
+ } | {
78
+ type: 'esc';
79
+ } | {
80
+ type: 'cancelled';
81
+ };
82
+ /** A new, empty draft. `id: null` is what makes `Save` a create rather than an update. */
83
+ export declare const EMPTY_PROMPT_DRAFT: PromptEditorDraft;
84
+ export declare const INITIAL_PROMPT_LIBRARY_STATE: PromptLibraryState;
85
+ /** The editable projection of a stored entry. */
86
+ export declare function draftFromEntry(entry: QuickPrompt): PromptEditorDraft;
87
+ /** A create draft seeded from a command (`/prompts add title :: body`). */
88
+ export declare function seededDraft(seed?: {
89
+ title?: string;
90
+ content?: string;
91
+ }): PromptEditorDraft;
92
+ /** Structural equality is enough: every field is a string or a boolean. */
93
+ export declare function draftIsDirty(draft: PromptEditorDraft, baseline: PromptEditorDraft | null): boolean;
94
+ /**
95
+ * Validate a draft and turn it into what the store accepts (D-12).
96
+ *
97
+ * THE VALIDATOR IS `store.ts`'S, not a second copy here: the sentence the editor
98
+ * shows and the rule the write applies have to be the same rule, or a user can be
99
+ * told a title is fine by the form and refused by the store.
100
+ */
101
+ export declare function finishPromptEditor(draft: PromptEditorDraft): PromptDraftVerdict;
102
+ /** Index of the first focusable row, for a refusal that named a field. */
103
+ export declare function fieldIndex(field: PromptEditorField | null): number;
104
+ /**
105
+ * The reducer.
106
+ *
107
+ * `init` is the ONLY action that can jump straight to the editor or the delete page:
108
+ * that is what makes `/prompts edit 2` land where it should (D-21). The intent's
109
+ * nonce is stored as `consumedIntentNonce` by the component before dispatching, so a
110
+ * replayed intent is a no-op at the component, not a second edit here.
111
+ */
112
+ export declare function promptLibraryReducer(state: PromptLibraryState, action: PromptLibraryAction, library: PromptLibrary): PromptLibraryState;
113
+ export interface PromptLibraryRow {
114
+ id: string;
115
+ /** One-line render text: the title plus its markers. The caret is the component's. */
116
+ label: string;
117
+ markers: {
118
+ favorite: boolean;
119
+ hasTags: boolean;
120
+ uses: number;
121
+ };
122
+ /** Body length in code points; the detail block and the delete page share it. */
123
+ chars: number;
124
+ }
125
+ /** `1.2k` for anything a fixed-width column cannot show in full. */
126
+ export declare function formatCount(count: number): string;
127
+ /** `3d ago` / `2h ago` / `just now`. Deliberately coarse: it is a hint, not a clock. */
128
+ export declare function formatAgo(timestamp: number | undefined, now: number): string;
129
+ /**
130
+ * The list, one row per entry, in the library's own order.
131
+ *
132
+ * DUPLICATE TITLES GET AN ID PREFIX (I-2), exactly as `kit-picker-state.ts` does for
133
+ * duplicate kit names: two rows reading the same words make `Enter` a coin flip.
134
+ */
135
+ export declare function libraryRows(library: PromptLibrary): PromptLibraryRow[];
136
+ /**
137
+ * The detail block under the list: at most `detailMaxLines` rows, fixed shape.
138
+ *
139
+ * FIXED, because `OverlayFrame`'s window is shared with the list (D-16): a body that
140
+ * participated in row height would squeeze six prompts into two visible rows. The
141
+ * full text has exactly one home, and it is the editor.
142
+ */
143
+ export declare function libraryDetail(row: PromptLibraryRow | undefined, library: PromptLibrary, cols: number, now?: number): string[];
144
+ /**
145
+ * The list page's footer.
146
+ *
147
+ * THE LAST CLAUSE IS ALWAYS THE WAY OUT (§9.5): a narrow terminal clips from the
148
+ * left, so the reader always keeps `Esc close`. The reorder keys are named here
149
+ * rather than in the row above it because they are the one affordance a user cannot
150
+ * discover by pressing a key they already know.
151
+ */
152
+ export declare function libraryHint(state: PromptLibraryState, count: number): string;
153
+ /** The delete page's four lines: what it is, how big, and what it costs. */
154
+ export declare function deleteDetail(library: PromptLibrary, id: string | null): string[];
155
+ /**
156
+ * The draft as the store accepts it.
157
+ *
158
+ * THE TAGS FIELD IS PARSED HERE, ONCE, and nowhere else: the editor edits a raw
159
+ * string, the store validates a `string[]`, and a second parser between them is how
160
+ * `a,b` and `a b` come to mean different things depending on which surface saved it.
161
+ */
162
+ export declare function draftToInput(draft: PromptEditorDraft): PromptDraftInput;