@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,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
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The prompt editor page (quick-prompt-library §4.4 / D-11).
3
+ *
4
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`. Labels and the footer
5
+ * are English like every other overlay; every sentence that explains a REFUSAL comes
6
+ * from `prompts/copy.ts`.
7
+ *
8
+ * SELF-MANAGED `OverlayFrame`, not the controlled one. The two modes are a
9
+ * discriminated union (`children?: undefined`), and this page lays out a multi-row
10
+ * body field that owns its own scrolling: handing it over as `rows` would mean the
11
+ * frame counts a body's lines as rows and windows them again (P2-2).
12
+ *
13
+ * TWO WAYS TO SAVE, AND THE SECOND ONE IS NOT DECORATION. `Ctrl+S` is the fast path,
14
+ * and it is a byte a terminal can eat (XON/XOFF) - so `Tab` to the `Save` row and
15
+ * `Enter` must do exactly the same thing. That is what keeps "I cannot save" from
16
+ * being a bug a whole class of terminals can reproduce.
17
+ */
18
+ import React from 'react';
19
+ import type { Theme } from '../theme.js';
20
+ import type { TermCapabilities } from '../capabilities.js';
21
+ import { type PromptLibraryState } from './prompt-library-state.js';
22
+ export interface PromptEditorPageProps {
23
+ isActive: boolean;
24
+ state: PromptLibraryState;
25
+ cols: number;
26
+ maxRows: number;
27
+ theme: Theme;
28
+ caps: TermCapabilities;
29
+ reducedMotion?: boolean;
30
+ /** One field changed; the page never owns the text, only the caret. */
31
+ onDraft: (patch: {
32
+ title?: string;
33
+ content?: string;
34
+ description?: string;
35
+ tags?: string;
36
+ }) => void;
37
+ onSave: () => void;
38
+ onEsc: () => void;
39
+ onCancel: () => void;
40
+ }
41
+ /**
42
+ * The page.
43
+ *
44
+ * The draft lives in the parent (the reducer owns it, so `Save` and `Esc` can see the
45
+ * same text); this component owns only what no other surface needs: which row has
46
+ * focus, and where each single-line field's caret sits.
47
+ */
48
+ export declare function PromptEditorPage(props: PromptEditorPageProps): React.ReactElement;
@@ -0,0 +1,227 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The prompt editor page (quick-prompt-library §4.4 / D-11).
4
+ *
5
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`. Labels and the footer
6
+ * are English like every other overlay; every sentence that explains a REFUSAL comes
7
+ * from `prompts/copy.ts`.
8
+ *
9
+ * SELF-MANAGED `OverlayFrame`, not the controlled one. The two modes are a
10
+ * discriminated union (`children?: undefined`), and this page lays out a multi-row
11
+ * body field that owns its own scrolling: handing it over as `rows` would mean the
12
+ * frame counts a body's lines as rows and windows them again (P2-2).
13
+ *
14
+ * TWO WAYS TO SAVE, AND THE SECOND ONE IS NOT DECORATION. `Ctrl+S` is the fast path,
15
+ * and it is a byte a terminal can eat (XON/XOFF) - so `Tab` to the `Save` row and
16
+ * `Enter` must do exactly the same thing. That is what keeps "I cannot save" from
17
+ * being a bug a whole class of terminals can reproduce.
18
+ */
19
+ import { useEffect, useState } from 'react';
20
+ import { Box, Text, useInput } from 'ink';
21
+ import { pickGlyphs } from '../glyphs.js';
22
+ import { stripPasteFrames } from '../paste-frames.js';
23
+ import { stripEnterFrames } from '../enter-frames.js';
24
+ import { stepGrapheme } from '../editor-navigation.js';
25
+ import { OverlayFrame, overlayBodyRows } from '../layout/OverlayFrame.js';
26
+ import { promptErrorCopy } from '../../prompts/copy.js';
27
+ import { PROMPT_EDITOR_FIELDS, fieldIndex, } from './prompt-library-state.js';
28
+ import { EMPTY_TEXT_AREA, PromptTextArea, promptTextAreaReducer, } from './PromptTextArea.js';
29
+ /** Columns the field labels occupy. Fixed, so the values line up. */
30
+ const LABEL_CELLS = 10;
31
+ /** Body rows the page reserves for chrome when the frame is generous. */
32
+ const MIN_BODY_ROWS = 3;
33
+ /** Text fields of the page, in the order Tab visits them. */
34
+ function nextField(at, delta) {
35
+ const count = PROMPT_EDITOR_FIELDS.length;
36
+ return (at + delta + count) % count;
37
+ }
38
+ /**
39
+ * Which deletion key was pressed, if any.
40
+ *
41
+ * `key.delete` IS TREATED AS BACKSPACE, and the reason is measured rather than assumed:
42
+ * ink's parser reports the `0x7f` byte - which is what the BACKSPACE KEY sends on every
43
+ * terminal this product runs on - as `name: 'delete'`, and it hands the component an
44
+ * EMPTY `input` for both it and `ESC [ 3 ~`. The two are therefore indistinguishable at
45
+ * this layer, and the safe reading is the one the keyboard actually produces: backward.
46
+ * A forward-delete binding that fired on Backspace would delete the character the user
47
+ * was not looking at, and there is no undo in this field.
48
+ */
49
+ function deletionKind(_input, key) {
50
+ return key.backspace === true || key.delete === true ? 'backspace' : null;
51
+ }
52
+ /**
53
+ * The page.
54
+ *
55
+ * The draft lives in the parent (the reducer owns it, so `Save` and `Esc` can see the
56
+ * same text); this component owns only what no other surface needs: which row has
57
+ * focus, and where each single-line field's caret sits.
58
+ */
59
+ export function PromptEditorPage(props) {
60
+ const { state, cols, maxRows, theme, caps } = props;
61
+ const draft = state.draft;
62
+ const glyphs = pickGlyphs(caps);
63
+ const [focus, setFocus] = useState(0);
64
+ // THE CARET STARTS AT THE END OF EACH FIELD, which is where a user expects to append:
65
+ // starting at 0 would put every typed character in FRONT of the prefilled text, so an
66
+ // edit to `Clean code review` would read `FixClean code review`.
67
+ const [cursors, setCursors] = useState(() => ({
68
+ title: draft.title.length,
69
+ description: draft.description.length,
70
+ tags: draft.tags.length,
71
+ }));
72
+ const [content, setContent] = useState({
73
+ ...EMPTY_TEXT_AREA,
74
+ buffer: draft.content,
75
+ cursor: draft.content.length,
76
+ });
77
+ const field = PROMPT_EDITOR_FIELDS[Math.min(focus, PROMPT_EDITOR_FIELDS.length - 1)];
78
+ const bodyRows = Math.max(MIN_BODY_ROWS, overlayBodyRows(maxRows, cols) - 5);
79
+ const fieldCols = Math.max(8, cols - LABEL_CELLS - 6);
80
+ // A REFUSAL OWNS THE FOCUS (D-12): the store already names the row that failed, so the
81
+ // caret is moved there instead of leaving the user to read an error in a footer and
82
+ // work out which of four fields it is about. Keyed on `state.error`, so it fires once
83
+ // per distinct refusal rather than on every render.
84
+ useEffect(() => {
85
+ if (state.error === null)
86
+ return;
87
+ setFocus(fieldIndex(state.errorField));
88
+ }, [state.error, state.errorField]);
89
+ const editSingle = (name, text, key, deletion) => {
90
+ const value = draft[name];
91
+ const cursor = Math.min(cursors[name], value.length);
92
+ if (key.leftArrow || key.rightArrow) {
93
+ setCursors({
94
+ ...cursors,
95
+ [name]: stepGrapheme(value, cursor, key.leftArrow ? 'left' : 'right'),
96
+ });
97
+ return;
98
+ }
99
+ if (deletion === 'backspace') {
100
+ if (cursor <= 0)
101
+ return;
102
+ const from = stepGrapheme(value, cursor, 'left');
103
+ props.onDraft({ [name]: value.slice(0, from) + value.slice(cursor) });
104
+ setCursors({ ...cursors, [name]: from });
105
+ return;
106
+ }
107
+ if (text.length === 0)
108
+ return;
109
+ const next = value.slice(0, cursor) + text + value.slice(cursor);
110
+ props.onDraft({ [name]: next });
111
+ setCursors({ ...cursors, [name]: cursor + text.length });
112
+ };
113
+ const applyContent = (action) => {
114
+ const current = { ...content, buffer: draft.content };
115
+ const next = promptTextAreaReducer(current, action);
116
+ // COMPARED ON THE FIELDS, not on object identity: the reducer echoes its input object
117
+ // back for a no-op move, and that object is rebuilt on every keystroke - so an
118
+ // identity check would both hide the no-op and trigger a render for it.
119
+ if (next.buffer !== current.buffer || next.cursor !== current.cursor)
120
+ setContent(next);
121
+ if (next.buffer !== draft.content)
122
+ props.onDraft({ content: next.buffer });
123
+ };
124
+ useInput((input, key) => {
125
+ if (props.isActive === false)
126
+ return;
127
+ // BEFORE EVERYTHING: a refused save and a dirty Esc are both two-step states, and
128
+ // the accelerator that resolves them must not be swallowed by the text handling
129
+ // below. `Ctrl+S` is the same precedent `SettingsScreen.tsx` set.
130
+ if (key.ctrl && (input === 's' || input === 'S')) {
131
+ props.onSave();
132
+ return;
133
+ }
134
+ if (key.escape) {
135
+ props.onEsc();
136
+ return;
137
+ }
138
+ if (key.tab) {
139
+ setFocus(nextField(focus, key.shift ? -1 : 1));
140
+ return;
141
+ }
142
+ const clean = stripPasteFrames(stripEnterFrames(input));
143
+ if (field === 'save') {
144
+ if (key.return)
145
+ props.onSave();
146
+ return;
147
+ }
148
+ if (field === 'cancel') {
149
+ if (key.return)
150
+ props.onCancel();
151
+ return;
152
+ }
153
+ if (field === 'content') {
154
+ if (key.upArrow) {
155
+ applyContent({ type: 'up', cols: fieldCols });
156
+ return;
157
+ }
158
+ if (key.downArrow) {
159
+ applyContent({ type: 'down', cols: fieldCols });
160
+ return;
161
+ }
162
+ if (key.return || clean.includes('\n')) {
163
+ applyContent({ type: 'newline' });
164
+ return;
165
+ }
166
+ const contentDeletion = deletionKind(input, key);
167
+ if (contentDeletion !== null) {
168
+ applyContent({ type: contentDeletion });
169
+ return;
170
+ }
171
+ if (key.leftArrow) {
172
+ applyContent({ type: 'left' });
173
+ return;
174
+ }
175
+ if (key.rightArrow) {
176
+ applyContent({ type: 'right' });
177
+ return;
178
+ }
179
+ if (key.ctrl || key.meta)
180
+ return;
181
+ if (clean.length > 0)
182
+ applyContent({ type: 'insert', text: clean });
183
+ return;
184
+ }
185
+ // Single-line fields: Enter advances rather than saving, because there is nothing
186
+ // for a newline to mean in a one-line field.
187
+ if (key.return) {
188
+ setFocus(nextField(focus, 1));
189
+ return;
190
+ }
191
+ if (key.upArrow) {
192
+ setFocus(nextField(focus, -1));
193
+ return;
194
+ }
195
+ if (key.downArrow) {
196
+ setFocus(nextField(focus, 1));
197
+ return;
198
+ }
199
+ if (key.ctrl || key.meta)
200
+ return;
201
+ editSingle(field, clean, key, deletionKind(input, key));
202
+ });
203
+ /** One text field, or an action row. The caret marks the focused one. */
204
+ const row = (name) => {
205
+ const focused = field === name;
206
+ const caret = focused ? glyphs.caret : ' ';
207
+ if (name === 'save' || name === 'cancel') {
208
+ const label = name === 'save' ? 'Save' : 'Cancel';
209
+ return (_jsx(Text, { wrap: "truncate", color: focused ? theme.accent : theme.muted, children: `${caret} [ ${label} ]` }, name));
210
+ }
211
+ const label = name === 'title' ? 'Title' : name === 'content' ? 'Body'
212
+ : name === 'description' ? 'Note' : 'Tags';
213
+ const value = draft[name];
214
+ if (name === 'content') {
215
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "truncate", color: focused ? theme.accent : theme.muted, children: `${caret} ${label.padEnd(LABEL_CELLS)}` }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: _jsx(PromptTextArea, { state: { ...content, buffer: draft.content }, cols: fieldCols, rows: bodyRows, focused: focused, theme: theme, caps: caps, ...(props.reducedMotion === undefined ? {} : { reducedMotion: props.reducedMotion }) }) })] }, name));
216
+ }
217
+ const shown = value.length === 0 ? '(empty)' : value;
218
+ return (_jsx(Text, { wrap: "truncate", color: focused ? theme.accent : theme.muted, children: `${caret} ${label.padEnd(LABEL_CELLS)}${shown}` }, name));
219
+ };
220
+ const title = state.error === null
221
+ ? (draft.id === null ? 'New prompt' : 'Edit prompt')
222
+ : promptErrorCopy(state.error);
223
+ const hint = state.escArmed
224
+ ? 'Esc again discards the draft | Tab next field'
225
+ : 'Tab next field | Enter next line | Ctrl+S save | Esc close';
226
+ return (_jsxs(OverlayFrame, { title: title, hint: hint, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: [_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [_jsx(Text, { color: draft.favorite ? theme.toolDone : theme.muted, children: draft.favorite ? '[fav] ' : ' ' }), `${[...draft.content].length} chars`] }), _jsx(Box, { flexDirection: "column", children: PROMPT_EDITOR_FIELDS.map((name) => row(name)) })] }));
227
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The prompt manager: `Ctrl+P` (quick-prompt-library §4.4 / §6.4).
3
+ *
4
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * A LIST PLUS A DETAIL BLOCK, the `KitPicker` idiom (F-17): three columns of prompt
7
+ * metadata never fit on a 72-column terminal, and the detail block is how this
8
+ * package already shows everything that does not fit in a row.
9
+ *
10
+ * THE OVERLAY OWNS NO DATA. Every mutation goes out through `onMutate`, and the new
11
+ * snapshot comes back through `library`. An overlay that patched its own copy would
12
+ * show a prompt the disk does not have (I-6), and the first time that happened the
13
+ * user would find out at the next launch.
14
+ */
15
+ import React from 'react';
16
+ import type { Theme } from '../theme.js';
17
+ import type { TermCapabilities } from '../capabilities.js';
18
+ import type { PromptLibrary, PromptLibraryIntent, PromptLibraryMutation, PromptMutationResult } from '../../prompts/types.js';
19
+ export interface PromptLibraryOverlayProps {
20
+ isActive: boolean;
21
+ library: PromptLibrary;
22
+ /** D-21's one-shot hand-off; consumed at most once (I-8). */
23
+ intent?: PromptLibraryIntent;
24
+ cols: number;
25
+ maxRows: number;
26
+ scrollOffset: number;
27
+ onScrollClamp?: (offset: number) => void;
28
+ theme: Theme;
29
+ caps: TermCapabilities;
30
+ reducedMotion?: boolean;
31
+ /** The store, reached through `App` so this component stays a view. */
32
+ onMutate: (action: PromptLibraryMutation) => PromptMutationResult;
33
+ /** One-shot feedback (save, delete, write failure) for `App`'s toast channel. */
34
+ onNotice?: (level: 'info' | 'warn' | 'error', text: string) => void;
35
+ onClose: () => void;
36
+ }
37
+ export declare function PromptLibraryOverlay(props: PromptLibraryOverlayProps): React.ReactElement;