tickmarkr 1.83.0 → 1.84.0

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,6 +1,21 @@
1
1
  import { type ReactElement, type ReactNode } from "react";
2
2
  import { TEXT_EMPHASIS_TOKENS } from "./theme.js";
3
3
  export { SPARKLINE_BUCKET_WINDOW } from "./derive.js";
4
+ /**
5
+ * The hover highlight's non-colour half: one cell wide, drawn in the separator
6
+ * the row already spends, from the block family the cockpit already draws with.
7
+ * Colour is the other half and the louder one, but Chalk suppresses inverse and
8
+ * colour together at level 0, so a highlight made of ink alone does not exist
9
+ * under `NO_COLOR` or a colourless TTY. This is a character; it always does.
10
+ */
11
+ export declare const HOVER_MARKER = "\u258C";
12
+ /**
13
+ * The columns below which a bordered panel no longer reads — the floor a band
14
+ * allocation defends, and the floor planFrame's session resize override clamps
15
+ * the body panel at: a drag surrenders an element whole or not at all, it
16
+ * never collapses one below what reads.
17
+ */
18
+ export declare const READABLE_PANEL_COLUMNS = 20;
4
19
  export declare const PANEL_CHROME_ROWS = 3;
5
20
  export declare const BAND_CONTINUATION_PREFIX = "\u21B3 ";
6
21
  export type BandColumnContent = {
@@ -98,11 +113,23 @@ export declare function StatusStrip({ items, width, }: {
98
113
  items: readonly StatusStripItem[];
99
114
  width: number;
100
115
  }): ReactElement;
101
- export declare function JournalRowPanel({ rows, width, focused, title, selection, }: {
116
+ export declare function JournalRowPanel({ rows, width, focused, title, selection, hover, }: {
102
117
  rows: readonly JournalRow[];
103
118
  width?: number | string;
104
119
  focused?: boolean;
105
120
  title?: string;
106
121
  /** Index of the row the pointer marks; undefined draws no pointer. */
107
122
  selection?: number;
123
+ /**
124
+ * Index of the row a live pointer is resting on; undefined draws no
125
+ * highlight, which is every surface nobody is pointing at — a capture above
126
+ * all. The highlight is ink AND shape: the brightest step of the existing
127
+ * data ramp worn inverted, plus {@link HOVER_MARKER} standing in the one
128
+ * separator cell the row already spends between its state glyph and its text.
129
+ * Ink alone would be no highlight at all under `NO_COLOR` or a colourless
130
+ * TTY, where Chalk suppresses inverse and colour alike; the marker is a drawn
131
+ * character, so it survives every colour level. It costs the row no cell it
132
+ * did not already own, so a hover shifts nothing an operator was reading.
133
+ */
134
+ hover?: number;
108
135
  }): ReactElement;
@@ -18,9 +18,23 @@ import { formatKeybarEntries, keybarEntries, SETUP_KEY_BINDINGS, } from "./keys.
18
18
  */
19
19
  import { cellWidth as stringWidth, sliceCells } from "./width.js";
20
20
  const SPARKLINE_GLYPHS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
21
+ /**
22
+ * The hover highlight's non-colour half: one cell wide, drawn in the separator
23
+ * the row already spends, from the block family the cockpit already draws with.
24
+ * Colour is the other half and the louder one, but Chalk suppresses inverse and
25
+ * colour together at level 0, so a highlight made of ink alone does not exist
26
+ * under `NO_COLOR` or a colourless TTY. This is a character; it always does.
27
+ */
28
+ export const HOVER_MARKER = "▌";
21
29
  const BAND_GAP_COLUMNS = 1;
22
30
  const PANEL_HORIZONTAL_CHROME = 4;
23
- const READABLE_PANEL_COLUMNS = 20;
31
+ /**
32
+ * The columns below which a bordered panel no longer reads — the floor a band
33
+ * allocation defends, and the floor planFrame's session resize override clamps
34
+ * the body panel at: a drag surrenders an element whole or not at all, it
35
+ * never collapses one below what reads.
36
+ */
37
+ export const READABLE_PANEL_COLUMNS = 20;
24
38
  export const PANEL_CHROME_ROWS = 3;
25
39
  export const BAND_CONTINUATION_PREFIX = "↳ ";
26
40
  const SETUP_KEYS = keybarEntries(SETUP_KEY_BINDINGS);
@@ -269,6 +283,8 @@ export function KeyRoster({ entries, width, }) {
269
283
  export function StatusStrip({ items, width, }) {
270
284
  return (_jsx(OneLineItems, { width: width, label: "cockpit status", children: items.map((item, index) => (_jsxs(Box, { flexShrink: 0, children: [index > 0 && _jsx(BodyText, { emphasis: "dim", children: " \u00B7 " }), _jsx(StateGlyph, { state: item.state }), _jsxs(BodyText, { children: [" ", item.text] })] }, `${item.state}:${item.text}`))) }));
271
285
  }
272
- export function JournalRowPanel({ rows, width, focused, title = "JOURNAL", selection, }) {
273
- return (_jsx(Panel, { title: title, focused: focused, width: width, children: rows.map((row, index) => (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "dim", children: row.time }), _jsx(BodyText, { children: " " }), _jsx(StateGlyph, { state: row.state }), _jsx(BodyText, { children: " " }), index === selection && (_jsxs(BodyText, { emphasis: "strong", children: [GLYPHS.pointer, " "] })), _jsx(BodyText, { children: row.text })] }, row.id))) }));
286
+ export function JournalRowPanel({ rows, width, focused, title = "JOURNAL", selection, hover, }) {
287
+ return (_jsx(Panel, { title: title, focused: focused, width: width, children: rows.map((row, index) => (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "dim", children: row.time }), _jsx(BodyText, { children: " " }), _jsx(StateGlyph, { state: row.state }), _jsx(BodyText, { emphasis: index === hover ? "strong" : "normal", children: index === hover ? HOVER_MARKER : " " }), index === selection && (_jsxs(BodyText, { emphasis: "strong", children: [GLYPHS.pointer, " "] })), index === hover
288
+ ? (_jsx(Text, { inverse: true, color: `ansi256(${COCKPIT_DATA_RAMP[0].xterm})`, children: row.text }))
289
+ : _jsx(BodyText, { children: row.text })] }, row.id))) }));
274
290
  }
@@ -87,11 +87,40 @@ export interface FrameState {
87
87
  detail?: boolean;
88
88
  /** Header caption, pre-measured in terminal cells by the caller. */
89
89
  captionCells?: number;
90
+ /**
91
+ * The session's panel resize override: the rail columns the operator dragged
92
+ * the boundary to. An INPUT to the plan, never a bypass of it — the plan
93
+ * recomputes from the measured size plus this override and the renderer
94
+ * draws the recomputed plan unmodified. It lives only in the running
95
+ * process: nothing writes it anywhere, and a relaunch plans the default
96
+ * layout. The plan clamps it at both panels' readable floors — see
97
+ * `planRailColumns`.
98
+ */
99
+ railColumns?: number;
90
100
  }
91
101
  export declare const FRAME_WIDTH_FLOOR = 40;
92
102
  export declare const FRAME_HEIGHT_FLOOR = 14;
93
103
  export declare const SIDEBAR_COLUMN_FLOOR = 64;
94
104
  export declare const RAIL_COLUMNS = 15;
105
+ /**
106
+ * The narrowest rail that still reads: the selection marker, one space and the
107
+ * longest view name, whole — the name measured in display cells by the width
108
+ * authority, never String length, so a name that ever gains a wide glyph moves
109
+ * the floor with it. A drag never takes the rail below it — below the floor
110
+ * the element stands at the floor, surrendered whole per the contract, never
111
+ * collapsed.
112
+ */
113
+ export declare const RAIL_READABLE_FLOOR_COLUMNS: number;
114
+ /**
115
+ * The rail width the plan draws for a session resize override, clamped at both
116
+ * panels' readable floors: the rail never narrows past its own floor, and
117
+ * never widens past what leaves the body its readable floor
118
+ * (`READABLE_PANEL_COLUMNS`). The clamp is the plan's — the override is a
119
+ * request, the floors are the contract, and an element is surrendered whole or
120
+ * not at all, never collapsed by a drag. No override is the contract's
121
+ * constant rail.
122
+ */
123
+ export declare function planRailColumns(columns: number, override?: number): number;
95
124
  export declare const STANDARD_TAIL_HEIGHT = 16;
96
125
  export declare const FULL_TAIL_HEIGHT = 24;
97
126
  export declare const TAIL_STANDARD_ROWS = 1;
@@ -127,7 +156,24 @@ export interface FrameFit {
127
156
  * ordering decision — not a comment about one.
128
157
  */
129
158
  export declare const FRAME_SURRENDER_FIT: Readonly<Record<SurrenderedRegionId, (fit: FrameFit) => boolean>>;
130
- export type FrameRegionId = "header" | "rule" | "strip" | "rail" | "body" | "rule2" | "tail" | "status" | "keybar" | "caption";
159
+ export type FrameRegionId = "header" | "rule" | "strip" | "rail" | "body" | "items" | "rule2" | "tail" | "status" | "keybar" | "caption";
160
+ /**
161
+ * Border plus horizontal padding the drawn panel charges around its body —
162
+ * half of it on each side. The plan owns this because the plan owns the item
163
+ * rows: the paint draws its list into the region planned below, and nothing
164
+ * downstream subtracts chrome of its own to find it again.
165
+ */
166
+ export declare const RUN_PANEL_HORIZONTAL_CHROME = 4;
167
+ /**
168
+ * Row bands drawn INSIDE another band rather than beside it, mapped to the band
169
+ * that hosts them. A nested band refines its host's rows, so the band walk skips
170
+ * it when tiling the height and asserts containment instead. It is no less a
171
+ * member of `rowSpans` for that: the oracle pins its span against the planned
172
+ * region, and demands the paint register a node at exactly that rectangle —
173
+ * which is precisely what a parallel field beside the plan could never be held
174
+ * to.
175
+ */
176
+ export declare const FRAME_NESTED_ROW_BANDS: Readonly<Partial<Record<FrameRegionId, FrameRegionId>>>;
131
177
  /** The vitals elements in block order, top first; a short rail surrenders from the top, so the meter is the last to go. */
132
178
  export declare const SIDEBAR_VITALS_ORDER: readonly ["tasks", "gates", "meter"];
133
179
  export type SidebarVitalsElement = (typeof SIDEBAR_VITALS_ORDER)[number];
@@ -139,6 +185,13 @@ export declare const SIDEBAR_VITALS_COMPACT_ELEMENT_ROWS = 1;
139
185
  export interface SidebarPlan {
140
186
  /** Rows the menu block occupies at the rail's top: the five views, plus the label row when it fits. */
141
187
  menuRows: number;
188
+ /**
189
+ * The frame row each view's name is drawn on. The plan does the menu's
190
+ * arithmetic once, here, where the label row is decided — so a rail hit is a
191
+ * lookup in the plan's own rows rather than a second reconstruction of the
192
+ * menu's shape from `menuRows`.
193
+ */
194
+ viewRows: Readonly<Record<FrameView, number>>;
142
195
  /**
143
196
  * Blank rows between the last menu row and the first vitals row. Never zero
144
197
  * while any vitals element draws — a short rail surrenders whole vitals
@@ -157,8 +210,14 @@ export interface SidebarPlan {
157
210
  * one gap row is the vitals budget, spent on the full two-row block when it
158
211
  * fits whole, else on the one-row elements, surrendered from the top of the
159
212
  * block with the meter surrendered last. An element draws whole or not at all.
213
+ * `railRow` is the frame row the rail band starts at, so the view rows the plan
214
+ * reports are frame rows a reported cell is compared against directly. It is
215
+ * required for that reason: a default here would answer rail-relative rows in a
216
+ * field documented as frame rows — the same defaulted-measurement defect the key
217
+ * delivery carried, and this one would land in the rows a rail hit resolves
218
+ * through.
160
219
  */
161
- export declare function planSidebar(railRows: number): SidebarPlan;
220
+ export declare function planSidebar(railRows: number, railRow: number): SidebarPlan;
162
221
  export interface FrameRegion {
163
222
  id: FrameRegionId;
164
223
  row: number;
@@ -182,8 +241,11 @@ export interface PlannedFrame {
182
241
  content: FrameBandContent;
183
242
  regions: readonly FrameRegion[];
184
243
  /**
185
- * Row bands, top to bottom; their spans tile the height exactly. A band
186
- * map: the nested caption sub-region is deliberately excluded.
244
+ * Row bands, top to bottom. `items` is nested inside the body band and so
245
+ * refines it rather than tiling beside it (see FRAME_NESTED_ROW_BANDS); the
246
+ * remaining spans tile the height exactly. The caption is excluded: it adds
247
+ * no row extent of its own, being the header's own row refined on the column
248
+ * axis, and the column map already carries no row.
187
249
  */
188
250
  rowSpans: Readonly<Record<string, number>>;
189
251
  /**
@@ -217,7 +279,9 @@ export type FramePlan = PlannedFrame | PlainFallbackPlan;
217
279
  * height exactly — header, rule, the view strip (40–63 columns), the body
218
280
  * band, a second rule, the journal tail surrendered whole by height tier,
219
281
  * status, keybar — and, at ≥64 columns, column bands that tile the width
220
- * exactly: the constant 15-column rail beside the body. The body is the sole
282
+ * exactly: the rail beside the body, at the constant 15 columns or at the
283
+ * session's dragged override clamped to both panels' readable floors
284
+ * (`planRailColumns`). The body is the sole
221
285
  * flexible element on both axes. The view and state decide what the flexible
222
286
  * bands draw (the Journal view is the full-height tail; SETUP turns the tail
223
287
  * into PENDING WRITES; an open detail owns the body) without moving any
@@ -231,7 +295,12 @@ export type FramePlan = PlannedFrame | PlainFallbackPlan;
231
295
  * of the block, the meter last, rather than closing the gap. rowSpans and
232
296
  * columnSpans are band maps: they deliberately exclude the nested caption,
233
297
  * and column bands are reported for the body band only, so each map's spans
234
- * sum to the full height and width respectively.
298
+ * sum to the full height and width respectively. The body band's item rows are
299
+ * a band of the plan in their own right — a region in `regions` and a span in
300
+ * `rowSpans`, nested inside the body band rather than tiling beside it — so the
301
+ * region the paint draws its list into and the region a pointer resolves a row
302
+ * through are the one thing the oracle pins, and neither subtracts the drawn
303
+ * panel's chrome for itself.
235
304
  */
236
305
  export declare function planFrame(size: MeasuredSize, view?: FrameView, state?: FrameState): FramePlan;
237
306
  export declare function resolveCockpitLayout(columns: number, rows: number): CockpitLayout;
@@ -1,3 +1,4 @@
1
+ import { PANEL_CHROME_ROWS, READABLE_PANEL_COLUMNS } from "./components.js";
1
2
  import { cellWidth } from "./width.js";
2
3
  export const COCKPIT_ROW_FLOOR = 14;
3
4
  export const COCKPIT_COLUMN_FLOOR = 40;
@@ -57,6 +58,30 @@ export const FRAME_WIDTH_FLOOR = COCKPIT_COLUMN_FLOOR;
57
58
  export const FRAME_HEIGHT_FLOOR = COCKPIT_ROW_FLOOR;
58
59
  export const SIDEBAR_COLUMN_FLOOR = 64;
59
60
  export const RAIL_COLUMNS = 15;
61
+ /**
62
+ * The narrowest rail that still reads: the selection marker, one space and the
63
+ * longest view name, whole — the name measured in display cells by the width
64
+ * authority, never String length, so a name that ever gains a wide glyph moves
65
+ * the floor with it. A drag never takes the rail below it — below the floor
66
+ * the element stands at the floor, surrendered whole per the contract, never
67
+ * collapsed.
68
+ */
69
+ export const RAIL_READABLE_FLOOR_COLUMNS = Math.max(...FRAME_VIEWS.map((view) => cellWidth(view))) + 2;
70
+ /**
71
+ * The rail width the plan draws for a session resize override, clamped at both
72
+ * panels' readable floors: the rail never narrows past its own floor, and
73
+ * never widens past what leaves the body its readable floor
74
+ * (`READABLE_PANEL_COLUMNS`). The clamp is the plan's — the override is a
75
+ * request, the floors are the contract, and an element is surrendered whole or
76
+ * not at all, never collapsed by a drag. No override is the contract's
77
+ * constant rail.
78
+ */
79
+ export function planRailColumns(columns, override) {
80
+ if (override === undefined || !Number.isFinite(override))
81
+ return RAIL_COLUMNS;
82
+ const ceiling = Math.max(RAIL_READABLE_FLOOR_COLUMNS, Math.floor(columns) - READABLE_PANEL_COLUMNS);
83
+ return Math.min(ceiling, Math.max(RAIL_READABLE_FLOOR_COLUMNS, Math.floor(override)));
84
+ }
60
85
  export const STANDARD_TAIL_HEIGHT = 16;
61
86
  export const FULL_TAIL_HEIGHT = 24;
62
87
  export const TAIL_STANDARD_ROWS = 1;
@@ -93,6 +118,23 @@ export const FRAME_SURRENDER_FIT = {
93
118
  caption: ({ captionCells, columns }) => captionCells >= 1 && captionCells <= columns - HEADER_FIXED_CELLS,
94
119
  tail: ({ tailRows }) => tailRows > 0,
95
120
  };
121
+ /**
122
+ * Border plus horizontal padding the drawn panel charges around its body —
123
+ * half of it on each side. The plan owns this because the plan owns the item
124
+ * rows: the paint draws its list into the region planned below, and nothing
125
+ * downstream subtracts chrome of its own to find it again.
126
+ */
127
+ export const RUN_PANEL_HORIZONTAL_CHROME = 4;
128
+ /**
129
+ * Row bands drawn INSIDE another band rather than beside it, mapped to the band
130
+ * that hosts them. A nested band refines its host's rows, so the band walk skips
131
+ * it when tiling the height and asserts containment instead. It is no less a
132
+ * member of `rowSpans` for that: the oracle pins its span against the planned
133
+ * region, and demands the paint register a node at exactly that rectangle —
134
+ * which is precisely what a parallel field beside the plan could never be held
135
+ * to.
136
+ */
137
+ export const FRAME_NESTED_ROW_BANDS = { items: "body" };
96
138
  /* ------------------------------------------------------------------ */
97
139
  /* The sidebar band's composition: the menu at the rail's top, the */
98
140
  /* vitals anchored to the rail's bottom, and at least one blank row */
@@ -113,17 +155,28 @@ export const SIDEBAR_VITALS_COMPACT_ELEMENT_ROWS = 1;
113
155
  * one gap row is the vitals budget, spent on the full two-row block when it
114
156
  * fits whole, else on the one-row elements, surrendered from the top of the
115
157
  * block with the meter surrendered last. An element draws whole or not at all.
158
+ * `railRow` is the frame row the rail band starts at, so the view rows the plan
159
+ * reports are frame rows a reported cell is compared against directly. It is
160
+ * required for that reason: a default here would answer rail-relative rows in a
161
+ * field documented as frame rows — the same defaulted-measurement defect the key
162
+ * delivery carried, and this one would land in the rows a rail hit resolves
163
+ * through.
116
164
  */
117
- export function planSidebar(railRows) {
165
+ export function planSidebar(railRows, railRow) {
118
166
  const menuRows = railRows >= SIDEBAR_MENU_LABEL_ROWS
119
167
  ? 1 + FRAME_VIEWS.length
120
168
  : FRAME_VIEWS.length;
169
+ // The views occupy the menu block's last rows: whatever the block exceeds
170
+ // them by is the label row the plan funded above them.
171
+ const firstViewRow = railRow + (menuRows - FRAME_VIEWS.length);
172
+ const viewRows = Object.fromEntries(FRAME_VIEWS.map((view, index) => [view, firstViewRow + index]));
121
173
  const fullRows = SIDEBAR_VITALS_ORDER.length * SIDEBAR_VITALS_FULL_ELEMENT_ROWS;
122
174
  // The gap is funded before the vitals are: the budget is what remains after it.
123
175
  const budget = railRows - menuRows - 1;
124
176
  if (budget >= fullRows) {
125
177
  return {
126
178
  menuRows,
179
+ viewRows,
127
180
  gapRows: railRows - menuRows - fullRows,
128
181
  vitalsMode: "full",
129
182
  vitals: SIDEBAR_VITALS_ORDER,
@@ -132,11 +185,29 @@ export function planSidebar(railRows) {
132
185
  const kept = Math.max(0, Math.min(SIDEBAR_VITALS_ORDER.length, Math.floor(budget)));
133
186
  return {
134
187
  menuRows,
188
+ viewRows,
135
189
  gapRows: kept > 0 ? railRows - menuRows - kept : 0,
136
190
  vitalsMode: "compact",
137
191
  vitals: SIDEBAR_VITALS_ORDER.slice(SIDEBAR_VITALS_ORDER.length - kept),
138
192
  };
139
193
  }
194
+ /**
195
+ * The item rows of a body band: the band less the chrome the drawn panel puts
196
+ * around its list — the border row and the title row above the first item, the
197
+ * border row below it, and a bordered padded column on each side. THE ONE
198
+ * PLACE that subtraction happens, and it happens in the plan: paint and hit
199
+ * resolution alike consume this region rather than deriving it again.
200
+ */
201
+ function planItemRows(body) {
202
+ const heading = PANEL_CHROME_ROWS - 1; // border row, then the title row
203
+ return {
204
+ id: "items",
205
+ row: body.row + heading,
206
+ rows: Math.max(1, body.rows - PANEL_CHROME_ROWS),
207
+ column: body.column + RUN_PANEL_HORIZONTAL_CHROME / 2,
208
+ columns: Math.max(1, body.columns - RUN_PANEL_HORIZONTAL_CHROME),
209
+ };
210
+ }
140
211
  function tailRowsFor(rows) {
141
212
  if (rows >= FULL_TAIL_HEIGHT)
142
213
  return TAIL_FULL_ROWS;
@@ -151,7 +222,9 @@ function tailRowsFor(rows) {
151
222
  * height exactly — header, rule, the view strip (40–63 columns), the body
152
223
  * band, a second rule, the journal tail surrendered whole by height tier,
153
224
  * status, keybar — and, at ≥64 columns, column bands that tile the width
154
- * exactly: the constant 15-column rail beside the body. The body is the sole
225
+ * exactly: the rail beside the body, at the constant 15 columns or at the
226
+ * session's dragged override clamped to both panels' readable floors
227
+ * (`planRailColumns`). The body is the sole
155
228
  * flexible element on both axes. The view and state decide what the flexible
156
229
  * bands draw (the Journal view is the full-height tail; SETUP turns the tail
157
230
  * into PENDING WRITES; an open detail owns the body) without moving any
@@ -165,7 +238,12 @@ function tailRowsFor(rows) {
165
238
  * of the block, the meter last, rather than closing the gap. rowSpans and
166
239
  * columnSpans are band maps: they deliberately exclude the nested caption,
167
240
  * and column bands are reported for the body band only, so each map's spans
168
- * sum to the full height and width respectively.
241
+ * sum to the full height and width respectively. The body band's item rows are
242
+ * a band of the plan in their own right — a region in `regions` and a span in
243
+ * `rowSpans`, nested inside the body band rather than tiling beside it — so the
244
+ * region the paint draws its list into and the region a pointer resolves a row
245
+ * through are the one thing the oracle pins, and neither subtracts the drawn
246
+ * panel's chrome for itself.
169
247
  */
170
248
  export function planFrame(size, view = "tasks", state = {}) {
171
249
  const columns = Math.floor(size.columns);
@@ -178,6 +256,9 @@ export function planFrame(size, view = "tasks", state = {}) {
178
256
  }
179
257
  const tab = state.tab ?? "watch";
180
258
  const band = columns >= SIDEBAR_COLUMN_FLOOR ? "sidebar" : "strip";
259
+ // The one panel the session can resize: the rail, at the operator's dragged
260
+ // override clamped to the readable floors, or the contract's constant rail.
261
+ const railColumns = planRailColumns(columns, state.railColumns);
181
262
  const stripRows = band === "strip" ? 1 : 0;
182
263
  const tailRows = tailRowsFor(rows);
183
264
  // Fixed chrome is five rows: header, rule, a second rule below the body
@@ -221,25 +302,21 @@ export function planFrame(size, view = "tasks", state = {}) {
221
302
  row: bodyRow,
222
303
  rows: bodyRows,
223
304
  column: 0,
224
- columns: RAIL_COLUMNS,
225
- });
226
- regions.push({
227
- id: "body",
228
- row: bodyRow,
229
- rows: bodyRows,
230
- column: RAIL_COLUMNS,
231
- columns: columns - RAIL_COLUMNS,
305
+ columns: railColumns,
232
306
  });
233
307
  }
234
- else {
235
- regions.push({
308
+ const bodyRegion = band === "sidebar"
309
+ ? {
236
310
  id: "body",
237
311
  row: bodyRow,
238
312
  rows: bodyRows,
239
- column: 0,
240
- columns,
241
- });
242
- }
313
+ column: railColumns,
314
+ columns: columns - railColumns,
315
+ }
316
+ : { id: "body", row: bodyRow, rows: bodyRows, column: 0, columns };
317
+ regions.push(bodyRegion);
318
+ const itemsRegion = planItemRows(bodyRegion);
319
+ regions.push(itemsRegion);
243
320
  if (captionFits) {
244
321
  regions.push({
245
322
  id: "caption",
@@ -253,13 +330,14 @@ export function planFrame(size, view = "tasks", state = {}) {
253
330
  if (stripRows > 0)
254
331
  rowSpans.strip = stripRows;
255
332
  rowSpans.body = bodyRows;
333
+ rowSpans.items = itemsRegion.rows;
256
334
  rowSpans.rule2 = 1;
257
335
  if (tailRows > 0)
258
336
  rowSpans.tail = tailRows;
259
337
  rowSpans.status = 1;
260
338
  rowSpans.keybar = 1;
261
339
  const columnSpans = band === "sidebar"
262
- ? { rail: RAIL_COLUMNS, body: columns - RAIL_COLUMNS }
340
+ ? { rail: railColumns, body: columns - railColumns }
263
341
  : { body: columns };
264
342
  return {
265
343
  kind: "frame",
@@ -275,7 +353,7 @@ export function planFrame(size, view = "tasks", state = {}) {
275
353
  rowSpans,
276
354
  columnSpans,
277
355
  flexible: { row: "body", column: "body" },
278
- sidebar: band === "sidebar" ? planSidebar(bodyRows) : null,
356
+ sidebar: band === "sidebar" ? planSidebar(bodyRows, bodyRow) : null,
279
357
  surrendered,
280
358
  };
281
359
  }
@@ -1,5 +1,6 @@
1
1
  import type { CockpitGraph, RunCockpitData, RunCockpitSource } from "./derive.js";
2
2
  import { type RunInteractionState, type RunKeyEvent } from "./keys.js";
3
+ import { type PointerReport, type PointerSurface } from "./pointer.js";
3
4
  /**
4
5
  * THE engagement selection rule — stated once, here, so it can change in one
5
6
  * place without touching the renderer or the command: an explicit engagement
@@ -46,6 +47,17 @@ export declare function reconcileLiveRunInteraction(interaction: RunInteractionS
46
47
  * one place rows enter the dispatcher, so no key can reach a journal row.
47
48
  */
48
49
  export declare function liveRunViewRowIds(interaction: RunInteractionState, data: RunCockpitData): readonly string[];
50
+ /**
51
+ * The suite's own planning of the surface a frame at this data, state and
52
+ * measured size commits — the same composition the renderer publishes through
53
+ * its commit seam, so a hit is tested against the frame production actually
54
+ * painted. The live input path never calls this: it resolves through the
55
+ * committed frame, and what nothing has painted yet has no geometry.
56
+ */
57
+ export declare function liveRunPointerSurface(data: RunCockpitData, interaction: RunInteractionState, size: {
58
+ readonly columns: number;
59
+ readonly rows: number;
60
+ }): PointerSurface | undefined;
49
61
  export type LiveCockpitSurface = {
50
62
  readonly data: RunCockpitData;
51
63
  readonly interaction: RunInteractionState;
@@ -62,7 +74,8 @@ export type LiveCockpitSurface = {
62
74
  export type LiveCockpitDelivery = {
63
75
  readonly snapshot: () => LiveCockpitSurface;
64
76
  readonly refresh: () => boolean;
65
- readonly key: (event: RunKeyEvent, columns?: number) => boolean;
77
+ readonly key: (event: RunKeyEvent) => boolean;
78
+ readonly pointer: (report: PointerReport) => boolean;
66
79
  readonly batch: <Result>(deliver: () => Result) => Result;
67
80
  };
68
81
  export declare function runLiveCockpit({ input, output, cwd, runId, binaryVersion, refreshMs, now, debug, onDelivery, }: {