@north-light/crouter 0.3.235 → 0.3.237

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 (181) hide show
  1. package/dist/api/client.d.ts +2 -1
  2. package/dist/api/client.js +3 -0
  3. package/dist/api/dto/broker-ops.d.ts +1 -1
  4. package/dist/api/dto/common.d.ts +2 -0
  5. package/dist/api/dto/lifecycle.d.ts +9 -2
  6. package/dist/api/dto/messages.d.ts +5 -0
  7. package/dist/api/dto/nodes.d.ts +14 -5
  8. package/dist/api/dto/worktree.d.ts +10 -0
  9. package/dist/api/routes.d.ts +1 -0
  10. package/dist/api/routes.js +1 -0
  11. package/dist/builtin-memory/00-runtime-base/00-authoring.md +1 -1
  12. package/dist/builtin-memory/internal/agent-shaping.md +1 -1
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +1 -1
  15. package/dist/clients/attach/render/tool-calls.js +18 -3
  16. package/dist/clients/attach/viewer.js +399 -399
  17. package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +6 -0
  18. package/dist/clients/inbox/review/roundtrip.js +1 -1
  19. package/dist/commands/__tests__/node-new.test.js +58 -0
  20. package/dist/commands/memory/lint.js +22 -13
  21. package/dist/commands/memory/shared.js +1 -1
  22. package/dist/commands/node/create.d.ts +1 -1
  23. package/dist/commands/node/create.js +4 -1
  24. package/dist/commands/node/lifecycle.js +2 -2
  25. package/dist/commands/node-worktree.js +39 -8
  26. package/dist/commands/revive.js +11 -2
  27. package/dist/commands/sys/context/admin/actions.d.ts +48 -0
  28. package/dist/commands/sys/context/admin/actions.js +246 -0
  29. package/dist/commands/sys/context/admin/axes-panel.d.ts +21 -0
  30. package/dist/commands/sys/context/admin/axes-panel.js +117 -0
  31. package/dist/commands/sys/context/admin/detail-panel.d.ts +53 -0
  32. package/dist/commands/sys/context/admin/detail-panel.js +178 -0
  33. package/dist/commands/sys/context/admin/docs-panel.d.ts +45 -0
  34. package/dist/commands/sys/context/admin/docs-panel.js +186 -0
  35. package/dist/commands/sys/context/admin/model.d.ts +75 -0
  36. package/dist/commands/sys/context/admin/model.js +209 -0
  37. package/dist/commands/sys/context/admin/shell.d.ts +82 -0
  38. package/dist/commands/sys/context/admin/shell.js +669 -0
  39. package/dist/commands/sys/context/admin.d.ts +1 -0
  40. package/dist/commands/sys/context/admin.js +124 -0
  41. package/dist/commands/sys/context/doc.d.ts +1 -0
  42. package/dist/commands/sys/context/doc.js +196 -0
  43. package/dist/commands/sys/context/prompt-review.d.ts +1 -0
  44. package/dist/commands/sys/{prompt-review.js → context/prompt-review.js} +10 -10
  45. package/dist/commands/sys/context/resolve.d.ts +55 -0
  46. package/dist/commands/sys/context/resolve.js +428 -0
  47. package/dist/commands/sys/context/sysprompt.d.ts +1 -0
  48. package/dist/commands/sys/{sysprompt.js → context/sysprompt.js} +11 -11
  49. package/dist/commands/sys/context.d.ts +2 -0
  50. package/dist/commands/sys/context.js +20 -0
  51. package/dist/commands/sys.js +3 -4
  52. package/dist/core/__tests__/canvas-inbox-watcher-naming.test.d.ts +1 -0
  53. package/dist/core/__tests__/canvas-inbox-watcher-naming.test.js +258 -0
  54. package/dist/core/__tests__/child-death-wake.test.js +38 -22
  55. package/dist/core/__tests__/cron-self-cancel-delivery.test.d.ts +1 -0
  56. package/dist/core/__tests__/cron-self-cancel-delivery.test.js +72 -0
  57. package/dist/core/__tests__/daemon-boot.test.js +40 -28
  58. package/dist/core/__tests__/helpers/harness.js +1 -0
  59. package/dist/core/__tests__/human-deliver.test.js +1 -1
  60. package/dist/core/__tests__/human-node-not-supervised.test.js +10 -15
  61. package/dist/core/__tests__/integration/revive.test.js +12 -12
  62. package/dist/core/__tests__/integration/spawn-root.test.js +6 -0
  63. package/dist/core/__tests__/integration/worktree-reap.test.js +32 -0
  64. package/dist/core/__tests__/lifecycle-terminal-reasons.test.d.ts +1 -0
  65. package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +61 -0
  66. package/dist/core/__tests__/migration.test.js +38 -1
  67. package/dist/core/__tests__/push-final-guard.test.js +1 -1
  68. package/dist/core/__tests__/revive-capacity.test.d.ts +1 -0
  69. package/dist/core/__tests__/revive-capacity.test.js +127 -0
  70. package/dist/core/__tests__/revive-parked-fresh.test.js +7 -7
  71. package/dist/core/__tests__/seam/broker-cap-freeze.test.d.ts +1 -0
  72. package/dist/core/__tests__/seam/broker-cap-freeze.test.js +81 -0
  73. package/dist/core/__tests__/seam/dormancy-release.test.js +76 -7
  74. package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +1 -1
  75. package/dist/core/canvas/browse/render.js +11 -3
  76. package/dist/core/canvas/canvas.d.ts +15 -3
  77. package/dist/core/canvas/canvas.js +52 -16
  78. package/dist/core/canvas/migrations.js +21 -0
  79. package/dist/core/canvas/nav-render.js +3 -1
  80. package/dist/core/canvas/remote-canvas-source.js +2 -0
  81. package/dist/core/canvas/render-source.d.ts +3 -1
  82. package/dist/core/canvas/render-source.js +7 -3
  83. package/dist/core/canvas/status-glyph.d.ts +10 -1
  84. package/dist/core/canvas/status-glyph.js +19 -3
  85. package/dist/core/canvas/types.d.ts +22 -3
  86. package/dist/core/feed/feed.js +1 -1
  87. package/dist/core/human/feedback-companion.js +5 -2
  88. package/dist/core/memory/lint.d.ts +19 -0
  89. package/dist/core/memory/lint.js +97 -0
  90. package/dist/core/memory-resolver.d.ts +4 -0
  91. package/dist/core/memory-resolver.js +7 -7
  92. package/dist/core/preview-registry.js +4 -4
  93. package/dist/core/review/realize.js +4 -2
  94. package/dist/core/runtime/close.js +1 -1
  95. package/dist/core/runtime/fleet.d.ts +29 -1
  96. package/dist/core/runtime/fleet.js +23 -0
  97. package/dist/core/runtime/host.js +32 -11
  98. package/dist/core/runtime/launch-prompt.d.ts +1 -1
  99. package/dist/core/runtime/launch-prompt.js +2 -2
  100. package/dist/core/runtime/launch-target.js +1 -1
  101. package/dist/core/runtime/lifecycle.d.ts +4 -2
  102. package/dist/core/runtime/lifecycle.js +12 -1
  103. package/dist/core/runtime/memory.js +1 -1
  104. package/dist/core/runtime/reopen.js +10 -1
  105. package/dist/core/runtime/reset.js +1 -1
  106. package/dist/core/runtime/revive-all.d.ts +3 -0
  107. package/dist/core/runtime/revive-all.js +10 -4
  108. package/dist/core/runtime/revive.d.ts +12 -0
  109. package/dist/core/runtime/revive.js +62 -3
  110. package/dist/core/runtime/spawn.d.ts +9 -13
  111. package/dist/core/runtime/spawn.js +40 -17
  112. package/dist/core/substrate/frontmatter-validation.js +1 -1
  113. package/dist/core/substrate/index.d.ts +6 -3
  114. package/dist/core/substrate/index.js +4 -3
  115. package/dist/core/substrate/on-read.d.ts +0 -8
  116. package/dist/core/substrate/on-read.js +45 -124
  117. package/dist/core/substrate/plan.d.ts +94 -0
  118. package/dist/core/substrate/plan.js +266 -0
  119. package/dist/core/substrate/render.d.ts +5 -6
  120. package/dist/core/substrate/render.js +76 -129
  121. package/dist/core/substrate/schema.d.ts +1 -6
  122. package/dist/core/substrate/schema.js +1 -15
  123. package/dist/core/substrate/session-cache.d.ts +2 -10
  124. package/dist/core/substrate/session-cache.js +9 -47
  125. package/dist/core/substrate/subject-fields.d.ts +1 -4
  126. package/dist/core/substrate/subject-fields.js +2 -5
  127. package/dist/core/substrate/surface-match.d.ts +37 -17
  128. package/dist/core/substrate/surface-match.js +44 -53
  129. package/dist/core/tui/page-host.d.ts +6 -0
  130. package/dist/core/tui/page-host.js +12 -3
  131. package/dist/core/worktree.d.ts +8 -0
  132. package/dist/core/worktree.js +65 -0
  133. package/dist/daemon/__tests__/reconciler-signature.test.js +3 -2
  134. package/dist/daemon/__tests__/thaw-order.test.d.ts +1 -0
  135. package/dist/daemon/__tests__/thaw-order.test.js +55 -0
  136. package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +6 -0
  137. package/dist/daemon/api/__tests__/node-create-description.test.js +6 -0
  138. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +1 -1
  139. package/dist/daemon/api/bridge.js +5 -1
  140. package/dist/daemon/api/handlers/attach.js +5 -1
  141. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  142. package/dist/daemon/api/handlers/messages.js +10 -2
  143. package/dist/daemon/api/handlers/nodes.js +8 -4
  144. package/dist/daemon/api/handlers/worktree.js +10 -1
  145. package/dist/daemon/api/map.js +7 -0
  146. package/dist/daemon/companion-retire.js +1 -1
  147. package/dist/daemon/cron/passes.d.ts +0 -5
  148. package/dist/daemon/cron/passes.js +0 -20
  149. package/dist/daemon/cron/sinks.js +1 -1
  150. package/dist/daemon/cron-run.d.ts +5 -6
  151. package/dist/daemon/cron-run.js +7 -16
  152. package/dist/daemon/crtrd.d.ts +0 -10
  153. package/dist/daemon/crtrd.js +23 -61
  154. package/dist/daemon/fleet.d.ts +10 -7
  155. package/dist/daemon/fleet.js +71 -12
  156. package/dist/daemon/messaging/node-message.js +3 -1
  157. package/dist/daemon/profile-delete.js +1 -1
  158. package/dist/daemon/reconcilers/broker-supervision.d.ts +26 -9
  159. package/dist/daemon/reconcilers/broker-supervision.js +39 -52
  160. package/dist/daemon/reconcilers/controller-death.d.ts +7 -0
  161. package/dist/daemon/reconcilers/controller-death.js +26 -0
  162. package/dist/daemon/reconcilers/cron-lane.d.ts +0 -3
  163. package/dist/daemon/reconcilers/cron-lane.js +0 -3
  164. package/dist/daemon/reconcilers/live-obligation.js +3 -1
  165. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +53 -0
  166. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +170 -0
  167. package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +5 -0
  168. package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +16 -0
  169. package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -0
  170. package/dist/daemon/reconcilers/node-lifecycle/terminating.js +13 -0
  171. package/dist/daemon/reconcilers/node-lifecycle/tick.d.ts +26 -0
  172. package/dist/daemon/reconcilers/node-lifecycle/tick.js +140 -0
  173. package/dist/pi-extensions/canvas-inbox-watcher.js +12 -0
  174. package/package.json +1 -1
  175. package/runtime.lock.json +2 -2
  176. package/dist/commands/sys/prompt-review.d.ts +0 -1
  177. package/dist/commands/sys/sysprompt.d.ts +0 -1
  178. package/dist/core/__tests__/cron-broker-capacity.test.js +0 -119
  179. package/dist/daemon/reconcilers/dormant-inbox.d.ts +0 -18
  180. package/dist/daemon/reconcilers/dormant-inbox.js +0 -157
  181. /package/dist/{core/__tests__/cron-broker-capacity.test.d.ts → commands/__tests__/node-new.test.d.ts} +0 -0
@@ -0,0 +1,669 @@
1
+ // The `crtr sys context admin` page: a snapshot rail, the document matrix, and
2
+ // the dossier that edits one document's routing. The rail decides WHAT is being
3
+ // planned, the matrix shows WHERE every document lands, and the dossier is the
4
+ // only place a write starts.
5
+ import { Input, matchesKey, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
6
+ import { formatGesture, matchesPiTuiInput, } from '../../../../core/keybindings/index.js';
7
+ import { runTuiPage } from '../../../../core/tui/page-host.js';
8
+ import { PAGE_CHROME_ROWS, columns, framed, pageFrame, theme, } from '../../../../core/tui/panel.js';
9
+ import { RUNGS, SURFACE_EVENTS, SURFACE_RUNGS, } from '../../../../core/substrate/schema.js';
10
+ import { profileNameFor } from '../../../../core/substrate/subject-fields.js';
11
+ import { editBufferInEditor } from '../../../../clients/inbox/review/roundtrip.js';
12
+ import { capOwnerFor, moveDoc, readBody, readOnlyReason, toggleFlag, writeBody, writeCap, writeGate, writeRoutingLine, writeShortForm, writeSurfaces, } from './actions.js';
13
+ import { axesOf, cycleAxis, setTypedAxis, renderAxes, typedAxisValue } from './axes-panel.js';
14
+ import { ContextDetailPanel } from './detail-panel.js';
15
+ import { ContextDocsPanel, buildRows } from './docs-panel.js';
16
+ import { PlanSet, plural } from './model.js';
17
+ const RAIL_WIDTH = 26;
18
+ const DOCS_WIDTH = 38;
19
+ const RAIL_MIN_TOTAL_WIDTH = 86;
20
+ const THREE_COLUMN_MIN_WIDTH = 112;
21
+ export class ContextAdminShell {
22
+ tui;
23
+ terminalRows;
24
+ bindings;
25
+ suspendForChild;
26
+ snapshot;
27
+ plans;
28
+ zone = 'axes';
29
+ axisIndex = 0;
30
+ docs;
31
+ detail = new ContextDetailPanel();
32
+ modal;
33
+ notice;
34
+ busy;
35
+ edited = new Set();
36
+ capChanges = [];
37
+ _focused = false;
38
+ onCancel;
39
+ onSubmit;
40
+ constructor(seed, tui, terminalRows, bindings, suspendForChild) {
41
+ this.tui = tui;
42
+ this.terminalRows = terminalRows;
43
+ this.bindings = bindings;
44
+ this.suspendForChild = suspendForChild;
45
+ this.snapshot = seed;
46
+ this.plans = new PlanSet(seed);
47
+ this.docs = new ContextDocsPanel(() => this.tui.requestRender());
48
+ this.rebuild();
49
+ }
50
+ set focused(value) {
51
+ this._focused = value;
52
+ }
53
+ get focused() {
54
+ return this._focused;
55
+ }
56
+ /** Required by `Component`. Nothing is cached between frames: a plan can land
57
+ * from a warm tick and an edit can land from an await, both between inputs. */
58
+ invalidate() { }
59
+ dispose() {
60
+ this.plans.dispose();
61
+ }
62
+ // planning
63
+ /** Recompute the focused event's plan, refresh the panels from it, and let
64
+ * the remaining events fill in behind it. */
65
+ rebuild(selectedKey) {
66
+ this.plans.plan(this.snapshot.event);
67
+ this.docs.setRows(buildRows(this.plans, this.snapshot.event), selectedKey ?? this.docs.selected?.key);
68
+ this.syncDetail();
69
+ this.plans.warm(() => {
70
+ this.docs.setRows(buildRows(this.plans, this.snapshot.event), this.docs.selected?.key);
71
+ this.syncDetail();
72
+ this.tui.requestRender();
73
+ });
74
+ }
75
+ syncDetail() {
76
+ const row = this.docs.selected;
77
+ const capOwner = row === undefined ? null : capOwnerFor(row.anchor, this.snapshot.profileId);
78
+ const readOnly = row === undefined ? null : readOnlyReason(row.anchor, this.snapshot.profileId);
79
+ this.detail.setContext(row, this.snapshot.event, capOwner, readOnly);
80
+ }
81
+ setSnapshot(next) {
82
+ this.snapshot = next;
83
+ this.plans.setSnapshot(next);
84
+ this.notice = undefined;
85
+ this.rebuild();
86
+ }
87
+ // input
88
+ matches(id, data) {
89
+ return matchesPiTuiInput(this.bindings, id, data, (input, gesture) => matchesKey(input, gesture));
90
+ }
91
+ label(id, fallback) {
92
+ const gestures = this.bindings.gestures(id);
93
+ return gestures.length === 0 ? fallback : formatGesture(gestures[0].split(' > ').pop());
94
+ }
95
+ isDown(data) {
96
+ return matchesKey(data, 'j') || matchesKey(data, 'down') || this.matches('crtr.setup.list-down', data);
97
+ }
98
+ isUp(data) {
99
+ return matchesKey(data, 'k') || matchesKey(data, 'up') || this.matches('crtr.setup.list-up', data);
100
+ }
101
+ isDescend(data) {
102
+ return (matchesKey(data, 'l') ||
103
+ matchesKey(data, 'right') ||
104
+ matchesKey(data, 'enter') ||
105
+ this.matches('crtr.setup.advance', data));
106
+ }
107
+ isAscend(data) {
108
+ return matchesKey(data, 'h') || matchesKey(data, 'left');
109
+ }
110
+ /** Tab steps the focused event from any zone — the one axis every panel is
111
+ * drawn against, so it earns the page-level gesture. */
112
+ cyclesEvent(data) {
113
+ const horizontal = matchesKey(data, 'right') || matchesKey(data, 'left');
114
+ if (matchesKey(data, 'tab') || (!horizontal && this.matches('crtr.setup.tab-next', data)))
115
+ return 1;
116
+ if (matchesKey(data, 'shift+tab') || (!horizontal && this.matches('crtr.setup.tab-previous', data)))
117
+ return -1;
118
+ return 0;
119
+ }
120
+ handleInput(data) {
121
+ if (this.busy !== undefined)
122
+ return;
123
+ if (this.modal !== undefined) {
124
+ this.handleModal(data);
125
+ return;
126
+ }
127
+ if (this.docs.ownsKeyboard && this.docs.handleInput(data)) {
128
+ this.syncDetail();
129
+ return;
130
+ }
131
+ const step = this.cyclesEvent(data);
132
+ if (step !== 0) {
133
+ this.setSnapshot(cycleAxis(this.snapshot, 'event', step));
134
+ return;
135
+ }
136
+ if (matchesKey(data, 'q') || this.matches('crtr.setup.cancel', data)) {
137
+ this.close();
138
+ return;
139
+ }
140
+ if (this.zone === 'axes')
141
+ this.handleAxes(data);
142
+ else if (this.zone === 'docs')
143
+ this.handleDocs(data);
144
+ else
145
+ this.handleDetail(data);
146
+ }
147
+ handleAxes(data) {
148
+ const axes = axesOf(this.snapshot, profileNameFor(this.snapshot.profileId) ?? 'none');
149
+ this.axisIndex = Math.max(0, Math.min(this.axisIndex, axes.length - 1));
150
+ const axis = axes[this.axisIndex];
151
+ if (this.isDown(data)) {
152
+ this.axisIndex = Math.min(axes.length - 1, this.axisIndex + 1);
153
+ }
154
+ else if (this.isUp(data)) {
155
+ this.axisIndex = Math.max(0, this.axisIndex - 1);
156
+ }
157
+ else if (matchesKey(data, 'space') || this.matches('crtr.setup.toggle', data)) {
158
+ if (axis.typed)
159
+ this.openAxisField(axis.id, axis.label);
160
+ else
161
+ this.setSnapshot(cycleAxis(this.snapshot, axis.id, 1));
162
+ }
163
+ else if (this.isDescend(data)) {
164
+ if (matchesKey(data, 'enter') && axis.typed)
165
+ this.openAxisField(axis.id, axis.label);
166
+ else
167
+ this.zone = 'docs';
168
+ }
169
+ else if (matchesKey(data, 'escape')) {
170
+ this.close();
171
+ }
172
+ }
173
+ handleDocs(data) {
174
+ if (this.docs.handleInput(data)) {
175
+ this.syncDetail();
176
+ return;
177
+ }
178
+ if (this.isDown(data)) {
179
+ this.docs.move(1);
180
+ this.syncDetail();
181
+ }
182
+ else if (this.isUp(data)) {
183
+ this.docs.move(-1);
184
+ this.syncDetail();
185
+ }
186
+ else if (matchesKey(data, 'd')) {
187
+ this.docs.toggleDeliveringOnly();
188
+ this.syncDetail();
189
+ }
190
+ else if (this.isDescend(data)) {
191
+ if (this.docs.selected !== undefined)
192
+ this.zone = 'detail';
193
+ }
194
+ else if (this.isAscend(data) || matchesKey(data, 'escape')) {
195
+ this.zone = 'axes';
196
+ }
197
+ }
198
+ handleDetail(data) {
199
+ if (this.isDown(data))
200
+ this.detail.move(1);
201
+ else if (this.isUp(data))
202
+ this.detail.move(-1);
203
+ else if (this.isAscend(data) || matchesKey(data, 'escape'))
204
+ this.zone = 'docs';
205
+ else if (matchesKey(data, 'enter') || matchesKey(data, 'space') || this.matches('crtr.setup.toggle', data)) {
206
+ this.activate();
207
+ }
208
+ else if (matchesKey(data, 'b'))
209
+ this.editBody();
210
+ else if (matchesKey(data, 'x'))
211
+ this.removeEntry();
212
+ else if (matchesKey(data, 'e'))
213
+ this.composeEntry();
214
+ else if (matchesKey(data, 'g'))
215
+ this.editGate();
216
+ else if (matchesKey(data, 'u'))
217
+ this.flip('unlisted');
218
+ else if (matchesKey(data, 's'))
219
+ this.flip('slash');
220
+ else if (matchesKey(data, 'm'))
221
+ this.editCap();
222
+ else if (matchesKey(data, 'r'))
223
+ this.editMove();
224
+ else if (matchesKey(data, 'w'))
225
+ this.editRouting();
226
+ else if (matchesKey(data, 'f'))
227
+ this.editShortForm();
228
+ }
229
+ /** What the focused dossier row does when opened — the same write its letter
230
+ * key starts, so a user who never learns the letters can still edit. */
231
+ activate() {
232
+ const row = this.detail.selected;
233
+ if (row === undefined)
234
+ return;
235
+ switch (row.kind) {
236
+ case 'entry': return this.cycleEntryRung(row.at);
237
+ case 'add-entry': return this.composeEntry();
238
+ case 'gate': return this.editGate();
239
+ case 'unlisted': return this.flip('unlisted');
240
+ case 'slash': return this.flip('slash');
241
+ case 'routing': return this.editRouting();
242
+ case 'short-form': return this.editShortForm();
243
+ case 'body': return this.editBody();
244
+ case 'cap': return this.editCap();
245
+ case 'move': return this.editMove();
246
+ }
247
+ }
248
+ // writes
249
+ get record() {
250
+ return this.docs.selected?.anchor;
251
+ }
252
+ apply(work) {
253
+ this.busy = 'writing…';
254
+ this.tui.requestRender();
255
+ void work.then((outcome) => {
256
+ this.busy = undefined;
257
+ this.notice = { text: outcome.message, ok: outcome.ok };
258
+ if (outcome.ok) {
259
+ if (outcome.name !== undefined)
260
+ this.edited.add(outcome.name);
261
+ this.plans.invalidate();
262
+ this.rebuild();
263
+ }
264
+ this.tui.requestRender();
265
+ });
266
+ }
267
+ cycleEntryRung(at) {
268
+ const record = this.record;
269
+ const entry = record?.doc.surfaces[at];
270
+ if (record === undefined || entry === undefined)
271
+ return;
272
+ const next = SURFACE_RUNGS[(SURFACE_RUNGS.indexOf(entry.at) + 1) % SURFACE_RUNGS.length];
273
+ const surfaces = record.doc.surfaces.map((held, index) => (index === at ? { ...held, at: next } : held));
274
+ this.apply(writeSurfaces(record, this.snapshot.profileId, surfaces, `${record.name} now delivers at "${next}" on ${entry.on}.`));
275
+ }
276
+ removeEntry() {
277
+ const record = this.record;
278
+ const at = this.detail.selectedEntry;
279
+ if (record === undefined || at === undefined) {
280
+ this.notice = { text: 'Select a surface entry to remove.', ok: false };
281
+ return;
282
+ }
283
+ const entry = record.doc.surfaces[at];
284
+ const surfaces = record.doc.surfaces.filter((_held, index) => index !== at);
285
+ this.modal = {
286
+ kind: 'confirm',
287
+ title: 'Remove surface entry',
288
+ message: `${record.name} will stop delivering on ${entry.on} at "${entry.at}".`,
289
+ submit: () => this.apply(writeSurfaces(record, this.snapshot.profileId, surfaces, `Removed the ${entry.on} entry from ${record.name}.`)),
290
+ };
291
+ }
292
+ /** Event, then match, then rung — the three fields a valid entry needs, asked
293
+ * in the order the schema reads them. */
294
+ composeEntry() {
295
+ const record = this.record;
296
+ if (record === undefined)
297
+ return;
298
+ this.modal = {
299
+ kind: 'choice',
300
+ title: 'New entry — event',
301
+ options: [...SURFACE_EVENTS],
302
+ index: SURFACE_EVENTS.indexOf(this.snapshot.event),
303
+ submit: (event) => {
304
+ const on = event;
305
+ const needsMatch = on !== 'boot' && on !== 'workspace-open';
306
+ const chooseRung = (match) => {
307
+ this.modal = {
308
+ kind: 'choice',
309
+ title: 'New entry — rung',
310
+ options: [...SURFACE_RUNGS],
311
+ index: 0,
312
+ submit: (rung) => {
313
+ this.modal = undefined;
314
+ const entry = {
315
+ on,
316
+ ...(match === '' ? {} : { match: [match] }),
317
+ at: rung,
318
+ };
319
+ this.apply(writeSurfaces(record, this.snapshot.profileId, [...record.doc.surfaces, entry], `${record.name} now delivers on ${on} at "${rung}".`));
320
+ },
321
+ };
322
+ };
323
+ if (!needsMatch) {
324
+ chooseRung('');
325
+ return;
326
+ }
327
+ this.modal = {
328
+ kind: 'field',
329
+ title: `New entry — ${on} match`,
330
+ hint: on === 'read' ? 'absolute path glob, or ./ for this document\u2019s own directory' : 'glob over the event\u2019s namespace',
331
+ input: seeded(''),
332
+ submit: (value) => {
333
+ const match = value.trim();
334
+ if (match === '') {
335
+ this.modal = undefined;
336
+ this.notice = { text: `A ${on} entry needs a match glob.`, ok: false };
337
+ return;
338
+ }
339
+ chooseRung(match);
340
+ },
341
+ };
342
+ },
343
+ };
344
+ }
345
+ editGate() {
346
+ const record = this.record;
347
+ if (record === undefined)
348
+ return;
349
+ this.modal = {
350
+ kind: 'field',
351
+ title: 'Document gate',
352
+ hint: 'YAML or JSON predicate over the node config — empty clears it',
353
+ input: seeded(record.doc.gate === undefined ? '' : JSON.stringify(record.doc.gate)),
354
+ submit: (value) => {
355
+ this.modal = undefined;
356
+ this.apply(writeGate(record, this.snapshot.profileId, value));
357
+ },
358
+ };
359
+ }
360
+ flip(field) {
361
+ const record = this.record;
362
+ if (record === undefined)
363
+ return;
364
+ this.apply(toggleFlag(record, this.snapshot.profileId, field));
365
+ }
366
+ editRouting() {
367
+ const record = this.record;
368
+ if (record === undefined)
369
+ return;
370
+ this.modal = {
371
+ kind: 'field',
372
+ title: 'Routing line',
373
+ hint: 'When <circumstance>, this <kind> should be read because <payoff>',
374
+ input: seeded(record.doc.whenAndWhyToRead),
375
+ submit: (value) => {
376
+ this.modal = undefined;
377
+ this.apply(writeRoutingLine(record, this.snapshot.profileId, value));
378
+ },
379
+ };
380
+ }
381
+ editShortForm() {
382
+ const record = this.record;
383
+ if (record === undefined)
384
+ return;
385
+ this.modal = {
386
+ kind: 'field',
387
+ title: 'Short form',
388
+ hint: 'human-facing abbreviation for listings — empty clears it',
389
+ input: seeded(record.doc.shortForm),
390
+ submit: (value) => {
391
+ this.modal = undefined;
392
+ this.apply(writeShortForm(record, this.snapshot.profileId, value));
393
+ },
394
+ };
395
+ }
396
+ /** The one write that leaves the page: the terminal goes to $EDITOR and comes
397
+ * back repainted, and the buffer that returns is saved through `memory edit`
398
+ * like every other field here. */
399
+ editBody() {
400
+ const record = this.record;
401
+ if (record === undefined)
402
+ return;
403
+ // Checked before the terminal leaves for $EDITOR, so a read-only record
404
+ // refuses in place rather than after a whole editing session.
405
+ const refusal = readOnlyReason(record, this.snapshot.profileId);
406
+ if (refusal !== null) {
407
+ this.notice = { text: refusal, ok: false };
408
+ return;
409
+ }
410
+ let before;
411
+ try {
412
+ before = readBody(record);
413
+ }
414
+ catch (error) {
415
+ this.notice = { text: error instanceof Error ? error.message : String(error), ok: false };
416
+ return;
417
+ }
418
+ const resume = this.suspendForChild();
419
+ let result;
420
+ try {
421
+ result = editBufferInEditor(before);
422
+ }
423
+ finally {
424
+ resume();
425
+ }
426
+ if (result.error !== undefined) {
427
+ this.notice = { text: result.error, ok: false };
428
+ return;
429
+ }
430
+ if (result.text === before) {
431
+ this.notice = { text: `${record.name} came back unchanged \u2014 nothing written.`, ok: false };
432
+ return;
433
+ }
434
+ this.apply(writeBody(record, this.snapshot.profileId, result.text));
435
+ }
436
+ editMove() {
437
+ const record = this.record;
438
+ if (record === undefined)
439
+ return;
440
+ this.modal = {
441
+ kind: 'field',
442
+ title: 'Move or rename',
443
+ hint: 'the new canonical name inside this document\u2019s own store',
444
+ input: seeded(record.name),
445
+ submit: (value) => {
446
+ this.modal = undefined;
447
+ this.apply(moveDoc(record, this.snapshot.profileId, value));
448
+ },
449
+ };
450
+ }
451
+ /** The ceiling belongs to the profile project entry that governs the store,
452
+ * not to the document, so this write is offered only when one exists. */
453
+ editCap() {
454
+ const record = this.record;
455
+ const profileId = this.snapshot.profileId;
456
+ if (record === undefined)
457
+ return;
458
+ const owner = profileId === null ? null : capOwnerFor(record, profileId);
459
+ if (owner === null || profileId === null) {
460
+ this.notice = { text: 'No profile project governs this store — its ceiling is neutral and not editable here.', ok: false };
461
+ return;
462
+ }
463
+ this.modal = {
464
+ kind: 'choice',
465
+ title: `Ceiling for ${owner.path}`,
466
+ options: [...RUNGS],
467
+ index: Math.max(0, RUNGS.indexOf(owner.memory)),
468
+ submit: (rung) => {
469
+ this.modal = undefined;
470
+ const outcome = writeCap(profileId, owner.path, rung);
471
+ this.notice = { text: outcome.message, ok: outcome.ok };
472
+ if (outcome.change !== undefined) {
473
+ const held = this.capChanges.findIndex((change) => change.path === outcome.change.path);
474
+ if (held >= 0)
475
+ this.capChanges[held] = outcome.change;
476
+ else
477
+ this.capChanges.push(outcome.change);
478
+ this.plans.invalidate();
479
+ this.rebuild();
480
+ }
481
+ },
482
+ };
483
+ }
484
+ openAxisField(id, label) {
485
+ this.modal = {
486
+ kind: 'field',
487
+ title: label,
488
+ hint: id === 'cwd' ? 'absolute directory the project stores resolve from' : 'value the planner should match against',
489
+ input: seeded(typedAxisValue(this.snapshot, id)),
490
+ submit: (value) => {
491
+ this.modal = undefined;
492
+ this.setSnapshot(setTypedAxis(this.snapshot, id, value));
493
+ },
494
+ };
495
+ }
496
+ close() {
497
+ if (this.edited.size === 0 && this.capChanges.length === 0) {
498
+ this.onCancel?.();
499
+ return;
500
+ }
501
+ this.onSubmit?.({ edited: [...this.edited], capChanges: [...this.capChanges] });
502
+ }
503
+ // modals
504
+ handleModal(data) {
505
+ const modal = this.modal;
506
+ if (modal === undefined)
507
+ return;
508
+ if (matchesKey(data, 'escape')) {
509
+ this.modal = undefined;
510
+ return;
511
+ }
512
+ if (modal.kind === 'choice') {
513
+ if (this.isDown(data))
514
+ modal.index = Math.min(modal.options.length - 1, modal.index + 1);
515
+ else if (this.isUp(data))
516
+ modal.index = Math.max(0, modal.index - 1);
517
+ else if (matchesKey(data, 'enter'))
518
+ modal.submit(modal.options[modal.index]);
519
+ return;
520
+ }
521
+ if (modal.kind === 'confirm') {
522
+ if (matchesKey(data, 'y') || matchesKey(data, 'enter')) {
523
+ this.modal = undefined;
524
+ modal.submit();
525
+ }
526
+ else if (matchesKey(data, 'n'))
527
+ this.modal = undefined;
528
+ return;
529
+ }
530
+ if (matchesKey(data, 'enter'))
531
+ modal.submit(modal.input.getValue());
532
+ else
533
+ modal.input.handleInput(data);
534
+ }
535
+ renderModal(width, height) {
536
+ const modal = this.modal;
537
+ if (modal === undefined)
538
+ return [];
539
+ let lines;
540
+ if (modal.kind === 'choice') {
541
+ lines = [
542
+ theme.fg('accent', theme.bold(modal.title)),
543
+ '',
544
+ ...modal.options.map((option, index) => `${index === modal.index ? theme.fg('accent', '▶') : ' '} ${option}`),
545
+ ];
546
+ }
547
+ else if (modal.kind === 'confirm') {
548
+ lines = [theme.fg('warning', theme.bold(modal.title)), '', ...wrap(modal.message, Math.max(1, width - 6))];
549
+ }
550
+ else {
551
+ lines = [
552
+ theme.fg('accent', theme.bold(modal.title)),
553
+ theme.fg('dim', modal.hint),
554
+ '',
555
+ modal.input.render(Math.max(1, width - 6))[0] ?? '',
556
+ ];
557
+ }
558
+ const out = framed(lines, width);
559
+ while (out.length < height)
560
+ out.push('');
561
+ return out.slice(0, height);
562
+ }
563
+ // rendering
564
+ footer(width) {
565
+ if (this.busy !== undefined)
566
+ return theme.fg('warning', this.busy);
567
+ if (this.modal?.kind === 'choice')
568
+ return theme.fg('dim', 'j/k choose · enter apply · esc cancel');
569
+ if (this.modal?.kind === 'confirm')
570
+ return theme.fg('dim', 'y/enter confirm · n/esc cancel');
571
+ if (this.modal?.kind === 'field')
572
+ return theme.fg('dim', 'type · enter apply · esc cancel');
573
+ if (this.docs.ownsKeyboard)
574
+ return theme.fg('dim', 'type to filter · enter keep · esc clear');
575
+ if (this.notice !== undefined)
576
+ return theme.fg(this.notice.ok ? 'success' : 'warning', truncateToWidth(this.notice.text, width));
577
+ const move = `${this.label('crtr.setup.list-down', 'j')}/${this.label('crtr.setup.list-up', 'k')}`;
578
+ const left = this.zone === 'axes'
579
+ ? `${move} axis · space change · enter documents`
580
+ : this.zone === 'docs'
581
+ ? `${move} doc · / filter · d ${this.docs.filteringDelivering ? 'all' : 'delivering'} · enter dossier`
582
+ : `${move} field · enter edit · e add · x remove · g gate · b body · u unlisted · s slash · m cap · r rename`;
583
+ const right = `${this.label('crtr.setup.tab-next', 'tab')} event · ${this.label('crtr.setup.cancel', 'q')} close`;
584
+ const gap = width - visibleWidth(left) - visibleWidth(right);
585
+ return gap >= 2
586
+ ? `${theme.fg('dim', left)}${' '.repeat(gap)}${theme.fg('dim', right)}`
587
+ : theme.fg('dim', truncateToWidth(`${left} · ${this.label('crtr.setup.cancel', 'q')} close`, width));
588
+ }
589
+ render(width) {
590
+ const height = this.terminalRows();
591
+ const bodyHeight = Math.max(1, height - PAGE_CHROME_ROWS);
592
+ const railVisible = width >= RAIL_MIN_TOTAL_WIDTH;
593
+ const axes = axesOf(this.snapshot, profileNameFor(this.snapshot.profileId) ?? 'none');
594
+ this.axisIndex = Math.max(0, Math.min(this.axisIndex, axes.length - 1));
595
+ let body;
596
+ if (width >= THREE_COLUMN_MIN_WIDTH) {
597
+ const detailWidth = Math.max(1, width - RAIL_WIDTH - DOCS_WIDTH - 6);
598
+ const middle = columns(this.docs.render(DOCS_WIDTH, bodyHeight, this.zone === 'docs', this.plans), DOCS_WIDTH, this.modal === undefined
599
+ ? this.detail.render(detailWidth, bodyHeight, this.zone === 'detail')
600
+ : this.renderModal(detailWidth, bodyHeight), detailWidth);
601
+ body = columns(renderAxes(axes, this.axisIndex, RAIL_WIDTH, bodyHeight, this.zone === 'axes'), RAIL_WIDTH, middle, width - RAIL_WIDTH - 3);
602
+ }
603
+ else if (railVisible) {
604
+ const panelWidth = width - RAIL_WIDTH - 3;
605
+ const panel = this.modal !== undefined
606
+ ? this.renderModal(panelWidth, bodyHeight)
607
+ : this.zone === 'detail'
608
+ ? this.detail.render(panelWidth, bodyHeight, true)
609
+ : this.docs.render(panelWidth, bodyHeight, this.zone === 'docs', this.plans);
610
+ body = columns(renderAxes(axes, this.axisIndex, RAIL_WIDTH, bodyHeight, this.zone === 'axes'), RAIL_WIDTH, panel, panelWidth);
611
+ }
612
+ else if (this.modal !== undefined) {
613
+ body = this.renderModal(width, bodyHeight);
614
+ }
615
+ else if (this.zone === 'axes') {
616
+ body = renderAxes(axes, this.axisIndex, width, bodyHeight, true);
617
+ }
618
+ else if (this.zone === 'detail') {
619
+ body = this.detail.render(width, bodyHeight, true);
620
+ }
621
+ else {
622
+ body = this.docs.render(width, bodyHeight, true, this.plans);
623
+ }
624
+ const pending = SURFACE_EVENTS.filter((event) => this.plans.peek(event) === undefined).length;
625
+ const meta = [
626
+ this.snapshot.event,
627
+ pending > 0 ? `${plural(pending, 'event', 'events')} planning` : 'all events planned',
628
+ this.edited.size > 0 ? `${plural(this.edited.size, 'doc', 'docs')} edited` : '',
629
+ ]
630
+ .filter((part) => part !== '')
631
+ .join(' · ');
632
+ return pageFrame({ width, height, title: 'crouter context', meta, body, footer: this.footer(width) });
633
+ }
634
+ }
635
+ /** Every field here is an edit-or-accept, never a blank to retype, so the
636
+ * cursor lands at the end (`\x05`) — `setValue` alone leaves it at 0, where
637
+ * typing prepends and backspace does nothing. */
638
+ function seeded(value) {
639
+ const input = new Input();
640
+ input.setValue(value);
641
+ input.handleInput('\x05');
642
+ input.focused = true;
643
+ return input;
644
+ }
645
+ function wrap(text, width) {
646
+ const out = [];
647
+ let line = '';
648
+ for (const word of text.trim().split(/\s+/)) {
649
+ const next = line === '' ? word : `${line} ${word}`;
650
+ if (visibleWidth(next) <= width)
651
+ line = next;
652
+ else {
653
+ if (line !== '')
654
+ out.push(line);
655
+ line = word;
656
+ }
657
+ }
658
+ if (line !== '')
659
+ out.push(line);
660
+ return out;
661
+ }
662
+ export async function runContextAdmin(seed) {
663
+ return runTuiPage(({ tui, terminal, bindings, suspendForChild }, control) => {
664
+ const shell = new ContextAdminShell(seed, tui, () => terminal.rows, bindings, suspendForChild);
665
+ shell.onCancel = control.cancel;
666
+ shell.onSubmit = control.submit;
667
+ return shell;
668
+ });
669
+ }
@@ -0,0 +1 @@
1
+ export declare const adminLeaf: import("../../../core/command.js").LeafDef;