@aragon-agent/cli 0.6.39 → 0.6.42
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.
- package/CHANGELOG.md +109 -0
- package/README.md +126 -20
- package/dist/agent/controller.d.ts +139 -14
- package/dist/agent/controller.js +299 -40
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +29 -1
- package/dist/agent/reducer.js +34 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +33 -2
- package/dist/agent/system-prompt.js +12 -4
- package/dist/cli.js +21 -5
- package/dist/commands/builtins.js +28 -185
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +30 -0
- package/dist/commands/subagent-command.js +39 -0
- package/dist/commands/team-command.d.ts +49 -0
- package/dist/commands/team-command.js +259 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/load.js +50 -3
- package/dist/config/model-kits.d.ts +2 -1
- package/dist/config/schema.d.ts +82 -6
- package/dist/config/schema.js +107 -9
- package/dist/config/store.js +10 -4
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/dispatch-gate.d.ts +86 -0
- package/dist/team/dispatch-gate.js +75 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/forced-mode.d.ts +116 -0
- package/dist/team/forced-mode.js +156 -0
- package/dist/team/forced-prompt.d.ts +48 -0
- package/dist/team/forced-prompt.js +84 -0
- package/dist/team/limits.d.ts +61 -1
- package/dist/team/limits.js +69 -1
- package/dist/team/model.d.ts +5 -5
- package/dist/team/model.js +5 -5
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/ui/App.js +130 -11
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +2 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- package/dist/ui/layout/status-layout.d.ts +27 -0
- package/dist/ui/layout/status-layout.js +27 -1
- package/dist/ui/overlays/HelpOverlay.js +8 -4
- package/dist/ui/overlays/SettingsScreen.d.ts +14 -0
- package/dist/ui/overlays/SettingsScreen.js +39 -5
- package/dist/ui/overlays/TeamFanoutPage.d.ts +51 -0
- package/dist/ui/overlays/TeamFanoutPage.js +90 -0
- package/dist/ui/overlays/kit-settings-state.js +3 -2
- package/dist/ui/overlays/model-profile-state.d.ts +20 -2
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +106 -0
- package/dist/ui/overlays/team-fanout-state.js +155 -0
- package/package.json +81 -81
|
@@ -27,6 +27,8 @@ import { canProbe, editorProbeTarget, visibleProbeViews } from './model-probe-vi
|
|
|
27
27
|
import { createModelProfileState, createCustomProfile, modelProfileReducer, profileUsers, draftTeam, draftSubagentProfileId, subagentModelSummary, subagentSelectedId, SUBAGENT_FAST_ROW_ID } from './model-profile-state.js';
|
|
28
28
|
import { KitEditor } from './KitEditor.js';
|
|
29
29
|
import { KitManager } from './KitManager.js';
|
|
30
|
+
import { TeamFanoutPage } from './TeamFanoutPage.js';
|
|
31
|
+
import { fanoutRows, teamFanoutValues, teamSummary } from './team-fanout-state.js';
|
|
30
32
|
import { heldSlots, kitManagerRows, kitPackRow, kitReducer } from './kit-settings-state.js';
|
|
31
33
|
/**
|
|
32
34
|
* THE ROOT PAGE'S ROW INDEXES, AS NAMES (subagent-model V-5).
|
|
@@ -40,10 +42,15 @@ import { heldSlots, kitManagerRows, kitPackRow, kitReducer } from './kit-setting
|
|
|
40
42
|
* fifth of these and the "five offsets agreed by accident" defect does not need
|
|
41
43
|
* a sequel. Order here IS the render order.
|
|
42
44
|
*/
|
|
43
|
-
const ROOT_ROWS = ['main', 'fast', 'subagent', 'kit', 'manage'];
|
|
45
|
+
const ROOT_ROWS = ['main', 'fast', 'subagent', 'team', 'kit', 'manage'];
|
|
44
46
|
const ROOT_MAIN = ROOT_ROWS.indexOf('main');
|
|
45
47
|
const ROOT_FAST = ROOT_ROWS.indexOf('fast');
|
|
46
48
|
const ROOT_SUBAGENT = ROOT_ROWS.indexOf('subagent');
|
|
49
|
+
// The fan-out page (subagent-fanout D-5). It is FOURTH, directly under the model
|
|
50
|
+
// row it complements: "which model children run on" and "how many children" are
|
|
51
|
+
// one decision, and splitting them across the page would make the pair harder to
|
|
52
|
+
// keep consistent than the pair of numbers inside it.
|
|
53
|
+
const ROOT_TEAM = ROOT_ROWS.indexOf('team');
|
|
47
54
|
const ROOT_KIT = ROOT_ROWS.indexOf('kit');
|
|
48
55
|
const ROOT_MANAGE = ROOT_ROWS.indexOf('manage');
|
|
49
56
|
const ROOT_FIRST_FIELD = ROOT_ROWS.length;
|
|
@@ -583,9 +590,9 @@ function useProfileSettingsState(props) {
|
|
|
583
590
|
&& ['provider', 'model', 'baseUrl', 'contextWindow', 'apiKey'].includes(item.key)),
|
|
584
591
|
...FIELDS.filter((item) => library.fastId === null
|
|
585
592
|
&& ['fastModel', 'fastProvider'].includes(item.key)), ...PROFILE_FIELDS];
|
|
586
|
-
//
|
|
587
|
-
// around the `fields` block - derived from ROOT_ROWS so
|
|
588
|
-
// offsets cannot drift apart (see those constants).
|
|
593
|
+
// EIGHT FIXED ROWS now (Main, Fast, Subagent model, Subagent fan-out, Kit pack,
|
|
594
|
+
// Manage, Save, Reload) around the `fields` block - derived from ROOT_ROWS so
|
|
595
|
+
// the count and the offsets cannot drift apart (see those constants).
|
|
589
596
|
const count = fields.length + ROOT_ROWS.length + 2;
|
|
590
597
|
const field = fields[index - ROOT_FIRST_FIELD];
|
|
591
598
|
const readError = state.draft.readError;
|
|
@@ -679,8 +686,15 @@ function activateRootSetting(props, view) {
|
|
|
679
686
|
// `draft.patch.team` rather than a `${role}Id` library key.
|
|
680
687
|
dispatch({ type: 'page', page: 'picker', role: 'subagent' });
|
|
681
688
|
}
|
|
689
|
+
else if (index === ROOT_TEAM) {
|
|
690
|
+
// MUST STAY ABOVE THE FALLBACK `else` BELOW, WHICH IS `saveProfileSettings`
|
|
691
|
+
// (R-P1-8). A missing or mis-derived constant here does not fail to compile:
|
|
692
|
+
// pressing Enter on this row would silently COMMIT the whole draft, so
|
|
693
|
+
// `team-fanout-page.test.tsx` asserts that Enter on this row saves nothing.
|
|
694
|
+
dispatch({ type: 'page', page: 'team' });
|
|
695
|
+
}
|
|
682
696
|
else if (index === ROOT_KIT) {
|
|
683
|
-
// The
|
|
697
|
+
// The fifth root row opens the manager, not a picker: kits are data the user
|
|
684
698
|
// owns here, and `ctrl+K` is where they are CONSUMED (agent-model-kits §4.5).
|
|
685
699
|
dispatch({ type: 'page', page: 'kits' });
|
|
686
700
|
}
|
|
@@ -836,6 +850,14 @@ function fastTierUnavailable(props, view) {
|
|
|
836
850
|
}
|
|
837
851
|
function renderProfileSubpage(props, view) {
|
|
838
852
|
const { state, library, dispatch, setConfirmIndex } = view;
|
|
853
|
+
// THE FAN-OUT PAGE (subagent-fanout 3.4.4). Placed before the picker branch
|
|
854
|
+
// because that branch is the unconditional fallback; the page name is unique,
|
|
855
|
+
// so its position among the others is arbitrary.
|
|
856
|
+
if (state.page === 'team')
|
|
857
|
+
return _jsx(TeamFanoutPage, { ...props, view: {
|
|
858
|
+
rows: fanoutRows(teamFanoutValues(state.draft), props.teamRegistered),
|
|
859
|
+
error: state.error,
|
|
860
|
+
}, dispatch: dispatch, onBack: () => dispatch({ type: 'page', page: 'root' }) });
|
|
839
861
|
// THE KIT PAGES (agent-model-kits §4.5). They are dispatched BEFORE the profile
|
|
840
862
|
// pages because `'kit-slot'` RENDERS a profile picker while belonging to the
|
|
841
863
|
// kit editor - the page name, not the component, decides where the user is.
|
|
@@ -945,9 +967,21 @@ function buildRootSettingsRows(props, view) {
|
|
|
945
967
|
profiles: state.draft.profiles,
|
|
946
968
|
hasKey: (id) => (props.apiKeys[id] ?? '').length > 0 })}${kitTag}`
|
|
947
969
|
: subagentLine;
|
|
970
|
+
// THE TEAM SUMMARY (subagent-fanout 3.4.4, renamed by
|
|
971
|
+
// team-command-forced-dispatch §3.9): a DERIVED row, like the three above it,
|
|
972
|
+
// and deliberately not a control - the values are adjusted on the page Enter
|
|
973
|
+
// opens, so the root row cannot change one by accident. It now summarises the
|
|
974
|
+
// WHOLE section, switch included, because `Team mode` is one of those values
|
|
975
|
+
// and a summary that always printed numbers would describe a capability the
|
|
976
|
+
// user may have just switched off.
|
|
977
|
+
const summary = teamSummary(teamFanoutValues(state.draft));
|
|
948
978
|
const lines = [`Main profile ${mainValue}`,
|
|
949
979
|
`Fast profile ${fastValue}`,
|
|
950
980
|
`Subagent model ${subagentValue}`,
|
|
981
|
+
// `Team`, not `Subagent fan-out`: the row heads a page that now owns
|
|
982
|
+
// four values, only two of which are about fan-out width. `padEnd(18)` is
|
|
983
|
+
// unchanged, so the value column still lines up with `Subagent model`.
|
|
984
|
+
`Team ${summary}`,
|
|
951
985
|
`Kit pack ${kitPackRow(state)}`,
|
|
952
986
|
`Manage profiles ${library.entries.length} saved`,
|
|
953
987
|
...fields.map((item) => `${item.label.padEnd(18)}${item.key === 'apiKey'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings > Team: four arrows-adjustable rows (subagent-fanout 3.4.3, extended
|
|
3
|
+
* by team-command-forced-dispatch §3.9).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* THE PAGE OWNS ONE PIECE OF STATE: its cursor. Every value comes from
|
|
8
|
+
* `props.view.rows`, which `SettingsScreen` derives from the reducer state - so
|
|
9
|
+
* this page cannot hold a second copy of the draft and then disagree with the
|
|
10
|
+
* root page about what is about to be saved (the discipline
|
|
11
|
+
* `model-profile-state.ts` records at its own head).
|
|
12
|
+
*
|
|
13
|
+
* FOUR ROWS, NOT THREE: `Team mode` moved here from `/team on|off`, because a
|
|
14
|
+
* command that both configures and executes leaves the user unable to predict
|
|
15
|
+
* what it did. The switch is the FIRST row - the other three describe a capability
|
|
16
|
+
* it turns on.
|
|
17
|
+
*
|
|
18
|
+
* IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13).
|
|
19
|
+
* `useRootSettingsInput` gates its `useInput` on `state.page === 'root'`, so a
|
|
20
|
+
* subpage has no save listener at all - the kit page included. A footer
|
|
21
|
+
* promising "Ctrl+S saves here" would therefore be a lie the user acts on. The
|
|
22
|
+
* honest instruction is the one already in their fingers: Esc back, Ctrl+S on the
|
|
23
|
+
* main page.
|
|
24
|
+
*/
|
|
25
|
+
import React from 'react';
|
|
26
|
+
import { type ProfileFrameProps } from './ModelProfilePicker.js';
|
|
27
|
+
import type { TeamFanoutField, TeamFanoutView } from './team-fanout-state.js';
|
|
28
|
+
/** What the page may ask the screen to change. */
|
|
29
|
+
export interface TeamFanoutAction {
|
|
30
|
+
type: 'team-fanout';
|
|
31
|
+
field: TeamFanoutField;
|
|
32
|
+
delta: -1 | 1;
|
|
33
|
+
}
|
|
34
|
+
interface TeamFanoutPageProps extends ProfileFrameProps {
|
|
35
|
+
view: TeamFanoutView;
|
|
36
|
+
dispatch: (action: TeamFanoutAction) => void;
|
|
37
|
+
/** Esc: back to the settings root. The draft is NOT discarded here. */
|
|
38
|
+
onBack: () => void;
|
|
39
|
+
/**
|
|
40
|
+
* Whether `task` is in this SESSION's tool list (`--no-team` is the false case).
|
|
41
|
+
*
|
|
42
|
+
* OPTIONAL, like every other pass-through on this screen: fixtures build
|
|
43
|
+
* `ProfileFrameProps` by literal, and "absent" honestly means "not disclosed"
|
|
44
|
+
* rather than "registered". When it is `false` and the draft says `on`, the
|
|
45
|
+
* `Team mode` row reads `on (next launch)` - the tool array is immutable, so
|
|
46
|
+
* that is the truth about what saving will achieve (I-7 / AC-9).
|
|
47
|
+
*/
|
|
48
|
+
teamRegistered?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare function TeamFanoutPage(props: TeamFanoutPageProps): React.ReactElement;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Settings > Team: four arrows-adjustable rows (subagent-fanout 3.4.3, extended
|
|
4
|
+
* by team-command-forced-dispatch §3.9).
|
|
5
|
+
*
|
|
6
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
7
|
+
*
|
|
8
|
+
* THE PAGE OWNS ONE PIECE OF STATE: its cursor. Every value comes from
|
|
9
|
+
* `props.view.rows`, which `SettingsScreen` derives from the reducer state - so
|
|
10
|
+
* this page cannot hold a second copy of the draft and then disagree with the
|
|
11
|
+
* root page about what is about to be saved (the discipline
|
|
12
|
+
* `model-profile-state.ts` records at its own head).
|
|
13
|
+
*
|
|
14
|
+
* FOUR ROWS, NOT THREE: `Team mode` moved here from `/team on|off`, because a
|
|
15
|
+
* command that both configures and executes leaves the user unable to predict
|
|
16
|
+
* what it did. The switch is the FIRST row - the other three describe a capability
|
|
17
|
+
* it turns on.
|
|
18
|
+
*
|
|
19
|
+
* IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13).
|
|
20
|
+
* `useRootSettingsInput` gates its `useInput` on `state.page === 'root'`, so a
|
|
21
|
+
* subpage has no save listener at all - the kit page included. A footer
|
|
22
|
+
* promising "Ctrl+S saves here" would therefore be a lie the user acts on. The
|
|
23
|
+
* honest instruction is the one already in their fingers: Esc back, Ctrl+S on the
|
|
24
|
+
* main page.
|
|
25
|
+
*/
|
|
26
|
+
import { useState } from 'react';
|
|
27
|
+
import { Text, useInput } from 'ink';
|
|
28
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
29
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
30
|
+
import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
|
|
31
|
+
/**
|
|
32
|
+
* Label column, matching the root page's `padEnd(18)` rows above and below this
|
|
33
|
+
* one. `Team mode` is 9 characters and `Max subagents` is 13, so 18 keeps the
|
|
34
|
+
* value column aligned with `Subagent model` on the root page - measured, not
|
|
35
|
+
* guessed.
|
|
36
|
+
*/
|
|
37
|
+
const LABEL_PAD = 18;
|
|
38
|
+
/**
|
|
39
|
+
* The two rows `Enter` toggles, and nothing else (R-P1-12).
|
|
40
|
+
*
|
|
41
|
+
* A stray Return that silently changed a NUMBER is indistinguishable from a bug,
|
|
42
|
+
* and the arrow keys are already the obvious control there.
|
|
43
|
+
*/
|
|
44
|
+
const TOGGLE_FIELDS = new Set(['enabled', 'overseer']);
|
|
45
|
+
export function TeamFanoutPage(props) {
|
|
46
|
+
const [index, setIndex] = useState(0);
|
|
47
|
+
const rows = props.view.rows;
|
|
48
|
+
const count = Math.max(1, rows.length);
|
|
49
|
+
const cursor = Math.min(index, count - 1);
|
|
50
|
+
const focused = rows[cursor];
|
|
51
|
+
const offset = useProfileScroll(props, cursor);
|
|
52
|
+
const nudge = (delta) => {
|
|
53
|
+
if (!focused)
|
|
54
|
+
return;
|
|
55
|
+
props.dispatch({ type: 'team-fanout', field: focused.field, delta });
|
|
56
|
+
};
|
|
57
|
+
useInput((_input, key) => {
|
|
58
|
+
if (props.isActive === false)
|
|
59
|
+
return;
|
|
60
|
+
if (key.escape) {
|
|
61
|
+
props.onBack();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
65
|
+
setIndex((cursor - 1 + count) % count);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (key.downArrow || key.tab) {
|
|
69
|
+
setIndex((cursor + 1) % count);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (key.leftArrow || key.rightArrow) {
|
|
73
|
+
nudge(key.leftArrow ? -1 : 1);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// `Enter` toggles the two boolean rows and does NOTHING on a numeric row.
|
|
77
|
+
if (key.return && focused && TOGGLE_FIELDS.has(focused.field))
|
|
78
|
+
nudge(1);
|
|
79
|
+
});
|
|
80
|
+
const lines = rows.map((row) => `${row.label.padEnd(LABEL_PAD)}${row.value}`);
|
|
81
|
+
const glyphs = pickGlyphs(props.caps);
|
|
82
|
+
const body = 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)));
|
|
83
|
+
// The hint explains the FOCUSED row rather than decorating the frame: numbers
|
|
84
|
+
// whose meaning is not obvious ("per dispatch" versus "at once") are the reason
|
|
85
|
+
// this page exists at all.
|
|
86
|
+
if (focused) {
|
|
87
|
+
body.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: clipProfileText(focused.hint, props.cols - 8) }, "hint"));
|
|
88
|
+
}
|
|
89
|
+
return _jsx(OverlayFrame, { ...props, rows: body, scrollOffset: offset, title: props.view.error ?? 'Team', hint: "Arrows adjust. Esc goes back; Ctrl+S on the main page saves." });
|
|
90
|
+
}
|
|
@@ -232,8 +232,9 @@ export function kitPackRow(state) {
|
|
|
232
232
|
const partial = session.held.length < 3;
|
|
233
233
|
const held = partial ? `: ${session.held.join('+')}` : '';
|
|
234
234
|
// THE SAME `~` THE CHIP SHOWS (D-12): dropping a slot is only ONE way to
|
|
235
|
-
// drift. The other is a connection that `/model`, `/fast` or
|
|
236
|
-
// rewrote after the kit was applied, and a row that
|
|
235
|
+
// drift. The other is a connection that `/model`, `/fast` or the settings
|
|
236
|
+
// `Subagent model` row rewrote after the kit was applied, and a row that
|
|
237
|
+
// stayed silent about that
|
|
237
238
|
// would disagree with the status bar about the same session.
|
|
238
239
|
const drift = liveDrift(state, session) ? '~' : '';
|
|
239
240
|
return `${sessionKit.name} [session${held}]${drift}${defaultClause}`;
|
|
@@ -3,7 +3,14 @@ import type { AgentKit } from '../../config/model-kits.js';
|
|
|
3
3
|
import type { ModelSettingsDraft } from '../../config/model-profile-store.js';
|
|
4
4
|
import type { SubagentModelMode } from '../../team/model.js';
|
|
5
5
|
import type { KitDraft } from './kit-settings-state.js';
|
|
6
|
-
|
|
6
|
+
import { type TeamFanoutField } from './team-fanout-state.js';
|
|
7
|
+
export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' | 'delete-confirm' | 'discard-confirm' | 'kits' | 'kit-edit' | 'kit-slot' | 'kit-delete' | 'team';
|
|
8
|
+
/**
|
|
9
|
+
* `'team'` is the fan-out page (subagent-fanout D-5), and it is a PAGE rather
|
|
10
|
+
* than three more fields on the root page for a measurable reason: the root page
|
|
11
|
+
* already scrolls on a 24-row terminal, and three arrows-adjustable rows would
|
|
12
|
+
* push `Save` and `Reload` off the first screen on every machine.
|
|
13
|
+
*/
|
|
7
14
|
/**
|
|
8
15
|
* Which role a PROFILE PICKER page is choosing for.
|
|
9
16
|
*
|
|
@@ -100,6 +107,17 @@ export type ModelProfileAction = {
|
|
|
100
107
|
} | {
|
|
101
108
|
type: 'reload';
|
|
102
109
|
draft: ModelSettingsDraft;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* One arrow press on the fan-out page (subagent-fanout 3.4.2).
|
|
113
|
+
*
|
|
114
|
+
* The reducer, not the page, owns the write: the page holds a cursor and
|
|
115
|
+
* nothing else, so there is exactly one place a draft can change.
|
|
116
|
+
*/
|
|
117
|
+
| {
|
|
118
|
+
type: 'team-fanout';
|
|
119
|
+
field: TeamFanoutField;
|
|
120
|
+
delta: -1 | 1;
|
|
103
121
|
};
|
|
104
122
|
/** Keep the original secret separate from the empty replacement field. */
|
|
105
123
|
export declare function createProfileEditor(profile: ModelProfile): ProfileEditorDraft;
|
|
@@ -128,7 +146,7 @@ export declare function draftTeam(draft: ModelSettingsDraft): {
|
|
|
128
146
|
export declare function draftSubagentProfileId(draft: ModelSettingsDraft): string | null;
|
|
129
147
|
/** The projected picker selection for the subagent role (6.2). */
|
|
130
148
|
export declare function subagentSelectedId(draft: ModelSettingsDraft): string | null;
|
|
131
|
-
/** What the settings root row and `/team
|
|
149
|
+
/** What the settings root row and `/team`'s status lines show for the current mode. */
|
|
132
150
|
export interface SubagentModelSummaryInput {
|
|
133
151
|
mode: SubagentModelMode;
|
|
134
152
|
modelProfileId: string | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { emptyModelProfiles, duplicateModelProfile, normalizeModelProfiles, validateModelProfiles } from '../../config/model-profiles.js';
|
|
3
3
|
import { parseContextWindowInput } from '../../config/schema.js';
|
|
4
|
+
import { stepFanoutValue, teamFanoutValues } from './team-fanout-state.js';
|
|
4
5
|
/** The picker's built-in `Fast tier model` row; never a profile id on disk. */
|
|
5
6
|
export const SUBAGENT_FAST_ROW_ID = '__fast__';
|
|
6
7
|
/** Keep the original secret separate from the empty replacement field. */
|
|
@@ -221,6 +222,17 @@ export function modelProfileReducer(state, action) {
|
|
|
221
222
|
}
|
|
222
223
|
case 'cancel-editor': return { ...state, editor: undefined, page: state.returnPage,
|
|
223
224
|
error: undefined };
|
|
225
|
+
case 'team-fanout': {
|
|
226
|
+
// THE FRAGMENT IS THE ONLY THING WRITTEN (D-17 / R-P0-1). `stepFanoutValue`
|
|
227
|
+
// returns `Partial<TeamFanoutPatch>`, so `model` and `modelProfileId` - the
|
|
228
|
+
// two keys `releasedSlots` reads as "the user re-decided the subagent slot" -
|
|
229
|
+
// cannot enter the patch from here. `teamFanoutValues` is the CLAMPED view
|
|
230
|
+
// (R-P0-2), so a step starts from the number the user is looking at.
|
|
231
|
+
const step = stepFanoutValue(teamFanoutValues(state.draft), action.field, action.delta);
|
|
232
|
+
return { ...state, dirty: true, error: undefined,
|
|
233
|
+
draft: { ...state.draft,
|
|
234
|
+
patch: { ...state.draft.patch, team: { ...state.draft.patch.team, ...step } } } };
|
|
235
|
+
}
|
|
224
236
|
case 'duplicate': {
|
|
225
237
|
if (library.entries.length >= 200)
|
|
226
238
|
return { ...state,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings > Team: the pure half of the page (subagent-fanout 3.4.2, extended by
|
|
3
|
+
* team-command-forced-dispatch §3.9).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* THE PAGE WRITES FOUR KEYS AND CANNOT BE MADE TO WRITE A FIFTH (D-17 / R-P0-1).
|
|
8
|
+
* `TeamFanoutPatch` is `Pick<TeamConfig, 'enabled' | 'maxSubagents' |
|
|
9
|
+
* 'maxConcurrent' | 'overseer'>`, and `stepFanoutValue` returns
|
|
10
|
+
* `Partial<TeamFanoutPatch>`: a width adjustment therefore CANNOT carry `model`
|
|
11
|
+
* or `modelProfileId` into `draft.patch.team`. That matters because
|
|
12
|
+
* `releasedSlots` reads those two keys as "the user re-decided the subagent
|
|
13
|
+
* slot", so a page that returned the whole section would silently release the
|
|
14
|
+
* model a session kit had just installed - and no test would notice, because
|
|
15
|
+
* every value on screen would still be right.
|
|
16
|
+
*
|
|
17
|
+
* FOUR KEYS BECAUSE `enabled` MOVED HERE (team-command-forced-dispatch D-3): the
|
|
18
|
+
* team switch used to be reachable only as `/team on|off`, which also wrote
|
|
19
|
+
* `config.json`. The switch belongs on the same page as width, concurrency and
|
|
20
|
+
* supervision - they are four faces of one decision - and `/team` is now the ACT
|
|
21
|
+
* ("arm this session"), never a configuration write (I-4).
|
|
22
|
+
*
|
|
23
|
+
* EVERYTHING HERE IS A PURE FUNCTION over the draft. The page owns exactly one
|
|
24
|
+
* piece of state (its cursor); values come from the reducer, which is the same
|
|
25
|
+
* discipline the rest of this screen follows.
|
|
26
|
+
*/
|
|
27
|
+
import type { TeamConfig } from '../../config/schema.js';
|
|
28
|
+
import type { ModelSettingsDraft } from '../../config/model-profile-store.js';
|
|
29
|
+
export type TeamFanoutField = 'enabled' | 'maxSubagents' | 'maxConcurrent' | 'overseer';
|
|
30
|
+
/**
|
|
31
|
+
* THE ONLY KEYS THIS PAGE MAY WRITE (R-P0-1).
|
|
32
|
+
*
|
|
33
|
+
* Not `Partial<TeamConfig>`: a fragment that could carry `model` would make a
|
|
34
|
+
* width adjustment read as a model decision at save time.
|
|
35
|
+
*/
|
|
36
|
+
export type TeamFanoutPatch = Pick<TeamConfig, 'enabled' | 'maxSubagents' | 'maxConcurrent' | 'overseer'>;
|
|
37
|
+
export interface TeamFanoutRow {
|
|
38
|
+
field: TeamFanoutField;
|
|
39
|
+
label: string;
|
|
40
|
+
/** The value as the page prints it (`'8'` / `'on'`). */
|
|
41
|
+
value: string;
|
|
42
|
+
/** One line, <= 60 columns: WHY this number, not a restatement of the label. */
|
|
43
|
+
hint: string;
|
|
44
|
+
}
|
|
45
|
+
export interface TeamFanoutView {
|
|
46
|
+
/** Exactly four, in the fixed order the cursor indexes. */
|
|
47
|
+
rows: TeamFanoutRow[];
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The CLAMPED team section this draft would save (R-P0-2), for display only.
|
|
52
|
+
*
|
|
53
|
+
* THE CLAMP IS NOT COSMETIC. Without it a patch carrying only
|
|
54
|
+
* `maxSubagents: 6` renders "6 per dispatch, 8 at once" on top of an 8-wide
|
|
55
|
+
* baseline, while `mergePersistedConfig` clamps the same patch to 6/6 on the way
|
|
56
|
+
* to disk - so the screen and the file disagree, and every arrow key makes the
|
|
57
|
+
* user reconcile them by hand.
|
|
58
|
+
*/
|
|
59
|
+
export declare function teamFanoutValues(draft: ModelSettingsDraft): TeamConfig;
|
|
60
|
+
/**
|
|
61
|
+
* The PATCH FRAGMENT for one arrow press, clamped exactly like `clampTeamConfig`.
|
|
62
|
+
*
|
|
63
|
+
* `current` must be the value the screen actually shows (`teamFanoutValues`), so
|
|
64
|
+
* a step always starts from a number the user can see.
|
|
65
|
+
*
|
|
66
|
+
* NO WRAPAROUND: a form control is not a ring, and `Left` at 1 jumping to 10
|
|
67
|
+
* would be a way to widen a dispatch while trying to narrow it.
|
|
68
|
+
*
|
|
69
|
+
* EVERY MEMBER IS HANDLED EXPLICITLY AND THE TAIL IS `never` (P0-1). The old
|
|
70
|
+
* body was three `if`s with the `maxSubagents` branch as the FALLTHROUGH, so
|
|
71
|
+
* adding `'enabled'` to the union compiled cleanly and made the new `Team mode`
|
|
72
|
+
* row adjust `Max subagents` instead - every number on screen self-consistent,
|
|
73
|
+
* no test red, and the user's intent to switch the team OFF arrived as a wider
|
|
74
|
+
* fan-out. The `never` assignment below turns that class of mistake into a
|
|
75
|
+
* compile error, which is the only place it can be caught cheaply.
|
|
76
|
+
*/
|
|
77
|
+
export declare function stepFanoutValue(current: TeamConfig, field: TeamFanoutField, delta: -1 | 1): Partial<TeamFanoutPatch>;
|
|
78
|
+
/**
|
|
79
|
+
* The page's four rows, in cursor order.
|
|
80
|
+
*
|
|
81
|
+
* `Team mode` IS FIRST because it is the premise of the other three: with `task`
|
|
82
|
+
* out of the tool list there is no width to configure and no supervisor to run,
|
|
83
|
+
* and a page that listed the numbers first would invite the user to tune a
|
|
84
|
+
* capability they had switched off.
|
|
85
|
+
*
|
|
86
|
+
* TWO HINTS CARRY A CONDITION THE DESIGN OWNS RATHER THAN DECORATION (R-1 / C-2):
|
|
87
|
+
* the default pool is 8, which is 8 simultaneous streams against one API key, and
|
|
88
|
+
* the honest mitigation for an HTTP 429 storm is "lower this number" - said HERE,
|
|
89
|
+
* where the number is, not only in a risk table nobody reads.
|
|
90
|
+
*
|
|
91
|
+
* `teamRegistered === false` (a `--no-team` session) changes TWO things on the
|
|
92
|
+
* `enabled` row and nothing else: `on (next launch)` because the tool array is
|
|
93
|
+
* immutable (I-7), and a hint that says why. Absent means "not disclosed",
|
|
94
|
+
* which every existing fixture relies on.
|
|
95
|
+
*/
|
|
96
|
+
export declare function fanoutRows(team: TeamConfig, teamRegistered?: boolean): TeamFanoutRow[];
|
|
97
|
+
/**
|
|
98
|
+
* The one-line summary the settings root row prints for this page.
|
|
99
|
+
*
|
|
100
|
+
* `off - no subagents` rather than the numbers, because with the capability off
|
|
101
|
+
* the numbers describe nothing the user can spend. This replaced
|
|
102
|
+
* `fanoutSummary()` outright instead of living beside it: two summary functions
|
|
103
|
+
* over one page is two authorities for one line, and the root row would sooner or
|
|
104
|
+
* later disagree with the page it summarises.
|
|
105
|
+
*/
|
|
106
|
+
export declare function teamSummary(team: TeamConfig): string;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings > Team: the pure half of the page (subagent-fanout 3.4.2, extended by
|
|
3
|
+
* team-command-forced-dispatch §3.9).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* THE PAGE WRITES FOUR KEYS AND CANNOT BE MADE TO WRITE A FIFTH (D-17 / R-P0-1).
|
|
8
|
+
* `TeamFanoutPatch` is `Pick<TeamConfig, 'enabled' | 'maxSubagents' |
|
|
9
|
+
* 'maxConcurrent' | 'overseer'>`, and `stepFanoutValue` returns
|
|
10
|
+
* `Partial<TeamFanoutPatch>`: a width adjustment therefore CANNOT carry `model`
|
|
11
|
+
* or `modelProfileId` into `draft.patch.team`. That matters because
|
|
12
|
+
* `releasedSlots` reads those two keys as "the user re-decided the subagent
|
|
13
|
+
* slot", so a page that returned the whole section would silently release the
|
|
14
|
+
* model a session kit had just installed - and no test would notice, because
|
|
15
|
+
* every value on screen would still be right.
|
|
16
|
+
*
|
|
17
|
+
* FOUR KEYS BECAUSE `enabled` MOVED HERE (team-command-forced-dispatch D-3): the
|
|
18
|
+
* team switch used to be reachable only as `/team on|off`, which also wrote
|
|
19
|
+
* `config.json`. The switch belongs on the same page as width, concurrency and
|
|
20
|
+
* supervision - they are four faces of one decision - and `/team` is now the ACT
|
|
21
|
+
* ("arm this session"), never a configuration write (I-4).
|
|
22
|
+
*
|
|
23
|
+
* EVERYTHING HERE IS A PURE FUNCTION over the draft. The page owns exactly one
|
|
24
|
+
* piece of state (its cursor); values come from the reducer, which is the same
|
|
25
|
+
* discipline the rest of this screen follows.
|
|
26
|
+
*/
|
|
27
|
+
import { clampTeamConfig, HARD_MAX_SUBAGENTS } from '../../config/schema.js';
|
|
28
|
+
function clampInt(value, min, max) {
|
|
29
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The CLAMPED team section this draft would save (R-P0-2), for display only.
|
|
33
|
+
*
|
|
34
|
+
* THE CLAMP IS NOT COSMETIC. Without it a patch carrying only
|
|
35
|
+
* `maxSubagents: 6` renders "6 per dispatch, 8 at once" on top of an 8-wide
|
|
36
|
+
* baseline, while `mergePersistedConfig` clamps the same patch to 6/6 on the way
|
|
37
|
+
* to disk - so the screen and the file disagree, and every arrow key makes the
|
|
38
|
+
* user reconcile them by hand.
|
|
39
|
+
*/
|
|
40
|
+
export function teamFanoutValues(draft) {
|
|
41
|
+
return clampTeamConfig({ ...draft.baseline.team, ...draft.patch.team });
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The PATCH FRAGMENT for one arrow press, clamped exactly like `clampTeamConfig`.
|
|
45
|
+
*
|
|
46
|
+
* `current` must be the value the screen actually shows (`teamFanoutValues`), so
|
|
47
|
+
* a step always starts from a number the user can see.
|
|
48
|
+
*
|
|
49
|
+
* NO WRAPAROUND: a form control is not a ring, and `Left` at 1 jumping to 10
|
|
50
|
+
* would be a way to widen a dispatch while trying to narrow it.
|
|
51
|
+
*
|
|
52
|
+
* EVERY MEMBER IS HANDLED EXPLICITLY AND THE TAIL IS `never` (P0-1). The old
|
|
53
|
+
* body was three `if`s with the `maxSubagents` branch as the FALLTHROUGH, so
|
|
54
|
+
* adding `'enabled'` to the union compiled cleanly and made the new `Team mode`
|
|
55
|
+
* row adjust `Max subagents` instead - every number on screen self-consistent,
|
|
56
|
+
* no test red, and the user's intent to switch the team OFF arrived as a wider
|
|
57
|
+
* fan-out. The `never` assignment below turns that class of mistake into a
|
|
58
|
+
* compile error, which is the only place it can be caught cheaply.
|
|
59
|
+
*/
|
|
60
|
+
export function stepFanoutValue(current, field, delta) {
|
|
61
|
+
if (field === 'enabled') {
|
|
62
|
+
// Both arrows toggle: `Team mode` has two states, and a direction has no
|
|
63
|
+
// meaning beyond "the user pressed an arrow" (the `Supervisor` rule below).
|
|
64
|
+
return { enabled: !current.enabled };
|
|
65
|
+
}
|
|
66
|
+
if (field === 'overseer') {
|
|
67
|
+
return { overseer: !current.overseer };
|
|
68
|
+
}
|
|
69
|
+
if (field === 'maxConcurrent') {
|
|
70
|
+
const ceiling = Math.min(HARD_MAX_SUBAGENTS, current.maxSubagents);
|
|
71
|
+
return { maxConcurrent: clampInt(current.maxConcurrent + delta, 1, ceiling) };
|
|
72
|
+
}
|
|
73
|
+
if (field === 'maxSubagents') {
|
|
74
|
+
const next = clampInt(current.maxSubagents + delta, 1, HARD_MAX_SUBAGENTS);
|
|
75
|
+
// THE FOLLOW-DOWN IS WRITTEN INTO THE PATCH (R-P1-9), not just rendered: a
|
|
76
|
+
// narrower population cannot keep a wider pool, but a DISPLAY-only correction
|
|
77
|
+
// would snap back the moment the user widened again, so "the change I just
|
|
78
|
+
// made" would not stick - the exact complaint this design exists to answer.
|
|
79
|
+
return current.maxConcurrent > next
|
|
80
|
+
? { maxSubagents: next, maxConcurrent: next }
|
|
81
|
+
: { maxSubagents: next };
|
|
82
|
+
}
|
|
83
|
+
// THE EXHAUSTIVE GUARD IS THE POINT (P0-1): a new member of `TeamFanoutField`
|
|
84
|
+
// must fail to COMPILE here, not silently fall through to `maxSubagents`.
|
|
85
|
+
//
|
|
86
|
+
// IT SITS AFTER EVERY BRANCH RETURNS, not before the last one. §3.9's snippet
|
|
87
|
+
// shows it above the `maxSubagents` branch, where TypeScript rejects the file
|
|
88
|
+
// outright (`'maxSubagents' is not assignable to 'never'`) - the intent
|
|
89
|
+
// survives, the position does not.
|
|
90
|
+
const _exhaustive = field;
|
|
91
|
+
return _exhaustive;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The page's four rows, in cursor order.
|
|
95
|
+
*
|
|
96
|
+
* `Team mode` IS FIRST because it is the premise of the other three: with `task`
|
|
97
|
+
* out of the tool list there is no width to configure and no supervisor to run,
|
|
98
|
+
* and a page that listed the numbers first would invite the user to tune a
|
|
99
|
+
* capability they had switched off.
|
|
100
|
+
*
|
|
101
|
+
* TWO HINTS CARRY A CONDITION THE DESIGN OWNS RATHER THAN DECORATION (R-1 / C-2):
|
|
102
|
+
* the default pool is 8, which is 8 simultaneous streams against one API key, and
|
|
103
|
+
* the honest mitigation for an HTTP 429 storm is "lower this number" - said HERE,
|
|
104
|
+
* where the number is, not only in a risk table nobody reads.
|
|
105
|
+
*
|
|
106
|
+
* `teamRegistered === false` (a `--no-team` session) changes TWO things on the
|
|
107
|
+
* `enabled` row and nothing else: `on (next launch)` because the tool array is
|
|
108
|
+
* immutable (I-7), and a hint that says why. Absent means "not disclosed",
|
|
109
|
+
* which every existing fixture relies on.
|
|
110
|
+
*/
|
|
111
|
+
export function fanoutRows(team, teamRegistered) {
|
|
112
|
+
const blocked = teamRegistered === false && team.enabled;
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
field: 'enabled',
|
|
116
|
+
label: 'Team mode',
|
|
117
|
+
value: blocked ? 'on (next launch)' : team.enabled ? 'on' : 'off',
|
|
118
|
+
hint: blocked
|
|
119
|
+
? '--no-team session: the task tool cannot be added live'
|
|
120
|
+
: 'off: no task tool next launch; this session may lack it',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
field: 'maxSubagents',
|
|
124
|
+
label: 'Max subagents',
|
|
125
|
+
value: String(team.maxSubagents),
|
|
126
|
+
hint: `children per dispatch; ${HARD_MAX_SUBAGENTS} is the hard ceiling`,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
field: 'maxConcurrent',
|
|
130
|
+
label: 'Run at once',
|
|
131
|
+
value: String(team.maxConcurrent),
|
|
132
|
+
hint: 'provider limits: lower this if you see rate-limit errors',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
field: 'overseer',
|
|
136
|
+
label: 'Supervisor',
|
|
137
|
+
value: team.overseer ? 'on' : 'off',
|
|
138
|
+
hint: 'checks stalled children; never kills them',
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The one-line summary the settings root row prints for this page.
|
|
144
|
+
*
|
|
145
|
+
* `off - no subagents` rather than the numbers, because with the capability off
|
|
146
|
+
* the numbers describe nothing the user can spend. This replaced
|
|
147
|
+
* `fanoutSummary()` outright instead of living beside it: two summary functions
|
|
148
|
+
* over one page is two authorities for one line, and the root row would sooner or
|
|
149
|
+
* later disagree with the page it summarises.
|
|
150
|
+
*/
|
|
151
|
+
export function teamSummary(team) {
|
|
152
|
+
if (!team.enabled)
|
|
153
|
+
return 'off - no subagents';
|
|
154
|
+
return `${team.maxSubagents} per dispatch, ${team.maxConcurrent} at once`;
|
|
155
|
+
}
|