@north-light/crouter 0.3.243 → 0.3.245
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clients/attach/viewer.js +64 -64
- package/dist/commands/sys/context/admin/detail-panel.d.ts +39 -14
- package/dist/commands/sys/context/admin/detail-panel.js +170 -130
- package/dist/commands/sys/context/admin/docs-panel.d.ts +10 -13
- package/dist/commands/sys/context/admin/docs-panel.js +48 -61
- package/dist/commands/sys/context/admin/list-view.d.ts +2 -4
- package/dist/commands/sys/context/admin/list-view.js +5 -6
- package/dist/commands/sys/context/admin/model.d.ts +16 -12
- package/dist/commands/sys/context/admin/model.js +17 -30
- package/dist/commands/sys/context/admin/rail-panel.d.ts +43 -7
- package/dist/commands/sys/context/admin/rail-panel.js +104 -37
- package/dist/commands/sys/context/admin/read-view.d.ts +31 -0
- package/dist/commands/sys/context/admin/read-view.js +189 -0
- package/dist/commands/sys/context/admin/shell.d.ts +26 -4
- package/dist/commands/sys/context/admin/shell.js +197 -84
- package/dist/commands/sys/context/doc.js +6 -5
- package/dist/commands/sys/context/resolve.d.ts +7 -3
- package/dist/commands/sys/context/resolve.js +19 -11
- package/dist/commands/sys/panels/profiles-panel.js +2 -1
- package/dist/core/fs-utils.d.ts +3 -0
- package/dist/core/fs-utils.js +10 -1
- package/dist/core/profiles/select.d.ts +0 -3
- package/dist/core/profiles/select.js +1 -11
- package/dist/core/substrate/gate-explain.d.ts +48 -0
- package/dist/core/substrate/gate-explain.js +353 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// The `crtr sys context admin` page: a rail, the document
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The `crtr sys context admin` page: a rail, the document list, and the dossier
|
|
2
|
+
// that edits one document's routing. The rail decides WHAT is being planned and
|
|
3
|
+
// WHICH slice of it is shown, the list states what each document does on the
|
|
4
|
+
// dialled event, and the dossier is the only place a write starts.
|
|
5
5
|
import { Input, matchesKey, truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
|
|
6
6
|
import { formatGesture, matchesPiTuiInput, } from '../../../../core/keybindings/index.js';
|
|
7
7
|
import { runTuiPage } from '../../../../core/tui/page-host.js';
|
|
@@ -10,7 +10,7 @@ import { RUNGS, SURFACE_EVENTS, SURFACE_RUNGS, } from '../../../../core/substrat
|
|
|
10
10
|
import { profileNameFor } from '../../../../core/substrate/subject-fields.js';
|
|
11
11
|
import { editBufferInEditor } from '../../../../clients/inbox/review/roundtrip.js';
|
|
12
12
|
import { capOwnerFor, moveDoc, readBody, readOnlyReason, toggleFlag, writeBody, writeCap, writeGate, writeRoutingLine, writeShortForm, writeSurfaces, } from './actions.js';
|
|
13
|
-
import { axesOf, cycleAxis, cycleFacet, facetsOf, renderRail, setTypedAxis, typedAxisValue, } from './rail-panel.js';
|
|
13
|
+
import { axesOf, cycleAxis, cycleFacet, facetsOf, payloadRowOf, railEnter, railRowKey, railRowsOf, renderRail, setTypedAxis, typedAxisValue, } from './rail-panel.js';
|
|
14
14
|
import { ContextDetailPanel } from './detail-panel.js';
|
|
15
15
|
import { ContextDocsPanel, buildRows } from './docs-panel.js';
|
|
16
16
|
import { NO_FILTERS, costTotals } from './list-view.js';
|
|
@@ -20,8 +20,8 @@ const RAIL_MIN_TOTAL_WIDTH = 86;
|
|
|
20
20
|
const THREE_COLUMN_MIN_WIDTH = 112;
|
|
21
21
|
/** Columns the two dividers between the three zones spend. */
|
|
22
22
|
const DIVIDER_WIDTH = 6;
|
|
23
|
-
/** Below the floor a row cannot hold a name beside
|
|
24
|
-
*
|
|
23
|
+
/** Below the floor a row cannot hold a name beside its state; above the ceiling
|
|
24
|
+
* extra width stops buying legibility and the dossier wants it. */
|
|
25
25
|
const DOCS_MIN_WIDTH = 48;
|
|
26
26
|
const DOCS_MAX_WIDTH = 100;
|
|
27
27
|
const DOCS_SHARE = 0.45;
|
|
@@ -78,14 +78,14 @@ export class ContextAdminShell {
|
|
|
78
78
|
// planning
|
|
79
79
|
/** Recompute the focused event's plan, refresh the panels from it, and let
|
|
80
80
|
* the remaining events fill in behind it. */
|
|
81
|
-
rebuild(
|
|
81
|
+
rebuild() {
|
|
82
82
|
this.plans.plan(this.snapshot.event);
|
|
83
83
|
this.rows = buildRows(this.plans, this.snapshot.event);
|
|
84
|
-
this.docs.setRows(this.rows, this.snapshot.event
|
|
84
|
+
this.docs.setRows(this.rows, this.snapshot.event);
|
|
85
85
|
this.syncDetail();
|
|
86
86
|
this.plans.warm(() => {
|
|
87
87
|
this.rows = buildRows(this.plans, this.snapshot.event);
|
|
88
|
-
this.docs.setRows(this.rows, this.snapshot.event
|
|
88
|
+
this.docs.setRows(this.rows, this.snapshot.event);
|
|
89
89
|
this.syncDetail();
|
|
90
90
|
this.tui.requestRender();
|
|
91
91
|
});
|
|
@@ -101,13 +101,29 @@ export class ContextAdminShell {
|
|
|
101
101
|
const row = this.docs.selected;
|
|
102
102
|
const capOwner = row === undefined ? null : capOwnerFor(row.anchor, this.snapshot.profileId);
|
|
103
103
|
const readOnly = row === undefined ? null : readOnlyReason(row.anchor, this.snapshot.profileId);
|
|
104
|
-
|
|
104
|
+
// The plan's own subject, not a re-derived one: what a failed gate is
|
|
105
|
+
// explained against must be exactly what it was judged against.
|
|
106
|
+
this.detail.setContext(row, this.snapshot.event, capOwner, readOnly, this.plans.plan(this.snapshot.event).subject);
|
|
105
107
|
}
|
|
106
108
|
setSnapshot(next) {
|
|
109
|
+
const held = this.railRows()[this.railIndex];
|
|
107
110
|
this.snapshot = next;
|
|
108
111
|
this.plans.setSnapshot(next);
|
|
109
112
|
this.notice = undefined;
|
|
110
113
|
this.rebuild();
|
|
114
|
+
this.holdRail(held);
|
|
115
|
+
}
|
|
116
|
+
/** Put the rail cursor back on the row it was on, since dialling a different
|
|
117
|
+
* event moves the payload row and every offset below it. The payload belongs
|
|
118
|
+
* to the dialled event, so when the new event reads none the cursor falls to
|
|
119
|
+
* that event's own row rather than to whatever slid into the offset. */
|
|
120
|
+
holdRail(held) {
|
|
121
|
+
if (held === undefined)
|
|
122
|
+
return;
|
|
123
|
+
const rows = this.railRows();
|
|
124
|
+
const key = railRowKey(held);
|
|
125
|
+
const at = rows.findIndex((row) => railRowKey(row) === key);
|
|
126
|
+
this.railIndex = Math.max(0, at >= 0 ? at : rows.findIndex((row) => row.kind === 'event' && row.event.dialled));
|
|
111
127
|
}
|
|
112
128
|
// input
|
|
113
129
|
matches(id, data) {
|
|
@@ -158,7 +174,10 @@ export class ContextAdminShell {
|
|
|
158
174
|
this.setSnapshot(cycleAxis(this.snapshot, 'event', step));
|
|
159
175
|
return;
|
|
160
176
|
}
|
|
161
|
-
|
|
177
|
+
// `crtr.setup.cancel` also claims ctrl+d, so the read face's half-pane
|
|
178
|
+
// gesture has to outrank it or paging a body would close the page instead.
|
|
179
|
+
const pagesBody = this.zone === 'detail' && !this.detail.editing && matchesKey(data, 'ctrl+d');
|
|
180
|
+
if (!pagesBody && (matchesKey(data, 'q') || this.matches('crtr.setup.cancel', data))) {
|
|
162
181
|
this.close();
|
|
163
182
|
return;
|
|
164
183
|
}
|
|
@@ -169,31 +188,47 @@ export class ContextAdminShell {
|
|
|
169
188
|
else
|
|
170
189
|
this.handleDetail(data);
|
|
171
190
|
}
|
|
172
|
-
/**
|
|
173
|
-
*
|
|
191
|
+
/** Every surface event with what it delivers, which is what the rail states
|
|
192
|
+
* instead of the list drawing a cell per event on every row. An event whose
|
|
193
|
+
* plan has not warmed yet has no count to state. */
|
|
194
|
+
eventRows() {
|
|
195
|
+
return SURFACE_EVENTS.map((event) => {
|
|
196
|
+
const plan = this.plans.peek(event);
|
|
197
|
+
return {
|
|
198
|
+
event,
|
|
199
|
+
delivers: plan === undefined ? undefined : plan.docs.filter((record) => record.finalRung !== 'none').length,
|
|
200
|
+
dialled: event === this.snapshot.event,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/** The rail's rows: the events, then the snapshot axes, then the filter
|
|
205
|
+
* facets, so one cursor and one gesture drive all three sections. */
|
|
206
|
+
railRows() {
|
|
207
|
+
return railRowsOf(this.eventRows(), payloadRowOf(this.snapshot), axesOf(this.snapshot, profileNameFor(this.snapshot.profileId) ?? 'none'), facetsOf(this.filters));
|
|
208
|
+
}
|
|
174
209
|
handleRail(data) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
this.railIndex = Math.max(0, Math.min(this.railIndex, total - 1));
|
|
179
|
-
const axis = axes[this.railIndex];
|
|
180
|
-
const facet = facets[this.railIndex - axes.length];
|
|
210
|
+
const rows = this.railRows();
|
|
211
|
+
this.railIndex = Math.max(0, Math.min(this.railIndex, rows.length - 1));
|
|
212
|
+
const row = rows[this.railIndex];
|
|
181
213
|
const change = (delta) => {
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
214
|
+
if (row === undefined)
|
|
215
|
+
return;
|
|
216
|
+
if (row.kind === 'event')
|
|
217
|
+
this.setSnapshot({ ...this.snapshot, event: row.event.event });
|
|
218
|
+
else if (row.kind === 'axis') {
|
|
219
|
+
if (row.axis.typed)
|
|
220
|
+
this.openAxisField(row.axis.id, row.axis.label);
|
|
185
221
|
else
|
|
186
|
-
this.setSnapshot(cycleAxis(this.snapshot, axis.id, delta));
|
|
187
|
-
}
|
|
188
|
-
else if (facet !== undefined) {
|
|
189
|
-
if (facet.typed)
|
|
190
|
-
this.openSearch();
|
|
191
|
-
else
|
|
192
|
-
this.setFilters(cycleFacet(this.filters, facet.id, delta));
|
|
222
|
+
this.setSnapshot(cycleAxis(this.snapshot, row.axis.id, delta));
|
|
193
223
|
}
|
|
224
|
+
else if (row.facet.typed)
|
|
225
|
+
this.openSearch();
|
|
226
|
+
else
|
|
227
|
+
this.setFilters(cycleFacet(this.filters, row.facet.id, delta));
|
|
194
228
|
};
|
|
229
|
+
const onEnter = railEnter(row);
|
|
195
230
|
if (this.isDown(data)) {
|
|
196
|
-
this.railIndex = Math.min(
|
|
231
|
+
this.railIndex = Math.min(rows.length - 1, this.railIndex + 1);
|
|
197
232
|
}
|
|
198
233
|
else if (this.isUp(data)) {
|
|
199
234
|
this.railIndex = Math.max(0, this.railIndex - 1);
|
|
@@ -202,7 +237,7 @@ export class ContextAdminShell {
|
|
|
202
237
|
change(1);
|
|
203
238
|
}
|
|
204
239
|
else if (this.isDescend(data)) {
|
|
205
|
-
if (matchesKey(data, 'enter') &&
|
|
240
|
+
if (matchesKey(data, 'enter') && onEnter !== 'documents')
|
|
206
241
|
change(1);
|
|
207
242
|
else
|
|
208
243
|
this.zone = 'docs';
|
|
@@ -228,9 +263,8 @@ export class ContextAdminShell {
|
|
|
228
263
|
this.syncDetail();
|
|
229
264
|
}
|
|
230
265
|
else if (this.isDescend(data)) {
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
if (this.docs.onGroup && !this.docs.onDocument) {
|
|
266
|
+
// One gesture per row: a header expands, a document opens its dossier.
|
|
267
|
+
if (this.docs.onGroup) {
|
|
234
268
|
this.docs.toggleGroup();
|
|
235
269
|
this.syncDetail();
|
|
236
270
|
}
|
|
@@ -268,35 +302,101 @@ export class ContextAdminShell {
|
|
|
268
302
|
this.tui.requestRender();
|
|
269
303
|
}
|
|
270
304
|
handleDetail(data) {
|
|
305
|
+
if (this.detail.editing)
|
|
306
|
+
this.handleEdit(data);
|
|
307
|
+
else
|
|
308
|
+
this.handleRead(data);
|
|
309
|
+
}
|
|
310
|
+
/** The resting face: reading, and the two gestures that leave it. */
|
|
311
|
+
handleRead(data) {
|
|
271
312
|
if (this.isDown(data))
|
|
272
313
|
this.detail.move(1);
|
|
273
314
|
else if (this.isUp(data))
|
|
274
315
|
this.detail.move(-1);
|
|
275
|
-
else if (
|
|
276
|
-
this.
|
|
277
|
-
else if (matchesKey(data, '
|
|
278
|
-
this.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
316
|
+
else if (matchesKey(data, 'ctrl+d'))
|
|
317
|
+
this.detail.page(1);
|
|
318
|
+
else if (matchesKey(data, 'ctrl+u'))
|
|
319
|
+
this.detail.page(-1);
|
|
320
|
+
else if (data === 'G' || matchesKey(data, 'shift+g'))
|
|
321
|
+
this.detail.jump('bottom');
|
|
322
|
+
else if (data === 'g' || matchesKey(data, 'g'))
|
|
323
|
+
this.detail.jump('top');
|
|
324
|
+
else if (data === 'e' || matchesKey(data, 'e'))
|
|
325
|
+
this.detail.edit();
|
|
326
|
+
else if (this.isDescend(data))
|
|
327
|
+
this.detail.openBody();
|
|
328
|
+
else if (this.isAscend(data) || matchesKey(data, 'escape')) {
|
|
329
|
+
if (!this.detail.back())
|
|
330
|
+
this.zone = 'docs';
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** The field list. Every write gesture is resolved before it is started, so a
|
|
334
|
+
* read-only document refuses at the keypress rather than after a modal. */
|
|
335
|
+
handleEdit(data) {
|
|
336
|
+
if (this.isDown(data)) {
|
|
337
|
+
this.detail.move(1);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (this.isUp(data)) {
|
|
341
|
+
this.detail.move(-1);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (this.isAscend(data) || matchesKey(data, 'escape')) {
|
|
345
|
+
this.detail.back();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const gesture = this.writeGesture(data);
|
|
349
|
+
if (gesture === undefined)
|
|
350
|
+
return;
|
|
351
|
+
if (gesture.document && this.refusesWrite())
|
|
352
|
+
return;
|
|
353
|
+
gesture.start();
|
|
354
|
+
}
|
|
355
|
+
/** The write one edit-face key starts, or undefined when the key writes
|
|
356
|
+
* nothing. `document` marks the writes a read-only document refuses — the
|
|
357
|
+
* profile ceiling is not one of them; it lives in the profile manifest. */
|
|
358
|
+
writeGesture(data) {
|
|
359
|
+
const doc = (start) => ({ start, document: true });
|
|
360
|
+
if (matchesKey(data, 'enter') || matchesKey(data, 'space') || this.matches('crtr.setup.toggle', data)) {
|
|
361
|
+
const row = this.detail.selected;
|
|
362
|
+
if (row === undefined)
|
|
363
|
+
return undefined;
|
|
364
|
+
return row.kind === 'cap' ? { start: () => this.editCap(), document: false } : doc(() => this.activate());
|
|
365
|
+
}
|
|
366
|
+
if (matchesKey(data, 'm'))
|
|
367
|
+
return { start: () => this.editCap(), document: false };
|
|
368
|
+
if (matchesKey(data, 'b'))
|
|
369
|
+
return doc(() => this.editBody());
|
|
370
|
+
if (matchesKey(data, 'x'))
|
|
371
|
+
return doc(() => this.removeEntry());
|
|
372
|
+
if (matchesKey(data, 'e'))
|
|
373
|
+
return doc(() => this.composeEntry());
|
|
374
|
+
if (matchesKey(data, 'g'))
|
|
375
|
+
return doc(() => this.editGate());
|
|
376
|
+
if (matchesKey(data, 'u'))
|
|
377
|
+
return doc(() => this.flip('unlisted'));
|
|
378
|
+
if (matchesKey(data, 's'))
|
|
379
|
+
return doc(() => this.flip('slash'));
|
|
380
|
+
if (matchesKey(data, 'r'))
|
|
381
|
+
return doc(() => this.editMove());
|
|
382
|
+
if (matchesKey(data, 'w'))
|
|
383
|
+
return doc(() => this.editRouting());
|
|
384
|
+
if (matchesKey(data, 'f'))
|
|
385
|
+
return doc(() => this.editShortForm());
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
/** The refusal a read-only document owes the write just attempted, carried by
|
|
389
|
+
* the same footer channel every other write outcome uses. */
|
|
390
|
+
refusesWrite() {
|
|
391
|
+
const record = this.record;
|
|
392
|
+
if (record === undefined)
|
|
393
|
+
return false;
|
|
394
|
+
const refusal = readOnlyReason(record, this.snapshot.profileId);
|
|
395
|
+
if (refusal === null)
|
|
396
|
+
return false;
|
|
397
|
+
this.notice = { text: refusal, ok: false };
|
|
398
|
+
this.tui.requestRender();
|
|
399
|
+
return true;
|
|
300
400
|
}
|
|
301
401
|
/** What the focused dossier row does when opened — the same write its letter
|
|
302
402
|
* key starts, so a user who never learns the letters can still edit. */
|
|
@@ -472,13 +572,6 @@ export class ContextAdminShell {
|
|
|
472
572
|
const record = this.record;
|
|
473
573
|
if (record === undefined)
|
|
474
574
|
return;
|
|
475
|
-
// Checked before the terminal leaves for $EDITOR, so a read-only record
|
|
476
|
-
// refuses in place rather than after a whole editing session.
|
|
477
|
-
const refusal = readOnlyReason(record, this.snapshot.profileId);
|
|
478
|
-
if (refusal !== null) {
|
|
479
|
-
this.notice = { text: refusal, ok: false };
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
575
|
let before;
|
|
483
576
|
try {
|
|
484
577
|
before = readBody(record);
|
|
@@ -633,7 +726,7 @@ export class ContextAdminShell {
|
|
|
633
726
|
return out.slice(0, height);
|
|
634
727
|
}
|
|
635
728
|
// rendering
|
|
636
|
-
footer(width) {
|
|
729
|
+
footer(width, railRow) {
|
|
637
730
|
if (this.busy !== undefined)
|
|
638
731
|
return theme.fg('warning', this.busy);
|
|
639
732
|
if (this.modal?.kind === 'choice')
|
|
@@ -648,33 +741,33 @@ export class ContextAdminShell {
|
|
|
648
741
|
return theme.fg(this.notice.ok ? 'success' : 'warning', truncateToWidth(this.notice.text, width));
|
|
649
742
|
const move = `${this.label('crtr.setup.list-down', 'j')}/${this.label('crtr.setup.list-up', 'k')}`;
|
|
650
743
|
const left = this.zone === 'rail'
|
|
651
|
-
? `${move} row ·
|
|
744
|
+
? `${move} row · ${railGesture(railRow)}`
|
|
652
745
|
: this.zone === 'docs'
|
|
653
746
|
? `${move} row · ${this.docsGesture()} · / search`
|
|
654
|
-
:
|
|
747
|
+
: this.detail.editing
|
|
748
|
+
? `${move} field · enter edit · e add · x remove · g gate · b body · u unlisted · s slash · m cap · r rename · esc read`
|
|
749
|
+
: this.detail.bodyOpen
|
|
750
|
+
? `${move} scroll · ctrl-d/u page · g/G ends · esc collapse`
|
|
751
|
+
: `${move} scroll · enter body · e edit · esc list`;
|
|
655
752
|
const right = `${this.label('crtr.setup.tab-next', 'tab')} event · ${this.label('crtr.setup.cancel', 'q')} close`;
|
|
656
753
|
const gap = width - visibleWidth(left) - visibleWidth(right);
|
|
657
754
|
return gap >= 2
|
|
658
755
|
? `${theme.fg('dim', left)}${' '.repeat(gap)}${theme.fg('dim', right)}`
|
|
659
756
|
: theme.fg('dim', truncateToWidth(`${left} · ${this.label('crtr.setup.cancel', 'q')} close`, width));
|
|
660
757
|
}
|
|
661
|
-
/** What the focused row does when opened. A directory
|
|
662
|
-
*
|
|
758
|
+
/** What the focused row does when opened. A directory expands; a document
|
|
759
|
+
* opens its dossier. */
|
|
663
760
|
docsGesture() {
|
|
664
|
-
|
|
665
|
-
return 'enter dossier';
|
|
666
|
-
return this.docs.onDocument ? 'space open · enter dossier' : 'space/enter open';
|
|
761
|
+
return this.docs.onGroup ? 'space/enter open' : 'enter dossier';
|
|
667
762
|
}
|
|
668
763
|
render(width) {
|
|
669
764
|
const height = this.terminalRows();
|
|
670
765
|
const bodyHeight = Math.max(1, height - PAGE_CHROME_ROWS);
|
|
671
766
|
const railVisible = width >= RAIL_MIN_TOTAL_WIDTH;
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
this.railIndex = Math.max(0, Math.min(this.railIndex, axes.length + facets.length - 1));
|
|
767
|
+
const rows = this.railRows();
|
|
768
|
+
this.railIndex = Math.max(0, Math.min(this.railIndex, rows.length - 1));
|
|
675
769
|
const rail = (railWidth) => renderRail({
|
|
676
|
-
|
|
677
|
-
facets,
|
|
770
|
+
rows,
|
|
678
771
|
index: this.railIndex,
|
|
679
772
|
width: railWidth,
|
|
680
773
|
height: bodyHeight,
|
|
@@ -686,7 +779,7 @@ export class ContextAdminShell {
|
|
|
686
779
|
if (width >= THREE_COLUMN_MIN_WIDTH) {
|
|
687
780
|
const docsWidth = docsWidthFor(width);
|
|
688
781
|
const detailWidth = Math.max(1, width - RAIL_WIDTH - docsWidth - DIVIDER_WIDTH);
|
|
689
|
-
const middle = columns(this.docs.render(docsWidth, bodyHeight, this.zone === 'docs'
|
|
782
|
+
const middle = columns(this.docs.render(docsWidth, bodyHeight, this.zone === 'docs'), docsWidth, this.modal === undefined
|
|
690
783
|
? this.detail.render(detailWidth, bodyHeight, this.zone === 'detail')
|
|
691
784
|
: this.renderModal(detailWidth, bodyHeight), detailWidth);
|
|
692
785
|
body = columns(rail(RAIL_WIDTH), RAIL_WIDTH, middle, width - RAIL_WIDTH - 3);
|
|
@@ -697,7 +790,7 @@ export class ContextAdminShell {
|
|
|
697
790
|
? this.renderModal(panelWidth, bodyHeight)
|
|
698
791
|
: this.zone === 'detail'
|
|
699
792
|
? this.detail.render(panelWidth, bodyHeight, true)
|
|
700
|
-
: this.docs.render(panelWidth, bodyHeight, this.zone === 'docs'
|
|
793
|
+
: this.docs.render(panelWidth, bodyHeight, this.zone === 'docs');
|
|
701
794
|
body = columns(rail(RAIL_WIDTH), RAIL_WIDTH, panel, panelWidth);
|
|
702
795
|
}
|
|
703
796
|
else if (this.modal !== undefined) {
|
|
@@ -710,7 +803,7 @@ export class ContextAdminShell {
|
|
|
710
803
|
body = this.detail.render(width, bodyHeight, true);
|
|
711
804
|
}
|
|
712
805
|
else {
|
|
713
|
-
body = this.docs.render(width, bodyHeight, true
|
|
806
|
+
body = this.docs.render(width, bodyHeight, true);
|
|
714
807
|
}
|
|
715
808
|
const pending = SURFACE_EVENTS.filter((event) => this.plans.peek(event) === undefined).length;
|
|
716
809
|
const meta = [
|
|
@@ -721,7 +814,14 @@ export class ContextAdminShell {
|
|
|
721
814
|
]
|
|
722
815
|
.filter((part) => part !== '')
|
|
723
816
|
.join(' · ');
|
|
724
|
-
return pageFrame({
|
|
817
|
+
return pageFrame({
|
|
818
|
+
width,
|
|
819
|
+
height,
|
|
820
|
+
title: 'crouter context',
|
|
821
|
+
meta,
|
|
822
|
+
body,
|
|
823
|
+
footer: this.footer(width, rows[this.railIndex]),
|
|
824
|
+
});
|
|
725
825
|
}
|
|
726
826
|
/** What the delivering documents spend on this event. An estimate, and a
|
|
727
827
|
* floor on the rendered prompt: boot wraps these documents in its own intro
|
|
@@ -737,6 +837,19 @@ export class ContextAdminShell {
|
|
|
737
837
|
return `docs load ${formatCost(total)} — ${split}`;
|
|
738
838
|
}
|
|
739
839
|
}
|
|
840
|
+
/** What the focused rail row does, which differs by row: an event dials, a
|
|
841
|
+
* typed row opens its field, and an enum cycles in place with enter left free
|
|
842
|
+
* to move zones. */
|
|
843
|
+
function railGesture(row) {
|
|
844
|
+
switch (railEnter(row)) {
|
|
845
|
+
case 'dial':
|
|
846
|
+
return 'space/enter dial · l documents';
|
|
847
|
+
case 'edit':
|
|
848
|
+
return 'space/enter edit · l documents';
|
|
849
|
+
default:
|
|
850
|
+
return 'space change · enter documents';
|
|
851
|
+
}
|
|
852
|
+
}
|
|
740
853
|
/** Every field here is an edit-or-accept, never a blank to retype, so the
|
|
741
854
|
* cursor lands at the end (`\x05`) — `setValue` alone leaves it at 0, where
|
|
742
855
|
* typing prepends and backspace does nothing. */
|
|
@@ -7,6 +7,7 @@ import { realpathOrSelf } from '../../../core/fs-utils.js';
|
|
|
7
7
|
import { resolveMemoryDocForTarget } from '../../../core/memory-resolver.js';
|
|
8
8
|
import { InputError } from '../../../core/io.js';
|
|
9
9
|
import { planDelivery } from '../../../core/substrate/plan.js';
|
|
10
|
+
import { explainRecordGate } from '../../../core/substrate/gate-explain.js';
|
|
10
11
|
import { parseSubstrateDoc, SURFACE_EVENTS } from '../../../core/substrate/schema.js';
|
|
11
12
|
import { buildSnapshot, cell, delivers, describeSubject, entryText, exclusionReason, gateText, optionalString, payloadFor, subjectSummary, } from './resolve.js';
|
|
12
13
|
export const docLeaf = defineLeaf({
|
|
@@ -58,7 +59,7 @@ export const docLeaf = defineLeaf({
|
|
|
58
59
|
const recordPathCache = new Map();
|
|
59
60
|
const rows = events.map((event) => {
|
|
60
61
|
const plan = planDelivery(snapshot.subject, snapshot.target, event, payloadFor(event, flags, snapshot.target));
|
|
61
|
-
return eventRow(event, findRecord(plan.docs, targetPath, recordPathCache));
|
|
62
|
+
return eventRow(event, findRecord(plan.docs, targetPath, recordPathCache), plan.subject);
|
|
62
63
|
});
|
|
63
64
|
return {
|
|
64
65
|
name: doc.name,
|
|
@@ -111,7 +112,7 @@ function surfaceRow(entry) {
|
|
|
111
112
|
at: entry.at,
|
|
112
113
|
};
|
|
113
114
|
}
|
|
114
|
-
function eventRow(event, record) {
|
|
115
|
+
function eventRow(event, record, subject) {
|
|
115
116
|
if (record === null) {
|
|
116
117
|
return {
|
|
117
118
|
event,
|
|
@@ -133,12 +134,12 @@ function eventRow(event, record) {
|
|
|
133
134
|
authored_rung: record.authoredRung,
|
|
134
135
|
capped_rung: record.cappedRung,
|
|
135
136
|
final_rung: record.finalRung,
|
|
136
|
-
doc_gate: gateText(record.docGate),
|
|
137
|
-
entry_gate: record.entryGate === null ? null : gateText(record.entryGate),
|
|
137
|
+
doc_gate: gateText(record.docGate, explainRecordGate(record, 'doc', subject)),
|
|
138
|
+
entry_gate: record.entryGate === null ? null : gateText(record.entryGate, explainRecordGate(record, 'entry', subject)),
|
|
138
139
|
matched_entry: entryText(record.matchedEntry),
|
|
139
140
|
winner: record.winner,
|
|
140
141
|
shadowed_by: record.shadowedBy === null ? null : `${record.shadowedBy.scope}:${record.shadowedBy.path}`,
|
|
141
|
-
excluded: delivers(record) ? null : exclusionReason(record),
|
|
142
|
+
excluded: delivers(record) ? null : exclusionReason(record, subject),
|
|
142
143
|
};
|
|
143
144
|
}
|
|
144
145
|
function corpusNote(event) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type MemoryTarget } from '../../../core/memory-resolver.js';
|
|
2
2
|
import { type DeliveryPayload, type DeliveryRecord, type GateOutcome } from '../../../core/substrate/plan.js';
|
|
3
|
+
import { type GateClause } from '../../../core/substrate/gate-explain.js';
|
|
3
4
|
import { type Rung, type SurfaceEntry, type SurfaceEvent } from '../../../core/substrate/schema.js';
|
|
4
5
|
import { type NodeConfigSubject } from '../../../core/substrate/subject-fields.js';
|
|
5
6
|
export declare const resolveLeaf: import("../../../core/command.js").LeafDef;
|
|
@@ -45,11 +46,14 @@ export interface DocRow {
|
|
|
45
46
|
* above the no-delivery floor — boot keeps rung-`none` winners in the plan so
|
|
46
47
|
* the catalog can still count them. */
|
|
47
48
|
export declare function delivers(record: DeliveryRecord): boolean;
|
|
48
|
-
export declare function docRow(record: DeliveryRecord, includeAll: boolean): DocRow;
|
|
49
|
-
|
|
49
|
+
export declare function docRow(record: DeliveryRecord, includeAll: boolean, subject: NodeConfigSubject | null): DocRow;
|
|
50
|
+
/** A gate verdict as one cell: `pass`, or `fail:` plus the clauses that failed
|
|
51
|
+
* — the planner's generic sentence only when the explainer cannot decompose
|
|
52
|
+
* the predicate. */
|
|
53
|
+
export declare function gateText(gate: GateOutcome, clauses: readonly GateClause[]): string;
|
|
50
54
|
export declare function entryText(entry: SurfaceEntry | null): string | null;
|
|
51
55
|
/** Why a considered document delivers nothing, in the order the planner applies
|
|
52
56
|
* its checks — the first failing one is the answer an engineer acts on. */
|
|
53
|
-
export declare function exclusionReason(record: DeliveryRecord): string;
|
|
57
|
+
export declare function exclusionReason(record: DeliveryRecord, subject: NodeConfigSubject | null): string;
|
|
54
58
|
export declare function describeSubject(subject: Record<string, unknown>): string;
|
|
55
59
|
export declare function cell(v: unknown): string;
|
|
@@ -16,6 +16,7 @@ import { envNodeId, envProfileId } from '../../../shared/env.js';
|
|
|
16
16
|
import { realpathOrSelf } from '../../../core/fs-utils.js';
|
|
17
17
|
import { ambientMemoryTarget, resolveMemoryDocForTarget, } from '../../../core/memory-resolver.js';
|
|
18
18
|
import { planDelivery, } from '../../../core/substrate/plan.js';
|
|
19
|
+
import { explainRecordGate, formatClause } from '../../../core/substrate/gate-explain.js';
|
|
19
20
|
import { emptyContextExposureState, exposureTarget } from '../../../core/substrate/injected-store.js';
|
|
20
21
|
import { renderPreferencesForSubject, renderKnowledgeForSubject } from '../../../core/substrate/render.js';
|
|
21
22
|
import { memoryReadDocBlocks, renderOnCommandDocsForSubject, renderOnReadDocsForSubject, renderPreCommandDocsForSubject, renderWorkspaceOpenDocsForSubject, } from '../../../core/substrate/on-read.js';
|
|
@@ -193,7 +194,7 @@ function planSection(snapshot, event, payload, granularity, includeAll) {
|
|
|
193
194
|
const plan = planDelivery(snapshot.subject, snapshot.target, event, payload);
|
|
194
195
|
if (granularity === 'summary')
|
|
195
196
|
return { event, counts: countsOf(plan) };
|
|
196
|
-
const docs = plan.docs.filter((r) => includeAll || delivers(r)).map((r) => docRow(r, includeAll));
|
|
197
|
+
const docs = plan.docs.filter((r) => includeAll || delivers(r)).map((r) => docRow(r, includeAll, plan.subject));
|
|
197
198
|
return { event, docs };
|
|
198
199
|
}
|
|
199
200
|
/** A document delivers when it won its canonical name AND resolved to a rung
|
|
@@ -202,7 +203,7 @@ function planSection(snapshot, event, payload, granularity, includeAll) {
|
|
|
202
203
|
export function delivers(record) {
|
|
203
204
|
return record.winner && rungAtLeast(record.finalRung, 'name');
|
|
204
205
|
}
|
|
205
|
-
export function docRow(record, includeAll) {
|
|
206
|
+
export function docRow(record, includeAll, subject) {
|
|
206
207
|
const row = {
|
|
207
208
|
name: record.name,
|
|
208
209
|
kind: record.kind,
|
|
@@ -214,16 +215,22 @@ export function docRow(record, includeAll) {
|
|
|
214
215
|
final_rung: record.finalRung,
|
|
215
216
|
winner: record.winner,
|
|
216
217
|
shadowed_by: record.shadowedBy === null ? null : `${record.shadowedBy.scope}:${record.shadowedBy.path}`,
|
|
217
|
-
doc_gate: gateText(record.docGate),
|
|
218
|
-
entry_gate: record.entryGate === null ? null : gateText(record.entryGate),
|
|
218
|
+
doc_gate: gateText(record.docGate, explainRecordGate(record, 'doc', subject)),
|
|
219
|
+
entry_gate: record.entryGate === null ? null : gateText(record.entryGate, explainRecordGate(record, 'entry', subject)),
|
|
219
220
|
matched_entry: entryText(record.matchedEntry),
|
|
220
221
|
};
|
|
221
222
|
if (includeAll && !delivers(record))
|
|
222
|
-
row.excluded = exclusionReason(record);
|
|
223
|
+
row.excluded = exclusionReason(record, subject);
|
|
223
224
|
return row;
|
|
224
225
|
}
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
/** A gate verdict as one cell: `pass`, or `fail:` plus the clauses that failed
|
|
227
|
+
* — the planner's generic sentence only when the explainer cannot decompose
|
|
228
|
+
* the predicate. */
|
|
229
|
+
export function gateText(gate, clauses) {
|
|
230
|
+
return gate.pass ? 'pass' : `fail: ${gateReason(gate.reason, clauses)}`;
|
|
231
|
+
}
|
|
232
|
+
function gateReason(fallback, clauses) {
|
|
233
|
+
return clauses.length === 0 ? fallback : clauses.map(formatClause).join('; ');
|
|
227
234
|
}
|
|
228
235
|
export function entryText(entry) {
|
|
229
236
|
if (entry === null)
|
|
@@ -239,16 +246,17 @@ export function entryText(entry) {
|
|
|
239
246
|
}
|
|
240
247
|
/** Why a considered document delivers nothing, in the order the planner applies
|
|
241
248
|
* its checks — the first failing one is the answer an engineer acts on. */
|
|
242
|
-
export function exclusionReason(record) {
|
|
243
|
-
if (!record.docGate.pass)
|
|
244
|
-
return `document gate: ${record.docGate.reason}`;
|
|
249
|
+
export function exclusionReason(record, subject) {
|
|
250
|
+
if (!record.docGate.pass) {
|
|
251
|
+
return `document gate: ${gateReason(record.docGate.reason, explainRecordGate(record, 'doc', subject))}`;
|
|
252
|
+
}
|
|
245
253
|
if (record.shadowedBy !== null)
|
|
246
254
|
return `shadowed by ${record.shadowedBy.scope}:${record.shadowedBy.path}`;
|
|
247
255
|
if (record.authoredEntries.length === 0)
|
|
248
256
|
return 'no surface entry for this event';
|
|
249
257
|
if (record.matchedEntry === null) {
|
|
250
258
|
return record.entryGate !== null && !record.entryGate.pass
|
|
251
|
-
? `entry gate: ${record.entryGate.reason}`
|
|
259
|
+
? `entry gate: ${gateReason(record.entryGate.reason, explainRecordGate(record, 'entry', subject))}`
|
|
252
260
|
: 'no authored entry matched this event\u2019s payload';
|
|
253
261
|
}
|
|
254
262
|
if (record.cappedRung === 'none') {
|
|
@@ -18,7 +18,8 @@ import { PROFILE_PROJECT_MEMORY_VALUES } from '../../../api/dto/profiles.js';
|
|
|
18
18
|
import { cliClient } from '../../api-client.js';
|
|
19
19
|
import { clearDefaultProfile, defaultProfileDirs, getDefaultProfileId, setDefaultProfileId, } from '../../../core/profiles/default-binding.js';
|
|
20
20
|
import { ROOT_PROFILE_ID, addProfileProject, createProfile, listProfiles, pauseProfile, removeProfileProject, renameProfile, resumeProfile, } from '../../../core/profiles/manifest.js';
|
|
21
|
-
import {
|
|
21
|
+
import { tildify } from '../../../core/fs-utils.js';
|
|
22
|
+
import { profileCoversCwd } from '../../../core/profiles/select.js';
|
|
22
23
|
import { padAnsi, theme, visibleRange, wrapText, } from '../../../core/tui/panel.js';
|
|
23
24
|
/** Coarse "used ..." for telling otherwise-alike profiles apart. */
|
|
24
25
|
function relativeUsed(iso) {
|
package/dist/core/fs-utils.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export declare function realpathOrSelf(p: string): string;
|
|
|
3
3
|
/** Expand a leading `~` against `homeDir` (pi's own convention). Other forms
|
|
4
4
|
* — absolute, relative, `~user` — pass through as pi leaves them. */
|
|
5
5
|
export declare function expandTilde(p: string, homeDir?: string): string;
|
|
6
|
+
/** Collapse the home prefix to `~` so a path reads short. The inverse of
|
|
7
|
+
* `expandTilde`, and the ONE collapse every surface prints paths through. */
|
|
8
|
+
export declare function tildify(p: string, homeDir?: string): string;
|
|
6
9
|
export declare function ensureDir(dir: string): void;
|
|
7
10
|
export declare function writeJson(path: string, data: unknown): void;
|
|
8
11
|
export interface AtomicWriteOptions {
|
package/dist/core/fs-utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, renameSync, chmodSync, realpathSync, } from 'node:fs';
|
|
2
|
-
import { dirname, join, relative } from 'node:path';
|
|
2
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
3
3
|
import { homedir, platform } from 'node:os';
|
|
4
4
|
/** `realpathSync`, tolerant of a path that doesn't exist or can't be resolved. */
|
|
5
5
|
export function realpathOrSelf(p) {
|
|
@@ -19,6 +19,15 @@ export function expandTilde(p, homeDir = homedir()) {
|
|
|
19
19
|
return join(homeDir, p.slice(2));
|
|
20
20
|
return p;
|
|
21
21
|
}
|
|
22
|
+
/** Collapse the home prefix to `~` so a path reads short. The inverse of
|
|
23
|
+
* `expandTilde`, and the ONE collapse every surface prints paths through. */
|
|
24
|
+
export function tildify(p, homeDir = homedir()) {
|
|
25
|
+
if (p === homeDir)
|
|
26
|
+
return '~';
|
|
27
|
+
if (p.startsWith(homeDir + sep))
|
|
28
|
+
return '~' + p.slice(homeDir.length);
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
22
31
|
export function ensureDir(dir) {
|
|
23
32
|
mkdirSync(dir, { recursive: true });
|
|
24
33
|
}
|
|
@@ -11,9 +11,6 @@ export declare function resolveProfileSearchAction(bindings: BindingResolution<B
|
|
|
11
11
|
export declare function profileCoversCwd(entry: ProfileEntry, cwd: string): boolean;
|
|
12
12
|
/** Resolve the headless selector against existing state without changing it. */
|
|
13
13
|
export declare function selectProfileForCwdReadOnly(cwd: string): string;
|
|
14
|
-
/** Collapse the home prefix to `~` so project paths read short. Exported so the
|
|
15
|
-
* Profiles settings panel prints a project dir exactly as this menu does. */
|
|
16
|
-
export declare function tildify(p: string): string;
|
|
17
14
|
/** Select the profile a node about to boot at `cwd` should run under.
|
|
18
15
|
*
|
|
19
16
|
* 1. `explicitProfile` present → resolve it as a user-typed operand (exact id,
|