@north-light/crouter 0.3.242 → 0.3.243

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.
@@ -26,27 +26,37 @@ export declare class ContextDocsPanel {
26
26
  private event;
27
27
  private lines;
28
28
  private index;
29
- /** Groups the user opened. Empty means every group is collapsed, which is how
30
- * the page opens: ~25 rows that name the whole corpus. */
29
+ /** Directories the user opened, by full name prefix. Empty means every level
30
+ * is collapsed, which is how the page opens: ~25 rows that name the whole
31
+ * corpus. */
31
32
  private readonly expanded;
32
33
  constructor(onChange: () => void);
33
34
  /** The document the dossier describes: the focused row, or the first member
34
35
  * of the focused group so the dossier is never blank. */
35
36
  get selected(): DocRow | undefined;
36
- /** Documents the current question selects — counted from the group rollups,
37
- * so a fully collapsed list still reports its size. */
37
+ /** Documents the current question selects — counted from the top-level
38
+ * rollups, which partition the corpus, so a fully collapsed list still
39
+ * reports its size and a nested rollup is never counted twice. */
38
40
  get visibleCount(): number;
39
41
  setRows(rows: DocRow[], event: SurfaceEvent, selectedKey?: string): void;
40
42
  setFilters(filters: Filters): void;
41
43
  move(delta: number): void;
42
- /** True when the focused line is a group, so the shell knows whether a
43
- * descend opens a group or the dossier. */
44
+ /** True when the focused line can be opened — a directory header. */
44
45
  get onGroup(): boolean;
46
+ /** True when the focused line IS a document rather than merely standing for
47
+ * one. A directory that is also a document is both, which is why the two
48
+ * gestures split there: space opens it, enter edits it. */
49
+ get onDocument(): boolean;
45
50
  toggleGroup(): void;
46
51
  /** Rebuild the drawn lines, keeping the cursor on the same document (or the
47
- * same group), and clamping when what it was on is gone. */
52
+ * same directory), and clamping when what it was on is gone. A document that
53
+ * owns a directory is drawn as that directory's header, so a held document
54
+ * key matches there too. */
48
55
  private relayout;
49
56
  render(width: number, height: number, focused: boolean, plans: PlanSet): string[];
57
+ /** A directory header. Its stats and cost are the whole subtree's; the matrix
58
+ * is drawn only when the directory is itself a document, which is what marks
59
+ * the one row as both a document and a directory. */
50
60
  private groupLine;
51
61
  private docLine;
52
62
  /** Pad rather than trim: this column sets the length of the divider drawn
@@ -1,7 +1,7 @@
1
- // The middle column: every document the snapshot can see, grouped by the first
2
- // segment of its canonical name and each carrying its own six-event matrix. One
3
- // row is one physical document, so two stores answering to the same canonical
4
- // name stay two rows and the shadowed one is visible.
1
+ // The middle column: every document the snapshot can see, grouped recursively
2
+ // by the directories its canonical name names, and each carrying its own
3
+ // six-event matrix. One row is one physical document, so two stores answering
4
+ // to the same canonical name stay two rows and the shadowed one is visible.
5
5
  import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
6
6
  import { padAnsi, theme, visibleRange } from '../../../../core/tui/panel.js';
7
7
  import { SURFACE_EVENTS } from '../../../../core/substrate/schema.js';
@@ -50,6 +50,9 @@ const COST_WIDTH = 6;
50
50
  /** The glyphs a group header counts its members with — the matrix vocabulary,
51
51
  * so a rung reads the same in both columns. */
52
52
  const RUNG_MARK = { content: '█', preview: '▄', name: '▁' };
53
+ /** Columns one nesting level spends, so a doc sits under its directory's label
54
+ * and the truncator sees the indent as part of the name it is eliding. */
55
+ const INDENT = 2;
53
56
  export class ContextDocsPanel {
54
57
  onChange;
55
58
  rows = [];
@@ -57,8 +60,9 @@ export class ContextDocsPanel {
57
60
  event = 'boot';
58
61
  lines = [];
59
62
  index = 0;
60
- /** Groups the user opened. Empty means every group is collapsed, which is how
61
- * the page opens: ~25 rows that name the whole corpus. */
63
+ /** Directories the user opened, by full name prefix. Empty means every level
64
+ * is collapsed, which is how the page opens: ~25 rows that name the whole
65
+ * corpus. */
62
66
  expanded = new Set();
63
67
  constructor(onChange) {
64
68
  this.onChange = onChange;
@@ -71,10 +75,11 @@ export class ContextDocsPanel {
71
75
  return undefined;
72
76
  return line.kind === 'doc' ? line.row : line.first;
73
77
  }
74
- /** Documents the current question selects — counted from the group rollups,
75
- * so a fully collapsed list still reports its size. */
78
+ /** Documents the current question selects — counted from the top-level
79
+ * rollups, which partition the corpus, so a fully collapsed list still
80
+ * reports its size and a nested rollup is never counted twice. */
76
81
  get visibleCount() {
77
- return this.lines.reduce((count, line) => count + (line.kind === 'group' ? line.docs : 0), 0);
82
+ return this.lines.reduce((count, line) => count + (line.kind === 'group' && line.depth === 0 ? line.docs : 0), 0);
78
83
  }
79
84
  setRows(rows, event, selectedKey) {
80
85
  this.rows = rows;
@@ -95,11 +100,17 @@ export class ContextDocsPanel {
95
100
  this.index = Math.max(0, Math.min(this.lines.length - 1, this.index + delta));
96
101
  this.onChange();
97
102
  }
98
- /** True when the focused line is a group, so the shell knows whether a
99
- * descend opens a group or the dossier. */
103
+ /** True when the focused line can be opened — a directory header. */
100
104
  get onGroup() {
101
105
  return this.lines[this.index]?.kind === 'group';
102
106
  }
107
+ /** True when the focused line IS a document rather than merely standing for
108
+ * one. A directory that is also a document is both, which is why the two
109
+ * gestures split there: space opens it, enter edits it. */
110
+ get onDocument() {
111
+ const line = this.lines[this.index];
112
+ return line !== undefined && (line.kind === 'doc' || line.isDoc);
113
+ }
103
114
  toggleGroup() {
104
115
  const line = this.lines[this.index];
105
116
  if (line?.kind !== 'group')
@@ -112,7 +123,9 @@ export class ContextDocsPanel {
112
123
  this.onChange();
113
124
  }
114
125
  /** Rebuild the drawn lines, keeping the cursor on the same document (or the
115
- * same group), and clamping when what it was on is gone. */
126
+ * same directory), and clamping when what it was on is gone. A document that
127
+ * owns a directory is drawn as that directory's header, so a held document
128
+ * key matches there too. */
116
129
  relayout(selectedKey, groupKey) {
117
130
  const held = selectedKey ?? (this.lines[this.index]?.kind === 'doc' ? this.lines[this.index].key : undefined);
118
131
  this.lines = buildListView(this.rows, this.filters, this.expanded, this.event);
@@ -120,7 +133,7 @@ export class ContextDocsPanel {
120
133
  ? this.lines.findIndex((line) => line.kind === 'group' && line.key === groupKey)
121
134
  : held === undefined
122
135
  ? -1
123
- : this.lines.findIndex((line) => line.kind === 'doc' && line.key === held);
136
+ : this.lines.findIndex((line) => (line.kind === 'doc' && line.key === held) || (line.kind === 'group' && line.isDoc && line.first.key === held));
124
137
  this.index = wanted >= 0 ? wanted : Math.max(0, Math.min(this.index, this.lines.length - 1));
125
138
  }
126
139
  render(width, height, focused, plans) {
@@ -140,7 +153,7 @@ export class ContextDocsPanel {
140
153
  const range = visibleRange(this.lines.length, this.index, capacity);
141
154
  for (let at = range.start; at < range.end; at += 1) {
142
155
  const line = this.lines[at];
143
- const body = line.kind === 'group' ? this.groupLine(line, nameRoom) : this.docLine(line, nameRoom, plans);
156
+ const body = line.kind === 'group' ? this.groupLine(line, nameRoom, plans) : this.docLine(line, nameRoom, plans);
144
157
  out.push(at === this.index && focused
145
158
  ? theme.bg('selectedBg', padAnsi(body, width))
146
159
  : at === this.index
@@ -149,9 +162,12 @@ export class ContextDocsPanel {
149
162
  }
150
163
  return this.fit(out, height);
151
164
  }
152
- groupLine(line, nameRoom) {
165
+ /** A directory header. Its stats and cost are the whole subtree's; the matrix
166
+ * is drawn only when the directory is itself a document, which is what marks
167
+ * the one row as both a document and a directory. */
168
+ groupLine(line, nameRoom, plans) {
153
169
  const marker = line.expanded ? '▾' : '▸';
154
- const label = `${marker} ${theme.bold(line.label)}`;
170
+ const label = `${' '.repeat(line.depth * INDENT)}${marker} ${theme.bold(line.label)}`;
155
171
  const marks = ['content', 'preview', 'name']
156
172
  .filter((rung) => line.rungs[rung] > 0)
157
173
  .map((rung) => `${line.rungs[rung]}${RUNG_MARK[rung]}`)
@@ -159,15 +175,12 @@ export class ContextDocsPanel {
159
175
  const stats = theme.fg('dim', marks === '' ? plural(line.docs, 'doc', 'docs') : `${line.docs} · ${marks}`);
160
176
  const room = Math.max(1, nameRoom - visibleWidth(stats) - 1);
161
177
  const field = `${padAnsi(truncateToWidth(label, room), room)} ${stats}`;
162
- return ` ${padAnsi(field, nameRoom)} ${cost(line.cost)} ${' '.repeat(MATRIX_WIDTH)}`;
178
+ const matrix = line.isDoc ? matrixOf(line.first, plans) : ' '.repeat(MATRIX_WIDTH);
179
+ return ` ${padAnsi(field, nameRoom)} ${cost(line.cost)} ${matrix}`;
163
180
  }
164
181
  docLine(line, nameRoom, plans) {
165
- const matrix = SURFACE_EVENTS.map((event) => {
166
- const state = plans.peek(event) === undefined ? 'pending' : cellStateOf(line.row.records.get(event));
167
- return cellGlyph(line.row.records.get(event), state);
168
- }).join(' ');
169
- const label = truncateToWidth(` ${line.label}`, nameRoom);
170
- return ` ${padAnsi(label, nameRoom)} ${cost(line.cost)} ${matrix}`;
182
+ const label = truncateToWidth(`${' '.repeat(line.depth * INDENT)}${line.label}`, nameRoom);
183
+ return ` ${padAnsi(label, nameRoom)} ${cost(line.cost)} ${matrixOf(line.row, plans)}`;
171
184
  }
172
185
  /** Pad rather than trim: this column sets the length of the divider drawn
173
186
  * beside it. */
@@ -177,6 +190,13 @@ export class ContextDocsPanel {
177
190
  return lines.slice(0, height).map((line) => (visibleWidth(line) === 0 ? '' : line));
178
191
  }
179
192
  }
193
+ /** One document's six events, in the order the heading names them. */
194
+ function matrixOf(row, plans) {
195
+ return SURFACE_EVENTS.map((event) => {
196
+ const state = plans.peek(event) === undefined ? 'pending' : cellStateOf(row.records.get(event));
197
+ return cellGlyph(row.records.get(event), state);
198
+ }).join(' ');
199
+ }
180
200
  /** Right-flushed into the cost column, dim because it is an estimate. */
181
201
  function cost(count) {
182
202
  const text = formatCost(count);
@@ -17,34 +17,45 @@ export interface Filters {
17
17
  query: string;
18
18
  }
19
19
  export declare const NO_FILTERS: Filters;
20
- /** One drawn line. A group carries what its members add up to, so a collapsed
21
- * list still states where the corpus and its cost sit. */
20
+ /** One drawn line. A group carries what its whole subtree adds up to, so a
21
+ * collapsed list still states where the corpus and its cost sit. `depth` is
22
+ * how many name segments sit above the line, which the renderer spends on
23
+ * indent. */
22
24
  export type ListLine = {
23
25
  kind: 'group';
24
26
  key: string;
25
27
  label: string;
28
+ depth: number;
26
29
  docs: number;
27
30
  rungs: Record<Exclude<Rung, 'none'>, number>;
28
31
  cost: number;
29
32
  expanded: boolean;
30
- /** The first member, so the dossier has a document to describe while the
31
- * cursor rests on the header. */
33
+ /** The document the dossier describes while the cursor rests on the
34
+ * header: this directory's own document when it has one, else the first
35
+ * document in its subtree. */
32
36
  first: DocRow;
37
+ /** True when the directory is itself a document — `first` is that
38
+ * document, and the one row is both selectable and expandable. */
39
+ isDoc: boolean;
33
40
  } | {
34
41
  kind: 'doc';
35
42
  key: string;
36
43
  row: DocRow;
37
44
  label: string;
45
+ depth: number;
38
46
  cost: number;
39
47
  };
40
- export declare function groupKeyOf(name: string): string;
41
- /** Every group key the rows can produce, so collapse state can drop the keys a
42
- * re-plan removed instead of accumulating them. */
48
+ /** Every group key the rows can produce — one per proper name prefix, since a
49
+ * prefix exists only because something sits under it — so collapse state can
50
+ * drop the keys a re-plan removed instead of accumulating them. The leading
51
+ * section is a key only when a name with no `/` owns no directory, which is
52
+ * exactly when it is not itself one of those prefixes. */
43
53
  export declare function groupKeysOf(rows: readonly DocRow[]): Set<string>;
44
- /** The lines to draw, in corpus order: the leading section, then each group in
45
- * order of first appearance. A live query force-expands every group holding a
46
- * match without touching `expanded`, so clearing it restores what the user
47
- * collapsed. */
54
+ /** The lines to draw, in corpus order: the leading section, then each directory
55
+ * in order of first appearance, each recursing into its own members only when
56
+ * it is open. A live query force-expands every directory, which is exactly the
57
+ * ancestor chain of every surviving match, without touching `expanded` — so
58
+ * clearing the query restores what the user collapsed. */
48
59
  export declare function buildListView(rows: readonly DocRow[], filters: Filters, expanded: ReadonlySet<string>, event: SurfaceEvent): ListLine[];
49
60
  /** What the whole filtered set costs, split by the rung it arrives at. */
50
61
  export declare function costTotals(rows: readonly DocRow[], event: SurfaceEvent): {
@@ -4,78 +4,137 @@
4
4
  import { fuzzyMatch } from '../../../../core/tui/fuzzy.js';
5
5
  import { deliveredCost, rungCostsOf } from './model.js';
6
6
  export const NO_FILTERS = { rung: 'any', origin: 'any', kind: 'any', state: 'any', query: '' };
7
- /** Documents whose canonical name carries no `/` share one leading section
8
- * rather than becoming a crowd of one-member groups. */
7
+ /** Documents whose canonical name carries no `/` and owns no directory share
8
+ * one leading section rather than becoming a crowd of one-member groups. */
9
9
  const LEADING_KEY = '';
10
10
  const LEADING_LABEL = 'top level';
11
- export function groupKeyOf(name) {
12
- const cut = name.indexOf('/');
13
- return cut < 0 ? LEADING_KEY : name.slice(0, cut);
14
- }
15
- /** Every group key the rows can produce, so collapse state can drop the keys a
16
- * re-plan removed instead of accumulating them. */
11
+ /** Every group key the rows can produce — one per proper name prefix, since a
12
+ * prefix exists only because something sits under it — so collapse state can
13
+ * drop the keys a re-plan removed instead of accumulating them. The leading
14
+ * section is a key only when a name with no `/` owns no directory, which is
15
+ * exactly when it is not itself one of those prefixes. */
17
16
  export function groupKeysOf(rows) {
18
- return new Set(rows.map((row) => groupKeyOf(row.name)));
17
+ const keys = new Set();
18
+ const unslashed = [];
19
+ for (const row of rows) {
20
+ if (row.name.includes('/')) {
21
+ for (let cut = row.name.indexOf('/'); cut >= 0; cut = row.name.indexOf('/', cut + 1)) {
22
+ keys.add(row.name.slice(0, cut));
23
+ }
24
+ }
25
+ else
26
+ unslashed.push(row.name);
27
+ }
28
+ if (unslashed.some((name) => !keys.has(name)))
29
+ keys.add(LEADING_KEY);
30
+ return keys;
19
31
  }
20
- /** The lines to draw, in corpus order: the leading section, then each group in
21
- * order of first appearance. A live query force-expands every group holding a
22
- * match without touching `expanded`, so clearing it restores what the user
23
- * collapsed. */
32
+ /** The lines to draw, in corpus order: the leading section, then each directory
33
+ * in order of first appearance, each recursing into its own members only when
34
+ * it is open. A live query force-expands every directory, which is exactly the
35
+ * ancestor chain of every surviving match, without touching `expanded` — so
36
+ * clearing the query restores what the user collapsed. */
24
37
  export function buildListView(rows, filters, expanded, event) {
25
- const groups = new Map();
26
- const order = [];
38
+ const root = { key: '', segment: '', depth: -1, children: new Map(), selves: [] };
27
39
  for (const row of rows) {
28
40
  if (!passes(row, row.records.get(event), filters))
29
41
  continue;
30
- const key = groupKeyOf(row.name);
31
- const held = groups.get(key);
32
- if (held === undefined) {
33
- groups.set(key, [row]);
34
- order.push(key);
42
+ let dir = root;
43
+ for (const segment of row.name.split('/')) {
44
+ let child = dir.children.get(segment);
45
+ if (child === undefined) {
46
+ child = {
47
+ key: dir.depth < 0 ? segment : `${dir.key}/${segment}`,
48
+ segment,
49
+ depth: dir.depth + 1,
50
+ children: new Map(),
51
+ selves: [],
52
+ };
53
+ dir.children.set(segment, child);
54
+ }
55
+ dir = child;
35
56
  }
36
- else
37
- held.push(row);
57
+ dir.selves.push(row);
38
58
  }
39
- if (groups.has(LEADING_KEY)) {
40
- order.splice(order.indexOf(LEADING_KEY), 1);
41
- order.unshift(LEADING_KEY);
59
+ const leading = [];
60
+ const dirs = [];
61
+ for (const child of root.children.values()) {
62
+ if (child.children.size === 0)
63
+ leading.push(...child.selves);
64
+ else
65
+ dirs.push(child);
42
66
  }
43
67
  const lines = [];
44
- for (const key of order) {
45
- const members = groups.get(key);
46
- const rungs = { name: 0, preview: 0, content: 0 };
47
- let cost = 0;
48
- for (const row of members) {
49
- const record = row.records.get(event);
50
- if (record !== undefined && record.finalRung !== 'none')
51
- rungs[record.finalRung] += 1;
52
- cost += deliveredCost(record);
53
- }
54
- const open = filters.query !== '' || expanded.has(key);
68
+ if (leading.length > 0) {
69
+ const open = filters.query !== '' || expanded.has(LEADING_KEY);
55
70
  lines.push({
56
71
  kind: 'group',
57
- key,
58
- label: key === LEADING_KEY ? LEADING_LABEL : `${key}/`,
59
- docs: members.length,
60
- rungs,
61
- cost,
72
+ key: LEADING_KEY,
73
+ label: LEADING_LABEL,
74
+ depth: 0,
75
+ ...rollup(leading, event),
62
76
  expanded: open,
63
- first: members[0],
77
+ first: leading[0],
78
+ isDoc: false,
64
79
  });
65
- if (!open)
66
- continue;
67
- for (const row of members) {
68
- lines.push({
69
- kind: 'doc',
70
- key: row.key,
71
- row,
72
- label: key === LEADING_KEY ? row.name : row.name.slice(key.length + 1),
73
- cost: deliveredCost(row.records.get(event)),
74
- });
75
- }
80
+ if (open)
81
+ for (const row of leading)
82
+ lines.push(docLine(row, row.name, 1, event));
76
83
  }
84
+ for (const dir of dirs)
85
+ emit(dir, filters, expanded, event, lines);
77
86
  return lines;
78
87
  }
88
+ /** A directory's rows in drawn order: its own document first, then each child
89
+ * segment's subtree — so the head of the list is also the document the header
90
+ * hands the dossier. */
91
+ function subtree(dir, out = []) {
92
+ out.push(...dir.selves);
93
+ for (const child of dir.children.values())
94
+ subtree(child, out);
95
+ return out;
96
+ }
97
+ function emit(dir, filters, expanded, event, lines) {
98
+ if (dir.children.size === 0) {
99
+ for (const row of dir.selves)
100
+ lines.push(docLine(row, dir.segment, dir.depth, event));
101
+ return;
102
+ }
103
+ const members = subtree(dir);
104
+ const open = filters.query !== '' || expanded.has(dir.key);
105
+ lines.push({
106
+ kind: 'group',
107
+ key: dir.key,
108
+ label: `${dir.segment}/`,
109
+ depth: dir.depth,
110
+ ...rollup(members, event),
111
+ expanded: open,
112
+ first: members[0],
113
+ isDoc: dir.selves.length > 0,
114
+ });
115
+ if (!open)
116
+ return;
117
+ // The header already stands for the first document named exactly this path;
118
+ // a second store answering the same name still needs its own row.
119
+ for (const shadowed of dir.selves.slice(1))
120
+ lines.push(docLine(shadowed, dir.segment, dir.depth + 1, event));
121
+ for (const child of dir.children.values())
122
+ emit(child, filters, expanded, event, lines);
123
+ }
124
+ function docLine(row, label, depth, event) {
125
+ return { kind: 'doc', key: row.key, row, label, depth, cost: deliveredCost(row.records.get(event)) };
126
+ }
127
+ function rollup(members, event) {
128
+ const rungs = { name: 0, preview: 0, content: 0 };
129
+ let cost = 0;
130
+ for (const row of members) {
131
+ const record = row.records.get(event);
132
+ if (record !== undefined && record.finalRung !== 'none')
133
+ rungs[record.finalRung] += 1;
134
+ cost += deliveredCost(record);
135
+ }
136
+ return { docs: members.length, rungs, cost };
137
+ }
79
138
  /** What the whole filtered set costs, split by the rung it arrives at. */
80
139
  export function costTotals(rows, event) {
81
140
  const byRung = { name: 0, preview: 0, content: 0 };
@@ -90,6 +90,9 @@ export declare class ContextAdminShell implements Component, Focusable {
90
90
  private handleModal;
91
91
  private renderModal;
92
92
  private footer;
93
+ /** What the focused row does when opened. A directory answers to space, a
94
+ * document to enter, and a directory that is also a document to both. */
95
+ private docsGesture;
93
96
  render(width: number): string[];
94
97
  /** What the delivering documents spend on this event. An estimate, and a
95
98
  * floor on the rendered prompt: boot wraps these documents in its own intro
@@ -228,7 +228,9 @@ export class ContextAdminShell {
228
228
  this.syncDetail();
229
229
  }
230
230
  else if (this.isDescend(data)) {
231
- if (this.docs.onGroup) {
231
+ // A directory that is also a document keeps both gestures: space opens it,
232
+ // and descending goes to its dossier like any other document.
233
+ if (this.docs.onGroup && !this.docs.onDocument) {
232
234
  this.docs.toggleGroup();
233
235
  this.syncDetail();
234
236
  }
@@ -648,7 +650,7 @@ export class ContextAdminShell {
648
650
  const left = this.zone === 'rail'
649
651
  ? `${move} row · space change · enter documents`
650
652
  : this.zone === 'docs'
651
- ? `${move} row · space ${this.docs.onGroup ? 'open group' : 'group'} · / search · enter dossier`
653
+ ? `${move} row · ${this.docsGesture()} · / search`
652
654
  : `${move} field · enter edit · e add · x remove · g gate · b body · u unlisted · s slash · m cap · r rename`;
653
655
  const right = `${this.label('crtr.setup.tab-next', 'tab')} event · ${this.label('crtr.setup.cancel', 'q')} close`;
654
656
  const gap = width - visibleWidth(left) - visibleWidth(right);
@@ -656,6 +658,13 @@ export class ContextAdminShell {
656
658
  ? `${theme.fg('dim', left)}${' '.repeat(gap)}${theme.fg('dim', right)}`
657
659
  : theme.fg('dim', truncateToWidth(`${left} · ${this.label('crtr.setup.cancel', 'q')} close`, width));
658
660
  }
661
+ /** What the focused row does when opened. A directory answers to space, a
662
+ * document to enter, and a directory that is also a document to both. */
663
+ docsGesture() {
664
+ if (!this.docs.onGroup)
665
+ return 'enter dossier';
666
+ return this.docs.onDocument ? 'space open · enter dossier' : 'space/enter open';
667
+ }
659
668
  render(width) {
660
669
  const height = this.terminalRows();
661
670
  const bodyHeight = Math.max(1, height - PAGE_CHROME_ROWS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.242",
3
+ "version": "0.3.243",
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.242",
3
+ "version": "0.3.243",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.242",
9
+ "version": "0.3.243",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {