@north-light/crouter 0.3.240 → 0.3.242
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/api/dto/broker-ops.d.ts +8 -3
- package/dist/api/dto/broker.d.ts +4 -2
- package/dist/api/dto/inbox.d.ts +1 -0
- package/dist/api/dto/nodes.d.ts +4 -0
- package/dist/api/dto/review-comments.d.ts +1 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/clients/attach/session/profile-files.js +4 -1
- package/dist/clients/attach/viewer.js +366 -366
- package/dist/commands/node/create.js +8 -3
- package/dist/commands/node/inspect.js +1 -1
- package/dist/commands/sys/context/admin/detail-panel.d.ts +3 -0
- package/dist/commands/sys/context/admin/detail-panel.js +32 -3
- package/dist/commands/sys/context/admin/docs-panel.d.ts +23 -13
- package/dist/commands/sys/context/admin/docs-panel.js +101 -103
- package/dist/commands/sys/context/admin/list-view.d.ts +53 -0
- package/dist/commands/sys/context/admin/list-view.js +125 -0
- package/dist/commands/sys/context/admin/model.d.ts +11 -0
- package/dist/commands/sys/context/admin/model.js +28 -1
- package/dist/commands/sys/context/admin/rail-panel.d.ts +46 -0
- package/dist/commands/sys/context/admin/{axes-panel.js → rail-panel.js} +64 -21
- package/dist/commands/sys/context/admin/shell.d.ts +19 -2
- package/dist/commands/sys/context/admin/shell.js +142 -46
- package/dist/commands/sys/daemon.js +1 -1
- package/dist/core/__tests__/child-death-wake.test.js +0 -48
- package/dist/core/__tests__/daemon-boot.test.js +0 -8
- package/dist/core/__tests__/dead-node-policy-table.test.js +1 -2
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +14 -2
- package/dist/core/__tests__/lifecycle.test.js +5 -5
- package/dist/core/__tests__/relaunch-root.test.js +91 -1
- package/dist/core/__tests__/revive-capacity.test.js +36 -1
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +73 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +1 -2
- package/dist/core/canvas/browse/model.d.ts +0 -9
- package/dist/core/canvas/browse/model.js +1 -37
- package/dist/core/canvas/browse/render.js +2 -1
- package/dist/core/canvas/canvas.d.ts +6 -5
- package/dist/core/canvas/canvas.js +8 -7
- package/dist/core/canvas/types.d.ts +6 -0
- package/dist/core/review/realize.js +1 -1
- package/dist/core/runtime/broker/node-named.d.ts +1 -0
- package/dist/core/runtime/host.js +114 -103
- package/dist/core/runtime/lifecycle.js +3 -2
- package/dist/core/runtime/naming.d.ts +26 -8
- package/dist/core/runtime/naming.js +75 -43
- package/dist/core/runtime/reset.js +12 -5
- package/dist/core/runtime/revive-all.d.ts +4 -11
- package/dist/core/runtime/revive-all.js +6 -13
- package/dist/core/runtime/tmux-bindings.js +4 -3
- package/dist/core/runtime/warm-pool.js +4 -4
- package/dist/core/tui/fuzzy.d.ts +9 -0
- package/dist/core/tui/fuzzy.js +38 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +1 -0
- package/dist/daemon/api/handlers/broker-ops.js +14 -7
- package/dist/daemon/api/handlers/feedback-comments.js +1 -1
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/crtrd.js +17 -20
- package/dist/daemon/messaging/node-message.d.ts +1 -0
- package/dist/daemon/messaging/node-message.js +6 -2
- package/dist/daemon/reconcilers/live-obligation.d.ts +1 -2
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +0 -5
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +9 -13
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +0 -6
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +0 -2
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +0 -10
- package/dist/daemon/reconcilers/node-lifecycle/tick.d.ts +2 -2
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +4 -2
- package/dist/daemon/reconcilers/storage-maintenance.js +2 -2
- package/dist/daemon/review/comment-notify.d.ts +1 -0
- package/dist/daemon/review/comment-notify.js +1 -0
- package/dist/pi-extensions/broker-local.d.ts +1 -0
- package/dist/pi-extensions/broker-local.js +12 -7
- package/dist/pi-extensions/canvas-recap.js +1 -0
- package/dist/pi-extensions/canvas-stophook.js +2 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/commands/sys/context/admin/axes-panel.d.ts +0 -21
|
@@ -5,7 +5,7 @@ import { resolve as resolvePath } from 'node:path';
|
|
|
5
5
|
import { theme } from '../../../../core/tui/panel.js';
|
|
6
6
|
import { realpathOrSelf } from '../../../../core/fs-utils.js';
|
|
7
7
|
import { planDelivery, } from '../../../../core/substrate/plan.js';
|
|
8
|
-
import { RUNGS, SURFACE_EVENTS, rungRank, } from '../../../../core/substrate/schema.js';
|
|
8
|
+
import { RUNGS, SURFACE_EVENTS, previewLine, rungRank, } from '../../../../core/substrate/schema.js';
|
|
9
9
|
import { scopeForCwd, profileNameFor } from '../../../../core/substrate/subject-fields.js';
|
|
10
10
|
import { resolveMemoryDocForTarget } from '../../../../core/memory-resolver.js';
|
|
11
11
|
export function subjectOf(snapshot) {
|
|
@@ -207,3 +207,30 @@ export function describeSurfaceEntry(entry) {
|
|
|
207
207
|
export function plural(count, one, many) {
|
|
208
208
|
return `${count} ${count === 1 ? one : many}`;
|
|
209
209
|
}
|
|
210
|
+
export function rungCostsOf(record) {
|
|
211
|
+
const entry = record.name.length;
|
|
212
|
+
const routing = previewLine(record.doc).length;
|
|
213
|
+
return {
|
|
214
|
+
none: 0,
|
|
215
|
+
name: tokens(entry),
|
|
216
|
+
preview: tokens(entry + routing),
|
|
217
|
+
content: tokens(record.doc.body.length),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** What this document actually spends in the event the record belongs to. */
|
|
221
|
+
export function deliveredCost(record) {
|
|
222
|
+
return record === undefined ? 0 : rungCostsOf(record)[record.finalRung];
|
|
223
|
+
}
|
|
224
|
+
function tokens(characters) {
|
|
225
|
+
return Math.ceil(characters / 4);
|
|
226
|
+
}
|
|
227
|
+
/** Approximate tokens in at most six columns, always marked as an estimate. */
|
|
228
|
+
export function formatCost(count) {
|
|
229
|
+
if (count <= 0)
|
|
230
|
+
return '';
|
|
231
|
+
if (count < 1000)
|
|
232
|
+
return `~${count}`;
|
|
233
|
+
if (count < 10_000)
|
|
234
|
+
return `~${(count / 1000).toFixed(1)}k`;
|
|
235
|
+
return `~${Math.round(count / 1000)}k`;
|
|
236
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Snapshot } from './model.js';
|
|
2
|
+
import type { Filters } from './list-view.js';
|
|
3
|
+
export type AxisId = 'event' | 'payload' | 'kind' | 'mode' | 'lifecycle' | 'manager' | 'depth' | 'profile' | 'cwd';
|
|
4
|
+
export interface Axis {
|
|
5
|
+
id: AxisId;
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
/** Free-text axes open a field; the rest cycle in place. */
|
|
9
|
+
typed: boolean;
|
|
10
|
+
}
|
|
11
|
+
export type FacetId = 'rung' | 'origin' | 'kind' | 'state' | 'query';
|
|
12
|
+
export interface Facet {
|
|
13
|
+
id: FacetId;
|
|
14
|
+
label: string;
|
|
15
|
+
value: string;
|
|
16
|
+
/** The query is typed; every other facet cycles through a closed set. */
|
|
17
|
+
typed: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** The FILTERS rows for one filter value. `doc kind` is spelled out because the
|
|
20
|
+
* snapshot above it dials the NODE's kind. */
|
|
21
|
+
export declare function facetsOf(filters: Filters): Facet[];
|
|
22
|
+
export declare function cycleFacet(filters: Filters, id: FacetId, delta: number): Filters;
|
|
23
|
+
/** The rail's rows for a snapshot. The payload row appears only for the events
|
|
24
|
+
* that read one, because on boot there is nothing it could change. */
|
|
25
|
+
export declare function axesOf(snapshot: Snapshot, profileName: string): Axis[];
|
|
26
|
+
/** Cycle one enum axis, returning the changed snapshot. A typed axis is never
|
|
27
|
+
* cycled — the shell opens a field for it instead. */
|
|
28
|
+
export declare function cycleAxis(snapshot: Snapshot, id: AxisId, delta: number): Snapshot;
|
|
29
|
+
/** Apply a typed axis's submitted value. */
|
|
30
|
+
export declare function setTypedAxis(snapshot: Snapshot, id: AxisId, value: string): Snapshot;
|
|
31
|
+
/** The value a typed axis's field is seeded with. */
|
|
32
|
+
export declare function typedAxisValue(snapshot: Snapshot, id: AxisId): string;
|
|
33
|
+
/** Both sections as one column. `index` runs over the axes and then the facets
|
|
34
|
+
* — the order the shell routes a cycle in — while the section headings and the
|
|
35
|
+
* blank rows between them are drawn but never selectable. */
|
|
36
|
+
export declare function renderRail(opts: {
|
|
37
|
+
axes: Axis[];
|
|
38
|
+
facets: Facet[];
|
|
39
|
+
index: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
focused: boolean;
|
|
43
|
+
/** Documents the filters select, out of the whole corpus. */
|
|
44
|
+
matched: number;
|
|
45
|
+
total: number;
|
|
46
|
+
}): string[];
|
|
@@ -1,11 +1,45 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The rail: two sections in one row grammar. SNAPSHOT holds the configuration
|
|
2
|
+
// axes the page plans against — changing one re-plans every event. FILTERS holds
|
|
3
|
+
// the question asked of that plan — changing one only changes what is shown.
|
|
4
|
+
// An enum cycles in place; a free-text row opens a field.
|
|
4
5
|
import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
5
|
-
import { padAnsi, theme
|
|
6
|
-
import { SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
|
|
6
|
+
import { padAnsi, theme } from '../../../../core/tui/panel.js';
|
|
7
|
+
import { KINDS, RUNGS, SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
|
|
7
8
|
import { listProfiles } from '../../../../core/profiles/manifest.js';
|
|
8
9
|
import { payloadAxisFor } from './model.js';
|
|
10
|
+
const RUNG_FACETS = ['any', ...RUNGS.filter((rung) => rung !== 'none'), 'silent'];
|
|
11
|
+
const ORIGIN_FACETS = ['any', 'project', 'user', 'profile', 'builtin', 'node'];
|
|
12
|
+
const KIND_FACETS = ['any', ...KINDS];
|
|
13
|
+
const STATE_FACETS = ['any', 'delivering', 'gated', 'shadowed', 'no-entry'];
|
|
14
|
+
/** The FILTERS rows for one filter value. `doc kind` is spelled out because the
|
|
15
|
+
* snapshot above it dials the NODE's kind. */
|
|
16
|
+
export function facetsOf(filters) {
|
|
17
|
+
return [
|
|
18
|
+
{ id: 'rung', label: 'rung', value: filters.rung, typed: false },
|
|
19
|
+
{ id: 'origin', label: 'origin', value: filters.origin, typed: false },
|
|
20
|
+
{ id: 'kind', label: 'doc kind', value: filters.kind, typed: false },
|
|
21
|
+
{ id: 'state', label: 'state', value: filters.state, typed: false },
|
|
22
|
+
{ id: 'query', label: 'search', value: filters.query === '' ? 'any' : filters.query, typed: true },
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
export function cycleFacet(filters, id, delta) {
|
|
26
|
+
const step = (values, current) => {
|
|
27
|
+
const at = values.indexOf(current);
|
|
28
|
+
return values[(Math.max(0, at) + delta + values.length) % values.length];
|
|
29
|
+
};
|
|
30
|
+
switch (id) {
|
|
31
|
+
case 'rung':
|
|
32
|
+
return { ...filters, rung: step(RUNG_FACETS, filters.rung) };
|
|
33
|
+
case 'origin':
|
|
34
|
+
return { ...filters, origin: step(ORIGIN_FACETS, filters.origin) };
|
|
35
|
+
case 'kind':
|
|
36
|
+
return { ...filters, kind: step(KIND_FACETS, filters.kind) };
|
|
37
|
+
case 'state':
|
|
38
|
+
return { ...filters, state: step(STATE_FACETS, filters.state) };
|
|
39
|
+
case 'query':
|
|
40
|
+
return filters;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
9
43
|
function tildify(path) {
|
|
10
44
|
const home = process.env['HOME'];
|
|
11
45
|
return home !== undefined && home !== '' && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
@@ -92,26 +126,35 @@ export function typedAxisValue(snapshot, id) {
|
|
|
92
126
|
return axis === 'file' ? snapshot.file : axis === 'docName' ? snapshot.docName : axis === 'command' ? snapshot.command : '';
|
|
93
127
|
}
|
|
94
128
|
const LABEL_WIDTH = 10;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
129
|
+
/** Both sections as one column. `index` runs over the axes and then the facets
|
|
130
|
+
* — the order the shell routes a cycle in — while the section headings and the
|
|
131
|
+
* blank rows between them are drawn but never selectable. */
|
|
132
|
+
export function renderRail(opts) {
|
|
133
|
+
const { axes, facets, index, width, height, focused, matched, total } = opts;
|
|
134
|
+
const lines = [];
|
|
135
|
+
let cursorLine = 0;
|
|
136
|
+
const push = (at, label, value) => {
|
|
100
137
|
const room = Math.max(1, width - LABEL_WIDTH - 2);
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const body = ` ${
|
|
138
|
+
const padded = padAnsi(truncateToWidth(label, LABEL_WIDTH), LABEL_WIDTH);
|
|
139
|
+
const shown = truncateToWidth(value, room);
|
|
140
|
+
const body = ` ${padded} ${shown}`;
|
|
141
|
+
if (at === index)
|
|
142
|
+
cursorLine = lines.length;
|
|
104
143
|
if (at === index && focused)
|
|
105
|
-
|
|
144
|
+
lines.push(theme.bg('selectedBg', padAnsi(body, width)));
|
|
106
145
|
else if (at === index)
|
|
107
|
-
|
|
146
|
+
lines.push(`${theme.fg('accent', '▌')}${padAnsi(theme.fg('accent', body.slice(1)), width - 1)}`);
|
|
108
147
|
else
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
148
|
+
lines.push(padAnsi(` ${theme.fg('dim', padded)} ${shown}`, width));
|
|
149
|
+
};
|
|
150
|
+
lines.push(theme.fg('dim', ' SNAPSHOT'), '');
|
|
151
|
+
axes.forEach((axis, at) => push(at, axis.label, axis.value));
|
|
152
|
+
lines.push('', theme.fg('dim', ` FILTERS · ${matched} of ${total}`), '');
|
|
153
|
+
facets.forEach((facet, at) => push(axes.length + at, facet.label, facet.value));
|
|
154
|
+
const maxStart = Math.max(0, lines.length - height);
|
|
155
|
+
const start = Math.min(Math.max(0, cursorLine - Math.floor(height / 2)), maxStart);
|
|
156
|
+
const out = lines.slice(start, start + height);
|
|
114
157
|
while (out.length < height)
|
|
115
158
|
out.push('');
|
|
116
|
-
return out
|
|
159
|
+
return out;
|
|
117
160
|
}
|
|
@@ -14,7 +14,11 @@ export declare class ContextAdminShell implements Component, Focusable {
|
|
|
14
14
|
private snapshot;
|
|
15
15
|
private plans;
|
|
16
16
|
private zone;
|
|
17
|
-
private
|
|
17
|
+
private railIndex;
|
|
18
|
+
private filters;
|
|
19
|
+
private rows;
|
|
20
|
+
/** The search field owns the keyboard while it is open. */
|
|
21
|
+
private searching;
|
|
18
22
|
private readonly docs;
|
|
19
23
|
private readonly detail;
|
|
20
24
|
private modal;
|
|
@@ -35,6 +39,9 @@ export declare class ContextAdminShell implements Component, Focusable {
|
|
|
35
39
|
/** Recompute the focused event's plan, refresh the panels from it, and let
|
|
36
40
|
* the remaining events fill in behind it. */
|
|
37
41
|
private rebuild;
|
|
42
|
+
/** A filter never re-plans: it is a question about the plan already computed,
|
|
43
|
+
* which is what keeps a facet change instant on a corpus this size. */
|
|
44
|
+
private setFilters;
|
|
38
45
|
private syncDetail;
|
|
39
46
|
private setSnapshot;
|
|
40
47
|
private matches;
|
|
@@ -47,8 +54,14 @@ export declare class ContextAdminShell implements Component, Focusable {
|
|
|
47
54
|
* drawn against, so it earns the page-level gesture. */
|
|
48
55
|
private cyclesEvent;
|
|
49
56
|
handleInput(data: string): void;
|
|
50
|
-
|
|
57
|
+
/** The rail's rows are the snapshot axes followed by the filter facets, so
|
|
58
|
+
* one cursor and one gesture drive both sections. */
|
|
59
|
+
private handleRail;
|
|
51
60
|
private handleDocs;
|
|
61
|
+
private openSearch;
|
|
62
|
+
/** Typing filters live, so the field is the list's own heading rather than a
|
|
63
|
+
* modal over the rows it is selecting. */
|
|
64
|
+
private handleSearch;
|
|
52
65
|
private handleDetail;
|
|
53
66
|
/** What the focused dossier row does when opened — the same write its letter
|
|
54
67
|
* key starts, so a user who never learns the letters can still edit. */
|
|
@@ -78,5 +91,9 @@ export declare class ContextAdminShell implements Component, Focusable {
|
|
|
78
91
|
private renderModal;
|
|
79
92
|
private footer;
|
|
80
93
|
render(width: number): string[];
|
|
94
|
+
/** What the delivering documents spend on this event. An estimate, and a
|
|
95
|
+
* floor on the rendered prompt: boot wraps these documents in its own intro
|
|
96
|
+
* prose and catalog tree. */
|
|
97
|
+
private costMeta;
|
|
81
98
|
}
|
|
82
99
|
export declare function runContextAdmin(seed: Snapshot): Promise<AdminResult | undefined>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// The `crtr sys context admin` page: a
|
|
2
|
-
//
|
|
3
|
-
// planned, the matrix shows WHERE every document
|
|
4
|
-
// only place a write starts.
|
|
1
|
+
// The `crtr sys context admin` page: a rail, the document matrix, and the
|
|
2
|
+
// dossier that edits one document's routing. The rail decides WHAT is being
|
|
3
|
+
// planned and WHICH slice of it is shown, the matrix shows WHERE every document
|
|
4
|
+
// lands, 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,14 +10,26 @@ 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,
|
|
13
|
+
import { axesOf, cycleAxis, cycleFacet, facetsOf, 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
|
-
import {
|
|
16
|
+
import { NO_FILTERS, costTotals } from './list-view.js';
|
|
17
|
+
import { PlanSet, formatCost, plural } from './model.js';
|
|
17
18
|
const RAIL_WIDTH = 26;
|
|
18
|
-
const DOCS_WIDTH = 38;
|
|
19
19
|
const RAIL_MIN_TOTAL_WIDTH = 86;
|
|
20
20
|
const THREE_COLUMN_MIN_WIDTH = 112;
|
|
21
|
+
/** Columns the two dividers between the three zones spend. */
|
|
22
|
+
const DIVIDER_WIDTH = 6;
|
|
23
|
+
/** Below the floor a row cannot hold a name beside the matrix; above the
|
|
24
|
+
* ceiling extra width stops buying legibility and the dossier wants it. */
|
|
25
|
+
const DOCS_MIN_WIDTH = 48;
|
|
26
|
+
const DOCS_MAX_WIDTH = 100;
|
|
27
|
+
const DOCS_SHARE = 0.45;
|
|
28
|
+
/** The list's share of what is left after the rail and the dividers. */
|
|
29
|
+
function docsWidthFor(width) {
|
|
30
|
+
const room = Math.max(1, width - RAIL_WIDTH - DIVIDER_WIDTH);
|
|
31
|
+
return Math.max(DOCS_MIN_WIDTH, Math.min(DOCS_MAX_WIDTH, Math.floor(room * DOCS_SHARE)));
|
|
32
|
+
}
|
|
21
33
|
export class ContextAdminShell {
|
|
22
34
|
tui;
|
|
23
35
|
terminalRows;
|
|
@@ -25,8 +37,12 @@ export class ContextAdminShell {
|
|
|
25
37
|
suspendForChild;
|
|
26
38
|
snapshot;
|
|
27
39
|
plans;
|
|
28
|
-
zone = '
|
|
29
|
-
|
|
40
|
+
zone = 'rail';
|
|
41
|
+
railIndex = 0;
|
|
42
|
+
filters = NO_FILTERS;
|
|
43
|
+
rows = [];
|
|
44
|
+
/** The search field owns the keyboard while it is open. */
|
|
45
|
+
searching = false;
|
|
30
46
|
docs;
|
|
31
47
|
detail = new ContextDetailPanel();
|
|
32
48
|
modal;
|
|
@@ -64,14 +80,23 @@ export class ContextAdminShell {
|
|
|
64
80
|
* the remaining events fill in behind it. */
|
|
65
81
|
rebuild(selectedKey) {
|
|
66
82
|
this.plans.plan(this.snapshot.event);
|
|
67
|
-
this.
|
|
83
|
+
this.rows = buildRows(this.plans, this.snapshot.event);
|
|
84
|
+
this.docs.setRows(this.rows, this.snapshot.event, selectedKey ?? this.docs.selected?.key);
|
|
68
85
|
this.syncDetail();
|
|
69
86
|
this.plans.warm(() => {
|
|
70
|
-
this.
|
|
87
|
+
this.rows = buildRows(this.plans, this.snapshot.event);
|
|
88
|
+
this.docs.setRows(this.rows, this.snapshot.event, this.docs.selected?.key);
|
|
71
89
|
this.syncDetail();
|
|
72
90
|
this.tui.requestRender();
|
|
73
91
|
});
|
|
74
92
|
}
|
|
93
|
+
/** A filter never re-plans: it is a question about the plan already computed,
|
|
94
|
+
* which is what keeps a facet change instant on a corpus this size. */
|
|
95
|
+
setFilters(next) {
|
|
96
|
+
this.filters = next;
|
|
97
|
+
this.docs.setFilters(next);
|
|
98
|
+
this.syncDetail();
|
|
99
|
+
}
|
|
75
100
|
syncDetail() {
|
|
76
101
|
const row = this.docs.selected;
|
|
77
102
|
const capOwner = row === undefined ? null : capOwnerFor(row.anchor, this.snapshot.profileId);
|
|
@@ -124,8 +149,8 @@ export class ContextAdminShell {
|
|
|
124
149
|
this.handleModal(data);
|
|
125
150
|
return;
|
|
126
151
|
}
|
|
127
|
-
if (this.
|
|
128
|
-
this.
|
|
152
|
+
if (this.searching) {
|
|
153
|
+
this.handleSearch(data);
|
|
129
154
|
return;
|
|
130
155
|
}
|
|
131
156
|
const step = this.cyclesEvent(data);
|
|
@@ -137,32 +162,48 @@ export class ContextAdminShell {
|
|
|
137
162
|
this.close();
|
|
138
163
|
return;
|
|
139
164
|
}
|
|
140
|
-
if (this.zone === '
|
|
141
|
-
this.
|
|
165
|
+
if (this.zone === 'rail')
|
|
166
|
+
this.handleRail(data);
|
|
142
167
|
else if (this.zone === 'docs')
|
|
143
168
|
this.handleDocs(data);
|
|
144
169
|
else
|
|
145
170
|
this.handleDetail(data);
|
|
146
171
|
}
|
|
147
|
-
|
|
172
|
+
/** The rail's rows are the snapshot axes followed by the filter facets, so
|
|
173
|
+
* one cursor and one gesture drive both sections. */
|
|
174
|
+
handleRail(data) {
|
|
148
175
|
const axes = axesOf(this.snapshot, profileNameFor(this.snapshot.profileId) ?? 'none');
|
|
149
|
-
|
|
150
|
-
const
|
|
176
|
+
const facets = facetsOf(this.filters);
|
|
177
|
+
const total = axes.length + facets.length;
|
|
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];
|
|
181
|
+
const change = (delta) => {
|
|
182
|
+
if (axis !== undefined) {
|
|
183
|
+
if (axis.typed)
|
|
184
|
+
this.openAxisField(axis.id, axis.label);
|
|
185
|
+
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));
|
|
193
|
+
}
|
|
194
|
+
};
|
|
151
195
|
if (this.isDown(data)) {
|
|
152
|
-
this.
|
|
196
|
+
this.railIndex = Math.min(total - 1, this.railIndex + 1);
|
|
153
197
|
}
|
|
154
198
|
else if (this.isUp(data)) {
|
|
155
|
-
this.
|
|
199
|
+
this.railIndex = Math.max(0, this.railIndex - 1);
|
|
156
200
|
}
|
|
157
201
|
else if (matchesKey(data, 'space') || this.matches('crtr.setup.toggle', data)) {
|
|
158
|
-
|
|
159
|
-
this.openAxisField(axis.id, axis.label);
|
|
160
|
-
else
|
|
161
|
-
this.setSnapshot(cycleAxis(this.snapshot, axis.id, 1));
|
|
202
|
+
change(1);
|
|
162
203
|
}
|
|
163
204
|
else if (this.isDescend(data)) {
|
|
164
|
-
if (matchesKey(data, 'enter') && axis
|
|
165
|
-
|
|
205
|
+
if (matchesKey(data, 'enter') && (axis?.typed === true || facet?.typed === true))
|
|
206
|
+
change(1);
|
|
166
207
|
else
|
|
167
208
|
this.zone = 'docs';
|
|
168
209
|
}
|
|
@@ -171,11 +212,10 @@ export class ContextAdminShell {
|
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
handleDocs(data) {
|
|
174
|
-
if (
|
|
175
|
-
this.
|
|
176
|
-
return;
|
|
215
|
+
if (data === '/' || matchesKey(data, '/')) {
|
|
216
|
+
this.openSearch();
|
|
177
217
|
}
|
|
178
|
-
if (this.isDown(data)) {
|
|
218
|
+
else if (this.isDown(data)) {
|
|
179
219
|
this.docs.move(1);
|
|
180
220
|
this.syncDetail();
|
|
181
221
|
}
|
|
@@ -183,18 +223,48 @@ export class ContextAdminShell {
|
|
|
183
223
|
this.docs.move(-1);
|
|
184
224
|
this.syncDetail();
|
|
185
225
|
}
|
|
186
|
-
else if (matchesKey(data, '
|
|
187
|
-
this.docs.
|
|
226
|
+
else if (matchesKey(data, 'space') || this.matches('crtr.setup.toggle', data)) {
|
|
227
|
+
this.docs.toggleGroup();
|
|
188
228
|
this.syncDetail();
|
|
189
229
|
}
|
|
190
230
|
else if (this.isDescend(data)) {
|
|
191
|
-
if (this.docs.
|
|
231
|
+
if (this.docs.onGroup) {
|
|
232
|
+
this.docs.toggleGroup();
|
|
233
|
+
this.syncDetail();
|
|
234
|
+
}
|
|
235
|
+
else if (this.docs.selected !== undefined)
|
|
192
236
|
this.zone = 'detail';
|
|
193
237
|
}
|
|
238
|
+
else if (matchesKey(data, 'escape') && this.filters.query !== '') {
|
|
239
|
+
this.setFilters({ ...this.filters, query: '' });
|
|
240
|
+
}
|
|
194
241
|
else if (this.isAscend(data) || matchesKey(data, 'escape')) {
|
|
195
|
-
this.zone = '
|
|
242
|
+
this.zone = 'rail';
|
|
196
243
|
}
|
|
197
244
|
}
|
|
245
|
+
openSearch() {
|
|
246
|
+
this.searching = true;
|
|
247
|
+
this.zone = 'docs';
|
|
248
|
+
this.tui.requestRender();
|
|
249
|
+
}
|
|
250
|
+
/** Typing filters live, so the field is the list's own heading rather than a
|
|
251
|
+
* modal over the rows it is selecting. */
|
|
252
|
+
handleSearch(data) {
|
|
253
|
+
if (matchesKey(data, 'escape')) {
|
|
254
|
+
this.searching = false;
|
|
255
|
+
this.setFilters({ ...this.filters, query: '' });
|
|
256
|
+
}
|
|
257
|
+
else if (matchesKey(data, 'enter')) {
|
|
258
|
+
this.searching = false;
|
|
259
|
+
}
|
|
260
|
+
else if (matchesKey(data, 'backspace')) {
|
|
261
|
+
this.setFilters({ ...this.filters, query: this.filters.query.slice(0, -1) });
|
|
262
|
+
}
|
|
263
|
+
else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
264
|
+
this.setFilters({ ...this.filters, query: this.filters.query + data });
|
|
265
|
+
}
|
|
266
|
+
this.tui.requestRender();
|
|
267
|
+
}
|
|
198
268
|
handleDetail(data) {
|
|
199
269
|
if (this.isDown(data))
|
|
200
270
|
this.detail.move(1);
|
|
@@ -570,15 +640,15 @@ export class ContextAdminShell {
|
|
|
570
640
|
return theme.fg('dim', 'y/enter confirm · n/esc cancel');
|
|
571
641
|
if (this.modal?.kind === 'field')
|
|
572
642
|
return theme.fg('dim', 'type · enter apply · esc cancel');
|
|
573
|
-
if (this.
|
|
574
|
-
return theme.fg('dim', 'type to
|
|
643
|
+
if (this.searching)
|
|
644
|
+
return theme.fg('dim', 'type to search names · enter keep · esc clear');
|
|
575
645
|
if (this.notice !== undefined)
|
|
576
646
|
return theme.fg(this.notice.ok ? 'success' : 'warning', truncateToWidth(this.notice.text, width));
|
|
577
647
|
const move = `${this.label('crtr.setup.list-down', 'j')}/${this.label('crtr.setup.list-up', 'k')}`;
|
|
578
|
-
const left = this.zone === '
|
|
579
|
-
? `${move}
|
|
648
|
+
const left = this.zone === 'rail'
|
|
649
|
+
? `${move} row · space change · enter documents`
|
|
580
650
|
: this.zone === 'docs'
|
|
581
|
-
? `${move}
|
|
651
|
+
? `${move} row · space ${this.docs.onGroup ? 'open group' : 'group'} · / search · enter dossier`
|
|
582
652
|
: `${move} field · enter edit · e add · x remove · g gate · b body · u unlisted · s slash · m cap · r rename`;
|
|
583
653
|
const right = `${this.label('crtr.setup.tab-next', 'tab')} event · ${this.label('crtr.setup.cancel', 'q')} close`;
|
|
584
654
|
const gap = width - visibleWidth(left) - visibleWidth(right);
|
|
@@ -591,14 +661,26 @@ export class ContextAdminShell {
|
|
|
591
661
|
const bodyHeight = Math.max(1, height - PAGE_CHROME_ROWS);
|
|
592
662
|
const railVisible = width >= RAIL_MIN_TOTAL_WIDTH;
|
|
593
663
|
const axes = axesOf(this.snapshot, profileNameFor(this.snapshot.profileId) ?? 'none');
|
|
594
|
-
|
|
664
|
+
const facets = facetsOf(this.filters);
|
|
665
|
+
this.railIndex = Math.max(0, Math.min(this.railIndex, axes.length + facets.length - 1));
|
|
666
|
+
const rail = (railWidth) => renderRail({
|
|
667
|
+
axes,
|
|
668
|
+
facets,
|
|
669
|
+
index: this.railIndex,
|
|
670
|
+
width: railWidth,
|
|
671
|
+
height: bodyHeight,
|
|
672
|
+
focused: this.zone === 'rail',
|
|
673
|
+
matched: this.docs.visibleCount,
|
|
674
|
+
total: this.rows.length,
|
|
675
|
+
});
|
|
595
676
|
let body;
|
|
596
677
|
if (width >= THREE_COLUMN_MIN_WIDTH) {
|
|
597
|
-
const
|
|
598
|
-
const
|
|
678
|
+
const docsWidth = docsWidthFor(width);
|
|
679
|
+
const detailWidth = Math.max(1, width - RAIL_WIDTH - docsWidth - DIVIDER_WIDTH);
|
|
680
|
+
const middle = columns(this.docs.render(docsWidth, bodyHeight, this.zone === 'docs', this.plans), docsWidth, this.modal === undefined
|
|
599
681
|
? this.detail.render(detailWidth, bodyHeight, this.zone === 'detail')
|
|
600
682
|
: this.renderModal(detailWidth, bodyHeight), detailWidth);
|
|
601
|
-
body = columns(
|
|
683
|
+
body = columns(rail(RAIL_WIDTH), RAIL_WIDTH, middle, width - RAIL_WIDTH - 3);
|
|
602
684
|
}
|
|
603
685
|
else if (railVisible) {
|
|
604
686
|
const panelWidth = width - RAIL_WIDTH - 3;
|
|
@@ -607,13 +689,13 @@ export class ContextAdminShell {
|
|
|
607
689
|
: this.zone === 'detail'
|
|
608
690
|
? this.detail.render(panelWidth, bodyHeight, true)
|
|
609
691
|
: this.docs.render(panelWidth, bodyHeight, this.zone === 'docs', this.plans);
|
|
610
|
-
body = columns(
|
|
692
|
+
body = columns(rail(RAIL_WIDTH), RAIL_WIDTH, panel, panelWidth);
|
|
611
693
|
}
|
|
612
694
|
else if (this.modal !== undefined) {
|
|
613
695
|
body = this.renderModal(width, bodyHeight);
|
|
614
696
|
}
|
|
615
|
-
else if (this.zone === '
|
|
616
|
-
body =
|
|
697
|
+
else if (this.zone === 'rail') {
|
|
698
|
+
body = rail(width);
|
|
617
699
|
}
|
|
618
700
|
else if (this.zone === 'detail') {
|
|
619
701
|
body = this.detail.render(width, bodyHeight, true);
|
|
@@ -624,6 +706,7 @@ export class ContextAdminShell {
|
|
|
624
706
|
const pending = SURFACE_EVENTS.filter((event) => this.plans.peek(event) === undefined).length;
|
|
625
707
|
const meta = [
|
|
626
708
|
this.snapshot.event,
|
|
709
|
+
this.costMeta(),
|
|
627
710
|
pending > 0 ? `${plural(pending, 'event', 'events')} planning` : 'all events planned',
|
|
628
711
|
this.edited.size > 0 ? `${plural(this.edited.size, 'doc', 'docs')} edited` : '',
|
|
629
712
|
]
|
|
@@ -631,6 +714,19 @@ export class ContextAdminShell {
|
|
|
631
714
|
.join(' · ');
|
|
632
715
|
return pageFrame({ width, height, title: 'crouter context', meta, body, footer: this.footer(width) });
|
|
633
716
|
}
|
|
717
|
+
/** What the delivering documents spend on this event. An estimate, and a
|
|
718
|
+
* floor on the rendered prompt: boot wraps these documents in its own intro
|
|
719
|
+
* prose and catalog tree. */
|
|
720
|
+
costMeta() {
|
|
721
|
+
const { total, byRung } = costTotals(this.rows, this.snapshot.event);
|
|
722
|
+
if (total === 0)
|
|
723
|
+
return 'no documents delivered';
|
|
724
|
+
const split = ['content', 'preview', 'name']
|
|
725
|
+
.filter((rung) => byRung[rung] > 0)
|
|
726
|
+
.map((rung) => `${formatCost(byRung[rung]).replace('~', '')} ${rung}`)
|
|
727
|
+
.join(' · ');
|
|
728
|
+
return `docs load ${formatCost(total)} — ${split}`;
|
|
729
|
+
}
|
|
634
730
|
}
|
|
635
731
|
/** Every field here is an edit-or-accept, never a blank to retype, so the
|
|
636
732
|
* cursor lands at the end (`\x05`) — `setValue` alone leaves it at 0, where
|
|
@@ -80,7 +80,7 @@ const daemonRestart = defineLeaf({
|
|
|
80
80
|
effects: [
|
|
81
81
|
'The daemon acknowledges immediately, then after a short grace tears its whole broker fleet down, releases its claim, spawns a successor on the runtime generation currently selected, and exits.',
|
|
82
82
|
'Each torn-down broker hands its foreground bash commands to the file-backed background job system; they keep running and report completion by urgent inbox message.',
|
|
83
|
-
'Every node it tore down — including the caller — is
|
|
83
|
+
'Every node it tore down — including the caller — is resolved by the successor through the ordinary row-driven supervision tick.',
|
|
84
84
|
'The calling process is killed as part of that teardown, AFTER this result is returned.',
|
|
85
85
|
],
|
|
86
86
|
},
|
|
@@ -1,30 +1,4 @@
|
|
|
1
1
|
// Run with: node --import tsx/esm --test src/core/__tests__/child-death-wake.test.ts
|
|
2
|
-
//
|
|
3
|
-
// DOCTRINE WAKE — "a parent that delegated and went dormant is still woken when
|
|
4
|
-
// its child reaches a genuine terminal outcome, and is NEVER spuriously woken on
|
|
5
|
-
// healthy dormancy." Drives the REAL daemon decision pass (superviseTick) + the
|
|
6
|
-
// REAL closeNode against canvas rows fabricated DIRECTLY in an isolated home — NO
|
|
7
|
-
// real tmux session, NO broker boot.
|
|
8
|
-
//
|
|
9
|
-
// BROKER CUT (this worktree): the daemon's OLD parent-wake mechanism is GONE.
|
|
10
|
-
// U7 deleted surfaceChildDeath and the entire pane-gone reaping block (the
|
|
11
|
-
// crash/finalize/release routing that used to mark a pane-gone child dead/done/
|
|
12
|
-
// idle and fan a system inbox entry to its subscribers). Under exit-event
|
|
13
|
-
// authority a viewer pane/window closing is NOT a node death, and the tick runs
|
|
14
|
-
// no liveness pass at all — a booted child's death is observed only through its
|
|
15
|
-
// broker's real exit event (fleet exit-policy job), which recovery-revives it
|
|
16
|
-
// on its saved session rather than surfacing it as dead. The doctrine wake
|
|
17
|
-
// therefore RELOCATED off the daemon's tick:
|
|
18
|
-
// • child finishes → `crtr push final` wakes subscribers via the PUSH itself;
|
|
19
|
-
// • child crashes → its exit event drives a recovery revive (it comes back
|
|
20
|
-
// and pushes later);
|
|
21
|
-
// • child never boots → the exit event terminalizes it and surfaceBootFailure
|
|
22
|
-
// pushes urgent (daemon-boot.test);
|
|
23
|
-
// • child `node close`d → close.ts fans the child-closed wake (tested below).
|
|
24
|
-
// So the daemon now fans NO liveness wake at all, which makes the old CRUX
|
|
25
|
-
// (healthy dormancy must not wake the parent) hold by construction. This file
|
|
26
|
-
// locks in (a) the new pid-only non-reaping + no-daemon-wake contract, and (b)
|
|
27
|
-
// the one genuine-terminal wake that still lives in core: close.ts.
|
|
28
2
|
import { test, before, after, beforeEach } from 'node:test';
|
|
29
3
|
import assert from 'node:assert/strict';
|
|
30
4
|
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
@@ -112,15 +86,6 @@ after(() => {
|
|
|
112
86
|
delete process.env['CRTR_HOME'];
|
|
113
87
|
resetFleetForTests();
|
|
114
88
|
});
|
|
115
|
-
// BROKER CUT — exit-event authority REPLACES daemon pane-gone reaping. A booted
|
|
116
|
-
// child whose engine pid is dead (and whose viewer pane is gone) is RECOVERED,
|
|
117
|
-
// not reaped, and the daemon's tick fans NO wake to the dormant parent. This
|
|
118
|
-
// single case subsumes the four deleted scenarios (crash / quiet-finalize /
|
|
119
|
-
// awaiting a live grandchild / pending self-wake): with pane state ignored and
|
|
120
|
-
// surfaceChildDeath gone, the daemon treats them all identically — no reap, no
|
|
121
|
-
// liveness wake — so the old CRUX (no spurious wake on healthy dormancy) holds
|
|
122
|
-
// by construction. The node-lifecycle tick relaunches the row through the same
|
|
123
|
-
// policy table its exit event would have used (D-12); that recovery is silent.
|
|
124
89
|
test('exit-event authority: a dead-engine booted child is recovered, NOT reaped, and the tick fans NO wake to its dormant parent', async () => {
|
|
125
90
|
const originalLaunch = headlessBrokerHost.launch;
|
|
126
91
|
const launches = [];
|
|
@@ -139,9 +104,6 @@ test('exit-event authority: a dead-engine booted child is recovered, NOT reaped,
|
|
|
139
104
|
// its saved session, never reaped to dead/done/idle.
|
|
140
105
|
assert.deepEqual(launches, ['CHILD'], 'the tick recovers the dead engine through the policy table');
|
|
141
106
|
assert.equal(getNode('CHILD').status, 'active', 'CHILD is recovered, NOT reaped');
|
|
142
|
-
// surfaceChildDeath is deleted: the daemon raises no liveness wake. The
|
|
143
|
-
// doctrine wake now rides the child's own `push final` (or close.ts), so a
|
|
144
|
-
// purely-inbox-waiting parent is never spuriously woken on dormancy/crash.
|
|
145
107
|
assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY — no daemon liveness wake');
|
|
146
108
|
}
|
|
147
109
|
finally {
|
|
@@ -163,13 +125,3 @@ test('node close of a child wakes its SURVIVING manager (the parent outside the
|
|
|
163
125
|
assert.ok(wake, 'node close fanned a child-closed entry to the surviving manager (D-1: previously none)');
|
|
164
126
|
assert.match(wake.label, /closed/i, 'the entry names the closed child');
|
|
165
127
|
});
|
|
166
|
-
// NOTE (broker cut): the two NEGATIVE "CRUX" tests that used to live here — a
|
|
167
|
-
// child dormant while awaiting a live grandchild, and a child dormant on a
|
|
168
|
-
// pending self-wake — asserted the daemon's pane-gone routing chose idle-release
|
|
169
|
-
// (status 'idle', intent 'idle-release') over finalize, so it would not spuriously
|
|
170
|
-
// wake the parent. U7 DELETED that routing entirely: pane state is ignored, the
|
|
171
|
-
// daemon never finalizes/releases on liveness, and surfaceChildDeath is gone, so
|
|
172
|
-
// the daemon raises no liveness wake at all. The no-spurious-wake invariant they
|
|
173
|
-
// protected now holds by construction and is covered by the pid-only test above
|
|
174
|
-
// (PARENT inbox stays empty). Removed rather than adapted because the
|
|
175
|
-
// idle-release-vs-finalize distinction they asserted no longer exists.
|