@north-light/crouter 0.3.235 → 0.3.237
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +2 -1
- package/dist/api/client.js +3 -0
- package/dist/api/dto/broker-ops.d.ts +1 -1
- package/dist/api/dto/common.d.ts +2 -0
- package/dist/api/dto/lifecycle.d.ts +9 -2
- package/dist/api/dto/messages.d.ts +5 -0
- package/dist/api/dto/nodes.d.ts +14 -5
- package/dist/api/dto/worktree.d.ts +10 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +1 -1
- package/dist/builtin-memory/internal/agent-shaping.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +1 -1
- package/dist/clients/attach/render/tool-calls.js +18 -3
- package/dist/clients/attach/viewer.js +399 -399
- package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +6 -0
- package/dist/clients/inbox/review/roundtrip.js +1 -1
- package/dist/commands/__tests__/node-new.test.js +58 -0
- package/dist/commands/memory/lint.js +22 -13
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/node/create.d.ts +1 -1
- package/dist/commands/node/create.js +4 -1
- package/dist/commands/node/lifecycle.js +2 -2
- package/dist/commands/node-worktree.js +39 -8
- package/dist/commands/revive.js +11 -2
- package/dist/commands/sys/context/admin/actions.d.ts +48 -0
- package/dist/commands/sys/context/admin/actions.js +246 -0
- package/dist/commands/sys/context/admin/axes-panel.d.ts +21 -0
- package/dist/commands/sys/context/admin/axes-panel.js +117 -0
- package/dist/commands/sys/context/admin/detail-panel.d.ts +53 -0
- package/dist/commands/sys/context/admin/detail-panel.js +178 -0
- package/dist/commands/sys/context/admin/docs-panel.d.ts +45 -0
- package/dist/commands/sys/context/admin/docs-panel.js +186 -0
- package/dist/commands/sys/context/admin/model.d.ts +75 -0
- package/dist/commands/sys/context/admin/model.js +209 -0
- package/dist/commands/sys/context/admin/shell.d.ts +82 -0
- package/dist/commands/sys/context/admin/shell.js +669 -0
- package/dist/commands/sys/context/admin.d.ts +1 -0
- package/dist/commands/sys/context/admin.js +124 -0
- package/dist/commands/sys/context/doc.d.ts +1 -0
- package/dist/commands/sys/context/doc.js +196 -0
- package/dist/commands/sys/context/prompt-review.d.ts +1 -0
- package/dist/commands/sys/{prompt-review.js → context/prompt-review.js} +10 -10
- package/dist/commands/sys/context/resolve.d.ts +55 -0
- package/dist/commands/sys/context/resolve.js +428 -0
- package/dist/commands/sys/context/sysprompt.d.ts +1 -0
- package/dist/commands/sys/{sysprompt.js → context/sysprompt.js} +11 -11
- package/dist/commands/sys/context.d.ts +2 -0
- package/dist/commands/sys/context.js +20 -0
- package/dist/commands/sys.js +3 -4
- package/dist/core/__tests__/canvas-inbox-watcher-naming.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-inbox-watcher-naming.test.js +258 -0
- package/dist/core/__tests__/child-death-wake.test.js +38 -22
- package/dist/core/__tests__/cron-self-cancel-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/cron-self-cancel-delivery.test.js +72 -0
- package/dist/core/__tests__/daemon-boot.test.js +40 -28
- package/dist/core/__tests__/helpers/harness.js +1 -0
- package/dist/core/__tests__/human-deliver.test.js +1 -1
- package/dist/core/__tests__/human-node-not-supervised.test.js +10 -15
- package/dist/core/__tests__/integration/revive.test.js +12 -12
- package/dist/core/__tests__/integration/spawn-root.test.js +6 -0
- package/dist/core/__tests__/integration/worktree-reap.test.js +32 -0
- package/dist/core/__tests__/lifecycle-terminal-reasons.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +61 -0
- package/dist/core/__tests__/migration.test.js +38 -1
- package/dist/core/__tests__/push-final-guard.test.js +1 -1
- package/dist/core/__tests__/revive-capacity.test.d.ts +1 -0
- package/dist/core/__tests__/revive-capacity.test.js +127 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +7 -7
- package/dist/core/__tests__/seam/broker-cap-freeze.test.d.ts +1 -0
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +81 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +76 -7
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +1 -1
- package/dist/core/canvas/browse/render.js +11 -3
- package/dist/core/canvas/canvas.d.ts +15 -3
- package/dist/core/canvas/canvas.js +52 -16
- package/dist/core/canvas/migrations.js +21 -0
- package/dist/core/canvas/nav-render.js +3 -1
- package/dist/core/canvas/remote-canvas-source.js +2 -0
- package/dist/core/canvas/render-source.d.ts +3 -1
- package/dist/core/canvas/render-source.js +7 -3
- package/dist/core/canvas/status-glyph.d.ts +10 -1
- package/dist/core/canvas/status-glyph.js +19 -3
- package/dist/core/canvas/types.d.ts +22 -3
- package/dist/core/feed/feed.js +1 -1
- package/dist/core/human/feedback-companion.js +5 -2
- package/dist/core/memory/lint.d.ts +19 -0
- package/dist/core/memory/lint.js +97 -0
- package/dist/core/memory-resolver.d.ts +4 -0
- package/dist/core/memory-resolver.js +7 -7
- package/dist/core/preview-registry.js +4 -4
- package/dist/core/review/realize.js +4 -2
- package/dist/core/runtime/close.js +1 -1
- package/dist/core/runtime/fleet.d.ts +29 -1
- package/dist/core/runtime/fleet.js +23 -0
- package/dist/core/runtime/host.js +32 -11
- package/dist/core/runtime/launch-prompt.d.ts +1 -1
- package/dist/core/runtime/launch-prompt.js +2 -2
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/lifecycle.d.ts +4 -2
- package/dist/core/runtime/lifecycle.js +12 -1
- package/dist/core/runtime/memory.js +1 -1
- package/dist/core/runtime/reopen.js +10 -1
- package/dist/core/runtime/reset.js +1 -1
- package/dist/core/runtime/revive-all.d.ts +3 -0
- package/dist/core/runtime/revive-all.js +10 -4
- package/dist/core/runtime/revive.d.ts +12 -0
- package/dist/core/runtime/revive.js +62 -3
- package/dist/core/runtime/spawn.d.ts +9 -13
- package/dist/core/runtime/spawn.js +40 -17
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/index.d.ts +6 -3
- package/dist/core/substrate/index.js +4 -3
- package/dist/core/substrate/on-read.d.ts +0 -8
- package/dist/core/substrate/on-read.js +45 -124
- package/dist/core/substrate/plan.d.ts +94 -0
- package/dist/core/substrate/plan.js +266 -0
- package/dist/core/substrate/render.d.ts +5 -6
- package/dist/core/substrate/render.js +76 -129
- package/dist/core/substrate/schema.d.ts +1 -6
- package/dist/core/substrate/schema.js +1 -15
- package/dist/core/substrate/session-cache.d.ts +2 -10
- package/dist/core/substrate/session-cache.js +9 -47
- package/dist/core/substrate/subject-fields.d.ts +1 -4
- package/dist/core/substrate/subject-fields.js +2 -5
- package/dist/core/substrate/surface-match.d.ts +37 -17
- package/dist/core/substrate/surface-match.js +44 -53
- package/dist/core/tui/page-host.d.ts +6 -0
- package/dist/core/tui/page-host.js +12 -3
- package/dist/core/worktree.d.ts +8 -0
- package/dist/core/worktree.js +65 -0
- package/dist/daemon/__tests__/reconciler-signature.test.js +3 -2
- package/dist/daemon/__tests__/thaw-order.test.d.ts +1 -0
- package/dist/daemon/__tests__/thaw-order.test.js +55 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +6 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +6 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +1 -1
- package/dist/daemon/api/bridge.js +5 -1
- package/dist/daemon/api/handlers/attach.js +5 -1
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/messages.js +10 -2
- package/dist/daemon/api/handlers/nodes.js +8 -4
- package/dist/daemon/api/handlers/worktree.js +10 -1
- package/dist/daemon/api/map.js +7 -0
- package/dist/daemon/companion-retire.js +1 -1
- package/dist/daemon/cron/passes.d.ts +0 -5
- package/dist/daemon/cron/passes.js +0 -20
- package/dist/daemon/cron/sinks.js +1 -1
- package/dist/daemon/cron-run.d.ts +5 -6
- package/dist/daemon/cron-run.js +7 -16
- package/dist/daemon/crtrd.d.ts +0 -10
- package/dist/daemon/crtrd.js +23 -61
- package/dist/daemon/fleet.d.ts +10 -7
- package/dist/daemon/fleet.js +71 -12
- package/dist/daemon/messaging/node-message.js +3 -1
- package/dist/daemon/profile-delete.js +1 -1
- package/dist/daemon/reconcilers/broker-supervision.d.ts +26 -9
- package/dist/daemon/reconcilers/broker-supervision.js +39 -52
- package/dist/daemon/reconcilers/controller-death.d.ts +7 -0
- package/dist/daemon/reconcilers/controller-death.js +26 -0
- package/dist/daemon/reconcilers/cron-lane.d.ts +0 -3
- package/dist/daemon/reconcilers/cron-lane.js +0 -3
- package/dist/daemon/reconcilers/live-obligation.js +3 -1
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +53 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +170 -0
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +5 -0
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +16 -0
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -0
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +13 -0
- package/dist/daemon/reconcilers/node-lifecycle/tick.d.ts +26 -0
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +140 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +12 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/commands/sys/prompt-review.d.ts +0 -1
- package/dist/commands/sys/sysprompt.d.ts +0 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +0 -119
- package/dist/daemon/reconcilers/dormant-inbox.d.ts +0 -18
- package/dist/daemon/reconcilers/dormant-inbox.js +0 -157
- /package/dist/{core/__tests__/cron-broker-capacity.test.d.ts → commands/__tests__/node-new.test.d.ts} +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Snapshot } from './model.js';
|
|
2
|
+
export type AxisId = 'event' | 'payload' | 'kind' | 'mode' | 'lifecycle' | 'manager' | 'depth' | 'profile' | 'cwd';
|
|
3
|
+
interface Axis {
|
|
4
|
+
id: AxisId;
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
/** Free-text axes open a field; the rest cycle in place. */
|
|
8
|
+
typed: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** The rail's rows for a snapshot. The payload row appears only for the events
|
|
11
|
+
* that read one, because on boot there is nothing it could change. */
|
|
12
|
+
export declare function axesOf(snapshot: Snapshot, profileName: string): Axis[];
|
|
13
|
+
/** Cycle one enum axis, returning the changed snapshot. A typed axis is never
|
|
14
|
+
* cycled — the shell opens a field for it instead. */
|
|
15
|
+
export declare function cycleAxis(snapshot: Snapshot, id: AxisId, delta: number): Snapshot;
|
|
16
|
+
/** Apply a typed axis's submitted value. */
|
|
17
|
+
export declare function setTypedAxis(snapshot: Snapshot, id: AxisId, value: string): Snapshot;
|
|
18
|
+
/** The value a typed axis's field is seeded with. */
|
|
19
|
+
export declare function typedAxisValue(snapshot: Snapshot, id: AxisId): string;
|
|
20
|
+
export declare function renderAxes(axes: Axis[], index: number, width: number, height: number, focused: boolean): string[];
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// The snapshot rail: the configuration axes the page plans against. Every row
|
|
2
|
+
// is a live control — an enum cycles in place, a free-text axis opens a field —
|
|
3
|
+
// and every change re-plans the whole surface.
|
|
4
|
+
import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
5
|
+
import { padAnsi, theme, visibleRange } from '../../../../core/tui/panel.js';
|
|
6
|
+
import { SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
|
|
7
|
+
import { listProfiles } from '../../../../core/profiles/manifest.js';
|
|
8
|
+
import { payloadAxisFor } from './model.js';
|
|
9
|
+
function tildify(path) {
|
|
10
|
+
const home = process.env['HOME'];
|
|
11
|
+
return home !== undefined && home !== '' && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
12
|
+
}
|
|
13
|
+
/** The rail's rows for a snapshot. The payload row appears only for the events
|
|
14
|
+
* that read one, because on boot there is nothing it could change. */
|
|
15
|
+
export function axesOf(snapshot, profileName) {
|
|
16
|
+
const payload = payloadAxisFor(snapshot.event);
|
|
17
|
+
const payloadValue = payload === 'file' ? snapshot.file : payload === 'docName' ? snapshot.docName : payload === 'command' ? snapshot.command : '';
|
|
18
|
+
return [
|
|
19
|
+
{ id: 'event', label: 'event', value: snapshot.event, typed: false },
|
|
20
|
+
...(payload === null
|
|
21
|
+
? []
|
|
22
|
+
: [{
|
|
23
|
+
id: 'payload',
|
|
24
|
+
label: payload === 'file' ? 'file' : payload === 'docName' ? 'doc' : 'command',
|
|
25
|
+
value: payloadValue === '' ? '(none — nothing matches)' : payloadValue,
|
|
26
|
+
typed: true,
|
|
27
|
+
}]),
|
|
28
|
+
{ id: 'kind', label: 'kind', value: snapshot.kind, typed: true },
|
|
29
|
+
{ id: 'mode', label: 'mode', value: snapshot.mode, typed: false },
|
|
30
|
+
{ id: 'lifecycle', label: 'lifecycle', value: snapshot.lifecycle, typed: false },
|
|
31
|
+
{ id: 'manager', label: 'manager', value: snapshot.hasManager ? 'yes' : 'no', typed: false },
|
|
32
|
+
{ id: 'depth', label: 'depth', value: String(snapshot.depth), typed: false },
|
|
33
|
+
{ id: 'profile', label: 'profile', value: profileName, typed: false },
|
|
34
|
+
{ id: 'cwd', label: 'cwd', value: tildify(snapshot.cwd), typed: true },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
/** Cycle one enum axis, returning the changed snapshot. A typed axis is never
|
|
38
|
+
* cycled — the shell opens a field for it instead. */
|
|
39
|
+
export function cycleAxis(snapshot, id, delta) {
|
|
40
|
+
const step = (values, current) => {
|
|
41
|
+
const at = values.indexOf(current);
|
|
42
|
+
return values[(Math.max(0, at) + delta + values.length) % values.length];
|
|
43
|
+
};
|
|
44
|
+
switch (id) {
|
|
45
|
+
case 'event':
|
|
46
|
+
return { ...snapshot, event: step(SURFACE_EVENTS, snapshot.event) };
|
|
47
|
+
case 'mode': {
|
|
48
|
+
const mode = step(['base', 'orchestrator'], snapshot.mode);
|
|
49
|
+
// Lifecycle follows mode the way node creation defaults it, so cycling to
|
|
50
|
+
// orchestrator does not leave a resident-only persona reading as terminal.
|
|
51
|
+
return { ...snapshot, mode, lifecycle: mode === 'orchestrator' ? 'resident' : 'terminal' };
|
|
52
|
+
}
|
|
53
|
+
case 'lifecycle':
|
|
54
|
+
return { ...snapshot, lifecycle: step(['terminal', 'resident'], snapshot.lifecycle) };
|
|
55
|
+
case 'manager':
|
|
56
|
+
return { ...snapshot, hasManager: !snapshot.hasManager };
|
|
57
|
+
case 'depth':
|
|
58
|
+
return { ...snapshot, depth: Math.max(0, snapshot.depth + delta) };
|
|
59
|
+
case 'profile': {
|
|
60
|
+
const ids = [null, ...listProfiles().map((entry) => entry.profileId)];
|
|
61
|
+
return { ...snapshot, profileId: step(ids, snapshot.profileId) };
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
return snapshot;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Apply a typed axis's submitted value. */
|
|
68
|
+
export function setTypedAxis(snapshot, id, value) {
|
|
69
|
+
const trimmed = value.trim();
|
|
70
|
+
if (id === 'kind')
|
|
71
|
+
return trimmed === '' ? snapshot : { ...snapshot, kind: trimmed };
|
|
72
|
+
if (id === 'cwd')
|
|
73
|
+
return trimmed === '' ? snapshot : { ...snapshot, cwd: trimmed };
|
|
74
|
+
if (id !== 'payload')
|
|
75
|
+
return snapshot;
|
|
76
|
+
const axis = payloadAxisFor(snapshot.event);
|
|
77
|
+
if (axis === 'file')
|
|
78
|
+
return { ...snapshot, file: trimmed };
|
|
79
|
+
if (axis === 'docName')
|
|
80
|
+
return { ...snapshot, docName: trimmed };
|
|
81
|
+
if (axis === 'command')
|
|
82
|
+
return { ...snapshot, command: trimmed };
|
|
83
|
+
return snapshot;
|
|
84
|
+
}
|
|
85
|
+
/** The value a typed axis's field is seeded with. */
|
|
86
|
+
export function typedAxisValue(snapshot, id) {
|
|
87
|
+
if (id === 'kind')
|
|
88
|
+
return snapshot.kind;
|
|
89
|
+
if (id === 'cwd')
|
|
90
|
+
return snapshot.cwd;
|
|
91
|
+
const axis = payloadAxisFor(snapshot.event);
|
|
92
|
+
return axis === 'file' ? snapshot.file : axis === 'docName' ? snapshot.docName : axis === 'command' ? snapshot.command : '';
|
|
93
|
+
}
|
|
94
|
+
const LABEL_WIDTH = 10;
|
|
95
|
+
export function renderAxes(axes, index, width, height, focused) {
|
|
96
|
+
const out = [theme.fg('dim', ' SNAPSHOT'), ''];
|
|
97
|
+
const headerRows = out.length;
|
|
98
|
+
const rows = [];
|
|
99
|
+
for (const [at, axis] of axes.entries()) {
|
|
100
|
+
const room = Math.max(1, width - LABEL_WIDTH - 2);
|
|
101
|
+
const label = padAnsi(truncateToWidth(axis.label, LABEL_WIDTH), LABEL_WIDTH);
|
|
102
|
+
const value = truncateToWidth(axis.value, room);
|
|
103
|
+
const body = ` ${label} ${value}`;
|
|
104
|
+
if (at === index && focused)
|
|
105
|
+
rows.push(theme.bg('selectedBg', padAnsi(body, width)));
|
|
106
|
+
else if (at === index)
|
|
107
|
+
rows.push(`${theme.fg('accent', '▌')}${padAnsi(theme.fg('accent', body.slice(1)), width - 1)}`);
|
|
108
|
+
else
|
|
109
|
+
rows.push(padAnsi(` ${theme.fg('dim', label)} ${value}`, width));
|
|
110
|
+
}
|
|
111
|
+
const capacity = Math.max(1, height - headerRows);
|
|
112
|
+
const range = visibleRange(rows.length, index, capacity);
|
|
113
|
+
out.push(...rows.slice(range.start, range.start + capacity));
|
|
114
|
+
while (out.length < height)
|
|
115
|
+
out.push('');
|
|
116
|
+
return out.slice(0, height);
|
|
117
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Rung, type SurfaceEvent } from '../../../../core/substrate/schema.js';
|
|
2
|
+
import type { DeliveryRecord } from '../../../../core/substrate/plan.js';
|
|
3
|
+
import type { DocRow } from './docs-panel.js';
|
|
4
|
+
/** A field of the focused document the shell can write. `entry` carries an
|
|
5
|
+
* index into the document's whole surfaces list, not the focused event's
|
|
6
|
+
* subset, because that list is what a write replaces. */
|
|
7
|
+
export type DetailRow = {
|
|
8
|
+
kind: 'entry';
|
|
9
|
+
at: number;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'add-entry';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'gate';
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'unlisted';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'slash';
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'routing';
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'short-form';
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'body';
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'cap';
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'move';
|
|
28
|
+
};
|
|
29
|
+
export interface CapOwner {
|
|
30
|
+
path: string;
|
|
31
|
+
memory: Rung;
|
|
32
|
+
}
|
|
33
|
+
export declare class ContextDetailPanel {
|
|
34
|
+
private row;
|
|
35
|
+
private event;
|
|
36
|
+
private capOwner;
|
|
37
|
+
private readOnly;
|
|
38
|
+
private rows;
|
|
39
|
+
private index;
|
|
40
|
+
/** Lines scrolled past the cursor's own window, so the verdict below the last
|
|
41
|
+
* editable row stays reachable. */
|
|
42
|
+
private tail;
|
|
43
|
+
setContext(row: DocRow | undefined, event: SurfaceEvent, capOwner: CapOwner | null, readOnly: string | null): void;
|
|
44
|
+
get selected(): DetailRow | undefined;
|
|
45
|
+
/** The entry index a rung cycle or a removal would act on. */
|
|
46
|
+
get selectedEntry(): number | undefined;
|
|
47
|
+
move(delta: number): void;
|
|
48
|
+
render(width: number, height: number, focused: boolean): string[];
|
|
49
|
+
private verdict;
|
|
50
|
+
private acrossEvents;
|
|
51
|
+
}
|
|
52
|
+
/** The editable rows for one document, in the order the panel draws them. */
|
|
53
|
+
export declare function buildDetailRows(record: DeliveryRecord): DetailRow[];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// The dossier: one document's editable configuration above the read-only
|
|
2
|
+
// verdict that configuration produces. Every selectable row here is a field the
|
|
3
|
+
// shell can write; everything below the rule is what the planner decided.
|
|
4
|
+
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
5
|
+
import { padAnsi, theme, wrapText } from '../../../../core/tui/panel.js';
|
|
6
|
+
import { SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
|
|
7
|
+
import { EVENT_INITIALS, cellStateOf, describeSurfaceEntry, plural } from './model.js';
|
|
8
|
+
const STATE_WORD = {
|
|
9
|
+
pending: 'planning…',
|
|
10
|
+
delivers: 'delivers',
|
|
11
|
+
capped: 'capped',
|
|
12
|
+
gated: 'gated out',
|
|
13
|
+
shadowed: 'shadowed',
|
|
14
|
+
silent: 'silent',
|
|
15
|
+
};
|
|
16
|
+
export class ContextDetailPanel {
|
|
17
|
+
row;
|
|
18
|
+
event = 'boot';
|
|
19
|
+
capOwner = null;
|
|
20
|
+
readOnly = null;
|
|
21
|
+
rows = [];
|
|
22
|
+
index = 0;
|
|
23
|
+
/** Lines scrolled past the cursor's own window, so the verdict below the last
|
|
24
|
+
* editable row stays reachable. */
|
|
25
|
+
tail = 0;
|
|
26
|
+
setContext(row, event, capOwner, readOnly) {
|
|
27
|
+
const changed = row?.key !== this.row?.key;
|
|
28
|
+
this.row = row;
|
|
29
|
+
this.event = event;
|
|
30
|
+
this.capOwner = capOwner;
|
|
31
|
+
this.readOnly = readOnly;
|
|
32
|
+
this.rows = row === undefined ? [] : buildDetailRows(row.anchor);
|
|
33
|
+
if (changed) {
|
|
34
|
+
this.index = 0;
|
|
35
|
+
this.tail = 0;
|
|
36
|
+
}
|
|
37
|
+
this.index = Math.max(0, Math.min(this.index, this.rows.length - 1));
|
|
38
|
+
}
|
|
39
|
+
get selected() {
|
|
40
|
+
return this.rows[this.index];
|
|
41
|
+
}
|
|
42
|
+
/** The entry index a rung cycle or a removal would act on. */
|
|
43
|
+
get selectedEntry() {
|
|
44
|
+
const row = this.selected;
|
|
45
|
+
return row?.kind === 'entry' ? row.at : undefined;
|
|
46
|
+
}
|
|
47
|
+
move(delta) {
|
|
48
|
+
const next = this.index + delta;
|
|
49
|
+
if (next < 0 || next >= this.rows.length) {
|
|
50
|
+
this.tail = Math.max(0, this.tail + delta);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.index = next;
|
|
54
|
+
this.tail = 0;
|
|
55
|
+
}
|
|
56
|
+
render(width, height, focused) {
|
|
57
|
+
const row = this.row;
|
|
58
|
+
if (row === undefined) {
|
|
59
|
+
return fit([theme.fg('dim', ' Select a document.')], height);
|
|
60
|
+
}
|
|
61
|
+
const record = row.records.get(this.event) ?? row.anchor;
|
|
62
|
+
const lines = [];
|
|
63
|
+
const rowLines = new Map();
|
|
64
|
+
lines.push(truncateToWidth(`${theme.fg('accent', theme.bold(record.name))}${this.readOnly === null ? '' : theme.fg('warning', ' read-only')}`, width));
|
|
65
|
+
lines.push(truncateToWidth(theme.fg('dim', `${record.kind} · ${record.scope}${record.nodeLocal ? ' · node-local' : ''} · ${record.source.representation}`), width));
|
|
66
|
+
lines.push(truncateToWidth(theme.fg('dim', record.storePath), width));
|
|
67
|
+
if (this.readOnly !== null)
|
|
68
|
+
lines.push(...wrapText(this.readOnly, width).map((line) => theme.fg('warning', line)));
|
|
69
|
+
lines.push('');
|
|
70
|
+
const push = (at, body) => {
|
|
71
|
+
rowLines.set(at, lines.length);
|
|
72
|
+
lines.push(at === this.index && focused
|
|
73
|
+
? theme.bg('selectedBg', padAnsi(truncateToWidth(body, width), width))
|
|
74
|
+
: at === this.index
|
|
75
|
+
? `${theme.fg('accent', '▌')}${truncateToWidth(body, Math.max(0, width - 1))}`
|
|
76
|
+
: ` ${truncateToWidth(body, Math.max(0, width - 1))}`);
|
|
77
|
+
};
|
|
78
|
+
let at = 0;
|
|
79
|
+
lines.push(theme.fg('dim', ' SURFACES'));
|
|
80
|
+
const surfaces = record.doc.surfaces;
|
|
81
|
+
if (surfaces.length === 0)
|
|
82
|
+
push(at++, theme.fg('dim', 'no entries — this document only appears in its listing'));
|
|
83
|
+
else {
|
|
84
|
+
for (const entry of surfaces) {
|
|
85
|
+
const text = describeSurfaceEntry(entry);
|
|
86
|
+
push(at++, entry.on === this.event ? text : theme.fg('dim', text));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
push(at++, theme.fg('success', '+ add a surface entry'));
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push(theme.fg('dim', ' DOCUMENT'));
|
|
92
|
+
push(at++, field('gate', record.doc.gate === undefined ? theme.fg('dim', 'none') : JSON.stringify(record.doc.gate), width));
|
|
93
|
+
push(at++, field('unlisted', record.doc.unlisted ? 'yes' : theme.fg('dim', 'no'), width));
|
|
94
|
+
push(at++, field('slash', record.doc.slash ? 'yes' : theme.fg('dim', 'no'), width));
|
|
95
|
+
push(at++, field('routing', record.doc.whenAndWhyToRead, width));
|
|
96
|
+
push(at++, field('short form', record.doc.shortForm === '' ? theme.fg('dim', 'none') : record.doc.shortForm, width));
|
|
97
|
+
push(at++, field('body', theme.fg('dim', `${plural(bodyLines(record.doc.body), 'line', 'lines')} \u2014 opens in $EDITOR`), width));
|
|
98
|
+
push(at++, field('profile cap', this.capOwner === null
|
|
99
|
+
? theme.fg('dim', 'no profile project governs this store — uncapped')
|
|
100
|
+
: `${this.capOwner.memory} ${theme.fg('dim', this.capOwner.path)}`, width));
|
|
101
|
+
push(at++, field('canonical name', record.name, width));
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push(theme.fg('border', '─'.repeat(Math.max(0, width))));
|
|
104
|
+
lines.push(theme.fg('dim', ` VERDICT · ${this.event}`));
|
|
105
|
+
lines.push(...this.verdict(record, width));
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push(theme.fg('dim', ' ACROSS EVENTS'));
|
|
108
|
+
lines.push(...this.acrossEvents(row, width));
|
|
109
|
+
const cursorLine = rowLines.get(this.index) ?? 0;
|
|
110
|
+
const maxStart = Math.max(0, lines.length - height);
|
|
111
|
+
let start = Math.min(Math.max(0, cursorLine - Math.floor(height / 3)), maxStart);
|
|
112
|
+
if (this.tail > 0)
|
|
113
|
+
start = Math.min(maxStart, start + this.tail);
|
|
114
|
+
return fit(lines.slice(start, start + height), height);
|
|
115
|
+
}
|
|
116
|
+
verdict(record, width) {
|
|
117
|
+
const out = [];
|
|
118
|
+
const cap = record.projectMemoryCap === null ? 'uncapped' : record.projectMemoryCap;
|
|
119
|
+
out.push(truncateToWidth(` authored ${theme.bold(record.authoredRung)} → cap ${cap} → ${theme.fg(record.finalRung === 'none' ? 'dim' : 'success', theme.bold(record.finalRung))}`, width));
|
|
120
|
+
if (!record.docGate.pass)
|
|
121
|
+
out.push(...wrapText(` document gate fails: ${record.docGate.reason}`, width).map((line) => theme.fg('warning', line)));
|
|
122
|
+
if (record.entryGate !== null && !record.entryGate.pass) {
|
|
123
|
+
out.push(...wrapText(` entry gate fails: ${record.entryGate.reason}`, width).map((line) => theme.fg('warning', line)));
|
|
124
|
+
}
|
|
125
|
+
if (record.matchedEntry === null && record.authoredEntries.length > 0) {
|
|
126
|
+
out.push(theme.fg('dim', ` ${plural(record.authoredEntries.length, 'entry', 'entries')} for this event, none matched`));
|
|
127
|
+
}
|
|
128
|
+
if (record.shadowedBy !== null) {
|
|
129
|
+
out.push(...wrapText(` shadowed by the ${record.shadowedBy.scope} copy at ${record.shadowedBy.path}`, width).map((line) => theme.fg('muted', line)));
|
|
130
|
+
}
|
|
131
|
+
else if (record.winner) {
|
|
132
|
+
out.push(theme.fg('dim', ' wins this canonical name'));
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
acrossEvents(row, width) {
|
|
137
|
+
return SURFACE_EVENTS.map((event) => {
|
|
138
|
+
const record = row.records.get(event);
|
|
139
|
+
const state = record === undefined ? 'silent' : cellStateOf(record);
|
|
140
|
+
const rung = record === undefined ? 'none' : record.finalRung;
|
|
141
|
+
const label = padAnsi(` ${EVENT_INITIALS[event]} ${event}`, 20);
|
|
142
|
+
const word = STATE_WORD[state] ?? state;
|
|
143
|
+
const body = `${label} ${padAnsi(word, 11)} ${rung}`;
|
|
144
|
+
return truncateToWidth(state === 'delivers' || state === 'capped' ? body : theme.fg('dim', body), width);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** The editable rows for one document, in the order the panel draws them. */
|
|
149
|
+
export function buildDetailRows(record) {
|
|
150
|
+
const entries = record.doc.surfaces.map((_entry, at) => ({ kind: 'entry', at }));
|
|
151
|
+
return [
|
|
152
|
+
...entries,
|
|
153
|
+
{ kind: 'add-entry' },
|
|
154
|
+
{ kind: 'gate' },
|
|
155
|
+
{ kind: 'unlisted' },
|
|
156
|
+
{ kind: 'slash' },
|
|
157
|
+
{ kind: 'routing' },
|
|
158
|
+
{ kind: 'short-form' },
|
|
159
|
+
{ kind: 'body' },
|
|
160
|
+
{ kind: 'cap' },
|
|
161
|
+
{ kind: 'move' },
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
function bodyLines(body) {
|
|
165
|
+
const trimmed = body.replace(/\n+$/, '');
|
|
166
|
+
return trimmed === '' ? 0 : trimmed.split('\n').length;
|
|
167
|
+
}
|
|
168
|
+
const FIELD_WIDTH = 15;
|
|
169
|
+
function field(label, value, width) {
|
|
170
|
+
const room = Math.max(1, width - FIELD_WIDTH - 2);
|
|
171
|
+
return `${padAnsi(theme.fg('dim', label), FIELD_WIDTH)} ${truncateToWidth(value, room)}`;
|
|
172
|
+
}
|
|
173
|
+
function fit(lines, height) {
|
|
174
|
+
const out = [...lines];
|
|
175
|
+
while (out.length < height)
|
|
176
|
+
out.push('');
|
|
177
|
+
return out.slice(0, height).map((line) => (visibleWidth(line) === 0 ? '' : line));
|
|
178
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type SurfaceEvent } from '../../../../core/substrate/schema.js';
|
|
2
|
+
import type { DeliveryRecord } from '../../../../core/substrate/plan.js';
|
|
3
|
+
import { PlanSet } from './model.js';
|
|
4
|
+
/** One physical document across every event. `records` is sparse: an event
|
|
5
|
+
* whose corpus never reaches this document has no record for it, and an event
|
|
6
|
+
* still being planned has none yet either. */
|
|
7
|
+
export interface DocRow {
|
|
8
|
+
key: string;
|
|
9
|
+
name: string;
|
|
10
|
+
/** The record for the focused event, or the first one any event carries —
|
|
11
|
+
* every panel needs a document to describe even when the focused event's
|
|
12
|
+
* corpus excludes it. */
|
|
13
|
+
anchor: DeliveryRecord;
|
|
14
|
+
records: Map<SurfaceEvent, DeliveryRecord>;
|
|
15
|
+
/** Present only in an event other than the focused one. */
|
|
16
|
+
offEvent: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The union of every planned event's corpus, ordered by the focused event's
|
|
19
|
+
* precedence order with the documents only other events see appended. */
|
|
20
|
+
export declare function buildRows(plans: PlanSet, event: SurfaceEvent): DocRow[];
|
|
21
|
+
export declare function rowDelivers(row: DocRow): boolean;
|
|
22
|
+
export declare class ContextDocsPanel {
|
|
23
|
+
private readonly onChange;
|
|
24
|
+
private rows;
|
|
25
|
+
private index;
|
|
26
|
+
private filter;
|
|
27
|
+
private filtering;
|
|
28
|
+
/** Off by default: the surface is legible only when the silent documents are
|
|
29
|
+
* visible beside the delivering ones. */
|
|
30
|
+
private deliveringOnly;
|
|
31
|
+
constructor(onChange: () => void);
|
|
32
|
+
get selected(): DocRow | undefined;
|
|
33
|
+
get ownsKeyboard(): boolean;
|
|
34
|
+
get filteringDelivering(): boolean;
|
|
35
|
+
setRows(rows: DocRow[], selectedKey?: string): void;
|
|
36
|
+
move(delta: number): void;
|
|
37
|
+
toggleDeliveringOnly(): void;
|
|
38
|
+
/** True only when a live filter consumed the input. */
|
|
39
|
+
handleInput(data: string): boolean;
|
|
40
|
+
private visible;
|
|
41
|
+
render(width: number, height: number, focused: boolean, plans: PlanSet): string[];
|
|
42
|
+
/** Pad rather than trim: this column sets the length of the divider drawn
|
|
43
|
+
* beside it. */
|
|
44
|
+
private fit;
|
|
45
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// The middle column: every document the snapshot can see, each carrying its own
|
|
2
|
+
// six-event matrix. One row is one physical document, so two stores answering
|
|
3
|
+
// to the same canonical name stay two rows and the shadowed one is visible.
|
|
4
|
+
import { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
5
|
+
import { padAnsi, theme, visibleRange } from '../../../../core/tui/panel.js';
|
|
6
|
+
import { SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
|
|
7
|
+
import { EVENT_INITIALS, cellGlyph, cellStateOf, plural } from './model.js';
|
|
8
|
+
/** The union of every planned event's corpus, ordered by the focused event's
|
|
9
|
+
* precedence order with the documents only other events see appended. */
|
|
10
|
+
export function buildRows(plans, event) {
|
|
11
|
+
const rows = new Map();
|
|
12
|
+
const order = [];
|
|
13
|
+
const absorb = (planEvent) => {
|
|
14
|
+
const plan = plans.peek(planEvent);
|
|
15
|
+
if (plan === undefined)
|
|
16
|
+
return;
|
|
17
|
+
for (const record of plan.docs) {
|
|
18
|
+
const key = record.storePath;
|
|
19
|
+
const held = rows.get(key);
|
|
20
|
+
if (held === undefined) {
|
|
21
|
+
rows.set(key, {
|
|
22
|
+
key,
|
|
23
|
+
name: record.name,
|
|
24
|
+
anchor: record,
|
|
25
|
+
records: new Map([[planEvent, record]]),
|
|
26
|
+
offEvent: planEvent !== event,
|
|
27
|
+
});
|
|
28
|
+
order.push(key);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
held.records.set(planEvent, record);
|
|
32
|
+
if (planEvent === event) {
|
|
33
|
+
held.anchor = record;
|
|
34
|
+
held.offEvent = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
absorb(event);
|
|
39
|
+
for (const other of SURFACE_EVENTS)
|
|
40
|
+
if (other !== event)
|
|
41
|
+
absorb(other);
|
|
42
|
+
return order.map((key) => rows.get(key));
|
|
43
|
+
}
|
|
44
|
+
export function rowDelivers(row) {
|
|
45
|
+
for (const record of row.records.values())
|
|
46
|
+
if (record.finalRung !== 'none')
|
|
47
|
+
return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/** Cells and their width, so the shell's legend and the list agree. */
|
|
51
|
+
const CELL_COUNT = SURFACE_EVENTS.length;
|
|
52
|
+
const MATRIX_WIDTH = CELL_COUNT * 2 - 1;
|
|
53
|
+
export class ContextDocsPanel {
|
|
54
|
+
onChange;
|
|
55
|
+
rows = [];
|
|
56
|
+
index = 0;
|
|
57
|
+
filter = '';
|
|
58
|
+
filtering = false;
|
|
59
|
+
/** Off by default: the surface is legible only when the silent documents are
|
|
60
|
+
* visible beside the delivering ones. */
|
|
61
|
+
deliveringOnly = false;
|
|
62
|
+
constructor(onChange) {
|
|
63
|
+
this.onChange = onChange;
|
|
64
|
+
}
|
|
65
|
+
get selected() {
|
|
66
|
+
const rows = this.visible();
|
|
67
|
+
this.index = Math.max(0, Math.min(this.index, rows.length - 1));
|
|
68
|
+
return rows[this.index];
|
|
69
|
+
}
|
|
70
|
+
get ownsKeyboard() {
|
|
71
|
+
return this.filtering;
|
|
72
|
+
}
|
|
73
|
+
get filteringDelivering() {
|
|
74
|
+
return this.deliveringOnly;
|
|
75
|
+
}
|
|
76
|
+
setRows(rows, selectedKey) {
|
|
77
|
+
this.rows = rows;
|
|
78
|
+
const visible = this.visible();
|
|
79
|
+
const found = selectedKey === undefined ? -1 : visible.findIndex((row) => row.key === selectedKey);
|
|
80
|
+
this.index = found >= 0 ? found : Math.min(this.index, Math.max(0, visible.length - 1));
|
|
81
|
+
}
|
|
82
|
+
move(delta) {
|
|
83
|
+
const rows = this.visible();
|
|
84
|
+
if (rows.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
this.index = Math.max(0, Math.min(rows.length - 1, this.index + delta));
|
|
87
|
+
this.onChange();
|
|
88
|
+
}
|
|
89
|
+
toggleDeliveringOnly() {
|
|
90
|
+
this.deliveringOnly = !this.deliveringOnly;
|
|
91
|
+
const held = this.selected?.key;
|
|
92
|
+
this.index = 0;
|
|
93
|
+
if (held !== undefined) {
|
|
94
|
+
const at = this.visible().findIndex((row) => row.key === held);
|
|
95
|
+
if (at >= 0)
|
|
96
|
+
this.index = at;
|
|
97
|
+
}
|
|
98
|
+
this.onChange();
|
|
99
|
+
}
|
|
100
|
+
/** True only when a live filter consumed the input. */
|
|
101
|
+
handleInput(data) {
|
|
102
|
+
if (this.filtering) {
|
|
103
|
+
if (matchesKey(data, 'escape')) {
|
|
104
|
+
this.filter = '';
|
|
105
|
+
this.filtering = false;
|
|
106
|
+
}
|
|
107
|
+
else if (matchesKey(data, 'enter')) {
|
|
108
|
+
this.filtering = false;
|
|
109
|
+
}
|
|
110
|
+
else if (matchesKey(data, 'backspace')) {
|
|
111
|
+
this.filter = this.filter.slice(0, -1);
|
|
112
|
+
this.index = 0;
|
|
113
|
+
}
|
|
114
|
+
else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
115
|
+
this.filter += data;
|
|
116
|
+
this.index = 0;
|
|
117
|
+
}
|
|
118
|
+
this.onChange();
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (data === '/' || matchesKey(data, '/')) {
|
|
122
|
+
this.filtering = true;
|
|
123
|
+
this.onChange();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (matchesKey(data, 'escape') && this.filter !== '') {
|
|
127
|
+
this.filter = '';
|
|
128
|
+
this.index = 0;
|
|
129
|
+
this.onChange();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
visible() {
|
|
135
|
+
const needle = this.filter.trim().toLowerCase();
|
|
136
|
+
return this.rows.filter((row) => {
|
|
137
|
+
if (this.deliveringOnly && !rowDelivers(row))
|
|
138
|
+
return false;
|
|
139
|
+
if (needle === '')
|
|
140
|
+
return true;
|
|
141
|
+
return `${row.name}\n${row.anchor.scope}\n${row.anchor.storePath}`.toLowerCase().includes(needle);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
render(width, height, focused, plans) {
|
|
145
|
+
const rows = this.visible();
|
|
146
|
+
const out = [];
|
|
147
|
+
const heading = this.filtering
|
|
148
|
+
? `/${this.filter}`
|
|
149
|
+
: this.deliveringOnly
|
|
150
|
+
? 'DELIVERING'
|
|
151
|
+
: 'DOCUMENTS';
|
|
152
|
+
const count = this.filter === '' ? plural(rows.length, 'doc', 'docs') : plural(rows.length, 'match', 'matches');
|
|
153
|
+
const matrixHead = SURFACE_EVENTS.map((event) => EVENT_INITIALS[event]).join(' ');
|
|
154
|
+
const nameRoom = Math.max(1, width - MATRIX_WIDTH - 2);
|
|
155
|
+
out.push(truncateToWidth(` ${padAnsi(`${theme.fg('accent', theme.bold(heading))} ${theme.fg('dim', count)}`, nameRoom - 1)} ${theme.fg('dim', matrixHead)}`, width));
|
|
156
|
+
if (rows.length === 0) {
|
|
157
|
+
out.push('', theme.fg('dim', this.filter === '' ? ' No documents in this snapshot\u2019s corpus.' : ' Nothing matches that filter.'));
|
|
158
|
+
return this.fit(out, height);
|
|
159
|
+
}
|
|
160
|
+
const capacity = Math.max(1, height - out.length);
|
|
161
|
+
const range = visibleRange(rows.length, this.index, capacity);
|
|
162
|
+
for (let at = range.start; at < range.end; at += 1) {
|
|
163
|
+
const row = rows[at];
|
|
164
|
+
const matrix = SURFACE_EVENTS.map((event) => {
|
|
165
|
+
const state = plans.peek(event) === undefined ? 'pending' : cellStateOf(row.records.get(event));
|
|
166
|
+
return cellGlyph(row.records.get(event), state);
|
|
167
|
+
}).join(' ');
|
|
168
|
+
const scope = theme.fg('dim', ` ${row.anchor.scope}`);
|
|
169
|
+
const label = truncateToWidth(`${row.name}${scope}`, nameRoom - 1);
|
|
170
|
+
const body = ` ${padAnsi(label, nameRoom - 1)} ${matrix}`;
|
|
171
|
+
out.push(at === this.index && focused
|
|
172
|
+
? theme.bg('selectedBg', padAnsi(body, width))
|
|
173
|
+
: at === this.index
|
|
174
|
+
? `${theme.fg('accent', '▌')}${truncateToWidth(body.slice(1), Math.max(0, width - 1))}`
|
|
175
|
+
: body);
|
|
176
|
+
}
|
|
177
|
+
return this.fit(out, height);
|
|
178
|
+
}
|
|
179
|
+
/** Pad rather than trim: this column sets the length of the divider drawn
|
|
180
|
+
* beside it. */
|
|
181
|
+
fit(lines, height) {
|
|
182
|
+
while (lines.length < height)
|
|
183
|
+
lines.push('');
|
|
184
|
+
return lines.slice(0, height).map((line) => (visibleWidth(line) === 0 ? '' : line));
|
|
185
|
+
}
|
|
186
|
+
}
|