@north-light/crouter 0.3.239 → 0.3.241
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 +400 -400
- package/dist/commands/node/bash.js +69 -5
- 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/context/admin.js +4 -2
- package/dist/commands/sys/context/resolve.js +5 -1
- package/dist/core/bash-jobs.d.ts +23 -0
- package/dist/core/bash-jobs.js +44 -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/runtime/tmux-bindings.js +4 -3
- package/dist/core/tui/fuzzy.d.ts +9 -0
- package/dist/core/tui/fuzzy.js +38 -0
- package/dist/core/wake.d.ts +3 -0
- package/dist/core/wake.js +3 -2
- package/dist/daemon/crtrd.js +3 -0
- package/dist/daemon/reconcilers/bash-deadline.d.ts +17 -0
- package/dist/daemon/reconcilers/bash-deadline.js +73 -0
- package/dist/pi-extensions/canvas-bash-valve.js +15 -2
- package/dist/shared/env.d.ts +4 -0
- package/dist/shared/env.js +7 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/commands/sys/context/admin/axes-panel.d.ts +0 -21
|
@@ -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
|
|
@@ -10,6 +10,7 @@ import { ApiError } from '../../../api/index.js';
|
|
|
10
10
|
import { cliClient } from '../../api-client.js';
|
|
11
11
|
import { SURFACE_EVENTS } from '../../../core/substrate/schema.js';
|
|
12
12
|
import { ambientMemoryTarget } from '../../../core/memory-resolver.js';
|
|
13
|
+
import { getDefaultProfileId } from '../../../core/profiles/default-binding.js';
|
|
13
14
|
import { resolveProfileOperand } from '../../../core/profiles/manifest.js';
|
|
14
15
|
export const adminLeaf = defineLeaf({
|
|
15
16
|
name: 'admin',
|
|
@@ -49,15 +50,16 @@ export const adminLeaf = defineLeaf({
|
|
|
49
50
|
const ambient = ambientMemoryTarget();
|
|
50
51
|
const profileId = input['profile']?.trim();
|
|
51
52
|
const cwd = input['cwd']?.trim();
|
|
53
|
+
const resolvedCwd = cwd === undefined || cwd === '' ? (node?.cwd ?? ambient.cwd) : resolvePath(cwd);
|
|
52
54
|
const seed = {
|
|
53
55
|
kind: input['kind']?.trim() || node?.kind || 'general',
|
|
54
56
|
mode: input['mode'] ?? node?.mode ?? 'base',
|
|
55
57
|
lifecycle: node?.lifecycle ?? 'terminal',
|
|
56
58
|
hasManager: node?.parent != null,
|
|
57
59
|
profileId: profileId === undefined || profileId === ''
|
|
58
|
-
? (node?.profile_id ?? ambient.profileId)
|
|
60
|
+
? (node?.profile_id ?? ambient.profileId ?? getDefaultProfileId(resolvedCwd))
|
|
59
61
|
: resolveProfileOperand(profileId).profileId,
|
|
60
|
-
cwd:
|
|
62
|
+
cwd: resolvedCwd,
|
|
61
63
|
nodeId: node?.node_id ?? null,
|
|
62
64
|
depth: node === null ? 0 : await spineDepth(node),
|
|
63
65
|
event: (input['event'] ?? 'boot'),
|
|
@@ -23,6 +23,7 @@ import { clearSessionCache } from '../../../core/substrate/session-cache.js';
|
|
|
23
23
|
import { rungAtLeast, SURFACE_EVENTS, } from '../../../core/substrate/schema.js';
|
|
24
24
|
import { scopeForCwd, profileNameFor } from '../../../core/substrate/subject-fields.js';
|
|
25
25
|
import { resetScopeCache } from '../../../core/scope.js';
|
|
26
|
+
import { getDefaultProfileId } from '../../../core/profiles/default-binding.js';
|
|
26
27
|
export const resolveLeaf = defineLeaf({
|
|
27
28
|
name: 'resolve',
|
|
28
29
|
description: 'resolve what a configuration snapshot loads for one surface event',
|
|
@@ -95,7 +96,10 @@ export async function buildSnapshot(input) {
|
|
|
95
96
|
?? derived?.lifecycle
|
|
96
97
|
?? (mode === 'orchestrator' ? 'resident' : 'terminal');
|
|
97
98
|
const cwd = resolveCwd(optionalString(input['cwd']) ?? derived?.cwd ?? process.cwd());
|
|
98
|
-
const profileId = optionalString(input['profile'])
|
|
99
|
+
const profileId = optionalString(input['profile'])
|
|
100
|
+
?? node?.profile_id
|
|
101
|
+
?? ambientMemoryTarget().profileId
|
|
102
|
+
?? getDefaultProfileId(cwd);
|
|
99
103
|
const hasManagerFlag = optionalString(input['hasManager']);
|
|
100
104
|
return {
|
|
101
105
|
subject: {
|
package/dist/core/bash-jobs.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export interface BashJobPaths {
|
|
|
14
14
|
jobPgid: string;
|
|
15
15
|
/** Human-readable label supplied with the bash call, when any. */
|
|
16
16
|
jobPurpose: string;
|
|
17
|
+
/** Epoch-ms cancel deadline for an AUTO-backgrounded job. Present only when
|
|
18
|
+
* the valve backgrounded the job on its own; its presence marks the job as
|
|
19
|
+
* subject to the daemon's claim-or-cancel deadline. Absent for a deliberate
|
|
20
|
+
* handoff (tmux menu, broker termination), which runs to completion. */
|
|
21
|
+
jobDeadline: string;
|
|
17
22
|
}
|
|
18
23
|
/** A purpose is display text, never shell input. Keep the file-backed control
|
|
19
24
|
* plane to one bounded, visible line so old or hand-written job directories
|
|
@@ -31,6 +36,17 @@ export declare function readJobPgid(contextDir: string, jobId: string): number |
|
|
|
31
36
|
/** The optional human-readable job label. Jobs created before labels existed
|
|
32
37
|
* have no file and deliberately project null. */
|
|
33
38
|
export declare function readJobPurpose(contextDir: string, jobId: string): string | null;
|
|
39
|
+
/** The auto-background cancel deadline (epoch ms), or undefined for a job with
|
|
40
|
+
* no deadline (a deliberate handoff, or a legacy job dir). A non-finite or
|
|
41
|
+
* malformed file reads as no deadline rather than an instant expiry. */
|
|
42
|
+
export declare function readJobDeadlineMs(contextDir: string, jobId: string): number | undefined;
|
|
43
|
+
/** Arm or move an auto-backgrounded job's cancel deadline to `deadlineMs`
|
|
44
|
+
* (epoch ms). Written by the valve at auto-handoff and rewritten when the
|
|
45
|
+
* owning node claims more time. */
|
|
46
|
+
export declare function writeJobDeadlineMs(paths: BashJobPaths, deadlineMs: number): void;
|
|
47
|
+
/** Drop a job's cancel deadline so it runs to completion — the node has claimed
|
|
48
|
+
* it. Best-effort: a job with no deadline file is already in this state. */
|
|
49
|
+
export declare function clearJobDeadline(paths: BashJobPaths): void;
|
|
34
50
|
/** Whether a persisted job supervisor process group is still present. EPERM
|
|
35
51
|
* means the group exists but cannot be signaled by this process; it is still
|
|
36
52
|
* live for the status surface. */
|
|
@@ -88,12 +104,19 @@ export interface BashJobStatus {
|
|
|
88
104
|
purpose: string | null;
|
|
89
105
|
startedAtMs: number;
|
|
90
106
|
logPath: string;
|
|
107
|
+
/** Auto-background cancel deadline (epoch ms), or null for a job that runs to
|
|
108
|
+
* completion (a deliberate handoff, or one the node has claimed). */
|
|
109
|
+
deadlineAtMs: number | null;
|
|
91
110
|
}
|
|
92
111
|
/** Every live backgrounded bash job for this node — job.bg present, job.exit
|
|
93
112
|
* absent — whether handed off by the menu, the valve deadline, or broker
|
|
94
113
|
* termination. Read-only; never mutates. Skips malformed/partially-written
|
|
95
114
|
* directories. Returns oldest-first by startedAtMs. */
|
|
96
115
|
export declare function activeBackgroundBashJobs(contextDir: string): BashJobStatus[];
|
|
116
|
+
/** Every live background job for this node whose auto-cancel deadline has
|
|
117
|
+
* passed at `now`. The daemon's deadline lane stops exactly these. A job with
|
|
118
|
+
* no deadline (deliberate handoff, or one the node claimed) never appears. */
|
|
119
|
+
export declare function expiredAutoBackgroundBashJobs(contextDir: string, now: number): BashJobStatus[];
|
|
97
120
|
export type BackgroundBashJobResult = 'backgrounded' | 'already-backgrounded' | 'finished' | 'missing';
|
|
98
121
|
/** Atomically hand one foreground job to its detached supervisor. The rename
|
|
99
122
|
* is the ownership decision: exactly one caller can move job.run to job.bg,
|
package/dist/core/bash-jobs.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// job.bg, while foreground completion renames it to job.done. The detached
|
|
4
4
|
// supervisor can therefore outlive pi and still decide whether to report exit.
|
|
5
5
|
import { randomBytes } from 'node:crypto';
|
|
6
|
-
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
const MAX_BASH_JOB_PURPOSE_BYTES = 280;
|
|
9
9
|
/** A purpose is display text, never shell input. Keep the file-backed control
|
|
@@ -42,6 +42,7 @@ export function bashJobPaths(contextDir, jobId) {
|
|
|
42
42
|
jobDone: join(dir, 'job.done'),
|
|
43
43
|
jobPgid: join(dir, 'job.pgid'),
|
|
44
44
|
jobPurpose: join(dir, 'purpose'),
|
|
45
|
+
jobDeadline: join(dir, 'job.deadline'),
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
/** The job's supervisor process group, or undefined for a job started before
|
|
@@ -65,6 +66,34 @@ export function readJobPurpose(contextDir, jobId) {
|
|
|
65
66
|
return null;
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
/** The auto-background cancel deadline (epoch ms), or undefined for a job with
|
|
70
|
+
* no deadline (a deliberate handoff, or a legacy job dir). A non-finite or
|
|
71
|
+
* malformed file reads as no deadline rather than an instant expiry. */
|
|
72
|
+
export function readJobDeadlineMs(contextDir, jobId) {
|
|
73
|
+
try {
|
|
74
|
+
const n = Number(readFileSync(bashJobPaths(contextDir, jobId).jobDeadline, 'utf8').trim());
|
|
75
|
+
return Number.isFinite(n) ? n : undefined;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Arm or move an auto-backgrounded job's cancel deadline to `deadlineMs`
|
|
82
|
+
* (epoch ms). Written by the valve at auto-handoff and rewritten when the
|
|
83
|
+
* owning node claims more time. */
|
|
84
|
+
export function writeJobDeadlineMs(paths, deadlineMs) {
|
|
85
|
+
writeFileSync(paths.jobDeadline, `${Math.trunc(deadlineMs)}\n`);
|
|
86
|
+
}
|
|
87
|
+
/** Drop a job's cancel deadline so it runs to completion — the node has claimed
|
|
88
|
+
* it. Best-effort: a job with no deadline file is already in this state. */
|
|
89
|
+
export function clearJobDeadline(paths) {
|
|
90
|
+
try {
|
|
91
|
+
rmSync(paths.jobDeadline, { force: true });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* best-effort — worst case the daemon cancels it at the old deadline */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
68
97
|
/** Whether a persisted job supervisor process group is still present. EPERM
|
|
69
98
|
* means the group exists but cannot be signaled by this process; it is still
|
|
70
99
|
* live for the status surface. */
|
|
@@ -222,10 +251,23 @@ export function activeBackgroundBashJobs(contextDir) {
|
|
|
222
251
|
catch {
|
|
223
252
|
continue;
|
|
224
253
|
}
|
|
225
|
-
jobs.push({
|
|
254
|
+
jobs.push({
|
|
255
|
+
jobId,
|
|
256
|
+
command,
|
|
257
|
+
purpose: readJobPurpose(contextDir, jobId),
|
|
258
|
+
startedAtMs,
|
|
259
|
+
logPath: paths.jobLog,
|
|
260
|
+
deadlineAtMs: readJobDeadlineMs(contextDir, jobId) ?? null,
|
|
261
|
+
});
|
|
226
262
|
}
|
|
227
263
|
return jobs.sort((a, b) => a.startedAtMs - b.startedAtMs);
|
|
228
264
|
}
|
|
265
|
+
/** Every live background job for this node whose auto-cancel deadline has
|
|
266
|
+
* passed at `now`. The daemon's deadline lane stops exactly these. A job with
|
|
267
|
+
* no deadline (deliberate handoff, or one the node claimed) never appears. */
|
|
268
|
+
export function expiredAutoBackgroundBashJobs(contextDir, now) {
|
|
269
|
+
return activeBackgroundBashJobs(contextDir).filter((job) => job.deadlineAtMs !== null && now >= job.deadlineAtMs);
|
|
270
|
+
}
|
|
229
271
|
/** Atomically hand one foreground job to its detached supervisor. The rename
|
|
230
272
|
* is the ownership decision: exactly one caller can move job.run to job.bg,
|
|
231
273
|
* while the supervisor races to move the same source to job.done. */
|
|
@@ -55,15 +55,6 @@ export declare function buildTree(rows: DashboardRow[], rootIds: string[], child
|
|
|
55
55
|
* reaped node's parent so a non-leaf reap never orphans them out of view. No-op
|
|
56
56
|
* if the id is absent. */
|
|
57
57
|
export declare function pruneNode(tree: Tree, id: string): void;
|
|
58
|
-
/** Case-insensitive subsequence match: every char of `query` appears in `text`
|
|
59
|
-
* in order (gaps allowed). Empty query matches everything. Substrings are a
|
|
60
|
-
* subsequence, so this subsumes substring matching too. */
|
|
61
|
-
export declare function fuzzyMatch(query: string, text: string): boolean;
|
|
62
|
-
/** Indices in `text` consumed by a greedy left-to-right subsequence match of
|
|
63
|
-
* `query` — the same walk as `fuzzyMatch`, but returning WHICH chars matched so
|
|
64
|
-
* the renderer can highlight them. Empty set when `query` is empty OR does not
|
|
65
|
-
* fully match (no partial highlights). */
|
|
66
|
-
export declare function matchIndices(query: string, text: string): Set<number>;
|
|
67
58
|
/** One word-wrapped preview line: its text + the column indices WITHIN that text
|
|
68
59
|
* to highlight (the query match). */
|
|
69
60
|
export interface SnippetLine {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// access (dashboardRowsAll / listNodes / subscriptionsOf) into buildTree, then
|
|
6
6
|
// drives flatten() on each keystroke.
|
|
7
7
|
import { isFinishedWork, isLiveStatus, statusRank } from '../node-order.js';
|
|
8
|
+
import { matchIndices } from '../../tui/fuzzy.js';
|
|
8
9
|
export const TABS = ['All', 'Live', 'Dormant', 'Attention', 'Pinned'];
|
|
9
10
|
export const SORTS = ['tree', 'attention', 'relevance', 'recency'];
|
|
10
11
|
/** Does a node belong to this tab's slice?
|
|
@@ -119,43 +120,6 @@ export function pruneNode(tree, id) {
|
|
|
119
120
|
}
|
|
120
121
|
tree.nodes.delete(id);
|
|
121
122
|
}
|
|
122
|
-
// Fuzzy match
|
|
123
|
-
/** Case-insensitive subsequence match: every char of `query` appears in `text`
|
|
124
|
-
* in order (gaps allowed). Empty query matches everything. Substrings are a
|
|
125
|
-
* subsequence, so this subsumes substring matching too. */
|
|
126
|
-
export function fuzzyMatch(query, text) {
|
|
127
|
-
if (query === '')
|
|
128
|
-
return true;
|
|
129
|
-
const q = query.toLowerCase();
|
|
130
|
-
const t = text.toLowerCase();
|
|
131
|
-
let qi = 0;
|
|
132
|
-
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
133
|
-
if (t[ti] === q[qi])
|
|
134
|
-
qi++;
|
|
135
|
-
}
|
|
136
|
-
return qi === q.length;
|
|
137
|
-
}
|
|
138
|
-
/** Indices in `text` consumed by a greedy left-to-right subsequence match of
|
|
139
|
-
* `query` — the same walk as `fuzzyMatch`, but returning WHICH chars matched so
|
|
140
|
-
* the renderer can highlight them. Empty set when `query` is empty OR does not
|
|
141
|
-
* fully match (no partial highlights). */
|
|
142
|
-
export function matchIndices(query, text) {
|
|
143
|
-
const out = new Set();
|
|
144
|
-
if (query === '')
|
|
145
|
-
return out;
|
|
146
|
-
const q = query.toLowerCase();
|
|
147
|
-
const t = text.toLowerCase();
|
|
148
|
-
let qi = 0;
|
|
149
|
-
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
150
|
-
if (t[ti] === q[qi]) {
|
|
151
|
-
out.add(ti);
|
|
152
|
-
qi++;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (qi < q.length)
|
|
156
|
-
out.clear(); // no full match → no highlight
|
|
157
|
-
return out;
|
|
158
|
-
}
|
|
159
123
|
/** Chars of leading context kept before the match start when windowing. */
|
|
160
124
|
const SNIPPET_LEAD = 12;
|
|
161
125
|
/** The literal case-insensitive substring span in a conversation preview.
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// reverse) is allowed always. See detectColorCaps() for the gate, and the
|
|
13
13
|
// canvas-browse color spec for the rationale + palette.
|
|
14
14
|
import { STATUS_GLYPH, STATUS_COLOR, isFrozenRow, resolveNodeVisual, faultSummary } from '../status-glyph.js';
|
|
15
|
-
import { TABS,
|
|
15
|
+
import { TABS, promptText, previewSnippet } from './model.js';
|
|
16
|
+
import { matchIndices } from '../../tui/fuzzy.js';
|
|
16
17
|
// Span/color primitives live in core/tui/draw.ts (one copy, shared with the
|
|
17
18
|
// `crtr view` host). Re-export the color caps so browse's importers + tests keep
|
|
18
19
|
// resolving them from this module.
|
|
@@ -36,9 +36,10 @@ const LEAF_MENU_ACTIONS = {
|
|
|
36
36
|
// default dialog-sized popup clipped both the rail and the models matrix.
|
|
37
37
|
'crtr.tmux.menu.providers': { description: 'settings', action: { kind: 'popup', run: 'sys settings', size: { w: '90%', h: '85%' } } },
|
|
38
38
|
'crtr.tmux.menu.plugins': { description: 'plugins', action: { kind: 'popup', run: 'pkg browse', size: { w: '90%', h: '85%' } } },
|
|
39
|
-
// Three zones side by side
|
|
40
|
-
//
|
|
41
|
-
|
|
39
|
+
// Three zones side by side over a corpus of hundreds of documents: the list
|
|
40
|
+
// spends every column it is given on canonical names. Near-full rather than
|
|
41
|
+
// full, so the pane border still marks it as a popup.
|
|
42
|
+
'crtr.tmux.menu.context-admin': { description: 'context surface', action: { kind: 'popup', run: 'sys context admin', size: { w: '98%', h: '96%' } } },
|
|
42
43
|
'crtr.tmux.menu.graph': { description: 'canvas graph', action: { kind: 'keys', keys: '/graph' } },
|
|
43
44
|
'crtr.tmux.menu.focus-manager': { description: 'go to manager', action: { kind: 'command', run: 'surface node focus {manager}' } },
|
|
44
45
|
'crtr.tmux.menu.copy-id': { description: 'copy node id', action: { kind: 'command', run: 'surface node id --pane {pane}' } },
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Case-insensitive subsequence match: every char of `query` appears in `text`
|
|
2
|
+
* in order (gaps allowed). Empty query matches everything. Substrings are a
|
|
3
|
+
* subsequence, so this subsumes substring matching too. */
|
|
4
|
+
export declare function fuzzyMatch(query: string, text: string): boolean;
|
|
5
|
+
/** Indices in `text` consumed by a greedy left-to-right subsequence match of
|
|
6
|
+
* `query` — the same walk as `fuzzyMatch`, but returning WHICH chars matched so
|
|
7
|
+
* the renderer can highlight them. Empty set when `query` is empty OR does not
|
|
8
|
+
* fully match (no partial highlights). */
|
|
9
|
+
export declare function matchIndices(query: string, text: string): Set<number>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Subsequence matching for TUI filters. One definition, shared by the canvas
|
|
2
|
+
// browser and the context admin list, so the two surfaces cannot drift apart.
|
|
3
|
+
/** Case-insensitive subsequence match: every char of `query` appears in `text`
|
|
4
|
+
* in order (gaps allowed). Empty query matches everything. Substrings are a
|
|
5
|
+
* subsequence, so this subsumes substring matching too. */
|
|
6
|
+
export function fuzzyMatch(query, text) {
|
|
7
|
+
if (query === '')
|
|
8
|
+
return true;
|
|
9
|
+
const q = query.toLowerCase();
|
|
10
|
+
const t = text.toLowerCase();
|
|
11
|
+
let qi = 0;
|
|
12
|
+
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
13
|
+
if (t[ti] === q[qi])
|
|
14
|
+
qi++;
|
|
15
|
+
}
|
|
16
|
+
return qi === q.length;
|
|
17
|
+
}
|
|
18
|
+
/** Indices in `text` consumed by a greedy left-to-right subsequence match of
|
|
19
|
+
* `query` — the same walk as `fuzzyMatch`, but returning WHICH chars matched so
|
|
20
|
+
* the renderer can highlight them. Empty set when `query` is empty OR does not
|
|
21
|
+
* fully match (no partial highlights). */
|
|
22
|
+
export function matchIndices(query, text) {
|
|
23
|
+
const out = new Set();
|
|
24
|
+
if (query === '')
|
|
25
|
+
return out;
|
|
26
|
+
const q = query.toLowerCase();
|
|
27
|
+
const t = text.toLowerCase();
|
|
28
|
+
let qi = 0;
|
|
29
|
+
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
30
|
+
if (t[ti] === q[qi]) {
|
|
31
|
+
out.add(ti);
|
|
32
|
+
qi++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (qi < q.length)
|
|
36
|
+
out.clear(); // no full match → no highlight
|
|
37
|
+
return out;
|
|
38
|
+
}
|
package/dist/core/wake.d.ts
CHANGED
|
@@ -35,6 +35,9 @@ export type Recur = {
|
|
|
35
35
|
};
|
|
36
36
|
/** Cadence floor: reject any interval / cron min-spacing below this (AC-N4). */
|
|
37
37
|
export declare const CADENCE_FLOOR_MS = 60000;
|
|
38
|
+
/** Total ms for a relative-duration string (`90s`, `10m`, `1h30m`), or null if
|
|
39
|
+
* it is not one. */
|
|
40
|
+
export declare function parseDurationMs(s: string): number | null;
|
|
38
41
|
/**
|
|
39
42
|
* Resolve a `<when>` to a single UTC `fireAt`:
|
|
40
43
|
* - relative duration (`90s`, `1h30m`) -> now + Σ
|