@aragon-agent/cli 0.6.43 → 0.6.45

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 (251) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +69 -2
  3. package/dist/agent/controller.d.ts +69 -0
  4. package/dist/agent/controller.js +224 -11
  5. package/dist/agent/governance-view.d.ts +40 -0
  6. package/dist/agent/governance-view.js +40 -0
  7. package/dist/agent/governance-wiring.d.ts +162 -0
  8. package/dist/agent/governance-wiring.js +470 -0
  9. package/dist/agent/plan-prompt.d.ts +7 -0
  10. package/dist/agent/plan-prompt.js +2 -1
  11. package/dist/agent/reducer.d.ts +33 -1
  12. package/dist/agent/reducer.js +45 -9
  13. package/dist/agent/system-prompt.d.ts +19 -0
  14. package/dist/agent/system-prompt.js +9 -1
  15. package/dist/cli.js +45 -1
  16. package/dist/commands/builtins.js +10 -4
  17. package/dist/commands/governance-command.d.ts +35 -0
  18. package/dist/commands/governance-command.js +292 -0
  19. package/dist/commands/governance-runs-table.d.ts +40 -0
  20. package/dist/commands/governance-runs-table.js +130 -0
  21. package/dist/commands/registry.d.ts +12 -0
  22. package/dist/config/app-paths.d.ts +10 -0
  23. package/dist/config/app-paths.js +12 -0
  24. package/dist/config/cli-commands.d.ts +18 -0
  25. package/dist/config/cli-commands.js +47 -1
  26. package/dist/config/env.d.ts +2 -1
  27. package/dist/config/env.js +10 -1
  28. package/dist/config/governance-config.d.ts +125 -0
  29. package/dist/config/governance-config.js +164 -0
  30. package/dist/config/governance-launch.d.ts +71 -0
  31. package/dist/config/governance-launch.js +58 -0
  32. package/dist/config/load.d.ts +15 -0
  33. package/dist/config/load.js +26 -1
  34. package/dist/config/model-profile-store.d.ts +4 -2
  35. package/dist/config/schema.d.ts +22 -0
  36. package/dist/config/schema.js +6 -0
  37. package/dist/config/store.js +14 -1
  38. package/dist/config/ui-state.d.ts +11 -0
  39. package/dist/config/ui-state.js +11 -0
  40. package/dist/diagnostics/doctor.js +20 -0
  41. package/dist/governance/card-normalize.d.ts +24 -0
  42. package/dist/governance/card-normalize.js +76 -0
  43. package/dist/governance/copy.d.ts +109 -0
  44. package/dist/governance/copy.js +223 -0
  45. package/dist/governance/engine/aftermath.d.ts +49 -0
  46. package/dist/governance/engine/aftermath.js +134 -0
  47. package/dist/governance/engine/artifact-persist.d.ts +35 -0
  48. package/dist/governance/engine/artifact-persist.js +86 -0
  49. package/dist/governance/engine/budget.d.ts +52 -0
  50. package/dist/governance/engine/budget.js +112 -0
  51. package/dist/governance/engine/cards.d.ts +18 -0
  52. package/dist/governance/engine/cards.js +186 -0
  53. package/dist/governance/engine/digest.d.ts +32 -0
  54. package/dist/governance/engine/digest.js +63 -0
  55. package/dist/governance/engine/engine-core.d.ts +219 -0
  56. package/dist/governance/engine/engine-core.js +562 -0
  57. package/dist/governance/engine/engine.d.ts +68 -0
  58. package/dist/governance/engine/engine.js +314 -0
  59. package/dist/governance/engine/estimate.d.ts +38 -0
  60. package/dist/governance/engine/estimate.js +46 -0
  61. package/dist/governance/engine/inspector.d.ts +16 -0
  62. package/dist/governance/engine/inspector.js +396 -0
  63. package/dist/governance/engine/integration.d.ts +35 -0
  64. package/dist/governance/engine/integration.js +220 -0
  65. package/dist/governance/engine/lifecycle.d.ts +29 -0
  66. package/dist/governance/engine/lifecycle.js +656 -0
  67. package/dist/governance/engine/pause.d.ts +56 -0
  68. package/dist/governance/engine/pause.js +107 -0
  69. package/dist/governance/engine/phase-members.d.ts +15 -0
  70. package/dist/governance/engine/phase-members.js +34 -0
  71. package/dist/governance/engine/phase-planner.d.ts +53 -0
  72. package/dist/governance/engine/phase-planner.js +282 -0
  73. package/dist/governance/engine/rebuild.d.ts +27 -0
  74. package/dist/governance/engine/rebuild.js +57 -0
  75. package/dist/governance/engine/report.d.ts +66 -0
  76. package/dist/governance/engine/report.js +220 -0
  77. package/dist/governance/engine/run-digest.d.ts +20 -0
  78. package/dist/governance/engine/run-digest.js +42 -0
  79. package/dist/governance/engine/spec-builder.d.ts +117 -0
  80. package/dist/governance/engine/spec-builder.js +321 -0
  81. package/dist/governance/engine/stage-runner.d.ts +22 -0
  82. package/dist/governance/engine/stage-runner.js +664 -0
  83. package/dist/governance/engine/start-input.d.ts +19 -0
  84. package/dist/governance/engine/start-input.js +111 -0
  85. package/dist/governance/engine/state-machine.d.ts +101 -0
  86. package/dist/governance/engine/state-machine.js +586 -0
  87. package/dist/governance/engine/submission-queries.d.ts +42 -0
  88. package/dist/governance/engine/submission-queries.js +97 -0
  89. package/dist/governance/engine/submission-render.d.ts +12 -0
  90. package/dist/governance/engine/submission-render.js +92 -0
  91. package/dist/governance/engine/submissions.d.ts +27 -0
  92. package/dist/governance/engine/submissions.js +455 -0
  93. package/dist/governance/engine/visibility.d.ts +104 -0
  94. package/dist/governance/engine/visibility.js +223 -0
  95. package/dist/governance/governed-dispatch.d.ts +19 -0
  96. package/dist/governance/governed-dispatch.js +17 -0
  97. package/dist/governance/limits.d.ts +107 -0
  98. package/dist/governance/limits.js +131 -0
  99. package/dist/governance/posture-transition.d.ts +38 -0
  100. package/dist/governance/posture-transition.js +44 -0
  101. package/dist/governance/posture.d.ts +35 -0
  102. package/dist/governance/posture.js +59 -0
  103. package/dist/governance/profiles.d.ts +48 -0
  104. package/dist/governance/profiles.js +140 -0
  105. package/dist/governance/prompts/orchestrator-block.d.ts +40 -0
  106. package/dist/governance/prompts/orchestrator-block.js +128 -0
  107. package/dist/governance/prompts/plan-addendum.d.ts +22 -0
  108. package/dist/governance/prompts/plan-addendum.js +60 -0
  109. package/dist/governance/prompts/refusals.d.ts +48 -0
  110. package/dist/governance/prompts/refusals.js +82 -0
  111. package/dist/governance/prompts/role-blocks.d.ts +40 -0
  112. package/dist/governance/prompts/role-blocks.js +255 -0
  113. package/dist/governance/report-reader.d.ts +31 -0
  114. package/dist/governance/report-reader.js +57 -0
  115. package/dist/governance/roles.d.ts +83 -0
  116. package/dist/governance/roles.js +142 -0
  117. package/dist/governance/rules/claims.d.ts +83 -0
  118. package/dist/governance/rules/claims.js +250 -0
  119. package/dist/governance/rules/elimination.d.ts +66 -0
  120. package/dist/governance/rules/elimination.js +105 -0
  121. package/dist/governance/rules/events-table.d.ts +46 -0
  122. package/dist/governance/rules/events-table.js +94 -0
  123. package/dist/governance/rules/failure-analysis.d.ts +28 -0
  124. package/dist/governance/rules/failure-analysis.js +93 -0
  125. package/dist/governance/rules/ledger-replay.d.ts +53 -0
  126. package/dist/governance/rules/ledger-replay.js +53 -0
  127. package/dist/governance/rules/ledger-state.d.ts +85 -0
  128. package/dist/governance/rules/ledger-state.js +175 -0
  129. package/dist/governance/rules/ledger.d.ts +228 -0
  130. package/dist/governance/rules/ledger.js +560 -0
  131. package/dist/governance/rules/rca.d.ts +68 -0
  132. package/dist/governance/rules/rca.js +220 -0
  133. package/dist/governance/rules/settlement.d.ts +129 -0
  134. package/dist/governance/rules/settlement.js +363 -0
  135. package/dist/governance/rules/suspicion.d.ts +66 -0
  136. package/dist/governance/rules/suspicion.js +169 -0
  137. package/dist/governance/store/artifacts.d.ts +81 -0
  138. package/dist/governance/store/artifacts.js +173 -0
  139. package/dist/governance/store/atomic.d.ts +79 -0
  140. package/dist/governance/store/atomic.js +119 -0
  141. package/dist/governance/store/events-log.d.ts +35 -0
  142. package/dist/governance/store/events-log.js +86 -0
  143. package/dist/governance/store/run-lock.d.ts +51 -0
  144. package/dist/governance/store/run-lock.js +122 -0
  145. package/dist/governance/store/run-store.d.ts +229 -0
  146. package/dist/governance/store/run-store.js +644 -0
  147. package/dist/governance/store/scoreboard-export.d.ts +53 -0
  148. package/dist/governance/store/scoreboard-export.js +65 -0
  149. package/dist/governance/tools/child-tools.d.ts +31 -0
  150. package/dist/governance/tools/child-tools.js +65 -0
  151. package/dist/governance/tools/guard.d.ts +71 -0
  152. package/dist/governance/tools/guard.js +194 -0
  153. package/dist/governance/tools/inspector-tools.d.ts +63 -0
  154. package/dist/governance/tools/inspector-tools.js +424 -0
  155. package/dist/governance/tools/lead-tools.d.ts +42 -0
  156. package/dist/governance/tools/lead-tools.js +226 -0
  157. package/dist/governance/tools/normalize-util.d.ts +101 -0
  158. package/dist/governance/tools/normalize-util.js +230 -0
  159. package/dist/governance/tools/scan-incomplete.d.ts +79 -0
  160. package/dist/governance/tools/scan-incomplete.js +486 -0
  161. package/dist/governance/tools/shared-tools.d.ts +44 -0
  162. package/dist/governance/tools/shared-tools.js +125 -0
  163. package/dist/governance/tools/submit-normalizers.d.ts +28 -0
  164. package/dist/governance/tools/submit-normalizers.js +474 -0
  165. package/dist/governance/tools/submit-tools.d.ts +26 -0
  166. package/dist/governance/tools/submit-tools.js +171 -0
  167. package/dist/governance/types.d.ts +661 -0
  168. package/dist/governance/types.js +27 -0
  169. package/dist/proc/kill-tree.d.ts +19 -1
  170. package/dist/proc/kill-tree.js +32 -5
  171. package/dist/proc/limits.d.ts +19 -0
  172. package/dist/proc/limits.js +19 -0
  173. package/dist/proc/supervisor.js +8 -1
  174. package/dist/session/validate-session.js +32 -1
  175. package/dist/team/runtime.d.ts +8 -0
  176. package/dist/team/runtime.js +9 -0
  177. package/dist/team/subagent.d.ts +23 -0
  178. package/dist/team/subagent.js +14 -3
  179. package/dist/team/types.d.ts +6 -0
  180. package/dist/tools/bounded-spawn.d.ts +56 -0
  181. package/dist/tools/bounded-spawn.js +76 -0
  182. package/dist/tools/index.d.ts +30 -7
  183. package/dist/tools/index.js +14 -0
  184. package/dist/tools/plan-tools.d.ts +14 -2
  185. package/dist/tools/plan-tools.js +5 -2
  186. package/dist/tools/search-tools.js +52 -44
  187. package/dist/ui/App.js +145 -25
  188. package/dist/ui/BottomStatusRow.js +1 -1
  189. package/dist/ui/Composer.d.ts +7 -1
  190. package/dist/ui/Composer.js +9 -6
  191. package/dist/ui/GovernancePanel.d.ts +50 -0
  192. package/dist/ui/GovernancePanel.js +77 -0
  193. package/dist/ui/GovernanceRun.d.ts +18 -0
  194. package/dist/ui/GovernanceRun.js +11 -0
  195. package/dist/ui/ScrollbackShell.d.ts +35 -7
  196. package/dist/ui/ScrollbackShell.js +1 -1
  197. package/dist/ui/StatusBar.d.ts +15 -2
  198. package/dist/ui/StatusBar.js +10 -4
  199. package/dist/ui/Transcript.js +3 -0
  200. package/dist/ui/entries/GovernanceCard.d.ts +30 -0
  201. package/dist/ui/entries/GovernanceCard.js +32 -0
  202. package/dist/ui/governance-copy.d.ts +69 -0
  203. package/dist/ui/governance-copy.js +104 -0
  204. package/dist/ui/governance-status.d.ts +145 -0
  205. package/dist/ui/governance-status.js +326 -0
  206. package/dist/ui/interaction-copy.d.ts +4 -0
  207. package/dist/ui/interaction-copy.js +5 -0
  208. package/dist/ui/interaction-hints.d.ts +17 -0
  209. package/dist/ui/interaction-hints.js +14 -2
  210. package/dist/ui/layout/governance-board.d.ts +121 -0
  211. package/dist/ui/layout/governance-board.js +382 -0
  212. package/dist/ui/layout/governance-card.d.ts +53 -0
  213. package/dist/ui/layout/governance-card.js +134 -0
  214. package/dist/ui/layout/governance-chip.d.ts +38 -0
  215. package/dist/ui/layout/governance-chip.js +64 -0
  216. package/dist/ui/layout/governance-panel.d.ts +85 -0
  217. package/dist/ui/layout/governance-panel.js +358 -0
  218. package/dist/ui/layout/governance-roster.d.ts +65 -0
  219. package/dist/ui/layout/governance-roster.js +275 -0
  220. package/dist/ui/layout/status-detail-layout.js +3 -3
  221. package/dist/ui/layout/status-layout.d.ts +5 -2
  222. package/dist/ui/layout/status-layout.js +10 -6
  223. package/dist/ui/layout/virtual-window.js +11 -0
  224. package/dist/ui/overlays/GovernanceBoardOverlay.d.ts +41 -0
  225. package/dist/ui/overlays/GovernanceBoardOverlay.js +76 -0
  226. package/dist/ui/overlays/GovernanceSettingsPage.d.ts +48 -0
  227. package/dist/ui/overlays/GovernanceSettingsPage.js +93 -0
  228. package/dist/ui/overlays/HelpOverlay.js +4 -3
  229. package/dist/ui/overlays/QuestionOverlay.d.ts +9 -1
  230. package/dist/ui/overlays/QuestionOverlay.js +4 -2
  231. package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
  232. package/dist/ui/overlays/SettingsScreen.js +21 -1
  233. package/dist/ui/overlays/governance-settings.d.ts +79 -0
  234. package/dist/ui/overlays/governance-settings.js +140 -0
  235. package/dist/ui/overlays/model-profile-state.d.ts +6 -1
  236. package/dist/ui/overlays/model-profile-state.js +10 -0
  237. package/dist/ui/palettes.d.ts +17 -0
  238. package/dist/ui/palettes.js +9 -0
  239. package/dist/ui/scrollback/LiveTail.d.ts +3 -1
  240. package/dist/ui/scrollback/budget.d.ts +12 -6
  241. package/dist/ui/scrollback/budget.js +11 -4
  242. package/dist/ui/scrollback/live-preview.js +2 -0
  243. package/dist/ui/scrollback/settled-boundary.js +3 -0
  244. package/dist/ui/scrollback/tail-anchor.d.ts +5 -2
  245. package/dist/ui/scrollback/tail-anchor.js +3 -1
  246. package/dist/ui/shortcuts.js +2 -2
  247. package/dist/ui/theme.d.ts +7 -0
  248. package/dist/ui/theme.js +1 -0
  249. package/dist/ui/transcript-text.js +9 -0
  250. package/dist/ui/use-wheel-routing.js +3 -1
  251. package/package.json +2 -2
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Roster geometry and cell text for the legion panel (governance-mode section 10.3).
3
+ *
4
+ * Pure and total: no Ink, no clock, no terminal. ASCII ONLY.
5
+ *
6
+ * WHY COMPUTED FIXED-WIDTH COLUMNS. Same lesson as `team-row.ts`, whose header records
7
+ * it at length: a fixed-height parent makes Yoga size a flex column to its widest CHILD,
8
+ * so a row placed by `flexGrow` stops short of the terminal and the cells to its right
9
+ * lose their absolute offsets. Every column below therefore has a COMPUTED integer width
10
+ * and exactly ONE column is elastic; it sits between the fixed cluster on its left and
11
+ * the fixed `delta` cell on its right, so the score change always lands in the row's
12
+ * last column whatever the activity text says.
13
+ *
14
+ * `columnPlan` is TOTAL: for any width (including 0 and 400) the widths never sum past
15
+ * the width it was given. `governance-panel-layout.test.ts` runs it for every width.
16
+ */
17
+ import { statusMarkerCell, statusMarkerWidth } from '../team-status.js';
18
+ import { GAUGE_CELLS, deltaCell, gaugeFilledCells, govChildStatus, labelTag, scoreText, scoreToneKey, segment, statusTone, } from '../governance-status.js';
19
+ import { NOT_SCORED, ROLE_LABEL, STATE_WORD, WORKING_VERB, replacedByText, } from '../governance-copy.js';
20
+ import { cellWidth, fitCells } from './cells.js';
21
+ /** The elastic column keeps at least this many columns, else it is dropped (activity) or kept (wp). */
22
+ const ELASTIC_MIN_CELLS = { activity: 12, wp: 6 };
23
+ /** One leading margin column, drawn by the component. */
24
+ export const PANEL_MARGIN_CELLS = 1;
25
+ function columnSpecs(tier, markerCells) {
26
+ const marker = { key: 'marker', width: markerCells };
27
+ const delta = { key: 'delta', width: 7, align: 'right', drop: 2 };
28
+ switch (tier) {
29
+ case 'full':
30
+ return [
31
+ marker,
32
+ { key: 'id', width: 8 },
33
+ { key: 'role', width: 6, drop: 3 },
34
+ { key: 'state', width: 12 },
35
+ { key: 'gauge', width: GAUGE_CELLS + 1, drop: 5 },
36
+ { key: 'score', width: 5, align: 'right' },
37
+ { key: 'prov', width: 7, drop: 4 },
38
+ { key: 'tag', width: 8, drop: 6 },
39
+ { key: 'wp', width: 11 },
40
+ { key: 'activity', width: 0, elastic: true },
41
+ delta,
42
+ ];
43
+ case 'standard':
44
+ return [
45
+ marker,
46
+ { key: 'id', width: 8 },
47
+ { key: 'role', width: 6, drop: 3 },
48
+ { key: 'state', width: 12 },
49
+ { key: 'gauge', width: GAUGE_CELLS + 1, drop: 5 },
50
+ { key: 'score', width: 5, align: 'right' },
51
+ { key: 'prov', width: 7, drop: 4 },
52
+ { key: 'tag', width: 8, drop: 6 },
53
+ { key: 'wp', width: 0, elastic: true },
54
+ delta,
55
+ ];
56
+ default:
57
+ return [
58
+ marker,
59
+ { key: 'id', width: 8 },
60
+ { key: 'state', width: 12, drop: 3 },
61
+ { key: 'score', width: 5, align: 'right' },
62
+ { key: 'wp', width: 0, elastic: true },
63
+ delta,
64
+ ];
65
+ }
66
+ }
67
+ function fixedSum(specs) {
68
+ return specs.reduce((sum, spec) => sum + (spec.elastic ? 0 : spec.width), 0);
69
+ }
70
+ /** The column that gives way first: the highest `drop` rank, or -1 when none can. */
71
+ function dropVictim(specs) {
72
+ let victim = -1;
73
+ let rank = 0;
74
+ specs.forEach((spec, index) => {
75
+ if (spec.drop !== undefined && spec.drop > rank) {
76
+ rank = spec.drop;
77
+ victim = index;
78
+ }
79
+ });
80
+ return victim;
81
+ }
82
+ /** Last-resort squeeze so the sum NEVER exceeds `inner`, from the right edge inward. */
83
+ function squeeze(columns, inner) {
84
+ let over = columns.reduce((sum, column) => sum + column.width, 0) - inner;
85
+ for (let index = columns.length - 1; index >= 0 && over > 0; index -= 1) {
86
+ const column = columns[index];
87
+ const cut = Math.min(column.width, over);
88
+ column.width -= cut;
89
+ over -= cut;
90
+ }
91
+ }
92
+ /**
93
+ * Give the activity column up and make the work-package cell the elastic one, which turns
94
+ * a `full` row into a `standard` one without losing the single-elastic-column invariant.
95
+ */
96
+ function promoteWorkPackage(specs) {
97
+ return specs
98
+ .filter((spec) => spec.key !== 'activity')
99
+ .map((spec) => (spec.key === 'wp' ? { key: 'wp', width: 0, elastic: true } : spec));
100
+ }
101
+ /**
102
+ * The row's columns for a tier at `inner` columns (the terminal width minus the margin).
103
+ *
104
+ * THE DROP LADDER IS THE SPECIFICATION, in the order the cells give way: the activity
105
+ * text first (the only column whose loss costs nothing structural - the work-package cell
106
+ * becomes the elastic one), then the tag, gauge, provisional, role and delta cells. The
107
+ * elastic column keeps at least its floor; when even the fixed cluster does not fit,
108
+ * `squeeze` trims from the right so the sum stays inside `inner` for any input.
109
+ */
110
+ export function columnPlan(tier, inner, markerCells) {
111
+ const width = Number.isFinite(inner) ? Math.max(0, Math.floor(inner)) : 0;
112
+ let specs = columnSpecs(tier, markerCells);
113
+ const floorOf = (spec) => spec?.key === 'activity' ? ELASTIC_MIN_CELLS.activity : ELASTIC_MIN_CELLS.wp;
114
+ for (;;) {
115
+ const elastic = specs.find((spec) => spec.elastic === true);
116
+ if (fixedSum(specs) + floorOf(elastic) <= width)
117
+ break;
118
+ if (elastic?.key === 'activity') {
119
+ specs = promoteWorkPackage(specs);
120
+ continue;
121
+ }
122
+ const victim = dropVictim(specs);
123
+ if (victim < 0)
124
+ break;
125
+ specs = specs.filter((_, index) => index !== victim);
126
+ }
127
+ const remaining = Math.max(0, width - fixedSum(specs));
128
+ const columns = specs.map((spec) => ({
129
+ key: spec.key,
130
+ width: spec.elastic ? remaining : spec.width,
131
+ elastic: spec.elastic === true,
132
+ align: spec.align ?? 'left',
133
+ }));
134
+ squeeze(columns, width);
135
+ return columns;
136
+ }
137
+ export function columnsWidth(columns) {
138
+ return columns.reduce((sum, column) => sum + column.width, 0);
139
+ }
140
+ const BLANK_TONE = 'muted';
141
+ function textCell(column, text, tone, ellipsis, bold = false) {
142
+ const clipped = column.width === 0 ? '' : fitCells(text, column.width, ellipsis);
143
+ return {
144
+ key: column.key,
145
+ width: column.width,
146
+ elastic: column.elastic,
147
+ align: column.align,
148
+ segments: clipped === '' ? [] : [segment(clipped, tone, bold)],
149
+ };
150
+ }
151
+ function leading(text) {
152
+ return text === '' ? '' : ` ${text}`;
153
+ }
154
+ /** `not scored` replaces the gauge for the inspector (spec 5.1: never scored). */
155
+ function gaugeCell(column, row, glyphs) {
156
+ if (row.confirmed === null)
157
+ return textCell(column, NOT_SCORED, 'muted', glyphs.ellipsis);
158
+ const filled = gaugeFilledCells(row.confirmed);
159
+ const bar = [
160
+ segment(glyphs.gaugeFull.repeat(filled), scoreToneKey(row.label)),
161
+ segment(glyphs.gaugeEmpty.repeat(GAUGE_CELLS - filled), 'border'),
162
+ ].filter((part) => part.text !== '');
163
+ return {
164
+ key: column.key,
165
+ width: column.width,
166
+ elastic: false,
167
+ align: 'left',
168
+ segments: bar,
169
+ };
170
+ }
171
+ function stateText(row) {
172
+ return row.state === 'working' ? WORKING_VERB[row.role] : STATE_WORD[row.state];
173
+ }
174
+ function rightAligned(column, text) {
175
+ return ' '.repeat(Math.max(0, column.width - cellWidth(text))) + text;
176
+ }
177
+ function scoreCell(column, row, ctx) {
178
+ const ell = ctx.glyphs.ellipsis;
179
+ if (row.confirmed === null)
180
+ return textCell(column, '', BLANK_TONE, ell);
181
+ const text = rightAligned(column, scoreText(row.confirmed));
182
+ return textCell(column, text, scoreToneKey(row.label), ell);
183
+ }
184
+ function deltaColumnCell(column, row, ctx) {
185
+ const delta = deltaCell(row, ctx.now, ctx.reducedMotion);
186
+ const text = delta.text === '' ? '' : rightAligned(column, delta.text);
187
+ return textCell(column, text, delta.tone, ctx.glyphs.ellipsis, delta.bold);
188
+ }
189
+ /** One builder per column: a table, so adding a column cannot raise a function's complexity. */
190
+ const CELL_BUILDERS = {
191
+ marker: (column, row, ctx) => {
192
+ const status = govChildStatus(row.state);
193
+ const text = ` ${statusMarkerCell(status, ctx.glyphs)} `;
194
+ return textCell(column, text, statusTone(status), ctx.glyphs.ellipsis);
195
+ },
196
+ id: (column, row, ctx) => textCell(column, row.id, 'primary', ctx.glyphs.ellipsis),
197
+ role: (column, row, ctx) => textCell(column, ROLE_LABEL[row.role], 'muted', ctx.glyphs.ellipsis),
198
+ state: (column, row, ctx) => {
199
+ const tone = statusTone(govChildStatus(row.state));
200
+ return textCell(column, stateText(row), tone, ctx.glyphs.ellipsis);
201
+ },
202
+ gauge: (column, row, ctx) => gaugeCell(column, row, ctx.glyphs),
203
+ score: scoreCell,
204
+ prov: (column, row, ctx) => {
205
+ const text = row.provisional > 0 ? leading(`+${row.provisional}P`) : '';
206
+ return textCell(column, text, 'muted', ctx.glyphs.ellipsis);
207
+ },
208
+ tag: (column, row, ctx) => {
209
+ const text = leading(labelTag(row.label));
210
+ return textCell(column, text, scoreToneKey(row.label), ctx.glyphs.ellipsis, true);
211
+ },
212
+ wp: (column, row, ctx) => textCell(column, leading(row.wpId ?? '-'), 'text', ctx.glyphs.ellipsis),
213
+ activity: (column, row, ctx) => {
214
+ const text = leading(row.activity.replace(/\s+/g, ' ').trim());
215
+ return textCell(column, text, 'muted', ctx.glyphs.ellipsis);
216
+ },
217
+ delta: deltaColumnCell,
218
+ note: (column, _row, ctx) => textCell(column, '', BLANK_TONE, ctx.glyphs.ellipsis),
219
+ };
220
+ function cellFor(column, row, ctx) {
221
+ return CELL_BUILDERS[column.key](column, row, ctx);
222
+ }
223
+ /** Columns a replaced member's one-line note spans. */
224
+ const NOTE_SPAN = new Set(['state', 'gauge', 'score', 'prov', 'tag']);
225
+ /**
226
+ * A replaced member: marker, id and role stay (so the row is recognisable), and one
227
+ * muted note spans the state-through-tag cells. The note NAMES the successor, which is the
228
+ * information the member's vanishing from the panel next phase would otherwise erase.
229
+ */
230
+ function eliminatedCells(columns, row, ctx) {
231
+ const cells = [];
232
+ let spanned = 0;
233
+ let spanColumn = null;
234
+ const flush = () => {
235
+ if (spanColumn === null)
236
+ return;
237
+ const merged = { ...spanColumn, key: 'note', width: spanned, align: 'left' };
238
+ const text = ` ${replacedByText(row.replacedBy)}`;
239
+ cells.push(textCell(merged, text, 'muted', ctx.glyphs.ellipsis));
240
+ spanColumn = null;
241
+ spanned = 0;
242
+ };
243
+ for (const column of columns) {
244
+ if (NOTE_SPAN.has(column.key)) {
245
+ spanColumn ??= column;
246
+ spanned += column.width;
247
+ continue;
248
+ }
249
+ flush();
250
+ const cell = cellFor(column, row, ctx);
251
+ const quiet = column.key === 'id' || column.key === 'role' || column.key === 'marker';
252
+ cells.push(quiet ? mute(cell) : blank(cell));
253
+ }
254
+ flush();
255
+ return cells;
256
+ }
257
+ function mute(cell) {
258
+ return { ...cell, segments: cell.segments.map((part) => ({ ...part, tone: 'muted' })) };
259
+ }
260
+ function blank(cell) {
261
+ return { ...cell, segments: [] };
262
+ }
263
+ /** One roster row's cells, text already clamped to its cell. */
264
+ export function buildRosterLine(input) {
265
+ const { columns, row, glyphs, now, reducedMotion } = input;
266
+ const ctx = { glyphs, now, reducedMotion };
267
+ if (row.state === 'eliminated') {
268
+ return { key: row.id, kind: 'eliminated', cells: eliminatedCells(columns, row, ctx) };
269
+ }
270
+ return { key: row.id, kind: 'agent', cells: columns.map((column) => cellFor(column, row, ctx)) };
271
+ }
272
+ /** The marker cell's width in the current glyph tier: 1 + widest marker + 1. */
273
+ export function markerCells(glyphs) {
274
+ return 1 + statusMarkerWidth(glyphs) + 1;
275
+ }
@@ -1,13 +1,13 @@
1
1
  import { formatCost, formatTokens, promptTokensOf } from '../../agent/usage.js';
2
2
  import { actionCandidates, clipStatusText, escapeClause } from '../interaction-hints.js';
3
- import { MODE_LABEL } from '../../agent/agent-mode.js';
3
+ import { isGovernedModeValue, modeWord } from '../governance-status.js';
4
4
  import { statusField, statusLinePlan, validContext } from './status-layout.js';
5
5
  /**
6
6
  * Uppercase mode word for the details row, mirroring the primary row's field:
7
7
  * same vocabulary (`MODE_LABEL`), same `CUR>PENDING` deferred-switch shape.
8
8
  */
9
9
  function detailModeText(mode, pendingMode) {
10
- const word = (value) => MODE_LABEL[value] ?? value.toUpperCase();
10
+ const word = (value) => modeWord(value, true) ?? value.toUpperCase();
11
11
  if (pendingMode)
12
12
  return `${word(mode || 'build')}>${word(pendingMode)}`;
13
13
  return word(mode || 'build');
@@ -41,7 +41,7 @@ export function planStatusDetail(input) {
41
41
  if (status.servicesActive?.live)
42
42
  add(statusField('services', 'Svc' + status.servicesActive.live));
43
43
  if (status.mode || status.pendingMode)
44
- add(statusField('mode', detailModeText(status.mode, status.pendingMode), 'accent'));
44
+ add(statusField('mode', detailModeText(status.mode, status.pendingMode), isGovernedModeValue(status.mode) || isGovernedModeValue(status.pendingMode) ? 'govern' : 'accent'));
45
45
  if (validContext(status.context)) {
46
46
  const approximate = status.context.source === 'estimate' || status.context.deltaTokens > 0 ? '~' : '';
47
47
  add(statusField('context', 'Context ' + approximate + formatTokens(status.context.occupied)
@@ -117,8 +117,11 @@ export interface StatusField {
117
117
  id: string;
118
118
  text: string;
119
119
  cells: number;
120
- /** 'accent' carries the mode word - the row's one always-on emphasis. */
121
- tone: 'normal' | 'muted' | 'warning' | 'error' | 'accent';
120
+ /**
121
+ * 'accent' carries the mode word - the row's one always-on emphasis. 'govern' is the
122
+ * same slot while the posture is GOVERN / PLAN+GOVERN (`governAccent`, its own hue).
123
+ */
124
+ tone: 'normal' | 'muted' | 'warning' | 'error' | 'accent' | 'govern';
122
125
  }
123
126
  export interface StatusLinePlan {
124
127
  fields: readonly StatusField[];
@@ -1,7 +1,7 @@
1
1
  import stringWidth from 'string-width';
2
2
  import stripAnsi from 'strip-ansi';
3
3
  import { formatDuration } from '../../agent/usage.js';
4
- import { MODE_LABEL } from '../../agent/agent-mode.js';
4
+ import { isGovernedModeValue, modeWord } from '../governance-status.js';
5
5
  import { interactionCopy as copy } from '../interaction-copy.js';
6
6
  export function cleanStatusText(text) {
7
7
  // eslint-disable-next-line no-control-regex
@@ -170,10 +170,12 @@ function modeFieldText(mode, pendingMode, full) {
170
170
  if (!mode && !pendingMode)
171
171
  return null;
172
172
  const word = (value) => {
173
- const label = MODE_LABEL[value];
174
- if (!label)
175
- return full ? value.toUpperCase() : (value[0] ?? '?').toUpperCase();
176
- return full ? label : label[0];
173
+ // `build` / `plan` answer exactly as before; the two governed postures are the only
174
+ // other values `modeWord` knows (`PG` is the short form of PLAN+GOVERN, since `P` is PLAN).
175
+ const known = modeWord(value, full);
176
+ if (known !== undefined)
177
+ return known;
178
+ return full ? value.toUpperCase() : (value[0] ?? '?').toUpperCase();
177
179
  };
178
180
  const current = word(mode || 'build');
179
181
  return pendingMode ? `${current}>${word(pendingMode)}` : current;
@@ -231,7 +233,9 @@ export function planPrimaryStatusFields(input) {
231
233
  return statusLinePlan([
232
234
  statusField('phase', glyph + (rung >= 5 ? '' : ' ') + label, phaseTone),
233
235
  ...(notice ? [statusField('feedback', notice, feedbackTone)] : []),
234
- ...(modeText ? [statusField('mode', modeText, 'accent')] : []),
236
+ ...(modeText
237
+ ? [statusField('mode', modeText, isGovernedModeValue(input.mode) || isGovernedModeValue(input.pendingMode) ? 'govern' : 'accent')]
238
+ : []),
235
239
  ...(forcedText ? [statusField('forcedTeam', forcedText, 'accent')] : []),
236
240
  statusField('context', (full ? 'Context ' : 'C') + pct
237
241
  + (input.compactionPaused === true ? pausedSuffix(full) : '')),
@@ -33,6 +33,7 @@
33
33
  * through the mounted set and the measured heights back to the offset.
34
34
  */
35
35
  import stringWidth from 'string-width';
36
+ import { governanceCardRows } from './governance-card.js';
36
37
  import { FAST_LIMITS } from '../../fast/limits.js';
37
38
  import { COMPACTION_LIMITS } from '../../compaction/limits.js';
38
39
  import { GUTTER_WIDTH } from './Gutter.js';
@@ -153,6 +154,11 @@ export function entryRevision(entry) {
153
154
  */
154
155
  case 'service':
155
156
  return `v${entry.status}.${entry.rowsSeen}.${entry.url ? 1 : 0}.${entry.exitCode ?? -1}.${entry.terminal ? 1 : 0}`;
157
+ // A governance card is IMMUTABLE once appended (it is a settled record), so its
158
+ // revision only has to tell two cards apart: the shape terms below are what decide
159
+ // its height (`governanceCardRows`), and they cannot change without a new entry.
160
+ case 'governance':
161
+ return `g${entry.card.rows.length}.${entry.card.notes.length}.${entry.card.headline.length}`;
156
162
  default:
157
163
  return 'x';
158
164
  }
@@ -402,6 +408,11 @@ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisibl
402
408
  const progress = entry.progress || entry.stalled ? SERVICE_PROGRESS_ROWS : 0;
403
409
  return separation + 1 + progress + url + tail;
404
410
  }
411
+ // EXACT, not an upper bound: every line of the card is `wrap="truncate"`, so its
412
+ // height is a function of its data alone (`layout/governance-card.ts`). It has to
413
+ // be added to BOTH of this file's switches - one alone yields scroll drift.
414
+ case 'governance':
415
+ return separation + governanceCardRows(entry.card);
405
416
  default:
406
417
  return separation + 1;
407
418
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GovernanceBoardOverlay - the read-only `/gov board` overview
3
+ * (governance-mode section 10.5).
4
+ *
5
+ * ASCII ONLY: every symbol comes through `glyphs`.
6
+ *
7
+ * READ-ONLY IS THE SAFETY BOUNDARY, as for the Agents board: nothing here scores, rules
8
+ * or resolves anything. The only side effect is `e`, which asks the owner to copy the
9
+ * `board.md` path.
10
+ *
11
+ * `OverlayFrame` SELF-MANAGED MODE, with this file owning the window. The tab strip must
12
+ * stay put while the rows scroll, which the frame's controlled mode (it windows EVERY row
13
+ * it is given) cannot do; so the body is "one strip row + a window of rows" and the
14
+ * windowing is `sliceWindow` through `boardWindow`, the arithmetic the frame itself uses.
15
+ * `PgUp` / `PgDn` / the wheel arrive through `App`'s `controlled` overlay list as
16
+ * `scrollOffset`; Up/Down and Left/Right are this component's own keys, the same split the
17
+ * Agents board makes.
18
+ *
19
+ * NO `ink-spinner` and no motion prop (spinner-census): this is a reading surface.
20
+ */
21
+ import React from 'react';
22
+ import type { Theme } from '../theme.js';
23
+ import type { TermCapabilities } from '../capabilities.js';
24
+ import { type BoardTab, type GovernanceBoardData } from '../layout/governance-board.js';
25
+ export interface GovernanceBoardOverlayProps {
26
+ /** The board's data, or null when there is no governed run to show. */
27
+ data: GovernanceBoardData | null;
28
+ /** The overlay's FRAME width (`App`'s `viewCols`). */
29
+ cols: number;
30
+ maxRows: number;
31
+ scrollOffset: number;
32
+ onScrollClamp: (offset: number) => void;
33
+ isActive: boolean;
34
+ theme: Theme;
35
+ caps: TermCapabilities;
36
+ /** `e`: copy the board file path. Absent = the key reports the path as unavailable. */
37
+ onCopyPath?: (path: string) => void;
38
+ onNotice?: (level: 'info' | 'warn', text: string) => void;
39
+ initialTab?: BoardTab;
40
+ }
41
+ export declare function GovernanceBoardOverlay({ data, cols, maxRows, scrollOffset, onScrollClamp, isActive, theme, caps, onCopyPath, onNotice, initialTab, }: GovernanceBoardOverlayProps): React.ReactElement;
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * GovernanceBoardOverlay - the read-only `/gov board` overview
4
+ * (governance-mode section 10.5).
5
+ *
6
+ * ASCII ONLY: every symbol comes through `glyphs`.
7
+ *
8
+ * READ-ONLY IS THE SAFETY BOUNDARY, as for the Agents board: nothing here scores, rules
9
+ * or resolves anything. The only side effect is `e`, which asks the owner to copy the
10
+ * `board.md` path.
11
+ *
12
+ * `OverlayFrame` SELF-MANAGED MODE, with this file owning the window. The tab strip must
13
+ * stay put while the rows scroll, which the frame's controlled mode (it windows EVERY row
14
+ * it is given) cannot do; so the body is "one strip row + a window of rows" and the
15
+ * windowing is `sliceWindow` through `boardWindow`, the arithmetic the frame itself uses.
16
+ * `PgUp` / `PgDn` / the wheel arrive through `App`'s `controlled` overlay list as
17
+ * `scrollOffset`; Up/Down and Left/Right are this component's own keys, the same split the
18
+ * Agents board makes.
19
+ *
20
+ * NO `ink-spinner` and no motion prop (spinner-census): this is a reading surface.
21
+ */
22
+ import { useEffect, useRef, useState } from 'react';
23
+ import { Box, Text, useInput } from 'ink';
24
+ import { pickGlyphs } from '../glyphs.js';
25
+ import { OverlayFrame, overlayBodyRows } from '../layout/OverlayFrame.js';
26
+ import { boardBodyRows, boardHint, boardPosition, boardWindow, buildGovernanceBoardLayout, stepTab, } from '../layout/governance-board.js';
27
+ import { cellWidth } from '../layout/cells.js';
28
+ import { GovernanceLine } from '../GovernanceRun.js';
29
+ import { BOARD_COPIED, BOARD_NO_PATH, BOARD_TITLE } from '../governance-copy.js';
30
+ /** `OverlayFrame`'s own breakpoint, mirrored so the chrome estimate cannot drift. */
31
+ const FRAME_BORDERLESS_COLS = 48;
32
+ export function GovernanceBoardOverlay({ data, cols, maxRows, scrollOffset, onScrollClamp, isActive, theme, caps, onCopyPath, onNotice, initialTab = 'roster', }) {
33
+ const glyphs = pickGlyphs(caps);
34
+ const [tab, setTab] = useState(initialTab);
35
+ // The frame spends 4 of `cols` on its border + padding (2 in the narrow rail form), so a
36
+ // board planned against the frame width would lose its last four columns to truncation.
37
+ const contentCols = Math.max(1, cols - (cols >= FRAME_BORDERLESS_COLS ? 4 : 2));
38
+ const layout = buildGovernanceBoardLayout({ cols: contentCols, tab, data, glyphs });
39
+ const height = boardBodyRows(overlayBodyRows(maxRows, cols));
40
+ const window = boardWindow(layout.lines.length, scrollOffset, height);
41
+ const position = boardPosition(window, glyphs);
42
+ useEffect(() => {
43
+ if (window.clamped !== scrollOffset)
44
+ onScrollClamp(window.clamped);
45
+ }, [window.clamped, scrollOffset, onScrollClamp]);
46
+ // The offset the NEXT key press starts from. A held Down key delivers several presses
47
+ // before React re-renders; stepping from the rendered value would turn three presses
48
+ // into one row of movement.
49
+ const offsetRef = useRef(window.clamped);
50
+ offsetRef.current = window.clamped;
51
+ useInput((input, key) => {
52
+ if (!isActive)
53
+ return;
54
+ if (key.leftArrow || key.rightArrow) {
55
+ setTab((current) => stepTab(current, key.rightArrow ? 1 : -1));
56
+ offsetRef.current = 0;
57
+ onScrollClamp(0);
58
+ return;
59
+ }
60
+ if (key.upArrow || key.downArrow) {
61
+ offsetRef.current = Math.max(0, offsetRef.current + (key.upArrow ? -1 : 1));
62
+ onScrollClamp(offsetRef.current);
63
+ }
64
+ else if (input === 'e' || input === 'E') {
65
+ const path = data?.boardPath ?? null;
66
+ if (path === null || onCopyPath === undefined) {
67
+ onNotice?.('warn', BOARD_NO_PATH);
68
+ return;
69
+ }
70
+ onCopyPath(path);
71
+ onNotice?.('info', BOARD_COPIED);
72
+ }
73
+ }, { isActive });
74
+ const visible = layout.lines.slice(window.start, window.start + window.visible);
75
+ return (_jsxs(OverlayFrame, { title: BOARD_TITLE, hint: boardHint(contentCols, glyphs), maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: [_jsxs(Box, { flexDirection: "row", height: 1, flexShrink: 0, children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(GovernanceLine, { segments: layout.tabStrip.segments, theme: theme }) }), position !== '' && (_jsx(Box, { width: cellWidth(position) + 1, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, children: position }) }))] }), visible.map((line) => (_jsx(GovernanceLine, { segments: line.segments, theme: theme }, line.key)))] }));
76
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Settings > Governance: eleven arrows-adjustable rows (governance-mode-v1-1 5, closing
3
+ * governance-mode IF-13).
4
+ *
5
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * THIS IS `TeamFanoutPage` WITH A DIFFERENT TABLE, ON PURPOSE. Every defence that page
8
+ * carries was paid for by a real defect, so each one is inherited verbatim:
9
+ *
10
+ * - THE PAGE OWNS ONE PIECE OF STATE, its cursor. Every value comes from `props.view.rows`,
11
+ * which `SettingsScreen` derives from the reducer; a second copy of the draft here could
12
+ * disagree with the root page about what Ctrl+S is about to save.
13
+ * - `Enter` TOGGLES THE SWITCH ROWS AND DOES NOTHING ON A NUMBER (R-P1-12). A stray Return
14
+ * that silently changed a number is indistinguishable from a bug, and the arrow keys are
15
+ * already the obvious control there.
16
+ * - IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13). `useRootSettingsInput`
17
+ * gates its `useInput` on `state.page === 'root'`, so a subpage has no save listener at
18
+ * all; a footer promising "Ctrl+S saves here" would be a lie the user acts on.
19
+ *
20
+ * THE RESTART NOTE IS THE ONE THING THIS PAGE ADDS. `enabled` is a REGISTRATION key: the tool
21
+ * array is fixed when the session is built, so flipping it here changes the file and the
22
+ * next launch, not this session. The note sits under that row so the consequence is read
23
+ * where the choice is made.
24
+ */
25
+ import React from 'react';
26
+ import { type ProfileFrameProps } from './ModelProfilePicker.js';
27
+ import type { GovernanceSettingKey, GovernanceSettingRow } from './governance-settings.js';
28
+ /** What the page may ask the screen to change. */
29
+ export interface GovernanceSettingsAction {
30
+ type: 'governance-step';
31
+ key: GovernanceSettingKey;
32
+ delta: -1 | 1;
33
+ }
34
+ export interface GovernanceSettingsView {
35
+ /** Exactly eleven, in the fixed order the cursor indexes. */
36
+ rows: GovernanceSettingRow[];
37
+ error?: string;
38
+ /** Shown under the first row; `null` when the draft agrees with this session. */
39
+ restartNote: string | null;
40
+ }
41
+ interface GovernanceSettingsPageProps extends ProfileFrameProps {
42
+ view: GovernanceSettingsView;
43
+ dispatch: (action: GovernanceSettingsAction) => void;
44
+ /** Esc: back to the settings root. The draft is NOT discarded here. */
45
+ onBack: () => void;
46
+ }
47
+ export declare function GovernanceSettingsPage(props: GovernanceSettingsPageProps): React.ReactElement;
48
+ export {};
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Settings > Governance: eleven arrows-adjustable rows (governance-mode-v1-1 5, closing
4
+ * governance-mode IF-13).
5
+ *
6
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
7
+ *
8
+ * THIS IS `TeamFanoutPage` WITH A DIFFERENT TABLE, ON PURPOSE. Every defence that page
9
+ * carries was paid for by a real defect, so each one is inherited verbatim:
10
+ *
11
+ * - THE PAGE OWNS ONE PIECE OF STATE, its cursor. Every value comes from `props.view.rows`,
12
+ * which `SettingsScreen` derives from the reducer; a second copy of the draft here could
13
+ * disagree with the root page about what Ctrl+S is about to save.
14
+ * - `Enter` TOGGLES THE SWITCH ROWS AND DOES NOTHING ON A NUMBER (R-P1-12). A stray Return
15
+ * that silently changed a number is indistinguishable from a bug, and the arrow keys are
16
+ * already the obvious control there.
17
+ * - IT DOES NOT LISTEN FOR Ctrl+S, AND THE FOOTER SAYS SO (R-P1-13). `useRootSettingsInput`
18
+ * gates its `useInput` on `state.page === 'root'`, so a subpage has no save listener at
19
+ * all; a footer promising "Ctrl+S saves here" would be a lie the user acts on.
20
+ *
21
+ * THE RESTART NOTE IS THE ONE THING THIS PAGE ADDS. `enabled` is a REGISTRATION key: the tool
22
+ * array is fixed when the session is built, so flipping it here changes the file and the
23
+ * next launch, not this session. The note sits under that row so the consequence is read
24
+ * where the choice is made.
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
+ /** Matches the root page's `padEnd(18)` rows so the value column lines up (measured). */
32
+ const LABEL_PAD = 18;
33
+ /** The rows `Enter` flips, and nothing else (R-P1-12). */
34
+ const TOGGLE_KEYS = new Set([
35
+ 'enabled',
36
+ 'shiftTab',
37
+ 'panel',
38
+ 'confirmCharter',
39
+ ]);
40
+ export function GovernanceSettingsPage(props) {
41
+ const [index, setIndex] = useState(0);
42
+ const rows = props.view.rows;
43
+ const count = Math.max(1, rows.length);
44
+ const cursor = Math.min(index, count - 1);
45
+ const focused = rows[cursor];
46
+ const note = props.view.restartNote;
47
+ // The note is one extra LINE after row 0, so every later row sits one line lower.
48
+ const offset = useProfileScroll(props, cursor + (note !== null && cursor > 0 ? 1 : 0));
49
+ const nudge = (delta) => {
50
+ if (focused)
51
+ props.dispatch({ type: 'governance-step', key: focused.key, delta });
52
+ };
53
+ const handleKey = (key) => {
54
+ if (key.escape) {
55
+ props.onBack();
56
+ return;
57
+ }
58
+ if (key.upArrow || (key.tab && key.shift)) {
59
+ setIndex((cursor - 1 + count) % count);
60
+ return;
61
+ }
62
+ if (key.downArrow || key.tab) {
63
+ setIndex((cursor + 1) % count);
64
+ return;
65
+ }
66
+ if (key.leftArrow || key.rightArrow) {
67
+ nudge(key.leftArrow ? -1 : 1);
68
+ return;
69
+ }
70
+ // `Enter` toggles the switch rows and does NOTHING on a numeric or choice row.
71
+ if (key.return && focused && TOGGLE_KEYS.has(focused.key))
72
+ nudge(1);
73
+ };
74
+ useInput((_input, key) => {
75
+ if (props.isActive === false)
76
+ return;
77
+ handleKey(key);
78
+ });
79
+ const glyphs = pickGlyphs(props.caps);
80
+ const body = [];
81
+ rows.forEach((row, at) => {
82
+ const line = `${row.label.padEnd(LABEL_PAD)}${row.value}`;
83
+ body.push(_jsx(Text, { wrap: "truncate", color: at === cursor ? props.theme.accent : props.theme.muted, children: `${at === cursor ? glyphs.caret : ' '} ${clipProfileText(line, props.cols - 8)}` }, row.key));
84
+ if (at === 0 && note !== null) {
85
+ body.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: ` ${clipProfileText(note, props.cols - 8)}` }, "restart-note"));
86
+ }
87
+ });
88
+ // The hint explains the FOCUSED row: the range and the reason are the point of the page.
89
+ if (focused) {
90
+ body.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: clipProfileText(focused.hint, props.cols - 8) }, "hint"));
91
+ }
92
+ return _jsx(OverlayFrame, { ...props, rows: body, scrollOffset: offset, title: props.view.error ?? 'Governance', hint: "Arrows adjust. Esc goes back; Ctrl+S on the main page saves." });
93
+ }