@aragon-agent/cli 0.6.23 → 0.6.25

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 (66) hide show
  1. package/CHANGELOG.md +1835 -1786
  2. package/README.md +2578 -2544
  3. package/dist/agent/controller.d.ts +60 -3
  4. package/dist/agent/controller.js +111 -6
  5. package/dist/agent/conversation-reset.d.ts +91 -0
  6. package/dist/agent/conversation-reset.js +66 -0
  7. package/dist/agent/model-probe-limits.d.ts +39 -0
  8. package/dist/agent/model-probe-limits.js +25 -0
  9. package/dist/agent/model-probe.d.ts +104 -0
  10. package/dist/agent/model-probe.js +391 -0
  11. package/dist/agent/model-profile-settings.js +29 -2
  12. package/dist/agent/reducer.js +45 -13
  13. package/dist/commands/builtins.js +101 -37
  14. package/dist/commands/clear-command.d.ts +27 -0
  15. package/dist/commands/clear-command.js +107 -0
  16. package/dist/commands/registry.d.ts +20 -1
  17. package/dist/compaction/child.d.ts +15 -0
  18. package/dist/compaction/child.js +14 -3
  19. package/dist/compaction/compactor.js +1 -0
  20. package/dist/compaction/context-command.js +10 -1
  21. package/dist/compaction/meter.d.ts +9 -0
  22. package/dist/compaction/meter.js +16 -1
  23. package/dist/compaction/pressure.d.ts +7 -0
  24. package/dist/compaction/pressure.js +5 -0
  25. package/dist/compaction/types.d.ts +11 -0
  26. package/dist/compaction/wiring.js +1 -0
  27. package/dist/config/cli-commands.d.ts +1 -1
  28. package/dist/config/cli-commands.js +12 -0
  29. package/dist/config/load.js +24 -6
  30. package/dist/config/model-profile-resolution.d.ts +8 -0
  31. package/dist/config/model-profile-resolution.js +13 -6
  32. package/dist/config/model-profile-store.d.ts +4 -2
  33. package/dist/config/model-profiles.d.ts +21 -0
  34. package/dist/config/model-profiles.js +28 -2
  35. package/dist/config/schema.d.ts +61 -0
  36. package/dist/config/schema.js +36 -0
  37. package/dist/session/clear-archive.d.ts +51 -0
  38. package/dist/session/clear-archive.js +160 -0
  39. package/dist/team/model.d.ts +88 -0
  40. package/dist/team/model.js +136 -0
  41. package/dist/team/report.d.ts +11 -0
  42. package/dist/team/report.js +24 -1
  43. package/dist/team/subagent.js +37 -21
  44. package/dist/team/task-tool.d.ts +25 -6
  45. package/dist/team/task-tool.js +15 -9
  46. package/dist/todo/types.d.ts +5 -3
  47. package/dist/ui/App.js +44 -7
  48. package/dist/ui/entries/CompactionCard.d.ts +5 -4
  49. package/dist/ui/entries/CompactionCard.js +5 -4
  50. package/dist/ui/overlays/HelpOverlay.js +8 -2
  51. package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
  52. package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
  53. package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
  54. package/dist/ui/overlays/ModelProfileEditor.js +117 -54
  55. package/dist/ui/overlays/ModelProfilePicker.d.ts +14 -2
  56. package/dist/ui/overlays/ModelProfilePicker.js +51 -10
  57. package/dist/ui/overlays/QuestionOverlay.js +47 -6
  58. package/dist/ui/overlays/SettingsScreen.d.ts +17 -0
  59. package/dist/ui/overlays/SettingsScreen.js +152 -18
  60. package/dist/ui/overlays/model-probe-view.d.ts +128 -0
  61. package/dist/ui/overlays/model-probe-view.js +225 -0
  62. package/dist/ui/overlays/model-profile-state.d.ts +62 -5
  63. package/dist/ui/overlays/model-profile-state.js +121 -14
  64. package/dist/ui/overlays/use-model-probe.d.ts +30 -0
  65. package/dist/ui/overlays/use-model-probe.js +109 -0
  66. package/package.json +2 -2
@@ -68,8 +68,14 @@ const COMMANDS = [
68
68
  'Todo planning: status, switches, the rail, follow-through',
69
69
  ],
70
70
  ['/tools', 'List active tools'],
71
- ['/clear', 'Clear the visible transcript and the todo panel'],
72
- ['/reset', 'New conversation'],
71
+ // `/clear` IS NOT "CLEAR THE SCREEN" ANY MORE (clear-command-context-reset D-1).
72
+ // Two rows, not one, because the intentions differ: `/clear` starts a NEW
73
+ // CONVERSATION (interrupt the run, archive the old one, drop the history), while
74
+ // `/clear screen` only wipes what is on screen. A user who wants the second has
75
+ // to be able to see that it exists before reaching for the first.
76
+ ['/clear', 'Start a new conversation: archive, then clear context and transcript'],
77
+ ['/clear screen', 'Clear the visible transcript only; keeps the conversation'],
78
+ ['/reset', 'Alias of /clear'],
73
79
  ['/cwd [dir]', 'Show or change the tool working directory'],
74
80
  ['/save [file]', 'Save the session to JSON'],
75
81
  ['/resume [file]', 'Load a saved session'],
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { type ProfileFrameProps } from './ModelProfilePicker.js';
3
+ import { type DiscoveryView } from './model-probe-view.js';
4
+ interface ModelDiscoveryListProps extends ProfileFrameProps {
5
+ provider: string;
6
+ /** The draft's current model: the cursor starts on it when the list has it. */
7
+ currentModel: string;
8
+ view: DiscoveryView;
9
+ busy: boolean;
10
+ onChoose: (model: string) => void;
11
+ onRetry: () => void;
12
+ onBack: () => void;
13
+ onCancelProbe?: () => void;
14
+ }
15
+ /**
16
+ * The discovered-model page (model-profile-connection-test §3.5).
17
+ *
18
+ * Pure display plus input: it receives the discovery outcome and three intents and
19
+ * issues no request itself. No spinner - the census of animated sites is a closed
20
+ * list, and this overlay can be open while a run is in flight.
21
+ */
22
+ export declare function ModelDiscoveryList(props: ModelDiscoveryListProps): React.ReactElement;
23
+ export {};
@@ -0,0 +1,161 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Text, useInput } from 'ink';
4
+ import { PROBE_LIMITS } from '../../agent/model-probe-limits.js';
5
+ import { pickGlyphs } from '../glyphs.js';
6
+ import { OverlayFrame } from '../layout/OverlayFrame.js';
7
+ import { stripPasteFrames } from '../paste-frames.js';
8
+ import { stripEnterFrames } from '../enter-frames.js';
9
+ import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
10
+ import { rankCandidates, windowLabel, clipName, LIST_ID_CHARS } from './model-probe-view.js';
11
+ function matches(candidate, query) {
12
+ const needle = query.toLocaleLowerCase();
13
+ return candidate.id.toLocaleLowerCase().includes(needle)
14
+ || candidate.name.toLocaleLowerCase().includes(needle);
15
+ }
16
+ /** One row: id column, name column, window right-aligned inside the frame budget. */
17
+ function listRowText(candidate, cols) {
18
+ const width = Math.max(20, cols - 8);
19
+ const label = windowLabel(candidate.contextWindow);
20
+ const idWidth = Math.max(10, Math.min(LIST_ID_CHARS, width - label.length - 14));
21
+ const nameWidth = Math.max(4, width - idWidth - label.length - 2);
22
+ const id = [...candidate.id].slice(0, idWidth).join('').padEnd(idWidth);
23
+ // `clipName` caps the NAME COLUMN itself, so a vendor that ships a paragraph in
24
+ // `name` cannot push the window label out of a wide frame either.
25
+ const name = [...clipName(candidate.name)].slice(0, nameWidth).join('').padEnd(nameWidth);
26
+ return clipProfileText(`${id} ${name} ${label}`, width);
27
+ }
28
+ /** What the page says when there is no row to select. */
29
+ function emptyLines(busy, provider, view) {
30
+ if (busy)
31
+ return [`Discovering ${provider} models...`];
32
+ if (view.kind === 'discovery' && !view.outcome.ok) {
33
+ return [view.outcome.detail ?? 'The model list could not be fetched.',
34
+ 'Fix: check the base URL and key, then press r to retry.'];
35
+ }
36
+ if (view.kind === 'discovery') {
37
+ return ['No models returned. Press r to retry, Esc to go back.',
38
+ 'Test the connection for a precise reason.'];
39
+ }
40
+ return ['No models discovered yet. Press r to retry, Esc to go back.'];
41
+ }
42
+ function buildRows(props, view, glyphs) {
43
+ const rows = [];
44
+ if (view.searchable) {
45
+ rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [view.searching ? `${glyphs.caret} ` : ' ', "Search: ", view.query || '(/ to search)'] }, "search"));
46
+ }
47
+ if (view.shown.length === 0) {
48
+ const lines = view.query.length > 0
49
+ ? [`No model matches "${view.query}". Esc clears the search.`]
50
+ : emptyLines(props.busy, props.provider, props.view);
51
+ for (const line of lines) {
52
+ rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: line }, `empty-${rows.length}`));
53
+ }
54
+ }
55
+ view.shown.forEach((candidate, at) => {
56
+ const focused = at === view.cursor && !view.searching;
57
+ rows.push(_jsxs(Text, { wrap: "truncate", color: at === view.cursor ? props.theme.accent : props.theme.muted, children: [focused ? `${glyphs.caret} ` : ' ', listRowText(candidate, props.cols)] }, candidate.id));
58
+ });
59
+ if (view.truncated) {
60
+ const total = props.view.kind === 'discovery'
61
+ ? props.view.outcome.total ?? view.shown.length : view.shown.length;
62
+ rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: `Showing first ${PROBE_LIMITS.maxListedModels} of ${total}. Narrow with search.` }, "truncated"));
63
+ }
64
+ return rows;
65
+ }
66
+ /**
67
+ * The discovered-model page (model-profile-connection-test §3.5).
68
+ *
69
+ * Pure display plus input: it receives the discovery outcome and three intents and
70
+ * issues no request itself. No spinner - the census of animated sites is a closed
71
+ * list, and this overlay can be open while a run is in flight.
72
+ */
73
+ export function ModelDiscoveryList(props) {
74
+ const [index, setIndex] = useState(0);
75
+ const [query, setQuery] = useState('');
76
+ const [searching, setSearching] = useState(false);
77
+ const glyphs = pickGlyphs(props.caps);
78
+ const candidates = props.view.kind === 'discovery'
79
+ ? props.view.outcome.models : [];
80
+ const ranked = rankCandidates(candidates, props.currentModel);
81
+ const shown = query.length > 0 ? ranked.filter((item) => matches(item, query)) : ranked;
82
+ const searchable = ranked.length > PROBE_LIMITS.searchThreshold;
83
+ const cursor = shown.length > 0 ? Math.min(index, shown.length - 1) : 0;
84
+ // The truncation notice is one row BELOW the last candidate, and `useProfileScroll`
85
+ // only guarantees the FOCUSED row is visible - the same blind spot that hid the
86
+ // editor's result rows (RV-P1-3). Standing on the last row therefore widens the
87
+ // window by one, or `Showing first 400 of ...` could never be read at all.
88
+ const truncatedNotice = props.view.kind === 'discovery' && props.view.outcome.truncated
89
+ && shown.length > 0;
90
+ const revealNotice = truncatedNotice && cursor >= shown.length - 1;
91
+ const offset = useProfileScroll(props, cursor + (searchable ? 1 : 0) + (revealNotice ? 1 : 0));
92
+ // Arrival, not every render: when the outcome lands, put the cursor on the model the
93
+ // draft already names, so "which one am I on" is answerable without scrolling.
94
+ const arrival = props.view.kind === 'discovery'
95
+ ? `${props.view.outcome.connectionId}|${ranked.length}` : 'none';
96
+ useEffect(() => {
97
+ const at = ranked.findIndex((item) => item.id === props.currentModel.trim());
98
+ setIndex(at > 0 ? at : 0);
99
+ }, [arrival]);
100
+ const move = (delta) => {
101
+ if (shown.length > 0)
102
+ setIndex((cursor + delta + shown.length) % shown.length);
103
+ };
104
+ useInput((input, key) => {
105
+ if (props.isActive === false)
106
+ return;
107
+ if (key.escape) {
108
+ if (searching) {
109
+ setSearching(false);
110
+ setQuery('');
111
+ setIndex(0);
112
+ return;
113
+ }
114
+ if (props.busy) {
115
+ props.onCancelProbe?.();
116
+ return;
117
+ }
118
+ props.onBack();
119
+ return;
120
+ }
121
+ if (key.upArrow || (key.tab && key.shift)) {
122
+ move(-1);
123
+ return;
124
+ }
125
+ if (key.downArrow || key.tab) {
126
+ move(1);
127
+ return;
128
+ }
129
+ if (key.return) {
130
+ const picked = shown[cursor];
131
+ if (picked)
132
+ props.onChoose(picked.id);
133
+ return;
134
+ }
135
+ if (searching) {
136
+ if (key.backspace || key.delete) {
137
+ setQuery((value) => [...value].slice(0, -1).join(''));
138
+ setIndex(0);
139
+ }
140
+ else if (input && !key.ctrl && !key.meta) {
141
+ setQuery((value) => value + stripPasteFrames(stripEnterFrames(input)));
142
+ setIndex(0);
143
+ }
144
+ return;
145
+ }
146
+ if (input === '/' && searchable) {
147
+ setSearching(true);
148
+ setIndex(0);
149
+ return;
150
+ }
151
+ // `r` retries ONLY outside the search state: sharing the key with the query would
152
+ // turn one typo into a network request (§3.5 / D-16).
153
+ if (!key.ctrl && !key.meta && input === 'r')
154
+ props.onRetry();
155
+ });
156
+ const hint = props.busy ? 'Esc cancel'
157
+ : searching ? 'Type search | Enter choose | Esc back'
158
+ : `Enter choose${searchable ? ' | / search' : ''} | r retry | Esc back`;
159
+ return _jsx(OverlayFrame, { ...props, scrollOffset: offset, hint: hint, rows: buildRows(props, { query, shown, cursor, searchable, searching,
160
+ truncated: truncatedNotice }, glyphs), title: `Models | ${props.provider} (${shown.length})` });
161
+ }
@@ -1,12 +1,29 @@
1
1
  import React from 'react';
2
+ import type { ProbeTarget } from '../../agent/model-probe.js';
2
3
  import { type ProfileFrameProps } from './ModelProfilePicker.js';
3
4
  import { type ProfileEditorDraft, type ModelProfileAction } from './model-profile-state.js';
5
+ import { type ProbeView } from './model-probe-view.js';
4
6
  interface ModelProfileEditorProps extends ProfileFrameProps {
5
7
  editor: ProfileEditorDraft;
6
8
  users: string;
7
9
  error?: string;
8
10
  dispatch: React.Dispatch<ModelProfileAction>;
11
+ /**
12
+ * The rendered probe state and its three intents. ALL OPTIONAL: a standalone or
13
+ * embedded mount has no probe port, and the two action rows then read
14
+ * `(unavailable)` instead of pretending to work (§6.1 / AC-15).
15
+ */
16
+ probeView?: ProbeView;
17
+ probeBusy?: boolean;
18
+ probeTarget?: ProbeTarget;
19
+ onTest?: () => void;
20
+ onDiscover?: () => void;
21
+ onCancelProbe?: () => void;
9
22
  }
10
- /** Replacement key input starts empty; retention across targets is explicit. */
23
+ /**
24
+ * The profile form. Row semantics come from `editorRows()` - never from index
25
+ * arithmetic - so `apiKey` stays at index 6 and the two probe actions sit at the end
26
+ * of the action block, after `retain` (P0-1 / I-4).
27
+ */
11
28
  export declare function ModelProfileEditor(props: ModelProfileEditorProps): React.ReactElement;
12
29
  export {};
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Text, useInput } from 'ink';
4
4
  import { ADAPTER_PROVIDERS } from '../../config/schema.js';
@@ -7,49 +7,47 @@ import { OverlayFrame } from '../layout/OverlayFrame.js';
7
7
  import { stripPasteFrames } from '../paste-frames.js';
8
8
  import { stripEnterFrames } from '../enter-frames.js';
9
9
  import { pickGlyphs } from '../glyphs.js';
10
- import { profileEndpointSummary, useProfileScroll } from './ModelProfilePicker.js';
10
+ import { useProfileScroll } from './ModelProfilePicker.js';
11
11
  import { finishProfileEditor } from './model-profile-state.js';
12
- const FIELDS = ['name', 'provider', 'model', 'baseUrl', 'credentialMode', 'apiKey'];
13
- const LABELS = ['Name', 'Provider', 'Model', 'Base URL', 'Credential mode', 'API key'];
14
- function useEditorInput(props, navigation) {
12
+ import { EDITOR_FIELDS, editorRows, editorProbeTarget, focusableRows, resultRowCount } from './model-probe-view.js';
13
+ function useEditorInput(props, focus) {
15
14
  const { editor, dispatch } = props;
16
- const { index, setIndex, retain, count } = navigation;
15
+ const { index, setIndex, count, focusable } = focus;
17
16
  function commit() {
18
17
  const result = finishProfileEditor(editor);
19
18
  if (result.field) {
20
- const at = FIELDS.indexOf(result.field);
19
+ // Field rows are the first focusables, in `EDITOR_FIELDS` order, so the jump an
20
+ // invalid field asks for is a lookup rather than an offset guess.
21
+ const at = EDITOR_FIELDS.indexOf(result.field);
21
22
  if (at >= 0)
22
23
  setIndex(at);
23
24
  }
24
25
  dispatch({ type: 'merge-editor' });
25
26
  }
26
- useInput((input, key) => {
27
- if (props.isActive === false)
28
- return;
29
- if (key.escape) {
30
- dispatch({ type: 'cancel-editor' });
31
- return;
32
- }
33
- if (key.upArrow || (key.tab && key.shift)) {
34
- setIndex((index - 1 + count) % count);
27
+ function activate(row) {
28
+ if (!row)
35
29
  return;
36
- }
37
- if (key.downArrow || key.tab) {
38
- setIndex((index + 1) % count);
30
+ if (row.kind === 'field') {
31
+ commit();
39
32
  return;
40
33
  }
41
- if (key.return) {
42
- if (retain && index === FIELDS.length)
43
- dispatch({ type: 'retain-key' });
44
- else if (index === count - 1)
45
- dispatch({ type: 'cancel-editor' });
46
- else
47
- commit();
34
+ if (row.kind !== 'action')
48
35
  return;
49
- }
50
- const field = FIELDS[index];
51
- if (!field || key.ctrl || key.meta)
36
+ if (row.action === 'retain')
37
+ dispatch({ type: 'retain-key' });
38
+ if (row.action === 'test')
39
+ props.onTest?.();
40
+ if (row.action === 'discover')
41
+ props.onDiscover?.();
42
+ if (row.action === 'apply')
43
+ commit();
44
+ if (row.action === 'cancel')
45
+ dispatch({ type: 'cancel-editor' });
46
+ }
47
+ function editField(row, input, key) {
48
+ if (row.kind !== 'field')
52
49
  return;
50
+ const field = row.field;
53
51
  if (field === 'provider' || field === 'credentialMode') {
54
52
  if (!key.leftArrow && !key.rightArrow)
55
53
  return;
@@ -70,36 +68,101 @@ function useEditorInput(props, navigation) {
70
68
  registerSecret(value);
71
69
  if (key.backspace || key.delete || text)
72
70
  dispatch({ type: 'editor-field', field, value });
71
+ }
72
+ useInput((input, key) => {
73
+ if (props.isActive === false)
74
+ return;
75
+ // Accelerators work on EVERY row (§3.3): the action rows are the primary entry,
76
+ // these are only the shortcut. Both keys are unused by `App`'s global branch.
77
+ if (key.ctrl && input === 'r') {
78
+ props.onTest?.();
79
+ return;
80
+ }
81
+ if (key.ctrl && input === 'f') {
82
+ props.onDiscover?.();
83
+ return;
84
+ }
85
+ if (key.escape) {
86
+ // THREE-LEVEL Esc (§3.4): a probe in flight is cancelled first and the draft
87
+ // survives; only the second press discards the form.
88
+ if (props.probeBusy) {
89
+ props.onCancelProbe?.();
90
+ return;
91
+ }
92
+ dispatch({ type: 'cancel-editor' });
93
+ return;
94
+ }
95
+ if (key.upArrow || (key.tab && key.shift)) {
96
+ setIndex((index - 1 + count) % count);
97
+ return;
98
+ }
99
+ if (key.downArrow || key.tab) {
100
+ setIndex((index + 1) % count);
101
+ return;
102
+ }
103
+ if (key.return) {
104
+ activate(focusable[index]);
105
+ return;
106
+ }
107
+ if (key.ctrl || key.meta)
108
+ return;
109
+ const row = focusable[index];
110
+ if (row)
111
+ editField(row, input, key);
73
112
  });
74
113
  }
75
- function buildEditorRows(props, navigation) {
76
- const { editor } = props;
77
- const { index, retain } = navigation;
114
+ function rowColor(props, row) {
115
+ if (row.kind === 'result') {
116
+ return row.tone === 'ok' ? props.theme.toolDone
117
+ : row.tone === 'fail' ? props.theme.noticeError : props.theme.muted;
118
+ }
119
+ return undefined;
120
+ }
121
+ function buildRows(props, focus) {
78
122
  const glyphs = pickGlyphs(props.caps);
79
- const values = FIELDS.map((field) => {
80
- if (field === 'apiKey')
81
- return editor.credentialMode === 'replace'
82
- ? editor.apiKey ? 'Configured (replacement)' : '(type replacement)'
83
- : editor.credentialMode === 'keep' ? 'Configured (unchanged)' : 'Shared / environment';
84
- if (field === 'credentialMode')
85
- return editor.credentialMode;
86
- return editor[field] || (field === 'name' ? '(defaults to model)' : field === 'baseUrl'
87
- ? '(provider default)' : '(required)');
123
+ const focused = focus.focusable[focus.index];
124
+ return focus.rows.map((row, at) => {
125
+ if (row.kind === 'note') {
126
+ return _jsx(Text, { wrap: "truncate", color: props.theme.muted, children: row.label }, at);
127
+ }
128
+ if (row.kind === 'result') {
129
+ return _jsx(Text, { wrap: "truncate", color: rowColor(props, row), children: row.label }, at);
130
+ }
131
+ const line = row.value.length > 0 ? `${row.label.padEnd(18)}${row.value}` : row.label;
132
+ const isFocused = row === focused;
133
+ return _jsxs(Text, { wrap: "truncate", color: isFocused ? props.theme.accent : props.theme.muted, children: [isFocused ? `${glyphs.caret} ` : ' ', line] }, at);
88
134
  });
89
- const lines = LABELS.map((label, at) => `${label.padEnd(18)}${values[at]}`);
90
- if (retain)
91
- lines.push('Retain key for new endpoint');
92
- lines.push('Apply to settings draft', 'Cancel editing');
93
- const rows = lines.map((line, at) => _jsxs(Text, { wrap: "truncate", color: at === index ? props.theme.accent : props.theme.muted, children: [at === index ? `${glyphs.caret} ` : ' ', line] }, at));
94
- rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: ["Target: ", editor.provider, " ", profileEndpointSummary(editor.baseUrl)] }, "target"));
95
- return rows;
96
135
  }
97
- /** Replacement key input starts empty; retention across targets is explicit. */
136
+ function editorHint(props) {
137
+ if (props.probeBusy)
138
+ return 'Esc cancel the probe | draft is kept';
139
+ return 'Enter apply to draft | Ctrl+R test | Ctrl+F discover | Esc cancel';
140
+ }
141
+ /**
142
+ * The profile form. Row semantics come from `editorRows()` - never from index
143
+ * arithmetic - so `apiKey` stays at index 6 and the two probe actions sit at the end
144
+ * of the action block, after `retain` (P0-1 / I-4).
145
+ */
98
146
  export function ModelProfileEditor(props) {
99
147
  const [index, setIndex] = useState(0);
100
- const retain = props.editor.endpointChanged && Boolean(props.editor.original.apiKey);
101
- const navigation = { index, setIndex, retain, count: FIELDS.length + (retain ? 1 : 0) + 2 };
102
- const offset = useProfileScroll(props, index);
103
- useEditorInput(props, navigation);
104
- return _jsx(OverlayFrame, { ...props, rows: buildEditorRows(props, navigation), scrollOffset: offset, title: props.error ?? `Edit profile${props.users ? ` | Used by ${props.users}` : ''}`, hint: "Enter apply to draft | Esc cancel" });
148
+ const editor = props.editor;
149
+ const retain = editor.endpointChanged && Boolean(editor.original.apiKey);
150
+ const probe = props.probeView ?? { kind: 'idle', reason: 'unavailable' };
151
+ const target = props.probeTarget ?? editorProbeTarget(editor, {});
152
+ const rows = editorRows({ editor, retain, probe, target, fastOnly: props.users === 'Fast' });
153
+ const focusable = focusableRows(rows);
154
+ const count = Math.max(1, focusable.length);
155
+ const clamped = Math.min(index, count - 1);
156
+ // The result rows sit BELOW the action block, so following the focused row alone
157
+ // would leave the verdict off-screen right after the user asked for it (§4.9 / AC-21).
158
+ // How many action rows there are comes from the row model, never from a constant: a
159
+ // sixth action would otherwise move this window boundary onto a FIELD row.
160
+ const actionRows = focusable.filter((row) => row.kind === 'action').length;
161
+ const results = resultRowCount(rows);
162
+ const inActions = clamped >= count - actionRows;
163
+ const viewFocus = results > 0 && inActions ? Math.max(clamped, rows.length - 1) : clamped;
164
+ const offset = useProfileScroll(props, viewFocus);
165
+ useEditorInput(props, { index: clamped, setIndex, count, rows, focusable });
166
+ return _jsx(OverlayFrame, { ...props, rows: buildRows(props, { index: clamped, setIndex, count,
167
+ rows, focusable }), scrollOffset: offset, title: props.error ?? `Edit profile${props.users ? ` | Used by ${props.users}` : ''}`, hint: editorHint(props) });
105
168
  }
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
- import type { ModelProfile, ModelProfilesConfig, ModelRole } from '../../config/model-profiles.js';
2
+ import type { ModelProfile, ModelProfilesConfig } from '../../config/model-profiles.js';
3
3
  import type { Theme } from '../theme.js';
4
4
  import type { TermCapabilities } from '../capabilities.js';
5
+ import { type ProfilePickerRole } from './model-profile-state.js';
5
6
  export interface ProfileFrameProps {
6
7
  isActive?: boolean;
7
8
  theme: Theme;
@@ -19,11 +20,22 @@ export declare function profileEndpointSummary(baseUrl: string | null): string;
19
20
  interface ModelProfilePickerProps extends ProfileFrameProps {
20
21
  profiles: ModelProfilesConfig;
21
22
  savedProfiles?: ModelProfilesConfig;
22
- role: ModelRole;
23
+ /** Which role this page selects for; `'subagent'` swaps the built-in rows (D-5). */
24
+ role: ProfilePickerRole;
23
25
  manager?: boolean;
24
26
  error?: string;
25
27
  apiKeys: Record<string, string | undefined>;
26
28
  customProfile?: ModelProfile;
29
+ /**
30
+ * The selection id for a role the library has no `${role}Id` key for: the
31
+ * subagent projection (`Same as main` = null, fast = the sentinel, profile
32
+ * = its id). Takes precedence over the `${role}Id` lookup when present.
33
+ */
34
+ selectedIdOverride?: string | null;
35
+ /** The subagent binding, for the reference suffix on profile labels (3.7). */
36
+ subagentProfileId?: string | null;
37
+ /** Whether to annotate the built-in `Fast tier model` row with `(fast tier off)`. */
38
+ fastTierOff?: boolean;
27
39
  onSelect: (id: string | null) => void;
28
40
  onCreate: () => void;
29
41
  onEdit: (profile: ModelProfile) => void;
@@ -6,7 +6,7 @@ import { pickGlyphs } from '../glyphs.js';
6
6
  import { OverlayFrame, overlayBodyRows } from '../layout/OverlayFrame.js';
7
7
  import { stripPasteFrames } from '../paste-frames.js';
8
8
  import { stripEnterFrames } from '../enter-frames.js';
9
- import { profileUsers } from './model-profile-state.js';
9
+ import { profileUsers, SUBAGENT_FAST_ROW_ID } from './model-profile-state.js';
10
10
  /** Move focus into view while accepting App's independent wheel/page scrolling. */
11
11
  export function useProfileScroll(props, focus) {
12
12
  const [offset, setOffset] = useState(0);
@@ -62,7 +62,9 @@ function usePickerState(props) {
62
62
  const entries = props.profiles.entries.filter((entry) => `${entry.name} ${entry.provider} ${entry.model}`.toLocaleLowerCase()
63
63
  .includes(query.toLocaleLowerCase()));
64
64
  const actions = props.manager ? ['New', 'Edit', 'Duplicate', 'Delete'] : ['Create profile'];
65
- const prefix = props.manager ? 0 : 1;
65
+ // THE SUBAGENT PICKER swaps the `Current custom` row for TWO built-in rows:
66
+ // `Same as main` (selection id null) and `Fast tier model` (the sentinel).
67
+ const prefix = props.role === 'subagent' ? 2 : props.manager ? 0 : 1;
66
68
  const count = entries.length + prefix + actions.length;
67
69
  const cursor = Math.min(index, count - 1);
68
70
  const focused = entries[cursor - prefix];
@@ -76,7 +78,18 @@ function usePickerState(props) {
76
78
  function usePickerInput(props, state) {
77
79
  const { cursor, focused, selected, actions, prefix, entries, count, searching, searchable, setIndex, setQuery, setSearching } = state;
78
80
  function activate() {
79
- if (!props.manager && cursor === 0) {
81
+ if (props.role === 'subagent') {
82
+ // Built-in rows first: `Same as main`, then `Fast tier model`.
83
+ if (cursor === 0) {
84
+ props.onSelect(null);
85
+ return;
86
+ }
87
+ if (cursor === 1) {
88
+ props.onSelect(SUBAGENT_FAST_ROW_ID);
89
+ return;
90
+ }
91
+ }
92
+ else if (!props.manager && cursor === 0) {
80
93
  props.onSelect(null);
81
94
  return;
82
95
  }
@@ -138,11 +151,26 @@ function usePickerInput(props, state) {
138
151
  else if (!key.ctrl && !key.meta && input.toLowerCase() === 'e' && focused) {
139
152
  props.onEdit(focused);
140
153
  }
154
+ // `d` deletes the focused entry (D-7), symmetric with `e`: same guards,
155
+ // same no-ops on the built-in and action rows (`focused` is undefined
156
+ // there), and the `searching` early-return above keeps `d` a query
157
+ // character while typing a search.
158
+ else if (!key.ctrl && !key.meta && input.toLowerCase() === 'd' && focused) {
159
+ props.onDelete(focused.id);
160
+ }
141
161
  });
142
162
  }
163
+ /** The selection id this page marks with `[Selected]`, by role (6.2). */
164
+ function currentSelectionId(props) {
165
+ if (props.selectedIdOverride !== undefined)
166
+ return props.selectedIdOverride;
167
+ if (props.role === 'subagent')
168
+ return undefined;
169
+ return props.profiles[`${props.role}Id`];
170
+ }
143
171
  function profilePickerLabel(props, entry) {
144
- const saved = profileUsers(props.savedProfiles, entry.id);
145
- const checked = props.profiles[`${props.role}Id`] === entry.id ? '[Selected] ' : '';
172
+ const saved = profileUsers(props.savedProfiles, entry.id, props.subagentProfileId);
173
+ const checked = currentSelectionId(props) === entry.id ? '[Selected] ' : '';
146
174
  const duplicate = props.profiles.entries.some((other) => other.id !== entry.id
147
175
  && other.name === entry.name);
148
176
  const identity = duplicate ? `#${uniqueId(entry, props.profiles.entries)} ` : '';
@@ -154,18 +182,30 @@ function buildPickerRows(props, state) {
154
182
  const { entries, actions, cursor, searching, searchable, query, focused, selected } = state;
155
183
  const glyphs = pickGlyphs(props.caps);
156
184
  const lines = [];
157
- if (!props.manager)
185
+ if (props.role === 'subagent') {
186
+ // TWO BUILT-IN ROWS (D-5), marked with the same `[Selected]` prefix the
187
+ // profile rows use; the fast row annotates an unusable tier honestly.
188
+ const current = props.selectedIdOverride ?? null;
189
+ lines.push(`${current === null ? '[Selected] ' : ''}Same as main`);
190
+ lines.push(`${current === SUBAGENT_FAST_ROW_ID ? '[Selected] ' : ''}Fast tier model`
191
+ + `${props.fastTierOff === true ? ' (fast tier off)' : ''}`);
192
+ }
193
+ else if (!props.manager)
158
194
  lines.push('Current custom');
159
195
  lines.push(...entries.map((entry) => profilePickerLabel(props, entry)));
160
196
  lines.push(...actions);
161
197
  const rows = lines.map((line, row) => _jsxs(Text, { wrap: "truncate", color: row === cursor ? props.theme.accent : props.theme.muted, children: [`${row === cursor && !searching ? glyphs.caret : ' '} `, clipProfileText(line, props.cols - 8)] }, row));
162
198
  if (searchable)
163
199
  rows.unshift(_jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [searching ? `${glyphs.caret} ` : ' ', "Search: ", query || '(/ to search)'] }, "search"));
164
- const detail = !props.manager && cursor === 0 ? props.customProfile : focused ?? selected;
200
+ // The subagent picker has no `Current custom` row to describe; its
201
+ // built-in rows already say everything there is to say.
202
+ const detail = props.role === 'subagent' ? focused
203
+ : !props.manager && cursor === 0 ? props.customProfile : focused ?? selected;
165
204
  if (detail) {
166
205
  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"));
167
206
  rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: detail.apiKey ? 'Profile key: Configured'
168
207
  : props.apiKeys[detail.provider] ? 'Shared / environment: Configured' : 'Missing API key' }, "key"));
208
+ rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: `Window: ${detail.contextWindow ?? 'auto'}` }, "window"));
169
209
  }
170
210
  return rows;
171
211
  }
@@ -174,7 +214,8 @@ export function ModelProfilePicker(props) {
174
214
  const state = usePickerState(props);
175
215
  usePickerInput(props, state);
176
216
  return _jsx(OverlayFrame, { ...props, title: props.error ?? (props.manager ? 'Manage profiles'
177
- : `${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'
178
- : props.manager ? 'Enter action | e edit | Esc back'
179
- : 'Enter select | e edit | Esc back' });
217
+ : 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'
219
+ : props.manager ? 'Enter action | e edit | d delete | Esc back'
220
+ : 'Enter select | e edit | d delete | Esc back' });
180
221
  }