@aragon-agent/cli 0.6.23 → 0.6.24

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 (42) hide show
  1. package/README.md +20 -2
  2. package/dist/agent/controller.d.ts +11 -0
  3. package/dist/agent/controller.js +33 -3
  4. package/dist/agent/model-profile-settings.js +29 -2
  5. package/dist/commands/builtins.js +87 -3
  6. package/dist/compaction/child.d.ts +15 -0
  7. package/dist/compaction/child.js +14 -3
  8. package/dist/compaction/compactor.js +1 -0
  9. package/dist/compaction/context-command.js +10 -1
  10. package/dist/compaction/meter.d.ts +9 -0
  11. package/dist/compaction/meter.js +16 -1
  12. package/dist/compaction/pressure.d.ts +7 -0
  13. package/dist/compaction/pressure.js +5 -0
  14. package/dist/compaction/types.d.ts +11 -0
  15. package/dist/compaction/wiring.js +1 -0
  16. package/dist/config/cli-commands.d.ts +1 -1
  17. package/dist/config/cli-commands.js +12 -0
  18. package/dist/config/load.js +24 -6
  19. package/dist/config/model-profile-resolution.d.ts +8 -0
  20. package/dist/config/model-profile-resolution.js +13 -6
  21. package/dist/config/model-profile-store.d.ts +4 -2
  22. package/dist/config/model-profiles.d.ts +13 -0
  23. package/dist/config/model-profiles.js +18 -2
  24. package/dist/config/schema.d.ts +61 -0
  25. package/dist/config/schema.js +36 -0
  26. package/dist/team/model.d.ts +88 -0
  27. package/dist/team/model.js +136 -0
  28. package/dist/team/report.d.ts +11 -0
  29. package/dist/team/report.js +24 -1
  30. package/dist/team/subagent.js +37 -21
  31. package/dist/team/task-tool.d.ts +25 -6
  32. package/dist/team/task-tool.js +15 -9
  33. package/dist/ui/App.js +5 -0
  34. package/dist/ui/overlays/ModelProfileEditor.js +18 -4
  35. package/dist/ui/overlays/ModelProfilePicker.d.ts +14 -2
  36. package/dist/ui/overlays/ModelProfilePicker.js +51 -10
  37. package/dist/ui/overlays/QuestionOverlay.js +47 -6
  38. package/dist/ui/overlays/SettingsScreen.d.ts +9 -0
  39. package/dist/ui/overlays/SettingsScreen.js +119 -17
  40. package/dist/ui/overlays/model-profile-state.d.ts +53 -4
  41. package/dist/ui/overlays/model-profile-state.js +111 -14
  42. package/package.json +2 -2
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { useEffect, useReducer, useRef, useState } from 'react';
12
12
  import { Text, useInput } from 'ink';
13
13
  import { DEFAULT_MAX_OUTPUT_TOKENS, resolveOutputTokens, } from '@aragon-agent/core';
14
- import { ADAPTER_PROVIDERS, THINKING_LEVELS, maskSecret, parseMaxTokensInput, parseThresholdInput, } from '../../config/schema.js';
14
+ import { ADAPTER_PROVIDERS, THINKING_LEVELS, maskSecret, parseContextWindowInput, parseMaxTokensInput, parseThresholdInput, } from '../../config/schema.js';
15
15
  import { LOG_LEVEL_NAMES } from '../../logging/levels.js';
16
16
  import { pickGlyphs } from '../glyphs.js';
17
17
  import { OverlayFrame } from '../layout/OverlayFrame.js';
@@ -21,7 +21,20 @@ import { emptyModelProfiles } from '../../config/model-profiles.js';
21
21
  import { registerSecret } from '../../logging/secret-registry.js';
22
22
  import { ModelProfilePicker, useProfileScroll } from './ModelProfilePicker.js';
23
23
  import { ModelProfileEditor } from './ModelProfileEditor.js';
24
- import { createModelProfileState, createCustomProfile, modelProfileReducer, profileUsers } from './model-profile-state.js';
24
+ import { createModelProfileState, createCustomProfile, modelProfileReducer, profileUsers, draftTeam, draftSubagentProfileId, subagentModelSummary, subagentSelectedId } from './model-profile-state.js';
25
+ /**
26
+ * THE ROOT PAGE'S ROW INDEXES, AS NAMES (subagent-model V-5).
27
+ *
28
+ * Five offset sites used to agree by accident on the number 3; the Subagent
29
+ * model row moves every one of them. Each site below now cites its constant,
30
+ * so adding a row later is a one-line change instead of a silent regression
31
+ * that focuses an error message on the wrong field.
32
+ */
33
+ const ROOT_MAIN = 0;
34
+ const ROOT_FAST = 1;
35
+ const ROOT_SUBAGENT = 2;
36
+ const ROOT_MANAGE = 3;
37
+ const ROOT_FIRST_FIELD = 4;
25
38
  const SHOW_THINKING_OPTIONS = ['off', 'on'];
26
39
  const LIVE_OUTPUT_OPTIONS = ['off', 'on'];
27
40
  const FAST_ENABLED_OPTIONS = ['off', 'on'];
@@ -172,6 +185,11 @@ const FIELDS = [
172
185
  // `.padEnd(18)` enforces below and that IF-H7 records as silently overflowing.
173
186
  { key: 'liveToolOutput', label: 'Live output', kind: 'enum', options: LIVE_OUTPUT_OPTIONS },
174
187
  { key: 'maxTokens', label: 'Max tokens', kind: 'text' },
188
+ // DIRECTLY UNDER `Max tokens`: the two token rows read together. `Context
189
+ // window` is 14 characters, inside the 18-column label budget `.padEnd(18)`
190
+ // enforces below (IF-H7). Only ever RENDERED while Main is unbound - see
191
+ // `SettingsValues.contextWindow` and the unbound-group filter below.
192
+ { key: 'contextWindow', label: 'Context window', kind: 'text' },
175
193
  // The fast tier sits AFTER `Max tokens` and BEFORE `API key`, because the key
176
194
  // row is the one a user scrolls to deliberately and putting five rows past it
177
195
  // would bury it (fast-model-tier §4.5). The rationale survives a fifth row;
@@ -348,6 +366,11 @@ function LegacySettingsScreen({ isActive = true, initial, apiKeys, maxRows, cols
348
366
  // setting, which is the one thing this field can never be.
349
367
  if (f.key === 'maxTokens' && raw.length === 0)
350
368
  return 'auto';
369
+ // An empty window field is AUTO, not "unset" - the same reading, and the
370
+ // second of the TWO render points that must agree (P2-1: the profile
371
+ // screen's row fallback below is the other).
372
+ if (f.key === 'contextWindow' && raw.length === 0)
373
+ return 'auto';
351
374
  // `(not set)` and NOT `(same as main)`, which is the honest reading of §3.2
352
375
  // rule 3: an empty `fast.model` does not inherit, it means the tier is
353
376
  // unconfigured. `(same as main)` is a fact the derived line below states
@@ -385,15 +408,23 @@ function LegacySettingsScreen({ isActive = true, initial, apiKeys, maxRows, cols
385
408
  rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: "Enter saves. Keys persist to the config file (0600)." }, "__save"));
386
409
  return (_jsx(OverlayFrame, { title: "Settings", hint: `${glyphs.arrowUp}${glyphs.arrowDown} field ${glyphs.midDot} ${glyphs.arrowLeft}${glyphs.arrowRight} change ${glyphs.midDot} Enter save ${glyphs.midDot} Esc close`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: focusedOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
387
410
  }
411
+ // `contextWindow` joins the exclusion list so a bound Main can never leak a
412
+ // SECOND copy of the row through PROFILE_FIELDS - the root row belongs to the
413
+ // unbound group below, exclusively (AC-11).
388
414
  const PROFILE_FIELDS = FIELDS.filter((field) => ![
389
415
  'provider', 'model', 'baseUrl', 'apiKey', 'fastProvider', 'fastModel', 'fastEnabled',
416
+ 'contextWindow',
390
417
  ].includes(field.key));
391
418
  function profileSettingsValues(initial, draft) {
392
419
  const file = draft.baseline;
393
420
  return { ...initial, provider: file.provider ?? initial.provider,
394
421
  model: file.model ?? initial.model,
395
422
  baseUrl: file.baseUrl ?? '', apiKey: '', fastModel: file.fast?.model ?? '',
396
- fastProvider: file.fast?.provider || FAST_INHERIT_PROVIDER };
423
+ fastProvider: file.fast?.provider || FAST_INHERIT_PROVIDER,
424
+ // SEEDED FROM THE FILE VALUE, like its provider/model/baseUrl siblings
425
+ // (AC-11 / P2-3): when env holds an override the file value is not the
426
+ // effective one, and this screen edits the FILE.
427
+ contextWindow: file.contextWindow == null ? 'auto' : String(file.contextWindow) };
397
428
  }
398
429
  /** Only fields changed by the user belong in a settings transaction. */
399
430
  export function readDirtySettings(initial, values) {
@@ -422,6 +453,15 @@ export function readDirtySettings(initial, values) {
422
453
  throw new Error('Max tokens: use a number or auto.');
423
454
  patch.maxTokens = parsed.kind === 'auto' ? null : parsed.value;
424
455
  }
456
+ if (dirty('contextWindow')) {
457
+ // TYPO CHANGES NOTHING: the message's `Context window:` prefix is what
458
+ // `saveProfileSettings`'s focus logic matches to jump to this row. The
459
+ // silent clamp on a value mirrors `config set contextWindow`.
460
+ const parsed = parseContextWindowInput(values.contextWindow);
461
+ if (parsed.kind === 'invalid')
462
+ throw new Error('Context window: use a number or auto.');
463
+ patch.contextWindow = parsed.kind === 'auto' ? null : parsed.value;
464
+ }
425
465
  const fast = {};
426
466
  const parsedFast = readFastSettings(values);
427
467
  if (dirty('fastModel'))
@@ -501,11 +541,13 @@ function useProfileSettingsState(props) {
501
541
  const dirty = state.dirty || Object.keys(values).some((key) => values[key] !== initial[key]);
502
542
  const fields = [FIELDS.find((item) => item.key === 'fastEnabled'),
503
543
  ...FIELDS.filter((item) => library.mainId === null
504
- && ['provider', 'model', 'baseUrl', 'apiKey'].includes(item.key)),
544
+ && ['provider', 'model', 'baseUrl', 'contextWindow', 'apiKey'].includes(item.key)),
505
545
  ...FIELDS.filter((item) => library.fastId === null
506
546
  && ['fastModel', 'fastProvider'].includes(item.key)), ...PROFILE_FIELDS];
507
- const count = fields.length + 5;
508
- const field = fields[index - 3];
547
+ // SIX FIXED ROWS now (Main, Fast, SUBAGENT, Manage, Save, Reload) around
548
+ // the `fields` block - see the ROOT_* constants for the five offset sites.
549
+ const count = fields.length + 6;
550
+ const field = fields[index - ROOT_FIRST_FIELD];
509
551
  const readError = state.draft.readError;
510
552
  const confirming = state.page === 'delete-confirm' || state.page === 'discard-confirm';
511
553
  return { state, dispatch, initial, setInitial, values, setValues, index, setIndex,
@@ -526,7 +568,7 @@ function saveProfileSettings(props, view) {
526
568
  setError(message);
527
569
  const at = fields.findIndex((item) => message.startsWith(`${item.label}:`));
528
570
  if (at >= 0)
529
- setIndex(at + 3);
571
+ setIndex(at + ROOT_FIRST_FIELD);
530
572
  return;
531
573
  }
532
574
  const result = props.onProfileSave({
@@ -549,6 +591,7 @@ function reloadProfileSettings(props, view) {
549
591
  showThinking: live.showThinking ? 'on' : 'off',
550
592
  liveToolOutput: live.liveToolOutput ? 'on' : 'off',
551
593
  maxTokens: live.maxTokens === undefined ? '' : String(live.maxTokens),
594
+ contextWindow: live.contextWindow == null ? 'auto' : String(live.contextWindow),
552
595
  logLevel: live.log?.level ?? props.initial.logLevel,
553
596
  ...(live.fast ? fastSettingsFrom(live.fast) : {}),
554
597
  ...(live.compaction ? compactionSettingsFrom(live.compaction) : {}),
@@ -585,9 +628,15 @@ function activateRootSetting(props, view) {
585
628
  }
586
629
  if (readError || persisted)
587
630
  return;
588
- if (index < 2)
589
- dispatch({ type: 'page', page: 'picker', role: index === 0 ? 'main' : 'fast' });
590
- else if (index === 2)
631
+ if (index === ROOT_MAIN || index === ROOT_FAST) {
632
+ dispatch({ type: 'page', page: 'picker', role: index === ROOT_MAIN ? 'main' : 'fast' });
633
+ }
634
+ else if (index === ROOT_SUBAGENT) {
635
+ // The user-owned cost switch (D-5): its own picker mode, writing
636
+ // `draft.patch.team` rather than a `${role}Id` library key.
637
+ dispatch({ type: 'page', page: 'picker', role: 'subagent' });
638
+ }
639
+ else if (index === ROOT_MANAGE)
591
640
  dispatch({ type: 'page', page: 'manager' });
592
641
  else
593
642
  saveProfileSettings(props, view);
@@ -669,16 +718,48 @@ function renderSettingsConfirmation(props, view) {
669
718
  const { state, library, confirmIndex, glyphs } = view;
670
719
  const deleting = state.page === 'delete-confirm';
671
720
  const name = library.entries.find((entry) => entry.id === state.deleteId)?.name ?? '';
672
- const labels = deleting ? ['Cancel', 'Delete'] : ['Continue', 'Discard'];
673
- return _jsx(OverlayFrame, { ...props, title: deleting ? `Delete ${name}?` : 'Discard unsaved changes?', hint: "Enter choose | Esc continue", rows: labels
674
- .map((label, at) => _jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [confirmIndex === at ? `${glyphs.caret} ` : ' ', label] }, label)), scrollOffset: confirmIndex });
721
+ // UNBIND-AND-DELETE (D-6): a referenced entry stays deletable in ONE flow,
722
+ // and the button names what will happen.
723
+ const labels = deleting
724
+ ? ['Cancel', state.deleteUsers ? 'Unbind and delete' : 'Delete']
725
+ : ['Continue', 'Discard'];
726
+ const rows = labels
727
+ .map((label, at) => _jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [confirmIndex === at ? `${glyphs.caret} ` : ' ', label] }, label));
728
+ // V-8: A RENDER-ONLY ROW, pushed AFTER the buttons. `confirmIndex` stays a
729
+ // 0..1 index over the two buttons and `scrollOffset` follows it, so this
730
+ // line must never sit between them or take an array slot of its own.
731
+ if (deleting && state.deleteUsers) {
732
+ rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: ["Used by ", state.deleteUsers, ". The binding(s) will be cleared."] }, "users"));
733
+ }
734
+ return _jsx(OverlayFrame, { ...props, title: deleting ? `Delete ${name}?` : 'Discard unsaved changes?', hint: "Enter choose | Esc continue", rows: rows, scrollOffset: confirmIndex });
735
+ }
736
+ /**
737
+ * Whether the fast tier resolves FOR DISPLAY while the subagent picker is
738
+ * open (D-5): the built-in `Fast tier model` row then annotates itself with
739
+ * `(fast tier off)`, so choosing it is never a surprise fallback. The same
740
+ * enabled + model + config-file-key approximation `fastTierLine` uses - and
741
+ * the same env-key blindness (V-10), accepted for consistency with it.
742
+ */
743
+ function fastTierUnavailable(props, view) {
744
+ const { values } = view;
745
+ if (values.fastEnabled !== 'on')
746
+ return true;
747
+ if (values.fastModel.trim().length === 0)
748
+ return true;
749
+ const provider = values.fastProvider === FAST_INHERIT_PROVIDER
750
+ || values.fastProvider.trim().length === 0 ? values.provider : values.fastProvider;
751
+ return (props.apiKeys[provider] ?? '').length === 0;
675
752
  }
676
753
  function renderProfileSubpage(props, view) {
677
754
  const { state, library, dispatch, setConfirmIndex } = view;
678
755
  if (state.page === 'editor' && state.editor)
679
- return _jsx(ModelProfileEditor, { ...props, editor: state.editor, users: profileUsers(library, state.editor.original.id), error: state.error, dispatch: dispatch });
756
+ return _jsx(ModelProfileEditor, { ...props, editor: state.editor, users: profileUsers(library, state.editor.original.id, draftSubagentProfileId(state.draft)), error: state.error, dispatch: dispatch });
757
+ // A NEW PROFILE SEEDED FROM THE MAIN CONNECTION while the subagent picker is
758
+ // open: 'subagent' is picker-only (6.2), so the seed role falls back to
759
+ // 'main' while `state.role` keeps driving the select that binds it.
760
+ const createRole = state.role === 'subagent' ? 'main' : state.role;
680
761
  if (state.page === 'picker' || state.page === 'manager')
681
- return _jsx(ModelProfilePicker, { ...props, profiles: library, savedProfiles: state.draft.baseline.modelProfiles, role: state.role, customProfile: createCustomProfile(state.draft, state.role), manager: state.page === 'manager', error: state.error, onSelect: (id) => dispatch({ type: 'select', role: state.role, id }), onCreate: () => dispatch({ type: 'edit', profile: createCustomProfile(state.draft, state.role),
762
+ return _jsx(ModelProfilePicker, { ...props, profiles: library, savedProfiles: state.draft.baseline.modelProfiles, role: state.role, customProfile: createCustomProfile(state.draft, createRole), manager: state.page === 'manager', error: state.error, selectedIdOverride: state.role === 'subagent' ? subagentSelectedId(state.draft) : undefined, subagentProfileId: draftSubagentProfileId(state.draft), fastTierOff: state.role === 'subagent' && fastTierUnavailable(props, view), onSelect: (id) => dispatch({ type: 'select', role: state.role, id }), onCreate: () => dispatch({ type: 'edit', profile: createCustomProfile(state.draft, createRole),
682
763
  bindNew: state.page === 'picker' }), onEdit: (profile) => dispatch({ type: 'edit', profile }), onDuplicate: (id) => dispatch({ type: 'duplicate', id }), onDelete: (id) => { setConfirmIndex(0); dispatch({ type: 'delete', id }); }, onCancel: () => dispatch({ type: 'page', page: 'root' }) });
683
764
  return null;
684
765
  }
@@ -691,13 +772,34 @@ function buildRootSettingsRows(props, view) {
691
772
  const missing = !entry.apiKey && !props.apiKeys[entry.provider];
692
773
  return `${entry.name} ${entry.provider}:${entry.model}${missing ? ' [Missing API key]' : ''}`;
693
774
  };
775
+ // THE SUBAGENT MODEL ROW (D-5), third on the page, summarized from the
776
+ // same draft/baseline sources every other derived row here reads. The fast
777
+ // tier check mirrors `fastTierLine`'s approximation (enabled + model + a
778
+ // config-file key) so the two derived lines can never disagree.
779
+ const team = draftTeam(state.draft);
780
+ const mainBound = library.entries.find((entry) => entry.id === library.mainId);
781
+ const mainConnection = mainBound
782
+ ? `${mainBound.provider}:${mainBound.model}`
783
+ : `${values.provider}:${values.model}`;
784
+ const fastModel = values.fastModel.trim();
785
+ const fastProvider = values.fastProvider === FAST_INHERIT_PROVIDER
786
+ || values.fastProvider.trim().length === 0 ? values.provider : values.fastProvider;
787
+ const fastConnection = values.fastEnabled === 'on' && fastModel.length > 0
788
+ && (props.apiKeys[fastProvider] ?? '').length > 0
789
+ ? `${fastProvider}:${fastModel}` : null;
790
+ const subagentLine = subagentModelSummary({ mode: team.model,
791
+ modelProfileId: team.modelProfileId, mainConnection, fastConnection,
792
+ profiles: state.draft.profiles,
793
+ hasKey: (id) => (props.apiKeys[id] ?? '').length > 0 });
694
794
  const lines = [`Main profile ${name(library.mainId)}`,
695
795
  `Fast profile ${name(library.fastId)}`,
796
+ `Subagent model ${subagentLine}`,
696
797
  `Manage profiles ${library.entries.length} saved`,
697
798
  ...fields.map((item) => `${item.label.padEnd(18)}${item.key === 'apiKey'
698
799
  ? values.apiKey ? 'Configured (replacement)' : props.apiKeys[values.provider]
699
800
  ? 'Configured (type to replace)' : '(type to set)'
700
- : values[item.key] || (item.key === 'maxTokens' ? 'auto' : '(not set)')}`),
801
+ : values[item.key] || (item.key === 'maxTokens' || item.key === 'contextWindow'
802
+ ? 'auto' : '(not set)')}`),
701
803
  persisted || readError ? 'Save (disabled)' : 'Save settings', 'Reload settings'];
702
804
  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));
703
805
  rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: effectiveCapLine(values) }, "effective"));
@@ -720,7 +822,7 @@ function ProfileSettingsScreen(props) {
720
822
  return subpage;
721
823
  const { error, readError, dirty, persisted, index, offset } = view;
722
824
  return _jsx(OverlayFrame, { ...props, rows: buildRootSettingsRows(props, view), scrollOffset: offset, title: error ?? (readError ? 'Cannot read config. Repair with aragon config edit; Reload.'
723
- : dirty ? 'Settings | Unsaved changes' : 'Settings'), hint: persisted ? 'Esc close; restart to apply' : index < 3
825
+ : dirty ? 'Settings | Unsaved changes' : 'Settings'), hint: persisted ? 'Esc close; restart to apply' : index < ROOT_FIRST_FIELD
724
826
  ? 'Enter select/edit | Ctrl+S save | Esc cancel'
725
827
  : 'Enter save/action | Ctrl+S save | Esc cancel' });
726
828
  }
@@ -1,11 +1,25 @@
1
1
  import { type ModelProfile, type ModelProfilesConfig, type ModelRole } from '../../config/model-profiles.js';
2
2
  import type { ModelSettingsDraft } from '../../config/model-profile-store.js';
3
+ import type { SubagentModelMode } from '../../team/model.js';
3
4
  export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'delete-confirm' | 'discard-confirm';
5
+ /**
6
+ * Which role a PROFILE PICKER page is choosing for.
7
+ *
8
+ * `'subagent'` is a PICKER-ONLY role (subagent-model 6.2): `ModelRole` stays
9
+ * `'main' | 'fast'` because it is the type basis of the config layer's
10
+ * `${role}Id` template keys - widening it would invent a `subagentId` library
11
+ * key nothing reads. The subagent picker writes `draft.patch.team` instead.
12
+ */
13
+ export type ProfilePickerRole = ModelRole | 'subagent';
14
+ /** The picker's built-in `Fast tier model` row; never a profile id on disk. */
15
+ export declare const SUBAGENT_FAST_ROW_ID = "__fast__";
4
16
  export interface ProfileEditorDraft {
5
17
  original: ModelProfile;
6
18
  name: string;
7
19
  provider: string;
8
20
  model: string;
21
+ /** Text exactly as typed; `''` renders `(auto)` and parses to AUTO (§6.5). */
22
+ contextWindow: string;
9
23
  baseUrl: string;
10
24
  credentialMode: 'keep' | 'replace' | 'shared';
11
25
  apiKey: string;
@@ -14,21 +28,23 @@ export interface ProfileEditorDraft {
14
28
  export interface ModelProfileState {
15
29
  draft: ModelSettingsDraft;
16
30
  page: ProfilePage;
17
- role: ModelRole;
31
+ role: ProfilePickerRole;
18
32
  editor?: ProfileEditorDraft;
19
33
  returnPage: 'picker' | 'manager';
20
34
  bindNew: boolean;
21
35
  dirty: boolean;
22
36
  error?: string;
23
37
  deleteId?: string;
38
+ /** Who referenced the entry being deleted ('Main + Subagent', or ''). */
39
+ deleteUsers?: string;
24
40
  }
25
41
  export type ModelProfileAction = {
26
42
  type: 'page';
27
43
  page: ProfilePage;
28
- role?: ModelRole;
44
+ role?: ProfilePickerRole;
29
45
  } | {
30
46
  type: 'select';
31
- role: ModelRole;
47
+ role: ProfilePickerRole;
32
48
  id: string | null;
33
49
  } | {
34
50
  type: 'edit';
@@ -67,7 +83,40 @@ export declare function finishProfileEditor(editor: ProfileEditorDraft): {
67
83
  /** Make an isolated root draft; opening the screen never mutates live config. */
68
84
  export declare function createModelProfileState(draft: ModelSettingsDraft): ModelProfileState;
69
85
  /** Describe references using text so ASCII and no-color modes remain usable. */
70
- export declare function profileUsers(profiles: ModelProfilesConfig | undefined, id: string): string;
86
+ export declare function profileUsers(profiles: ModelProfilesConfig | undefined, id: string, subagentProfileId?: string | null): string;
87
+ /**
88
+ * The team section a draft would save: the patch first, then the disk baseline
89
+ * (subagent-model §3.6). The explicit-`undefined` test is what makes an
90
+ * explicit `null` in the patch win over the baseline - `??` would fold it away.
91
+ */
92
+ export declare function draftTeam(draft: ModelSettingsDraft): {
93
+ model: SubagentModelMode;
94
+ modelProfileId: string | null;
95
+ };
96
+ /** The profile id the draft's team section binds, for reference checks (3.7). */
97
+ export declare function draftSubagentProfileId(draft: ModelSettingsDraft): string | null;
98
+ /** The projected picker selection for the subagent role (6.2). */
99
+ export declare function subagentSelectedId(draft: ModelSettingsDraft): string | null;
100
+ /** What the settings root row and `/team model` show for the current mode. */
101
+ export interface SubagentModelSummaryInput {
102
+ mode: SubagentModelMode;
103
+ modelProfileId: string | null;
104
+ /** `<provider>:<model>` of the connection the main role runs on. */
105
+ mainConnection: string;
106
+ /** The fast tier's `<provider>:<model>`, or null when it does not resolve. */
107
+ fastConnection: string | null;
108
+ profiles: ModelProfilesConfig | undefined;
109
+ /**
110
+ * KEY EXISTENCE AS THE SETTINGS SCREEN SEES IT (V-10): derived from the
111
+ * config-file keys, exactly like the `name()` row and `fastTierLine` beside
112
+ * it. A key that exists ONLY as an environment variable is invisible here,
113
+ * so an env-key user may see `(off ...)` / `[Missing API key]` where the
114
+ * runtime would succeed - the accepted approximation both sibling lines
115
+ * already ship.
116
+ */
117
+ hasKey: (providerId: string) => boolean;
118
+ }
119
+ export declare function subagentModelSummary(input: SubagentModelSummaryInput): string;
71
120
  /** Adopt persisted fields only, resolving the old fast inheritance once. */
72
121
  export declare function createCustomProfile(draft: ModelSettingsDraft, role: ModelRole): ModelProfile;
73
122
  /** All child actions modify only the root draft; persistence belongs to Save. */
@@ -1,17 +1,29 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { emptyModelProfiles, duplicateModelProfile, normalizeModelProfiles, validateModelProfiles } from '../../config/model-profiles.js';
3
+ import { parseContextWindowInput } from '../../config/schema.js';
4
+ /** The picker's built-in `Fast tier model` row; never a profile id on disk. */
5
+ export const SUBAGENT_FAST_ROW_ID = '__fast__';
3
6
  /** Keep the original secret separate from the empty replacement field. */
4
7
  export function createProfileEditor(profile) {
5
8
  return { original: { ...profile }, name: profile.name, provider: profile.provider,
6
9
  model: profile.model, baseUrl: profile.baseUrl ?? '', apiKey: '',
10
+ contextWindow: profile.contextWindow === null ? '' : String(profile.contextWindow),
7
11
  credentialMode: profile.apiKey ? 'keep' : 'shared', endpointChanged: false };
8
12
  }
9
13
  /** Validate locally before merging the child form into the root transaction. */
10
14
  export function finishProfileEditor(editor) {
15
+ // STRICT IN THE UI, TOLERANT ON DISK (§6.5 / invariant 4): a typo is refused
16
+ // here with an inline error and the focus jump its `field` enables; it never
17
+ // reaches the draft. The message prefix matches the editor's LABELS row.
18
+ const window = parseContextWindowInput(editor.contextWindow);
19
+ if (window.kind === 'invalid') {
20
+ return { error: 'Context window: use a number or auto.', field: 'contextWindow' };
21
+ }
11
22
  const profile = { id: editor.original.id, name: editor.name.trim(),
12
23
  provider: editor.provider, model: editor.model.trim(), baseUrl: editor.baseUrl.trim() || null,
13
24
  apiKey: editor.credentialMode === 'keep' ? editor.original.apiKey
14
- : editor.credentialMode === 'replace' ? editor.apiKey.trim() : null };
25
+ : editor.credentialMode === 'replace' ? editor.apiKey.trim() : null,
26
+ contextWindow: window.kind === 'auto' ? null : window.value };
15
27
  const library = { ...emptyModelProfiles(), entries: [profile] };
16
28
  const issue = validateModelProfiles(library)[0];
17
29
  if (issue)
@@ -30,10 +42,52 @@ export function createModelProfileState(draft) {
30
42
  bindNew: false, dirty: false };
31
43
  }
32
44
  /** Describe references using text so ASCII and no-color modes remain usable. */
33
- export function profileUsers(profiles, id) {
34
- return [profiles?.mainId === id ? 'Main' : '', profiles?.fastId === id ? 'Fast' : '']
45
+ export function profileUsers(profiles, id, subagentProfileId) {
46
+ return [profiles?.mainId === id ? 'Main' : '', profiles?.fastId === id ? 'Fast' : '',
47
+ subagentProfileId === id ? 'Subagent' : '']
35
48
  .filter(Boolean).join(' + ');
36
49
  }
50
+ /**
51
+ * The team section a draft would save: the patch first, then the disk baseline
52
+ * (subagent-model §3.6). The explicit-`undefined` test is what makes an
53
+ * explicit `null` in the patch win over the baseline - `??` would fold it away.
54
+ */
55
+ export function draftTeam(draft) {
56
+ const patch = draft.patch.team;
57
+ const baseline = draft.baseline.team;
58
+ return {
59
+ model: patch?.model !== undefined ? patch.model : baseline?.model ?? 'main',
60
+ modelProfileId: patch?.modelProfileId !== undefined ? patch.modelProfileId
61
+ : baseline?.modelProfileId ?? null,
62
+ };
63
+ }
64
+ /** The profile id the draft's team section binds, for reference checks (3.7). */
65
+ export function draftSubagentProfileId(draft) {
66
+ return draftTeam(draft).model === 'profile' ? draftTeam(draft).modelProfileId : null;
67
+ }
68
+ /** The projected picker selection for the subagent role (6.2). */
69
+ export function subagentSelectedId(draft) {
70
+ const team = draftTeam(draft);
71
+ if (team.model === 'profile')
72
+ return team.modelProfileId;
73
+ return team.model === 'fast' ? SUBAGENT_FAST_ROW_ID : null;
74
+ }
75
+ export function subagentModelSummary(input) {
76
+ if (input.mode === 'main')
77
+ return `Same as main (${input.mainConnection})`;
78
+ if (input.mode === 'fast') {
79
+ return input.fastConnection
80
+ ? `Fast tier (${input.fastConnection})`
81
+ : 'Fast tier (off - falls back to main)';
82
+ }
83
+ const entry = input.modelProfileId
84
+ ? input.profiles?.entries.find((candidate) => candidate.id === input.modelProfileId)
85
+ : undefined;
86
+ if (!entry)
87
+ return 'Profile missing - falls back to main';
88
+ const missing = !entry.apiKey && !input.hasKey(entry.provider);
89
+ return `${entry.name} ${entry.provider}:${entry.model}${missing ? ' [Missing API key]' : ''}`;
90
+ }
37
91
  /** Adopt persisted fields only, resolving the old fast inheritance once. */
38
92
  export function createCustomProfile(draft, role) {
39
93
  const file = draft.baseline;
@@ -45,7 +99,11 @@ export function createCustomProfile(draft, role) {
45
99
  || (provider === (file.provider || 'anthropic') ? file.baseUrl : null)
46
100
  : file.baseUrl) || null;
47
101
  return { id: randomUUID(), name: '', provider, model, baseUrl,
48
- apiKey: role === 'fast' && !model ? null : file.apiKeys?.[provider] || null };
102
+ apiKey: role === 'fast' && !model ? null : file.apiKeys?.[provider] || null,
103
+ // Carry the user's existing top-level correction into the first profile
104
+ // (§6.5): binding an AUTO profile would otherwise silently retire it
105
+ // (tradeoff B's migration risk, mitigated exactly here).
106
+ contextWindow: file.contextWindow ?? null };
49
107
  }
50
108
  function editField(state, action) {
51
109
  if (!state.editor)
@@ -85,9 +143,24 @@ export function modelProfileReducer(state, action) {
85
143
  case 'reload': return createModelProfileState(action.draft);
86
144
  case 'page': return { ...state, page: action.page, role: action.role ?? state.role,
87
145
  error: undefined };
88
- case 'select': return { ...state, page: 'root', dirty: true, error: undefined,
89
- draft: { ...state.draft, profiles: { ...library, [`${action.role}Id`]: action.id },
90
- activateRoles: [...new Set([...state.draft.activateRoles, action.role])] } };
146
+ case 'select': {
147
+ // THE SUBAGENT BRANCH (D-5): selection writes the DRAFT's team patch -
148
+ // `profiles` and `activateRoles` are untouched, because 'subagent' is a
149
+ // picker-only role with no `${role}Id` library key (6.2). Save is the
150
+ // one place the patch ever reaches disk (I-5).
151
+ if (action.role === 'subagent') {
152
+ const team = action.id === null
153
+ ? { model: 'main', modelProfileId: null }
154
+ : action.id === SUBAGENT_FAST_ROW_ID
155
+ ? { model: 'fast', modelProfileId: null }
156
+ : { model: 'profile', modelProfileId: action.id };
157
+ return { ...state, page: 'root', dirty: true, error: undefined,
158
+ draft: { ...state.draft, patch: { ...state.draft.patch, team } } };
159
+ }
160
+ 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])] } };
163
+ }
91
164
  case 'edit': return { ...state, editor: createProfileEditor(action.profile), page: 'editor',
92
165
  returnPage: state.page === 'picker' ? 'picker' : 'manager',
93
166
  bindNew: action.bindNew ?? false, error: undefined };
@@ -107,13 +180,37 @@ export function modelProfileReducer(state, action) {
107
180
  : state;
108
181
  }
109
182
  case 'delete': {
110
- const users = profileUsers(library, action.id);
111
- return users ? { ...state, error: `Used by ${users}. Select another profile first.` }
112
- : { ...state, page: 'delete-confirm', deleteId: action.id, error: undefined };
183
+ // UNBIND-AND-DELETE (D-6): being referenced no longer refuses the flow.
184
+ // The references - subagent included - are computed here, shown on the
185
+ // confirm page, and cleared by `confirm-delete` inside the draft.
186
+ const users = profileUsers(library, action.id, draftSubagentProfileId(state.draft));
187
+ return { ...state, page: 'delete-confirm', deleteId: action.id,
188
+ deleteUsers: users, error: undefined };
189
+ }
190
+ case 'confirm-delete': {
191
+ const id = state.deleteId;
192
+ if (!id)
193
+ return state;
194
+ // THE DRAFT-SIDE UNBIND: Main/Fast bindings pointing at the entry are
195
+ // nulled here (not refused) and the subagent binding resets to main
196
+ // when it named THIS entry. `removeModelProfile`'s throw stays as the
197
+ // bottom-line defense - the UI has already unbound, so it cannot fire
198
+ // on this path.
199
+ const profiles = {
200
+ ...library,
201
+ mainId: library.mainId === id ? null : library.mainId,
202
+ fastId: library.fastId === id ? null : library.fastId,
203
+ entries: library.entries.filter((entry) => entry.id !== id),
204
+ };
205
+ const teamReset = draftSubagentProfileId(state.draft) === id
206
+ ? { team: { model: 'main', modelProfileId: null } }
207
+ : {};
208
+ return { ...state, page: 'manager', dirty: true,
209
+ draft: { ...state.draft, profiles,
210
+ patch: { ...state.draft.patch, ...teamReset } },
211
+ deleteId: undefined, deleteUsers: undefined,
212
+ // No orphan editor for a deleted entry (3.7).
213
+ editor: state.editor?.original.id === id ? undefined : state.editor };
113
214
  }
114
- case 'confirm-delete': return { ...state, page: 'manager', dirty: true,
115
- draft: { ...state.draft, profiles: { ...library,
116
- entries: library.entries.filter((entry) => entry.id !== state.deleteId) } },
117
- deleteId: undefined };
118
215
  }
119
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.23",
3
+ "version": "0.6.24",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@aragon-agent/core": "^0.2.35",
59
+ "@aragon-agent/core": "^0.2.36",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-highlight": "^2.0.0",
62
62
  "commander": "^12.1.0",