@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.
Files changed (209) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +544 -11
  3. package/dist/agent/controller.d.ts +245 -10
  4. package/dist/agent/controller.js +367 -22
  5. package/dist/agent/model-profile-settings.js +12 -1
  6. package/dist/agent/plan-prompt.d.ts +1 -3
  7. package/dist/agent/plan-prompt.js +13 -7
  8. package/dist/agent/reducer.d.ts +70 -2
  9. package/dist/agent/reducer.js +59 -6
  10. package/dist/agent/skill-triage-prompt.d.ts +42 -0
  11. package/dist/agent/skill-triage-prompt.js +74 -0
  12. package/dist/agent/system-prompt.d.ts +28 -2
  13. package/dist/agent/system-prompt.js +12 -4
  14. package/dist/cli.js +106 -32
  15. package/dist/commands/builtins.js +24 -3
  16. package/dist/commands/keys-command.d.ts +23 -0
  17. package/dist/commands/keys-command.js +38 -0
  18. package/dist/commands/registry.d.ts +39 -0
  19. package/dist/commands/skills.js +86 -1
  20. package/dist/commands/subagent-command.d.ts +25 -0
  21. package/dist/commands/subagent-command.js +242 -0
  22. package/dist/commands/task-command.d.ts +39 -0
  23. package/dist/commands/task-command.js +102 -0
  24. package/dist/compaction/command.js +16 -3
  25. package/dist/compaction/compactor.d.ts +25 -0
  26. package/dist/compaction/compactor.js +111 -10
  27. package/dist/compaction/digest.d.ts +24 -1
  28. package/dist/compaction/digest.js +139 -17
  29. package/dist/compaction/limits.d.ts +21 -0
  30. package/dist/compaction/limits.js +21 -0
  31. package/dist/compaction/reason-text.d.ts +36 -0
  32. package/dist/compaction/reason-text.js +78 -0
  33. package/dist/compaction/types.d.ts +82 -14
  34. package/dist/compaction/wiring.d.ts +13 -8
  35. package/dist/compaction/wiring.js +1 -0
  36. package/dist/config/cli-commands.d.ts +23 -0
  37. package/dist/config/cli-commands.js +36 -0
  38. package/dist/config/env.js +17 -4
  39. package/dist/config/load.js +55 -3
  40. package/dist/config/schema.d.ts +125 -10
  41. package/dist/config/schema.js +138 -14
  42. package/dist/config/store.js +23 -3
  43. package/dist/diagnostics/doctor.d.ts +9 -0
  44. package/dist/diagnostics/doctor.js +34 -0
  45. package/dist/diagnostics/info.d.ts +71 -0
  46. package/dist/diagnostics/info.js +91 -5
  47. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  48. package/dist/exec/bridge/caller-tools.js +213 -0
  49. package/dist/exec/bridge/decisions.d.ts +78 -0
  50. package/dist/exec/bridge/decisions.js +116 -0
  51. package/dist/exec/bridge/human-answers.d.ts +38 -0
  52. package/dist/exec/bridge/human-answers.js +164 -0
  53. package/dist/exec/bridge/limits.d.ts +68 -0
  54. package/dist/exec/bridge/limits.js +68 -0
  55. package/dist/exec/bridge/ndjson.d.ts +17 -0
  56. package/dist/exec/bridge/ndjson.js +83 -0
  57. package/dist/exec/bridge/pending.d.ts +70 -0
  58. package/dist/exec/bridge/pending.js +132 -0
  59. package/dist/exec/bridge/protocol.d.ts +346 -0
  60. package/dist/exec/bridge/protocol.js +256 -0
  61. package/dist/exec/bridge/server.d.ts +142 -0
  62. package/dist/exec/bridge/server.js +502 -0
  63. package/dist/exec/bridge/source.d.ts +58 -0
  64. package/dist/exec/bridge/source.js +145 -0
  65. package/dist/exec/bridge/writer.d.ts +26 -0
  66. package/dist/exec/bridge/writer.js +156 -0
  67. package/dist/exec/cli-commands.js +30 -2
  68. package/dist/exec/emitter.d.ts +16 -2
  69. package/dist/exec/emitter.js +26 -6
  70. package/dist/exec/events.d.ts +465 -10
  71. package/dist/exec/events.js +47 -1
  72. package/dist/exec/hooks/dispatch.d.ts +76 -0
  73. package/dist/exec/hooks/dispatch.js +169 -0
  74. package/dist/exec/hooks/index.d.ts +83 -0
  75. package/dist/exec/hooks/index.js +205 -0
  76. package/dist/exec/hooks/limits.d.ts +56 -0
  77. package/dist/exec/hooks/limits.js +49 -0
  78. package/dist/exec/hooks/registry.d.ts +49 -0
  79. package/dist/exec/hooks/registry.js +91 -0
  80. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  81. package/dist/exec/hooks/tool-wrapper.js +154 -0
  82. package/dist/exec/hooks/turn.d.ts +61 -0
  83. package/dist/exec/hooks/turn.js +59 -0
  84. package/dist/exec/hooks/validate.d.ts +85 -0
  85. package/dist/exec/hooks/validate.js +310 -0
  86. package/dist/exec/index.d.ts +130 -4
  87. package/dist/exec/index.js +501 -10
  88. package/dist/exec/options.d.ts +124 -2
  89. package/dist/exec/options.js +413 -6
  90. package/dist/exec/permission.d.ts +9 -1
  91. package/dist/exec/permission.js +7 -1
  92. package/dist/exec/request/limits.d.ts +51 -0
  93. package/dist/exec/request/limits.js +49 -0
  94. package/dist/exec/request/scope.d.ts +106 -0
  95. package/dist/exec/request/scope.js +149 -0
  96. package/dist/exec/result/limits.d.ts +74 -0
  97. package/dist/exec/result/limits.js +72 -0
  98. package/dist/exec/result/prompt.d.ts +28 -0
  99. package/dist/exec/result/prompt.js +74 -0
  100. package/dist/exec/result/schema.d.ts +99 -0
  101. package/dist/exec/result/schema.js +464 -0
  102. package/dist/exec/result/sink.d.ts +56 -0
  103. package/dist/exec/result/sink.js +50 -0
  104. package/dist/exec/result/tool.d.ts +37 -0
  105. package/dist/exec/result/tool.js +118 -0
  106. package/dist/exec/runner.d.ts +395 -1
  107. package/dist/exec/runner.js +860 -8
  108. package/dist/exec/threads/aggregate.d.ts +34 -0
  109. package/dist/exec/threads/aggregate.js +30 -0
  110. package/dist/exec/threads/capacity.d.ts +23 -0
  111. package/dist/exec/threads/capacity.js +38 -0
  112. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  113. package/dist/exec/threads/dispatch-source.js +106 -0
  114. package/dist/exec/threads/execute-threads.d.ts +53 -0
  115. package/dist/exec/threads/execute-threads.js +234 -0
  116. package/dist/exec/threads/gate.d.ts +42 -0
  117. package/dist/exec/threads/gate.js +81 -0
  118. package/dist/exec/threads/identity.d.ts +36 -0
  119. package/dist/exec/threads/identity.js +67 -0
  120. package/dist/exec/threads/interrupt.d.ts +29 -0
  121. package/dist/exec/threads/interrupt.js +79 -0
  122. package/dist/exec/threads/limits.d.ts +58 -0
  123. package/dist/exec/threads/limits.js +35 -0
  124. package/dist/exec/threads/outcome.d.ts +47 -0
  125. package/dist/exec/threads/outcome.js +26 -0
  126. package/dist/exec/threads/registry.d.ts +81 -0
  127. package/dist/exec/threads/registry.js +427 -0
  128. package/dist/exec/threads/runtime.d.ts +107 -0
  129. package/dist/exec/threads/runtime.js +523 -0
  130. package/dist/exec/threads/thread-source.d.ts +20 -0
  131. package/dist/exec/threads/thread-source.js +134 -0
  132. package/dist/fast/limits.d.ts +54 -0
  133. package/dist/fast/limits.js +54 -0
  134. package/dist/fast/summary-call.d.ts +72 -0
  135. package/dist/fast/summary-call.js +133 -0
  136. package/dist/fast/summary-prompt.d.ts +106 -0
  137. package/dist/fast/summary-prompt.js +320 -0
  138. package/dist/fast/task-summary-types.d.ts +66 -0
  139. package/dist/fast/task-summary-types.js +17 -0
  140. package/dist/logging/logger.d.ts +1 -1
  141. package/dist/proc/limits.d.ts +7 -0
  142. package/dist/proc/limits.js +7 -0
  143. package/dist/proc/log-mirror.d.ts +21 -4
  144. package/dist/proc/log-mirror.js +29 -7
  145. package/dist/proc/output-streams.d.ts +50 -0
  146. package/dist/proc/output-streams.js +54 -0
  147. package/dist/skills/service.d.ts +41 -0
  148. package/dist/skills/service.js +62 -0
  149. package/dist/skills/triage.d.ts +194 -0
  150. package/dist/skills/triage.js +393 -0
  151. package/dist/team/forced-dispatch.d.ts +95 -0
  152. package/dist/team/forced-dispatch.js +208 -0
  153. package/dist/team/limits.d.ts +21 -1
  154. package/dist/team/limits.js +21 -1
  155. package/dist/team/normalize.d.ts +15 -0
  156. package/dist/team/normalize.js +21 -4
  157. package/dist/team/prompt.d.ts +6 -3
  158. package/dist/team/prompt.js +9 -5
  159. package/dist/team/runtime.d.ts +9 -0
  160. package/dist/team/runtime.js +8 -0
  161. package/dist/team/subagent.d.ts +15 -0
  162. package/dist/team/subagent.js +45 -1
  163. package/dist/team/task-tool.js +11 -7
  164. package/dist/tools/bash-tool.d.ts +10 -0
  165. package/dist/tools/bash-tool.js +71 -6
  166. package/dist/tools/human-input.d.ts +35 -2
  167. package/dist/tools/human-input.js +38 -0
  168. package/dist/tools/index.d.ts +93 -1
  169. package/dist/tools/index.js +112 -3
  170. package/dist/tools/plan-tools.d.ts +20 -12
  171. package/dist/tools/plan-tools.js +24 -17
  172. package/dist/tools/tool-gate.d.ts +100 -0
  173. package/dist/tools/tool-gate.js +87 -0
  174. package/dist/ui/App.js +238 -12
  175. package/dist/ui/Header.d.ts +28 -16
  176. package/dist/ui/Header.js +21 -19
  177. package/dist/ui/StatusBar.d.ts +1 -0
  178. package/dist/ui/StatusBar.js +1 -0
  179. package/dist/ui/Transcript.js +1 -1
  180. package/dist/ui/entries/CompactionCard.d.ts +11 -1
  181. package/dist/ui/entries/CompactionCard.js +65 -4
  182. package/dist/ui/interaction-hints.js +6 -0
  183. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  184. package/dist/ui/layout/OverlayFrame.js +19 -4
  185. package/dist/ui/layout/header-hints.d.ts +55 -62
  186. package/dist/ui/layout/header-hints.js +59 -166
  187. package/dist/ui/layout/header-left.d.ts +115 -0
  188. package/dist/ui/layout/header-left.js +206 -0
  189. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  190. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  191. package/dist/ui/layout/status-detail-layout.js +5 -1
  192. package/dist/ui/layout/status-layout.d.ts +10 -0
  193. package/dist/ui/layout/status-layout.js +18 -1
  194. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  195. package/dist/ui/layout/task-summary-cell.js +65 -0
  196. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  197. package/dist/ui/overlays/HelpOverlay.js +29 -2
  198. package/dist/ui/overlays/SettingsScreen.js +29 -5
  199. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  200. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  201. package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
  202. package/dist/ui/overlays/TeamFanoutPage.js +78 -0
  203. package/dist/ui/overlays/model-profile-state.d.ts +19 -1
  204. package/dist/ui/overlays/model-profile-state.js +12 -0
  205. package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
  206. package/dist/ui/overlays/team-fanout-state.js +97 -0
  207. package/dist/ui/shortcuts.d.ts +81 -0
  208. package/dist/ui/shortcuts.js +221 -0
  209. 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
- function allKeyRows(times) {
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
- ['Ctrl+G', 'Expand/collapse status details; disabled while an overlay is open'],
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
- // SEVEN FIXED ROWS now (Main, Fast, SUBAGENT, Kit pack, Manage, Save, Reload)
587
- // around the `fields` block - derived from ROOT_ROWS so the count and the
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 fourth root row opens the manager, not a picker: kits are data the user
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
- export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' | 'delete-confirm' | 'discard-confirm' | 'kits' | 'kit-edit' | 'kit-slot' | 'kit-delete';
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;