@aragon-agent/cli 0.6.38 → 0.6.40
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 +263 -0
- package/README.md +544 -11
- package/dist/agent/controller.d.ts +245 -10
- package/dist/agent/controller.js +367 -22
- 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 +70 -2
- package/dist/agent/reducer.js +59 -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 +28 -2
- package/dist/agent/system-prompt.js +12 -4
- package/dist/cli.js +106 -32
- package/dist/commands/builtins.js +24 -3
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -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/cli-commands.d.ts +23 -0
- package/dist/config/cli-commands.js +36 -0
- package/dist/config/env.js +17 -4
- package/dist/config/load.js +55 -3
- package/dist/config/schema.d.ts +125 -10
- package/dist/config/schema.js +138 -14
- package/dist/config/store.js +23 -3
- package/dist/diagnostics/doctor.d.ts +9 -0
- package/dist/diagnostics/doctor.js +34 -0
- package/dist/diagnostics/info.d.ts +71 -0
- package/dist/diagnostics/info.js +91 -5
- package/dist/exec/bridge/caller-tools.d.ts +96 -0
- package/dist/exec/bridge/caller-tools.js +213 -0
- package/dist/exec/bridge/decisions.d.ts +78 -0
- package/dist/exec/bridge/decisions.js +116 -0
- package/dist/exec/bridge/human-answers.d.ts +38 -0
- package/dist/exec/bridge/human-answers.js +164 -0
- package/dist/exec/bridge/limits.d.ts +68 -0
- package/dist/exec/bridge/limits.js +68 -0
- package/dist/exec/bridge/ndjson.d.ts +17 -0
- package/dist/exec/bridge/ndjson.js +83 -0
- package/dist/exec/bridge/pending.d.ts +70 -0
- package/dist/exec/bridge/pending.js +132 -0
- package/dist/exec/bridge/protocol.d.ts +346 -0
- package/dist/exec/bridge/protocol.js +256 -0
- package/dist/exec/bridge/server.d.ts +142 -0
- package/dist/exec/bridge/server.js +502 -0
- package/dist/exec/bridge/source.d.ts +58 -0
- package/dist/exec/bridge/source.js +145 -0
- package/dist/exec/bridge/writer.d.ts +26 -0
- package/dist/exec/bridge/writer.js +156 -0
- package/dist/exec/cli-commands.js +30 -2
- package/dist/exec/emitter.d.ts +16 -2
- package/dist/exec/emitter.js +26 -6
- package/dist/exec/events.d.ts +465 -10
- package/dist/exec/events.js +47 -1
- package/dist/exec/hooks/dispatch.d.ts +76 -0
- package/dist/exec/hooks/dispatch.js +169 -0
- package/dist/exec/hooks/index.d.ts +83 -0
- package/dist/exec/hooks/index.js +205 -0
- package/dist/exec/hooks/limits.d.ts +56 -0
- package/dist/exec/hooks/limits.js +49 -0
- package/dist/exec/hooks/registry.d.ts +49 -0
- package/dist/exec/hooks/registry.js +91 -0
- package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
- package/dist/exec/hooks/tool-wrapper.js +154 -0
- package/dist/exec/hooks/turn.d.ts +61 -0
- package/dist/exec/hooks/turn.js +59 -0
- package/dist/exec/hooks/validate.d.ts +85 -0
- package/dist/exec/hooks/validate.js +310 -0
- package/dist/exec/index.d.ts +130 -4
- package/dist/exec/index.js +501 -10
- package/dist/exec/options.d.ts +124 -2
- package/dist/exec/options.js +413 -6
- package/dist/exec/permission.d.ts +9 -1
- package/dist/exec/permission.js +7 -1
- package/dist/exec/request/limits.d.ts +51 -0
- package/dist/exec/request/limits.js +49 -0
- package/dist/exec/request/scope.d.ts +106 -0
- package/dist/exec/request/scope.js +149 -0
- package/dist/exec/result/limits.d.ts +74 -0
- package/dist/exec/result/limits.js +72 -0
- package/dist/exec/result/prompt.d.ts +28 -0
- package/dist/exec/result/prompt.js +74 -0
- package/dist/exec/result/schema.d.ts +99 -0
- package/dist/exec/result/schema.js +464 -0
- package/dist/exec/result/sink.d.ts +56 -0
- package/dist/exec/result/sink.js +50 -0
- package/dist/exec/result/tool.d.ts +37 -0
- package/dist/exec/result/tool.js +118 -0
- package/dist/exec/runner.d.ts +395 -1
- package/dist/exec/runner.js +860 -8
- package/dist/exec/threads/aggregate.d.ts +34 -0
- package/dist/exec/threads/aggregate.js +30 -0
- package/dist/exec/threads/capacity.d.ts +23 -0
- package/dist/exec/threads/capacity.js +38 -0
- package/dist/exec/threads/dispatch-source.d.ts +60 -0
- package/dist/exec/threads/dispatch-source.js +106 -0
- package/dist/exec/threads/execute-threads.d.ts +53 -0
- package/dist/exec/threads/execute-threads.js +234 -0
- package/dist/exec/threads/gate.d.ts +42 -0
- package/dist/exec/threads/gate.js +81 -0
- package/dist/exec/threads/identity.d.ts +36 -0
- package/dist/exec/threads/identity.js +67 -0
- package/dist/exec/threads/interrupt.d.ts +29 -0
- package/dist/exec/threads/interrupt.js +79 -0
- package/dist/exec/threads/limits.d.ts +58 -0
- package/dist/exec/threads/limits.js +35 -0
- package/dist/exec/threads/outcome.d.ts +47 -0
- package/dist/exec/threads/outcome.js +26 -0
- package/dist/exec/threads/registry.d.ts +81 -0
- package/dist/exec/threads/registry.js +427 -0
- package/dist/exec/threads/runtime.d.ts +107 -0
- package/dist/exec/threads/runtime.js +523 -0
- package/dist/exec/threads/thread-source.d.ts +20 -0
- package/dist/exec/threads/thread-source.js +134 -0
- package/dist/fast/limits.d.ts +54 -0
- package/dist/fast/limits.js +54 -0
- package/dist/fast/summary-call.d.ts +72 -0
- package/dist/fast/summary-call.js +133 -0
- package/dist/fast/summary-prompt.d.ts +106 -0
- package/dist/fast/summary-prompt.js +320 -0
- package/dist/fast/task-summary-types.d.ts +66 -0
- package/dist/fast/task-summary-types.js +17 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/limits.d.ts +7 -0
- package/dist/proc/limits.js +7 -0
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- 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/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- 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/runtime.d.ts +9 -0
- package/dist/team/runtime.js +8 -0
- package/dist/team/subagent.d.ts +15 -0
- package/dist/team/subagent.js +45 -1
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +238 -12
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -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/interaction-hints.js +6 -0
- package/dist/ui/layout/OverlayFrame.d.ts +18 -0
- package/dist/ui/layout/OverlayFrame.js +19 -4
- package/dist/ui/layout/header-hints.d.ts +55 -62
- package/dist/ui/layout/header-hints.js +59 -166
- package/dist/ui/layout/header-left.d.ts +115 -0
- package/dist/ui/layout/header-left.js +206 -0
- package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
- package/dist/ui/layout/shortcut-panel-layout.js +321 -0
- package/dist/ui/layout/status-detail-layout.js +5 -1
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +18 -1
- package/dist/ui/layout/task-summary-cell.d.ts +61 -0
- package/dist/ui/layout/task-summary-cell.js +65 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.js +29 -2
- package/dist/ui/overlays/SettingsScreen.js +29 -5
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task-summary cell's decisions (tui-shortcut-drawer-and-task-summary §6.3).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/ui/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS A SEPARATE MODULE FROM `header-left.ts`: the planner answers "how
|
|
7
|
+
* wide is the cell", this one answers "should there be a cell at all, and what is
|
|
8
|
+
* in it". The second question is the trigger rule (D-7) and it has to be testable
|
|
9
|
+
* without a renderer - `task-summary-app.test.tsx` pins "one call per session" by
|
|
10
|
+
* calling THIS function, not by counting renders.
|
|
11
|
+
*
|
|
12
|
+
* THE TRIGGER IS NARROW ON PURPOSE. A header label is an identity strip, not a
|
|
13
|
+
* live subtitle: refreshing it every turn would flicker, cost money, and teach the
|
|
14
|
+
* user to distrust the one thing on the row that is not a status field.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* D-7's trigger rule.
|
|
18
|
+
*
|
|
19
|
+
* FIVE INPUTS, ONE ANSWER. `userInitiated` is the second guard on the same fact
|
|
20
|
+
* `submitMessage` already knows (`opts.userInitiated !== false`): an automatic
|
|
21
|
+
* continuation is not a new task, and paying for a label for it would spend the
|
|
22
|
+
* user's money to describe work they did not start.
|
|
23
|
+
*/
|
|
24
|
+
export function shouldSummarize(input) {
|
|
25
|
+
// ORDER IS THE DOCUMENTED PRECEDENCE: a prompt that is not a task cannot start
|
|
26
|
+
// one even when everything else says yes, and an OFF feature must report the
|
|
27
|
+
// switch rather than a downstream reason the user cannot act on.
|
|
28
|
+
if (!input.enabled)
|
|
29
|
+
return { start: false, reason: 'disabled' };
|
|
30
|
+
if (input.anchor.trim().length === 0)
|
|
31
|
+
return { start: false, reason: 'no-anchor' };
|
|
32
|
+
if (input.hasSummary)
|
|
33
|
+
return { start: false, reason: 'already-set' };
|
|
34
|
+
if (input.pending)
|
|
35
|
+
return { start: false, reason: 'pending' };
|
|
36
|
+
// ITS OWN REASON, not `no-anchor`: an automatic continuation HAS an anchor, it is
|
|
37
|
+
// simply not a new task, and a skip reason a maintainer cannot trust is worse than
|
|
38
|
+
// no reason at all (review fix RF-3).
|
|
39
|
+
if (!input.userInitiated)
|
|
40
|
+
return { start: false, reason: 'not-user-initiated' };
|
|
41
|
+
return { start: true };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The drawer's `Task` section, flattened from the view state so the pure layout
|
|
45
|
+
* module never learns what a `ViewState` is.
|
|
46
|
+
*/
|
|
47
|
+
export function taskSectionFrom(input) {
|
|
48
|
+
if (input.view === null) {
|
|
49
|
+
return {
|
|
50
|
+
text: null, tone: null, reason: null, by: '', ageMs: null,
|
|
51
|
+
enabled: input.enabled, pending: input.pending,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
text: input.view.text,
|
|
56
|
+
tone: input.view.tone,
|
|
57
|
+
reason: input.view.reason,
|
|
58
|
+
by: input.view.by,
|
|
59
|
+
// The age is computed from the view's own timestamp and the caller's clock,
|
|
60
|
+
// so the drawer never introduces a second one.
|
|
61
|
+
ageMs: input.now >= input.view.at ? input.now - input.view.at : null,
|
|
62
|
+
enabled: input.enabled,
|
|
63
|
+
pending: input.pending,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -13,5 +13,17 @@
|
|
|
13
13
|
import React from 'react';
|
|
14
14
|
import type { Theme } from '../theme.js';
|
|
15
15
|
import type { TermCapabilities } from '../capabilities.js';
|
|
16
|
+
/**
|
|
17
|
+
* The key column of every binding row, EXPORTED so the drawer's drift guard can read
|
|
18
|
+
* the real table instead of scraping this file with a regex
|
|
19
|
+
* (tui-shortcut-drawer-and-task-summary D-8 / AC-5).
|
|
20
|
+
*
|
|
21
|
+
* A SOURCE SCRAPE WOULD PASS ON A COMMENT: the regex has no way to tell a key row
|
|
22
|
+
* from a legend row from a line inside a block comment, and the guard's whole job is
|
|
23
|
+
* to notice a row someone forgot.
|
|
24
|
+
*/
|
|
25
|
+
export declare function allKeyRows(times: string): [string, string][];
|
|
26
|
+
/** The slash-command table's key column, for the same guard (commands are not keys). */
|
|
27
|
+
export declare function allCommandRows(): [string, string][];
|
|
16
28
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
17
29
|
export declare function helpRows(theme: Theme, caps: TermCapabilities, native?: boolean): React.ReactElement[];
|
|
@@ -20,7 +20,16 @@ function keyRows(times, native = false) {
|
|
|
20
20
|
.map(([key, description]) => key === 'PgUp / PgDn' ? [key, 'Page through an open overlay'] : [key, description])
|
|
21
21
|
.concat([['Scroll', 'Use your terminal (wheel, Shift+PgUp, scrollbar, search)']]);
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The key column of every binding row, EXPORTED so the drawer's drift guard can read
|
|
25
|
+
* the real table instead of scraping this file with a regex
|
|
26
|
+
* (tui-shortcut-drawer-and-task-summary D-8 / AC-5).
|
|
27
|
+
*
|
|
28
|
+
* A SOURCE SCRAPE WOULD PASS ON A COMMENT: the regex has no way to tell a key row
|
|
29
|
+
* from a legend row from a line inside a block comment, and the guard's whole job is
|
|
30
|
+
* to notice a row someone forgot.
|
|
31
|
+
*/
|
|
32
|
+
export function allKeyRows(times) {
|
|
24
33
|
return [
|
|
25
34
|
['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
|
|
26
35
|
['Alt+Enter / Shift+Enter', 'Newline when supported; the host may intercept either binding'],
|
|
@@ -57,7 +66,15 @@ function allKeyRows(times) {
|
|
|
57
66
|
// went - and because `^K` is only advertised in the expanded header row.
|
|
58
67
|
['Ctrl+K', 'Agent kits: pick a preset for this session (see /kit)'],
|
|
59
68
|
['Alt+K', 'Delete from the cursor to the end of the line (was Ctrl+K)'],
|
|
60
|
-
|
|
69
|
+
// Ctrl+G OPENED THE STATUS DETAIL ROW UNTIL THE DRAWER TOOK THE KEY
|
|
70
|
+
// (tui-shortcut-drawer-and-task-summary D-6). The detail row did not disappear,
|
|
71
|
+
// it moved to `d` inside this panel, and saying so here is the only place a
|
|
72
|
+
// user's muscle memory can find out.
|
|
73
|
+
['Ctrl+G', 'Keyboard drawer: every shortcut, with keywords (d = status details)'],
|
|
74
|
+
// Ctrl+O WAS MISSING FROM THIS TABLE ENTIRELY while it worked in the app, which
|
|
75
|
+
// is exactly the gap the drawer's reverse drift guard exists to force closed: a
|
|
76
|
+
// key that only exists in the code is a key only its author knows.
|
|
77
|
+
['Ctrl+O', 'Expand the newest tool output (native mode reprints it as a block)'],
|
|
61
78
|
// The board's key is listed HERE and in the team table below: the roster shows
|
|
62
79
|
// `ctrl+b` only on a terminal wide enough for the hint cell, and a key a user can
|
|
63
80
|
// only learn from a wide terminal is a key the rest never discover (AC-18).
|
|
@@ -92,6 +109,12 @@ function allKeyRows(times) {
|
|
|
92
109
|
}
|
|
93
110
|
const COMMANDS = [
|
|
94
111
|
['/help', 'Show this help'],
|
|
112
|
+
// THE DRAWER'S COMMAND-SIDE ENTRY (tui-shortcut-drawer-and-task-summary §4.7).
|
|
113
|
+
// Listed here as well as behind Ctrl+G, because a key is unreachable in a script
|
|
114
|
+
// and `/keys` is how a paste or a screen reader gets to the same list.
|
|
115
|
+
['/keys [query]', 'List every shortcut in the drawer; a query pre-filters it'],
|
|
116
|
+
['/task', 'Show the header task summary: what it says, where it came from'],
|
|
117
|
+
['/task refresh', 'Regenerate the header task summary from the last prompt'],
|
|
95
118
|
['/model', 'Open the model picker'],
|
|
96
119
|
['/settings', 'Open the settings screen'],
|
|
97
120
|
['/plan [on|off|status]', 'Toggle plan mode (same as Shift+Tab)'],
|
|
@@ -172,6 +195,10 @@ const FAST_ROWS = [
|
|
|
172
195
|
['--fast / --no-fast', 'Start a session with the tier on or off (also ARAGON_FAST=1)'],
|
|
173
196
|
['<fast_review>', 'An automated second opinion from the fast model - advice, not the user'],
|
|
174
197
|
];
|
|
198
|
+
/** The slash-command table's key column, for the same guard (commands are not keys). */
|
|
199
|
+
export function allCommandRows() {
|
|
200
|
+
return COMMANDS;
|
|
201
|
+
}
|
|
175
202
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
176
203
|
export function helpRows(theme, caps, native = false) {
|
|
177
204
|
const glyphs = pickGlyphs(caps);
|
|
@@ -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, fanoutSummary, teamFanoutValues } 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,11 @@ 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: { rows: fanoutRows(teamFanoutValues(state.draft)), error: state.error }, dispatch: dispatch, onBack: () => dispatch({ type: 'page', page: 'root' }) });
|
|
839
858
|
// THE KIT PAGES (agent-model-kits §4.5). They are dispatched BEFORE the profile
|
|
840
859
|
// pages because `'kit-slot'` RENDERS a profile picker while belonging to the
|
|
841
860
|
// kit editor - the page name, not the component, decides where the user is.
|
|
@@ -945,9 +964,14 @@ function buildRootSettingsRows(props, view) {
|
|
|
945
964
|
profiles: state.draft.profiles,
|
|
946
965
|
hasKey: (id) => (props.apiKeys[id] ?? '').length > 0 })}${kitTag}`
|
|
947
966
|
: subagentLine;
|
|
967
|
+
// THE FAN-OUT SUMMARY (subagent-fanout 3.4.4): a DERIVED row, like the three
|
|
968
|
+
// above it, and deliberately not a control - the numbers are adjusted on the
|
|
969
|
+
// page Enter opens, so the root row cannot change a value by accident.
|
|
970
|
+
const fanout = fanoutSummary(teamFanoutValues(state.draft));
|
|
948
971
|
const lines = [`Main profile ${mainValue}`,
|
|
949
972
|
`Fast profile ${fastValue}`,
|
|
950
973
|
`Subagent model ${subagentValue}`,
|
|
974
|
+
`Subagent fan-out ${fanout}`,
|
|
951
975
|
`Kit pack ${kitPackRow(state)}`,
|
|
952
976
|
`Manage profiles ${library.entries.length} saved`,
|
|
953
977
|
...fields.map((item) => `${item.label.padEnd(18)}${item.key === 'apiKey'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keyboard drawer (tui-shortcut-drawer-and-task-summary §4.5).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* IT OWNS NO CHROME AND NO ARITHMETIC. `OverlayFrame` draws the frame, the
|
|
7
|
+
* window and the position indicator; `shortcut-panel-layout.ts` decides the
|
|
8
|
+
* columns, the rows and which window contains the cursor. This component adds two
|
|
9
|
+
* things and nothing else: its own key handling and the text-run colours. v1
|
|
10
|
+
* planned 260 lines that re-implemented the frame's windowing and clamping - a
|
|
11
|
+
* second source of truth for the one thing overlays have already been rewritten
|
|
12
|
+
* once to unify.
|
|
13
|
+
*
|
|
14
|
+
* IT DOES NOT REGISTER `Esc` AND DOES NOT TOUCH ANY `ctrl`/`meta` COMBINATION:
|
|
15
|
+
* `Esc` belongs to `App` (it closes every overlay and cancels the pending
|
|
16
|
+
* question/plan), and `Ctrl+G` is handled there too. Two listeners both acting on
|
|
17
|
+
* one byte is how a panel closes twice.
|
|
18
|
+
*/
|
|
19
|
+
import React from 'react';
|
|
20
|
+
import type { Theme } from '../theme.js';
|
|
21
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
22
|
+
import { type ShortcutTaskSection } from '../layout/shortcut-panel-layout.js';
|
|
23
|
+
export interface ShortcutPanelProps {
|
|
24
|
+
/** The terminal width, which is what `OverlayFrame` needs to pick its form. */
|
|
25
|
+
cols: number;
|
|
26
|
+
maxRows: number;
|
|
27
|
+
theme: Theme;
|
|
28
|
+
caps: TermCapabilities;
|
|
29
|
+
isActive: boolean;
|
|
30
|
+
native: boolean;
|
|
31
|
+
running: boolean;
|
|
32
|
+
/** `/keys <query>` pre-filter. */
|
|
33
|
+
query?: string;
|
|
34
|
+
task: ShortcutTaskSection;
|
|
35
|
+
detailsExpanded: boolean;
|
|
36
|
+
onToggleDetails(): void;
|
|
37
|
+
onRefresh(): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function ShortcutPanel(props: ShortcutPanelProps): React.ReactElement;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The keyboard drawer (tui-shortcut-drawer-and-task-summary §4.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* IT OWNS NO CHROME AND NO ARITHMETIC. `OverlayFrame` draws the frame, the
|
|
8
|
+
* window and the position indicator; `shortcut-panel-layout.ts` decides the
|
|
9
|
+
* columns, the rows and which window contains the cursor. This component adds two
|
|
10
|
+
* things and nothing else: its own key handling and the text-run colours. v1
|
|
11
|
+
* planned 260 lines that re-implemented the frame's windowing and clamping - a
|
|
12
|
+
* second source of truth for the one thing overlays have already been rewritten
|
|
13
|
+
* once to unify.
|
|
14
|
+
*
|
|
15
|
+
* IT DOES NOT REGISTER `Esc` AND DOES NOT TOUCH ANY `ctrl`/`meta` COMBINATION:
|
|
16
|
+
* `Esc` belongs to `App` (it closes every overlay and cancels the pending
|
|
17
|
+
* question/plan), and `Ctrl+G` is handled there too. Two listeners both acting on
|
|
18
|
+
* one byte is how a panel closes twice.
|
|
19
|
+
*/
|
|
20
|
+
import { useMemo, useState } from 'react';
|
|
21
|
+
import { Text, useInput } from 'ink';
|
|
22
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
23
|
+
import { OverlayFrame, overlayBodyRows, overlayPageSize } from '../layout/OverlayFrame.js';
|
|
24
|
+
import { buildShortcutRows, shortcutPanelWidth, } from '../layout/shortcut-panel-layout.js';
|
|
25
|
+
import { filterShortcuts, isAvailable, SHORTCUTS } from '../shortcuts.js';
|
|
26
|
+
/** Colour per tone. `dim` is `muted` plus the dim attribute, never a second colour. */
|
|
27
|
+
function toneOf(row, theme) {
|
|
28
|
+
if (row.tone === 'accent')
|
|
29
|
+
return { color: theme.accent };
|
|
30
|
+
if (row.tone === 'primary')
|
|
31
|
+
return { color: theme.primary };
|
|
32
|
+
if (row.tone === 'dim')
|
|
33
|
+
return { color: theme.muted, dimColor: true };
|
|
34
|
+
return { color: theme.muted };
|
|
35
|
+
}
|
|
36
|
+
export function ShortcutPanel(props) {
|
|
37
|
+
const { cols, maxRows, theme, caps, isActive, native, running } = props;
|
|
38
|
+
const glyphs = pickGlyphs(caps);
|
|
39
|
+
const [cursor, setCursor] = useState(0);
|
|
40
|
+
const entries = useMemo(() => filterShortcuts(SHORTCUTS, props.query ?? ''), [props.query]);
|
|
41
|
+
const bodyRows = overlayBodyRows(maxRows, cols);
|
|
42
|
+
const page = overlayPageSize(maxRows, cols);
|
|
43
|
+
const result = useMemo(() => buildShortcutRows({
|
|
44
|
+
cols,
|
|
45
|
+
rows: bodyRows,
|
|
46
|
+
cursor,
|
|
47
|
+
entries,
|
|
48
|
+
task: props.task,
|
|
49
|
+
entryAvailable: (entry) => isAvailable(entry, { native, running }),
|
|
50
|
+
unavailableReason: native ? 'this mode leaves it to the terminal' : 'a run is in flight',
|
|
51
|
+
glyphs: {
|
|
52
|
+
ellipsis: glyphs.ellipsis,
|
|
53
|
+
cursor: glyphs.cursor,
|
|
54
|
+
hrule: glyphs.hrule,
|
|
55
|
+
midDot: glyphs.midDot,
|
|
56
|
+
},
|
|
57
|
+
}), [cols, bodyRows, cursor, entries, props.task, native, running,
|
|
58
|
+
glyphs.ellipsis, glyphs.cursor, glyphs.hrule, glyphs.midDot]);
|
|
59
|
+
const last = Math.max(0, entries.length - 1);
|
|
60
|
+
useInput((input, key) => {
|
|
61
|
+
// `ctrl`/`meta` ARE NOT OURS: `Ctrl+G` closes this panel and `Ctrl+C` owns the
|
|
62
|
+
// exit ladder, both in `App`. Consuming them here would double-handle a byte.
|
|
63
|
+
if (key.ctrl || key.meta)
|
|
64
|
+
return;
|
|
65
|
+
if (key.upArrow || input === 'k') {
|
|
66
|
+
setCursor((value) => Math.max(0, value - 1));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (key.downArrow || input === 'j') {
|
|
70
|
+
setCursor((value) => Math.min(last, value + 1));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// PAGING MOVES THE CURSOR, one page minus one row of overlap - the same rule
|
|
74
|
+
// `overlayPageSize` uses for every other overlay, so a page turn keeps a row of
|
|
75
|
+
// context. An independent offset would let the selected row - and the detail
|
|
76
|
+
// line that explains it - slide off screen (P1-7).
|
|
77
|
+
if (key.pageUp) {
|
|
78
|
+
setCursor((value) => Math.max(0, value - Math.max(1, page - 1)));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (key.pageDown) {
|
|
82
|
+
setCursor((value) => Math.min(last, value + Math.max(1, page - 1)));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (input === 'd') {
|
|
86
|
+
props.onToggleDetails();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (input === 'r') {
|
|
90
|
+
props.onRefresh();
|
|
91
|
+
}
|
|
92
|
+
}, { isActive });
|
|
93
|
+
const rows = result.rows.map((row, index) => (_jsx(Text, { wrap: "truncate", ...toneOf(row, theme), children: row.text }, `${row.kind}-${row.entryId ?? 'x'}-${index}`)));
|
|
94
|
+
// THE `d` MARKER LIVES ON THE FRAME'S OWN TITLE LINE (D-6), which is chrome that is
|
|
95
|
+
// already paid for. It is spelled `d:on` / `d:off` rather than `details on|off`
|
|
96
|
+
// because the title row also carries the frame's position indicator (`v 1-13/37`,
|
|
97
|
+
// 13 cells) and the drawer's content is only 26 cells wide at 80 columns: the long
|
|
98
|
+
// form is truncated to `details...`, which is a marker that says nothing. The
|
|
99
|
+
// footer's `d details` clause names the key, so the pair reads as one sentence.
|
|
100
|
+
const title = `Keys d:${props.detailsExpanded ? 'on' : 'off'}`;
|
|
101
|
+
return (_jsx(OverlayFrame, { title: title, hint: result.hint, maxRows: maxRows, cols: cols, width: shortcutPanelWidth(cols), align: "right", rows: rows,
|
|
102
|
+
// THE WINDOW IS DERIVED FROM THE CURSOR, so the offset handed to the frame is
|
|
103
|
+
// a conclusion rather than state. `onScrollClamp` is deliberately not passed:
|
|
104
|
+
// there is nothing here that could drift out of step with it.
|
|
105
|
+
scrollOffset: result.window.start, theme: theme, caps: caps }));
|
|
106
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings > Team fan-out: three arrows-adjustable rows (subagent-fanout 3.4.3).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* THE PAGE OWNS ONE PIECE OF STATE: its cursor. Every value comes from
|
|
7
|
+
* `props.view.rows`, which `SettingsScreen` derives from the reducer state - so
|
|
8
|
+
* this page cannot hold a second copy of the draft and then disagree with the
|
|
9
|
+
* root page about what is about to be saved (the discipline
|
|
10
|
+
* `model-profile-state.ts` records at its own head).
|
|
11
|
+
*
|
|
12
|
+
* IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13).
|
|
13
|
+
* `useRootSettingsInput` gates its `useInput` on `state.page === 'root'`, so a
|
|
14
|
+
* subpage has no save listener at all - the kit page included. A footer
|
|
15
|
+
* promising "Ctrl+S saves here" would therefore be a lie the user acts on. The
|
|
16
|
+
* honest instruction is the one already in their fingers: Esc back, Ctrl+S on the
|
|
17
|
+
* main page.
|
|
18
|
+
*/
|
|
19
|
+
import React from 'react';
|
|
20
|
+
import { type ProfileFrameProps } from './ModelProfilePicker.js';
|
|
21
|
+
import type { TeamFanoutField, TeamFanoutView } from './team-fanout-state.js';
|
|
22
|
+
/** What the page may ask the screen to change. */
|
|
23
|
+
export interface TeamFanoutAction {
|
|
24
|
+
type: 'team-fanout';
|
|
25
|
+
field: TeamFanoutField;
|
|
26
|
+
delta: -1 | 1;
|
|
27
|
+
}
|
|
28
|
+
interface TeamFanoutPageProps extends ProfileFrameProps {
|
|
29
|
+
view: TeamFanoutView;
|
|
30
|
+
dispatch: (action: TeamFanoutAction) => void;
|
|
31
|
+
/** Esc: back to the settings root. The draft is NOT discarded here. */
|
|
32
|
+
onBack: () => void;
|
|
33
|
+
}
|
|
34
|
+
export declare function TeamFanoutPage(props: TeamFanoutPageProps): React.ReactElement;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Settings > Team fan-out: three arrows-adjustable rows (subagent-fanout 3.4.3).
|
|
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
|
+
* IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13).
|
|
14
|
+
* `useRootSettingsInput` gates its `useInput` on `state.page === 'root'`, so a
|
|
15
|
+
* subpage has no save listener at all - the kit page included. A footer
|
|
16
|
+
* promising "Ctrl+S saves here" would therefore be a lie the user acts on. The
|
|
17
|
+
* honest instruction is the one already in their fingers: Esc back, Ctrl+S on the
|
|
18
|
+
* main page.
|
|
19
|
+
*/
|
|
20
|
+
import { useState } from 'react';
|
|
21
|
+
import { Text, useInput } from 'ink';
|
|
22
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
23
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
24
|
+
import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
|
|
25
|
+
/**
|
|
26
|
+
* Label column, matching the root page's `padEnd(18)` rows above and below this
|
|
27
|
+
* one. `Subagent fan-out` is 16 characters, so two spaces of padding are all it
|
|
28
|
+
* needs to line up with `Subagent model` - measured, not guessed.
|
|
29
|
+
*/
|
|
30
|
+
const LABEL_PAD = 18;
|
|
31
|
+
export function TeamFanoutPage(props) {
|
|
32
|
+
const [index, setIndex] = useState(0);
|
|
33
|
+
const rows = props.view.rows;
|
|
34
|
+
const count = Math.max(1, rows.length);
|
|
35
|
+
const cursor = Math.min(index, count - 1);
|
|
36
|
+
const focused = rows[cursor];
|
|
37
|
+
const offset = useProfileScroll(props, cursor);
|
|
38
|
+
const nudge = (delta) => {
|
|
39
|
+
if (!focused)
|
|
40
|
+
return;
|
|
41
|
+
props.dispatch({ type: 'team-fanout', field: focused.field, delta });
|
|
42
|
+
};
|
|
43
|
+
useInput((_input, key) => {
|
|
44
|
+
if (props.isActive === false)
|
|
45
|
+
return;
|
|
46
|
+
if (key.escape) {
|
|
47
|
+
props.onBack();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
51
|
+
setIndex((cursor - 1 + count) % count);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.downArrow || key.tab) {
|
|
55
|
+
setIndex((cursor + 1) % count);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.leftArrow || key.rightArrow) {
|
|
59
|
+
nudge(key.leftArrow ? -1 : 1);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// `Enter` toggles the Supervisor row and does NOTHING on a numeric row:
|
|
63
|
+
// a stray Return that silently changed a number is indistinguishable from a
|
|
64
|
+
// bug, and the arrow keys are already the obvious control.
|
|
65
|
+
if (key.return && focused?.field === 'overseer')
|
|
66
|
+
nudge(1);
|
|
67
|
+
});
|
|
68
|
+
const lines = rows.map((row) => `${row.label.padEnd(LABEL_PAD)}${row.value}`);
|
|
69
|
+
const glyphs = pickGlyphs(props.caps);
|
|
70
|
+
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)));
|
|
71
|
+
// The hint explains the FOCUSED row rather than decorating the frame: three
|
|
72
|
+
// numbers whose meaning is not obvious ("per dispatch" versus "at once") are
|
|
73
|
+
// the reason this page exists at all.
|
|
74
|
+
if (focused) {
|
|
75
|
+
body.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: clipProfileText(focused.hint, props.cols - 8) }, "hint"));
|
|
76
|
+
}
|
|
77
|
+
return _jsx(OverlayFrame, { ...props, rows: body, scrollOffset: offset, title: props.view.error ?? 'Team fan-out', hint: "Arrows adjust. Esc goes back; Ctrl+S on the main page saves." });
|
|
78
|
+
}
|
|
@@ -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;
|
|
@@ -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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings > Team fan-out: the pure half of the page (subagent-fanout 3.4.2).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* THE PAGE WRITES THREE KEYS AND CANNOT BE MADE TO WRITE A FOURTH (D-17 /
|
|
7
|
+
* R-P0-1). `TeamFanoutPatch` is `Pick<TeamConfig, 'maxSubagents' |
|
|
8
|
+
* 'maxConcurrent' | 'overseer'>`, and `stepFanoutValue` returns
|
|
9
|
+
* `Partial<TeamFanoutPatch>`: a width adjustment therefore CANNOT carry `model`
|
|
10
|
+
* or `modelProfileId` into `draft.patch.team`. That matters because
|
|
11
|
+
* `releasedSlots` reads those two keys as "the user re-decided the subagent
|
|
12
|
+
* slot", so a page that returned the whole section would silently release the
|
|
13
|
+
* model a session kit had just installed - and no test would notice, because
|
|
14
|
+
* every value on screen would still be right.
|
|
15
|
+
*
|
|
16
|
+
* EVERYTHING HERE IS A PURE FUNCTION over the draft. The page owns exactly one
|
|
17
|
+
* piece of state (its cursor); values come from the reducer, which is the same
|
|
18
|
+
* discipline the rest of this screen follows.
|
|
19
|
+
*/
|
|
20
|
+
import type { TeamConfig } from '../../config/schema.js';
|
|
21
|
+
import type { ModelSettingsDraft } from '../../config/model-profile-store.js';
|
|
22
|
+
export type TeamFanoutField = 'maxSubagents' | 'maxConcurrent' | 'overseer';
|
|
23
|
+
/**
|
|
24
|
+
* THE ONLY KEYS THIS PAGE MAY WRITE (R-P0-1).
|
|
25
|
+
*
|
|
26
|
+
* Not `Partial<TeamConfig>`: a fragment that could carry `model` would make a
|
|
27
|
+
* width adjustment read as a model decision at save time.
|
|
28
|
+
*/
|
|
29
|
+
export type TeamFanoutPatch = Pick<TeamConfig, 'maxSubagents' | 'maxConcurrent' | 'overseer'>;
|
|
30
|
+
export interface TeamFanoutRow {
|
|
31
|
+
field: TeamFanoutField;
|
|
32
|
+
label: string;
|
|
33
|
+
/** The value as the page prints it (`'8'` / `'on'`). */
|
|
34
|
+
value: string;
|
|
35
|
+
/** One line, <= 60 columns: WHY this number, not a restatement of the label. */
|
|
36
|
+
hint: string;
|
|
37
|
+
}
|
|
38
|
+
export interface TeamFanoutView {
|
|
39
|
+
/** Exactly three, in the fixed order the cursor indexes. */
|
|
40
|
+
rows: TeamFanoutRow[];
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The CLAMPED team section this draft would save (R-P0-2), for display only.
|
|
45
|
+
*
|
|
46
|
+
* THE CLAMP IS NOT COSMETIC. Without it a patch carrying only
|
|
47
|
+
* `maxSubagents: 6` renders "6 per dispatch, 8 at once" on top of an 8-wide
|
|
48
|
+
* baseline, while `mergePersistedConfig` clamps the same patch to 6/6 on the way
|
|
49
|
+
* to disk - so the screen and the file disagree, and every arrow key makes the
|
|
50
|
+
* user reconcile them by hand.
|
|
51
|
+
*/
|
|
52
|
+
export declare function teamFanoutValues(draft: ModelSettingsDraft): TeamConfig;
|
|
53
|
+
/**
|
|
54
|
+
* The PATCH FRAGMENT for one arrow press, clamped exactly like `clampTeamConfig`.
|
|
55
|
+
*
|
|
56
|
+
* `current` must be the value the screen actually shows (`teamFanoutValues`), so
|
|
57
|
+
* a step always starts from a number the user can see.
|
|
58
|
+
*
|
|
59
|
+
* NO WRAPAROUND: a form control is not a ring, and `Left` at 1 jumping to 10
|
|
60
|
+
* would be a way to widen a dispatch while trying to narrow it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function stepFanoutValue(current: TeamConfig, field: TeamFanoutField, delta: -1 | 1): Partial<TeamFanoutPatch>;
|
|
63
|
+
/**
|
|
64
|
+
* The page's three rows.
|
|
65
|
+
*
|
|
66
|
+
* TWO HINTS CARRY A CONDITION THE DESIGN OWNS RATHER THAN DECORATION (R-1 / C-2):
|
|
67
|
+
* the default pool is now 8, which is 8 simultaneous streams against one API key,
|
|
68
|
+
* and the honest mitigation for an HTTP 429 storm is "lower this number" - said
|
|
69
|
+
* HERE, where the number is, not only in a risk table nobody reads.
|
|
70
|
+
*/
|
|
71
|
+
export declare function fanoutRows(team: TeamConfig): TeamFanoutRow[];
|
|
72
|
+
/** The one-line summary the settings root row prints for this page. */
|
|
73
|
+
export declare function fanoutSummary(team: TeamConfig): string;
|