@north-light/crouter 0.3.244 → 0.3.246
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 +3 -1
- package/dist/commands/sys/context/admin/detail-panel.js +4 -1
- 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 +4 -0
- package/dist/commands/sys/context/admin/read-view.js +41 -4
- package/dist/commands/sys/context/admin/shell.d.ts +14 -4
- package/dist/commands/sys/context/admin/shell.js +97 -50
- 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 +424 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SurfaceEvent } from '../../../../core/substrate/schema.js';
|
|
2
2
|
import type { DeliveryRecord } from '../../../../core/substrate/plan.js';
|
|
3
|
+
import type { NodeConfigSubject } from '../../../../core/substrate/subject-fields.js';
|
|
3
4
|
import { type CapOwner } from './read-view.js';
|
|
4
5
|
import type { DocRow } from './docs-panel.js';
|
|
5
6
|
export type { CapOwner } from './read-view.js';
|
|
@@ -33,6 +34,7 @@ export declare class ContextDetailPanel {
|
|
|
33
34
|
private event;
|
|
34
35
|
private capOwner;
|
|
35
36
|
private readOnly;
|
|
37
|
+
private subject;
|
|
36
38
|
private rows;
|
|
37
39
|
private index;
|
|
38
40
|
private face;
|
|
@@ -42,7 +44,7 @@ export declare class ContextDetailPanel {
|
|
|
42
44
|
* measure against real content rather than a counter nobody can see. */
|
|
43
45
|
private lastLines;
|
|
44
46
|
private lastHeight;
|
|
45
|
-
setContext(row: DocRow | undefined, event: SurfaceEvent, capOwner: CapOwner | null, readOnly: string | null): void;
|
|
47
|
+
setContext(row: DocRow | undefined, event: SurfaceEvent, capOwner: CapOwner | null, readOnly: string | null, subject: NodeConfigSubject | null): void;
|
|
46
48
|
/** True while the field list owns the pane, so the shell routes the write
|
|
47
49
|
* letters and teaches them in the footer. */
|
|
48
50
|
get editing(): boolean;
|
|
@@ -14,6 +14,7 @@ export class ContextDetailPanel {
|
|
|
14
14
|
event = 'boot';
|
|
15
15
|
capOwner = null;
|
|
16
16
|
readOnly = null;
|
|
17
|
+
subject = null;
|
|
17
18
|
rows = [];
|
|
18
19
|
index = 0;
|
|
19
20
|
face = 'read';
|
|
@@ -23,12 +24,13 @@ export class ContextDetailPanel {
|
|
|
23
24
|
* measure against real content rather than a counter nobody can see. */
|
|
24
25
|
lastLines = 1;
|
|
25
26
|
lastHeight = 1;
|
|
26
|
-
setContext(row, event, capOwner, readOnly) {
|
|
27
|
+
setContext(row, event, capOwner, readOnly, subject) {
|
|
27
28
|
const changed = row?.key !== this.row?.key;
|
|
28
29
|
this.row = row;
|
|
29
30
|
this.event = event;
|
|
30
31
|
this.capOwner = capOwner;
|
|
31
32
|
this.readOnly = readOnly;
|
|
33
|
+
this.subject = subject;
|
|
32
34
|
this.rows = row === undefined ? [] : buildDetailRows(row.anchor);
|
|
33
35
|
if (changed) {
|
|
34
36
|
this.index = 0;
|
|
@@ -124,6 +126,7 @@ export class ContextDetailPanel {
|
|
|
124
126
|
records: this.row?.records ?? new Map(),
|
|
125
127
|
capOwner: this.capOwner,
|
|
126
128
|
readOnly: this.readOnly !== null,
|
|
129
|
+
subject: this.subject,
|
|
127
130
|
};
|
|
128
131
|
}
|
|
129
132
|
// rendering
|
|
@@ -38,25 +38,22 @@ export declare class ContextDocsPanel {
|
|
|
38
38
|
* rollups, which partition the corpus, so a fully collapsed list still
|
|
39
39
|
* reports its size and a nested rollup is never counted twice. */
|
|
40
40
|
get visibleCount(): number;
|
|
41
|
-
|
|
41
|
+
/** Documents that actually load on the dialled event, counted the same way —
|
|
42
|
+
* the numerator of the heading's `N of M`. */
|
|
43
|
+
get loadedCount(): number;
|
|
44
|
+
setRows(rows: DocRow[], event: SurfaceEvent): void;
|
|
42
45
|
setFilters(filters: Filters): void;
|
|
43
46
|
move(delta: number): void;
|
|
44
47
|
/** True when the focused line can be opened — a directory header. */
|
|
45
48
|
get onGroup(): boolean;
|
|
46
|
-
/** True when the focused line IS a document rather than merely standing for
|
|
47
|
-
* one. A directory that is also a document is both, which is why the two
|
|
48
|
-
* gestures split there: space opens it, enter edits it. */
|
|
49
|
-
get onDocument(): boolean;
|
|
50
49
|
toggleGroup(): void;
|
|
51
|
-
/** Rebuild the drawn lines, keeping the cursor on
|
|
52
|
-
* same
|
|
53
|
-
*
|
|
54
|
-
* key matches there too. */
|
|
50
|
+
/** Rebuild the drawn lines, keeping the cursor on whatever it holds — the
|
|
51
|
+
* same document, or the same directory header, which a collapsed group draws
|
|
52
|
+
* no document row for — and clamping when what it was on is gone. */
|
|
55
53
|
private relayout;
|
|
56
|
-
render(width: number, height: number, focused: boolean
|
|
57
|
-
/** A directory header
|
|
58
|
-
*
|
|
59
|
-
* the one row as both a document and a directory. */
|
|
54
|
+
render(width: number, height: number, focused: boolean): string[];
|
|
55
|
+
/** A directory header: a pure rollup of its whole subtree — how many of its
|
|
56
|
+
* documents load, and what they cost together. */
|
|
60
57
|
private groupLine;
|
|
61
58
|
private docLine;
|
|
62
59
|
/** Pad rather than trim: this column sets the length of the divider drawn
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// The middle column: every document the snapshot can see, grouped recursively
|
|
2
|
-
// by the directories its canonical name names,
|
|
3
|
-
//
|
|
4
|
-
// to the same canonical name stay two rows
|
|
2
|
+
// by the directories its canonical name names, each stating in one word what it
|
|
3
|
+
// does on the dialled event and what that costs. One row is one physical
|
|
4
|
+
// document, so two stores answering to the same canonical name stay two rows
|
|
5
|
+
// and the shadowed one is visible.
|
|
5
6
|
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
6
7
|
import { padAnsi, theme, visibleRange } from '../../../../core/tui/panel.js';
|
|
7
8
|
import { SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
|
|
8
|
-
import {
|
|
9
|
+
import { deliveryStateOf, formatCost, plural, stateLabel } from './model.js';
|
|
9
10
|
import { NO_FILTERS, buildListView, groupKeysOf } from './list-view.js';
|
|
10
11
|
/** The union of every planned event's corpus, ordered by the focused event's
|
|
11
12
|
* precedence order with the documents only other events see appended. */
|
|
@@ -43,13 +44,10 @@ export function buildRows(plans, event) {
|
|
|
43
44
|
absorb(other);
|
|
44
45
|
return order.map((key) => rows.get(key));
|
|
45
46
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
const
|
|
47
|
+
/** The word a row's state prints in, wide enough for `shadowed` and for the
|
|
48
|
+
* `N of M` a group rolls its subtree up to. */
|
|
49
|
+
const STATE_WIDTH = 10;
|
|
49
50
|
const COST_WIDTH = 6;
|
|
50
|
-
/** The glyphs a group header counts its members with — the matrix vocabulary,
|
|
51
|
-
* so a rung reads the same in both columns. */
|
|
52
|
-
const RUNG_MARK = { content: '█', preview: '▄', name: '▁' };
|
|
53
51
|
/** Columns one nesting level spends, so a doc sits under its directory's label
|
|
54
52
|
* and the truncator sees the indent as part of the name it is eliding. */
|
|
55
53
|
const INDENT = 2;
|
|
@@ -81,18 +79,24 @@ export class ContextDocsPanel {
|
|
|
81
79
|
get visibleCount() {
|
|
82
80
|
return this.lines.reduce((count, line) => count + (line.kind === 'group' && line.depth === 0 ? line.docs : 0), 0);
|
|
83
81
|
}
|
|
84
|
-
|
|
82
|
+
/** Documents that actually load on the dialled event, counted the same way —
|
|
83
|
+
* the numerator of the heading's `N of M`. */
|
|
84
|
+
get loadedCount() {
|
|
85
|
+
return this.lines.reduce((count, line) => count +
|
|
86
|
+
(line.kind === 'group' && line.depth === 0 ? line.rungs.content + line.rungs.preview + line.rungs.name : 0), 0);
|
|
87
|
+
}
|
|
88
|
+
setRows(rows, event) {
|
|
85
89
|
this.rows = rows;
|
|
86
90
|
this.event = event;
|
|
87
91
|
const live = groupKeysOf(rows);
|
|
88
92
|
for (const key of this.expanded)
|
|
89
93
|
if (!live.has(key))
|
|
90
94
|
this.expanded.delete(key);
|
|
91
|
-
this.relayout(
|
|
95
|
+
this.relayout();
|
|
92
96
|
}
|
|
93
97
|
setFilters(filters) {
|
|
94
98
|
this.filters = filters;
|
|
95
|
-
this.relayout(
|
|
99
|
+
this.relayout();
|
|
96
100
|
}
|
|
97
101
|
move(delta) {
|
|
98
102
|
if (this.lines.length === 0)
|
|
@@ -104,13 +108,6 @@ export class ContextDocsPanel {
|
|
|
104
108
|
get onGroup() {
|
|
105
109
|
return this.lines[this.index]?.kind === 'group';
|
|
106
110
|
}
|
|
107
|
-
/** True when the focused line IS a document rather than merely standing for
|
|
108
|
-
* one. A directory that is also a document is both, which is why the two
|
|
109
|
-
* gestures split there: space opens it, enter edits it. */
|
|
110
|
-
get onDocument() {
|
|
111
|
-
const line = this.lines[this.index];
|
|
112
|
-
return line !== undefined && (line.kind === 'doc' || line.isDoc);
|
|
113
|
-
}
|
|
114
111
|
toggleGroup() {
|
|
115
112
|
const line = this.lines[this.index];
|
|
116
113
|
if (line?.kind !== 'group')
|
|
@@ -119,32 +116,28 @@ export class ContextDocsPanel {
|
|
|
119
116
|
this.expanded.delete(line.key);
|
|
120
117
|
else
|
|
121
118
|
this.expanded.add(line.key);
|
|
122
|
-
this.relayout(
|
|
119
|
+
this.relayout();
|
|
123
120
|
this.onChange();
|
|
124
121
|
}
|
|
125
|
-
/** Rebuild the drawn lines, keeping the cursor on
|
|
126
|
-
* same
|
|
127
|
-
*
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const held = selectedKey ?? (this.lines[this.index]?.kind === 'doc' ? this.lines[this.index].key : undefined);
|
|
122
|
+
/** Rebuild the drawn lines, keeping the cursor on whatever it holds — the
|
|
123
|
+
* same document, or the same directory header, which a collapsed group draws
|
|
124
|
+
* no document row for — and clamping when what it was on is gone. */
|
|
125
|
+
relayout() {
|
|
126
|
+
const held = this.lines[this.index];
|
|
131
127
|
this.lines = buildListView(this.rows, this.filters, this.expanded, this.event);
|
|
132
|
-
const wanted =
|
|
133
|
-
? this.lines.findIndex((line) => line.kind === 'group' && line.key === groupKey)
|
|
134
|
-
: held === undefined
|
|
135
|
-
? -1
|
|
136
|
-
: this.lines.findIndex((line) => (line.kind === 'doc' && line.key === held) || (line.kind === 'group' && line.isDoc && line.first.key === held));
|
|
128
|
+
const wanted = held === undefined ? -1 : this.lines.findIndex((line) => line.kind === held.kind && line.key === held.key);
|
|
137
129
|
this.index = wanted >= 0 ? wanted : Math.max(0, Math.min(this.index, this.lines.length - 1));
|
|
138
130
|
}
|
|
139
|
-
render(width, height, focused
|
|
131
|
+
render(width, height, focused) {
|
|
140
132
|
const out = [];
|
|
141
|
-
const nameRoom = Math.max(1, width -
|
|
133
|
+
const nameRoom = Math.max(1, width - STATE_WIDTH - COST_WIDTH - 3);
|
|
142
134
|
const heading = this.filters.query === '' ? 'DOCUMENTS' : `/${this.filters.query}`;
|
|
135
|
+
// The question the page is asking, in the terms the rows answer it in.
|
|
143
136
|
const count = this.filters.query === ''
|
|
144
|
-
?
|
|
137
|
+
? `${this.loadedCount} of ${this.visibleCount} load on ${this.event}`
|
|
145
138
|
: plural(this.visibleCount, 'match', 'matches');
|
|
146
|
-
const
|
|
147
|
-
out.push(
|
|
139
|
+
const title = `${theme.fg('accent', theme.bold(heading))} ${theme.fg('dim', count)}`;
|
|
140
|
+
out.push(` ${padAnsi(truncateToWidth(title, nameRoom), nameRoom)} ${field(theme.fg('dim', 'loads'), STATE_WIDTH)} ${field(theme.fg('dim', 'tokens'), COST_WIDTH)}`);
|
|
148
141
|
if (this.lines.length === 0) {
|
|
149
142
|
out.push('', theme.fg('dim', ' Nothing matches this question — cycle a facet back to "any".'));
|
|
150
143
|
return this.fit(out, height);
|
|
@@ -153,7 +146,7 @@ export class ContextDocsPanel {
|
|
|
153
146
|
const range = visibleRange(this.lines.length, this.index, capacity);
|
|
154
147
|
for (let at = range.start; at < range.end; at += 1) {
|
|
155
148
|
const line = this.lines[at];
|
|
156
|
-
const body = line.kind === 'group' ? this.groupLine(line, nameRoom
|
|
149
|
+
const body = line.kind === 'group' ? this.groupLine(line, nameRoom) : this.docLine(line, nameRoom);
|
|
157
150
|
out.push(at === this.index && focused
|
|
158
151
|
? theme.bg('selectedBg', padAnsi(body, width))
|
|
159
152
|
: at === this.index
|
|
@@ -162,25 +155,21 @@ export class ContextDocsPanel {
|
|
|
162
155
|
}
|
|
163
156
|
return this.fit(out, height);
|
|
164
157
|
}
|
|
165
|
-
/** A directory header
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
groupLine(line, nameRoom, plans) {
|
|
158
|
+
/** A directory header: a pure rollup of its whole subtree — how many of its
|
|
159
|
+
* documents load, and what they cost together. */
|
|
160
|
+
groupLine(line, nameRoom) {
|
|
169
161
|
const marker = line.expanded ? '▾' : '▸';
|
|
170
162
|
const label = `${' '.repeat(line.depth * INDENT)}${marker} ${theme.bold(line.label)}`;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.join(' ');
|
|
175
|
-
const stats = theme.fg('dim', marks === '' ? plural(line.docs, 'doc', 'docs') : `${line.docs} · ${marks}`);
|
|
176
|
-
const room = Math.max(1, nameRoom - visibleWidth(stats) - 1);
|
|
177
|
-
const field = `${padAnsi(truncateToWidth(label, room), room)} ${stats}`;
|
|
178
|
-
const matrix = line.isDoc ? matrixOf(line.first, plans) : ' '.repeat(MATRIX_WIDTH);
|
|
179
|
-
return ` ${padAnsi(field, nameRoom)} ${cost(line.cost)} ${matrix}`;
|
|
163
|
+
const loads = line.rungs.content + line.rungs.preview + line.rungs.name;
|
|
164
|
+
const rollup = theme.fg(loads > 0 ? 'success' : 'dim', `${loads} of ${line.docs}`);
|
|
165
|
+
return ` ${padAnsi(truncateToWidth(label, nameRoom), nameRoom)} ${field(rollup, STATE_WIDTH)} ${cost(line.cost)}`;
|
|
180
166
|
}
|
|
181
|
-
docLine(line, nameRoom
|
|
167
|
+
docLine(line, nameRoom) {
|
|
182
168
|
const label = truncateToWidth(`${' '.repeat(line.depth * INDENT)}${line.label}`, nameRoom);
|
|
183
|
-
|
|
169
|
+
const record = line.row.records.get(this.event);
|
|
170
|
+
const state = stateLabel(record, deliveryStateOf(record));
|
|
171
|
+
const word = state.text === '' ? '' : theme.fg(state.tone, state.text);
|
|
172
|
+
return ` ${padAnsi(label, nameRoom)} ${field(word, STATE_WIDTH)} ${cost(line.cost)}`;
|
|
184
173
|
}
|
|
185
174
|
/** Pad rather than trim: this column sets the length of the divider drawn
|
|
186
175
|
* beside it. */
|
|
@@ -190,15 +179,13 @@ export class ContextDocsPanel {
|
|
|
190
179
|
return lines.slice(0, height).map((line) => (visibleWidth(line) === 0 ? '' : line));
|
|
191
180
|
}
|
|
192
181
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}).join(' ');
|
|
182
|
+
/** Right-flushed into a fixed column, so the state and cost fields read as one
|
|
183
|
+
* block against the pane's right edge. */
|
|
184
|
+
function field(text, width) {
|
|
185
|
+
const used = visibleWidth(text);
|
|
186
|
+
return used >= width ? truncateToWidth(text, width) : `${' '.repeat(width - used)}${text}`;
|
|
199
187
|
}
|
|
200
188
|
/** Right-flushed into the cost column, dim because it is an estimate. */
|
|
201
189
|
function cost(count) {
|
|
202
|
-
|
|
203
|
-
return theme.fg('dim', ' '.repeat(Math.max(0, COST_WIDTH - text.length)) + text);
|
|
190
|
+
return field(theme.fg('dim', formatCost(count)), COST_WIDTH);
|
|
204
191
|
}
|
|
@@ -32,11 +32,9 @@ export type ListLine = {
|
|
|
32
32
|
expanded: boolean;
|
|
33
33
|
/** The document the dossier describes while the cursor rests on the
|
|
34
34
|
* header: this directory's own document when it has one, else the first
|
|
35
|
-
* document in its subtree.
|
|
35
|
+
* document in its subtree. A header never stands in for that document's
|
|
36
|
+
* own row — it only keeps the dossier from going blank. */
|
|
36
37
|
first: DocRow;
|
|
37
|
-
/** True when the directory is itself a document — `first` is that
|
|
38
|
-
* document, and the one row is both selectable and expandable. */
|
|
39
|
-
isDoc: boolean;
|
|
40
38
|
} | {
|
|
41
39
|
kind: 'doc';
|
|
42
40
|
key: string;
|
|
@@ -75,7 +75,6 @@ export function buildListView(rows, filters, expanded, event) {
|
|
|
75
75
|
...rollup(leading, event),
|
|
76
76
|
expanded: open,
|
|
77
77
|
first: leading[0],
|
|
78
|
-
isDoc: false,
|
|
79
78
|
});
|
|
80
79
|
if (open)
|
|
81
80
|
for (const row of leading)
|
|
@@ -110,14 +109,14 @@ function emit(dir, filters, expanded, event, lines) {
|
|
|
110
109
|
...rollup(members, event),
|
|
111
110
|
expanded: open,
|
|
112
111
|
first: members[0],
|
|
113
|
-
isDoc: dir.selves.length > 0,
|
|
114
112
|
});
|
|
115
113
|
if (!open)
|
|
116
114
|
return;
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
// A directory that is also a document is a header plus a row, never one row
|
|
116
|
+
// doing both: the header is a pure rollup that expands, and the document sits
|
|
117
|
+
// inside it under its own name — no trailing slash — like any other member.
|
|
118
|
+
for (const self of dir.selves)
|
|
119
|
+
lines.push(docLine(self, dir.segment, dir.depth + 1, event));
|
|
121
120
|
for (const child of dir.children.values())
|
|
122
121
|
emit(child, filters, expanded, event, lines);
|
|
123
122
|
}
|
|
@@ -46,24 +46,28 @@ export declare class PlanSet {
|
|
|
46
46
|
dispose(): void;
|
|
47
47
|
/** The plan for one event, computing it now. */
|
|
48
48
|
plan(event: SurfaceEvent): DeliveryPlan;
|
|
49
|
-
/** The plan for one event only if it is already computed — what the
|
|
50
|
-
*
|
|
49
|
+
/** The plan for one event only if it is already computed — what the rail's
|
|
50
|
+
* EVENTS counts read, so an unwarmed event draws `…` instead of stalling the
|
|
51
51
|
* frame on five extra corpus loads. */
|
|
52
52
|
peek(event: SurfaceEvent): DeliveryPlan | undefined;
|
|
53
53
|
/** Compute one pending event per macrotask, calling back after each so the
|
|
54
|
-
*
|
|
54
|
+
* rail's counts fill in row by row. */
|
|
55
55
|
warm(onProgress: () => void): void;
|
|
56
56
|
private payload;
|
|
57
57
|
}
|
|
58
|
-
/**
|
|
59
|
-
export type
|
|
60
|
-
export declare function
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
/** What one document does in one event. */
|
|
59
|
+
export type DeliveryState = 'delivers' | 'capped' | 'gated' | 'shadowed' | 'silent';
|
|
60
|
+
export declare function deliveryStateOf(record: DeliveryRecord | undefined): DeliveryState;
|
|
61
|
+
/** The word the list prints for one document's state, and the tone it prints it
|
|
62
|
+
* in. The vocabulary is the one the author already writes in frontmatter
|
|
63
|
+
* (`at: content`), so the column needs no legend: a delivery names the rung it
|
|
64
|
+
* arrives at, warning-toned when the profile ceiling trimmed it below what the
|
|
65
|
+
* entry authored. A document that does not load renders nothing at all unless
|
|
66
|
+
* its silence has a name the page can act on — `gated` or `shadowed`. */
|
|
67
|
+
export declare function stateLabel(record: DeliveryRecord | undefined, state: DeliveryState): {
|
|
68
|
+
text: string;
|
|
69
|
+
tone: 'success' | 'warning' | 'muted';
|
|
70
|
+
};
|
|
67
71
|
export declare function nextRung(rung: Rung, delta: number): Rung;
|
|
68
72
|
/** A parsed entry back in the shape `memory edit --surface` accepts. The parsed
|
|
69
73
|
* form always carries `match` as a list and spells its frontmatter key
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// The read model behind `crtr sys context admin`: the configuration snapshot
|
|
2
2
|
// the page dials, the per-event delivery plans it resolves from, and the
|
|
3
|
-
// vocabulary both the
|
|
3
|
+
// vocabulary both the list and the dossier read from.
|
|
4
4
|
import { resolve as resolvePath } from 'node:path';
|
|
5
|
-
import { theme } from '../../../../core/tui/panel.js';
|
|
6
5
|
import { realpathOrSelf } from '../../../../core/fs-utils.js';
|
|
7
6
|
import { planDelivery, } from '../../../../core/substrate/plan.js';
|
|
8
7
|
import { RUNGS, SURFACE_EVENTS, previewLine, rungRank, } from '../../../../core/substrate/schema.js';
|
|
@@ -76,14 +75,14 @@ export class PlanSet {
|
|
|
76
75
|
this.plans.set(event, computed);
|
|
77
76
|
return computed;
|
|
78
77
|
}
|
|
79
|
-
/** The plan for one event only if it is already computed — what the
|
|
80
|
-
*
|
|
78
|
+
/** The plan for one event only if it is already computed — what the rail's
|
|
79
|
+
* EVENTS counts read, so an unwarmed event draws `…` instead of stalling the
|
|
81
80
|
* frame on five extra corpus loads. */
|
|
82
81
|
peek(event) {
|
|
83
82
|
return this.plans.get(event);
|
|
84
83
|
}
|
|
85
84
|
/** Compute one pending event per macrotask, calling back after each so the
|
|
86
|
-
*
|
|
85
|
+
* rail's counts fill in row by row. */
|
|
87
86
|
warm(onProgress) {
|
|
88
87
|
if (this.warming !== undefined)
|
|
89
88
|
return;
|
|
@@ -130,7 +129,7 @@ function resolvedDocRealpath(name, target) {
|
|
|
130
129
|
return '';
|
|
131
130
|
}
|
|
132
131
|
}
|
|
133
|
-
export function
|
|
132
|
+
export function deliveryStateOf(record) {
|
|
134
133
|
if (record === undefined)
|
|
135
134
|
return 'silent';
|
|
136
135
|
if (!record.docGate.pass)
|
|
@@ -146,33 +145,21 @@ export function cellStateOf(record) {
|
|
|
146
145
|
return 'gated';
|
|
147
146
|
return 'silent';
|
|
148
147
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
/** The word the list prints for one document's state, and the tone it prints it
|
|
149
|
+
* in. The vocabulary is the one the author already writes in frontmatter
|
|
150
|
+
* (`at: content`), so the column needs no legend: a delivery names the rung it
|
|
151
|
+
* arrives at, warning-toned when the profile ceiling trimmed it below what the
|
|
152
|
+
* entry authored. A document that does not load renders nothing at all unless
|
|
153
|
+
* its silence has a name the page can act on — `gated` or `shadowed`. */
|
|
154
|
+
export function stateLabel(record, state) {
|
|
156
155
|
if (state === 'gated')
|
|
157
|
-
return
|
|
156
|
+
return { text: 'gated', tone: 'muted' };
|
|
158
157
|
if (state === 'shadowed')
|
|
159
|
-
return
|
|
160
|
-
if (record === undefined ||
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
if (state === 'capped')
|
|
164
|
-
return theme.fg('warning', record.finalRung === 'none' ? '⌐' : glyph);
|
|
165
|
-
return theme.fg('success', glyph);
|
|
158
|
+
return { text: 'shadowed', tone: 'muted' };
|
|
159
|
+
if (record === undefined || record.finalRung === 'none')
|
|
160
|
+
return { text: '', tone: 'muted' };
|
|
161
|
+
return { text: record.finalRung, tone: state === 'capped' ? 'warning' : 'success' };
|
|
166
162
|
}
|
|
167
|
-
/** The one-letter column heads over the matrix, in `SURFACE_EVENTS` order. */
|
|
168
|
-
export const EVENT_INITIALS = {
|
|
169
|
-
boot: 'b',
|
|
170
|
-
'workspace-open': 'w',
|
|
171
|
-
read: 'r',
|
|
172
|
-
'memory-read': 'm',
|
|
173
|
-
command: 'c',
|
|
174
|
-
'pre-command': 'p',
|
|
175
|
-
};
|
|
176
163
|
export function nextRung(rung, delta) {
|
|
177
164
|
const at = (rungRank(rung) + delta + RUNGS.length) % RUNGS.length;
|
|
178
165
|
return RUNGS[at];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SurfaceEvent } from '../../../../core/substrate/schema.js';
|
|
1
2
|
import { type Snapshot } from './model.js';
|
|
2
3
|
import type { Filters } from './list-view.js';
|
|
3
4
|
export type AxisId = 'event' | 'payload' | 'kind' | 'mode' | 'lifecycle' | 'manager' | 'depth' | 'profile' | 'cwd';
|
|
@@ -20,9 +21,14 @@ export interface Facet {
|
|
|
20
21
|
* snapshot above it dials the NODE's kind. */
|
|
21
22
|
export declare function facetsOf(filters: Filters): Facet[];
|
|
22
23
|
export declare function cycleFacet(filters: Filters, id: FacetId, delta: number): Filters;
|
|
23
|
-
/** The
|
|
24
|
-
*
|
|
24
|
+
/** The configuration rows for a snapshot. The event and its payload are not
|
|
25
|
+
* here — they belong to the EVENTS section, where the payload sits under the
|
|
26
|
+
* count it explains. */
|
|
25
27
|
export declare function axesOf(snapshot: Snapshot, profileName: string): Axis[];
|
|
28
|
+
/** The payload field for the dialled event, or nothing when the event reads no
|
|
29
|
+
* payload — on boot there is nothing it could change. It renders inside the
|
|
30
|
+
* EVENTS section, directly under the count it explains. */
|
|
31
|
+
export declare function payloadRowOf(snapshot: Snapshot): Axis | undefined;
|
|
26
32
|
/** Cycle one enum axis, returning the changed snapshot. A typed axis is never
|
|
27
33
|
* cycled — the shell opens a field for it instead. */
|
|
28
34
|
export declare function cycleAxis(snapshot: Snapshot, id: AxisId, delta: number): Snapshot;
|
|
@@ -30,12 +36,42 @@ export declare function cycleAxis(snapshot: Snapshot, id: AxisId, delta: number)
|
|
|
30
36
|
export declare function setTypedAxis(snapshot: Snapshot, id: AxisId, value: string): Snapshot;
|
|
31
37
|
/** The value a typed axis's field is seeded with. */
|
|
32
38
|
export declare function typedAxisValue(snapshot: Snapshot, id: AxisId): string;
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
39
|
+
/** One surface event as the rail states it: how many documents it delivers, and
|
|
40
|
+
* whether it is the one the page is planning. `delivers` is undefined while
|
|
41
|
+
* that event's plan is still warming. */
|
|
42
|
+
export interface EventRow {
|
|
43
|
+
event: SurfaceEvent;
|
|
44
|
+
delivers: number | undefined;
|
|
45
|
+
dialled: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** Every selectable rail row in drawn order: the events, the dialled event's
|
|
48
|
+
* payload field directly under it, the configuration axes, then the filter
|
|
49
|
+
* facets. One list, so the shell's single index is also the reading order. */
|
|
50
|
+
export type RailRow = {
|
|
51
|
+
kind: 'event';
|
|
52
|
+
event: EventRow;
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'axis';
|
|
55
|
+
axis: Axis;
|
|
56
|
+
indent: boolean;
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'facet';
|
|
59
|
+
facet: Facet;
|
|
60
|
+
};
|
|
61
|
+
/** A rail row's identity, which the shell holds the cursor by. The list's
|
|
62
|
+
* composition changes whenever the dialled event moves the payload row, so an
|
|
63
|
+
* offset kept across that change points at a different row. */
|
|
64
|
+
export declare function railRowKey(row: RailRow): string;
|
|
65
|
+
/** What enter does on a rail row: an event dials it, a typed row opens its
|
|
66
|
+
* field, and every other row hands the keyboard to the documents. Space does
|
|
67
|
+
* the same thing on the first two and cycles the value on the last. */
|
|
68
|
+
export declare function railEnter(row: RailRow | undefined): 'dial' | 'edit' | 'documents';
|
|
69
|
+
export declare function railRowsOf(events: EventRow[], payload: Axis | undefined, axes: Axis[], facets: Facet[]): RailRow[];
|
|
70
|
+
/** All three sections as one column. `index` runs over `rows`, while the
|
|
71
|
+
* section headings and the blank rows between them are drawn but never
|
|
72
|
+
* selectable. */
|
|
36
73
|
export declare function renderRail(opts: {
|
|
37
|
-
|
|
38
|
-
facets: Facet[];
|
|
74
|
+
rows: RailRow[];
|
|
39
75
|
index: number;
|
|
40
76
|
width: number;
|
|
41
77
|
height: number;
|