@north-light/crouter 0.3.240 → 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 +1 -1
- 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/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/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
|
|
@@ -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/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.241",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.241",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
@@ -1,21 +0,0 @@
|
|
|
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 {};
|