@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.
@@ -1,7 +1,7 @@
1
- // The `crtr sys context admin` page: a snapshot rail, the document matrix, and
2
- // the dossier that edits one document's routing. The rail decides WHAT is being
3
- // planned, the matrix shows WHERE every document lands, and the dossier is the
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, setTypedAxis, renderAxes, typedAxisValue } from './axes-panel.js';
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 { PlanSet, plural } from './model.js';
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 = 'axes';
29
- axisIndex = 0;
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.docs.setRows(buildRows(this.plans, this.snapshot.event), selectedKey ?? this.docs.selected?.key);
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.docs.setRows(buildRows(this.plans, this.snapshot.event), this.docs.selected?.key);
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.docs.ownsKeyboard && this.docs.handleInput(data)) {
128
- this.syncDetail();
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 === 'axes')
141
- this.handleAxes(data);
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
- handleAxes(data) {
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
- this.axisIndex = Math.max(0, Math.min(this.axisIndex, axes.length - 1));
150
- const axis = axes[this.axisIndex];
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.axisIndex = Math.min(axes.length - 1, this.axisIndex + 1);
196
+ this.railIndex = Math.min(total - 1, this.railIndex + 1);
153
197
  }
154
198
  else if (this.isUp(data)) {
155
- this.axisIndex = Math.max(0, this.axisIndex - 1);
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
- if (axis.typed)
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.typed)
165
- this.openAxisField(axis.id, axis.label);
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 (this.docs.handleInput(data)) {
175
- this.syncDetail();
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, 'd')) {
187
- this.docs.toggleDeliveringOnly();
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.selected !== undefined)
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 = 'axes';
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.docs.ownsKeyboard)
574
- return theme.fg('dim', 'type to filter · enter keep · esc clear');
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 === 'axes'
579
- ? `${move} axis · space change · enter documents`
648
+ const left = this.zone === 'rail'
649
+ ? `${move} row · space change · enter documents`
580
650
  : this.zone === 'docs'
581
- ? `${move} doc · / filter · d ${this.docs.filteringDelivering ? 'all' : 'delivering'} · enter dossier`
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
- this.axisIndex = Math.max(0, Math.min(this.axisIndex, axes.length - 1));
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 detailWidth = Math.max(1, width - RAIL_WIDTH - DOCS_WIDTH - 6);
598
- const middle = columns(this.docs.render(DOCS_WIDTH, bodyHeight, this.zone === 'docs', this.plans), DOCS_WIDTH, this.modal === undefined
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(renderAxes(axes, this.axisIndex, RAIL_WIDTH, bodyHeight, this.zone === 'axes'), RAIL_WIDTH, middle, width - RAIL_WIDTH - 3);
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(renderAxes(axes, this.axisIndex, RAIL_WIDTH, bodyHeight, this.zone === 'axes'), RAIL_WIDTH, panel, panelWidth);
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 === 'axes') {
616
- body = renderAxes(axes, this.axisIndex, width, bodyHeight, true);
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, matchIndices, promptText, previewSnippet } from './model.js';
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; the snapshot rail and document list both lose
40
- // usable width before the dossier does, so this takes the settings size.
41
- 'crtr.tmux.menu.context-admin': { description: 'context surface', action: { kind: 'popup', run: 'sys context admin', size: { w: '90%', h: '85%' } } },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.240",
3
+ "version": "0.3.241",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.240",
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.240",
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 {};