tickmarkr 1.83.0 → 1.85.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.
Files changed (37) hide show
  1. package/dist/adapters/claude-code.d.ts +1 -0
  2. package/dist/adapters/claude-code.js +57 -1
  3. package/dist/adapters/fake.js +9 -0
  4. package/dist/adapters/types.d.ts +3 -0
  5. package/dist/adapters/types.js +21 -0
  6. package/dist/cli/commands/status.js +160 -30
  7. package/dist/compile/collateral.d.ts +86 -2
  8. package/dist/compile/collateral.js +294 -3
  9. package/dist/config/config.d.ts +62 -0
  10. package/dist/config/config.js +157 -2
  11. package/dist/drivers/herdr.d.ts +20 -3
  12. package/dist/drivers/herdr.js +288 -105
  13. package/dist/gates/baseline.d.ts +1 -0
  14. package/dist/gates/baseline.js +91 -13
  15. package/dist/gates/review.d.ts +7 -0
  16. package/dist/gates/review.js +99 -6
  17. package/dist/gates/run-gates.d.ts +9 -0
  18. package/dist/gates/run-gates.js +285 -41
  19. package/dist/run/daemon.d.ts +48 -2
  20. package/dist/run/daemon.js +1417 -315
  21. package/dist/run/journal.d.ts +56 -3
  22. package/dist/run/journal.js +275 -1
  23. package/dist/run/stall.d.ts +35 -1
  24. package/dist/run/stall.js +118 -8
  25. package/dist/tui/cockpit/components.d.ts +30 -1
  26. package/dist/tui/cockpit/components.js +19 -3
  27. package/dist/tui/cockpit/derive.d.ts +29 -2
  28. package/dist/tui/cockpit/derive.js +219 -23
  29. package/dist/tui/cockpit/layout.d.ts +75 -6
  30. package/dist/tui/cockpit/layout.js +97 -19
  31. package/dist/tui/cockpit/live.d.ts +14 -1
  32. package/dist/tui/cockpit/live.js +223 -29
  33. package/dist/tui/cockpit/pointer.d.ts +261 -0
  34. package/dist/tui/cockpit/pointer.js +610 -0
  35. package/dist/tui/cockpit/run-cockpit.d.ts +36 -5
  36. package/dist/tui/cockpit/run-cockpit.js +270 -51
  37. package/package.json +1 -1
@@ -1,16 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, useApp, useStdout } from "ink";
3
- import { cloneElement, useInsertionEffect, useMemo, useRef, } from "react";
3
+ import { cloneElement, createContext, useContext, useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore, } from "react";
4
4
  import { GLYPHS, PLAIN_COMPACT_LOCKUP, } from "../../brand.js";
5
5
  import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel, keyRosterLines, Panel, PANEL_CHROME_ROWS, ProgressMeter, Sparkline, StatTile, StatusStrip, } from "./components.js";
6
6
  import { fieldReading } from "./derive.js";
7
7
  import { initialRunInteractionState, projectRunKeyEntries, reconcileRunInteraction, RUN_SIDE_RAIL_COLUMN_FLOOR, runPanelFocusOrder, runSideRailVisible, selectableRunViewRowIds, } from "./keys.js";
8
8
  import { DECISIONS_TAB_TITLE, RUN_VIEWS, runViewById, setupSectionAt, SETUP_SECTIONS, } from "./views.js";
9
- import { FRAME_HEADER_GAP, FULL_JOURNAL_ROWS, planFrame, SIDEBAR_COLUMN_FLOOR, } from "./layout.js";
9
+ import { FRAME_HEADER_GAP, FRAME_NESTED_ROW_BANDS, FULL_JOURNAL_ROWS, planFrame, RUN_PANEL_HORIZONTAL_CHROME, SIDEBAR_COLUMN_FLOOR, } from "./layout.js";
10
+ import { pointerHoverRow, pointerRestingCell, sessionRailOverride, watchPointerRest, watchSessionRailOverride, } from "./pointer.js";
10
11
  import { cellWidth, fitCells, wrapCells } from "./width.js";
11
12
  export { deriveRunCockpitData } from "./derive.js";
12
13
  export { PANEL_CHROME_ROWS } from "./components.js";
13
- /** The width focus and key routing resolve at. */
14
+ /**
15
+ * The width focus and key routing resolve at, from a measured width — the form
16
+ * used before a plan exists. Once one does, `plannedKeyColumns` (pointer.ts)
17
+ * reads the same answer off the bands the plan actually drew.
18
+ */
14
19
  export function runKeyColumns(columns) {
15
20
  return columns >= SIDEBAR_COLUMN_FLOOR
16
21
  ? Math.max(columns, RUN_SIDE_RAIL_COLUMN_FLOOR)
@@ -48,35 +53,111 @@ function IdentityHeader({ data, columns, secondary = true, }) {
48
53
  {
49
54
  title: "",
50
55
  lines: [
51
- `${data.runId} · ${data.branch} ${data.status} · ${data.elapsed}`,
56
+ frameCaption(data),
52
57
  ],
53
58
  },
54
59
  ];
55
60
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(NarrowHeaderBand, { columns: columns, content: primary, rightStrong: true }), secondary && (_jsx(NarrowHeaderBand, { columns: columns, content: secondaryBand }))] }));
56
61
  }
57
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[0] ?? "tickmarkr" }), _jsxs(BodyText, { emphasis: "strong", children: ["v", data.binaryVersion, " \u00B7 binary ", GLYPHS.pass] })] }), secondary && (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[1] ?? "tickmarkr" }), _jsxs(BodyText, { children: [data.runId, " \u00B7 ", data.branch, " ", data.status, " \u00B7 ", data.elapsed] })] }))] }));
62
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[0] ?? "tickmarkr" }), _jsxs(BodyText, { emphasis: "strong", children: ["v", data.binaryVersion, " \u00B7 binary ", GLYPHS.pass] })] }), secondary && (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[1] ?? "tickmarkr" }), _jsx(BodyText, { children: frameCaption(data) })] }))] }));
58
63
  }
59
64
  function NarrowHeaderBand({ columns, content, rightStrong = false, }) {
60
65
  const widths = allocateBandColumns(columns, content, NARROW_HEADER_ALLOCATION);
61
66
  return (_jsxs(CockpitGrid, { columns: columns, columnContents: content, allocation: NARROW_HEADER_ALLOCATION, children: [_jsx(BandLines, { columns: Math.max(1, widths[0]), lines: content[0].lines, emphasis: "strong" }), _jsx(Box, { alignItems: "flex-end", flexDirection: "column", children: _jsx(BandLines, { columns: Math.max(1, widths[1]), lines: content[1].lines, emphasis: rightStrong ? "strong" : "normal" }) })] }));
62
67
  }
63
68
  const RUN_SIDE_RAIL_WIDTH = 16;
64
- /** Border plus horizontal padding charged by Panel around its body. */
65
- const RUN_PANEL_HORIZONTAL_CHROME = 4;
69
+ function runInstant(runId) {
70
+ const match = runId.match(/\brun-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})/u);
71
+ return match === null
72
+ ? new Date()
73
+ : new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), Number(match[6]));
74
+ }
75
+ function utcClock(value) {
76
+ const match = value.match(/^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/u);
77
+ return match === null
78
+ ? undefined
79
+ : [Number(match[1]), Number(match[2]), Number(match[3])];
80
+ }
81
+ function localZoneLabel(data) {
82
+ const newestTimestamp = data.journalRows.find((row) => row.timestamp !== undefined)?.timestamp;
83
+ const recorded = newestTimestamp === undefined ? undefined : new Date(newestTimestamp);
84
+ const reference = recorded !== undefined && Number.isFinite(recorded.getTime())
85
+ ? recorded
86
+ : runInstant(data.runId);
87
+ const offset = -reference.getTimezoneOffset();
88
+ if (offset === 0)
89
+ return "UTC";
90
+ const sign = offset < 0 ? "-" : "+";
91
+ const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0");
92
+ const minutes = Math.abs(offset) % 60;
93
+ return minutes === 0
94
+ ? `${sign}${hours}`
95
+ : `${sign}${hours}:${String(minutes).padStart(2, "0")}`;
96
+ }
97
+ function localClockTime(value, timestamp) {
98
+ if (utcClock(value) === undefined)
99
+ return value;
100
+ if (timestamp === undefined)
101
+ return value;
102
+ const completeInstant = new Date(timestamp);
103
+ if (!Number.isFinite(completeInstant.getTime()))
104
+ return value;
105
+ const hours = String(completeInstant.getHours()).padStart(2, "0");
106
+ const minutes = String(completeInstant.getMinutes()).padStart(2, "0");
107
+ const seconds = String(completeInstant.getSeconds()).padStart(2, "0");
108
+ return `${hours}:${minutes}:${seconds}`;
109
+ }
110
+ /** Name the ruler an already-built caption's bare attempt number is counted on. */
111
+ function nameAttemptRuler(caption) {
112
+ return caption.replace(/(?<!engagement |run )\battempt (?=\d)/gu, "engagement attempt ");
113
+ }
114
+ /**
115
+ * The fold owns active-task identity and full event order. This surface only names the attempt
116
+ * ruler and fits that authoritative caption; TaskRows are never ranked into a competing answer.
117
+ */
118
+ function engagementProgressCaption(data, columns = Number.POSITIVE_INFINITY) {
119
+ const caption = nameAttemptRuler(data.progressCaption);
120
+ if (cellWidth(caption) <= columns)
121
+ return caption;
122
+ const actorSeparator = caption.lastIndexOf(" · ");
123
+ return actorSeparator < 0
124
+ ? caption
125
+ : `${caption.slice(0, actorSeparator)} ${caption.slice(actorSeparator + 3)}`;
126
+ }
127
+ function runAttemptLabel(attempts) {
128
+ return attempts === 1
129
+ ? "run attempt 1"
130
+ : `run attempts ${fieldReading(attempts)}`;
131
+ }
66
132
  /** The header caption: the run's identity line, planned as the header's refinable sub-region. */
67
133
  function frameCaption(data) {
68
- return `${data.runId} · ${data.branch} · ${data.status} · ${data.elapsed}`;
134
+ return `${data.runId} · ${data.branch} · ${data.status} · ${data.elapsed} · zone ${localZoneLabel(data)}`;
69
135
  }
70
136
  /** The header's fixed left part: brand, version and the two tab labels. */
71
- function headerLead(data, plan) {
137
+ function headerLead(data, plan, columns) {
72
138
  const tabs = plan.tab === "watch"
73
139
  ? `[ WATCH ]${FRAME_HEADER_GAP}${DECISIONS_TAB_TITLE}`
74
140
  : `WATCH${FRAME_HEADER_GAP}[ ${DECISIONS_TAB_TITLE} ]`;
75
- return `tickmarkr ${data.binaryVersion}${FRAME_HEADER_GAP}${tabs}`;
141
+ if (plan.regions.some((region) => region.id === "caption")) {
142
+ return `tickmarkr ${data.binaryVersion}${FRAME_HEADER_GAP}${tabs}`;
143
+ }
144
+ const withVersion = `tickmarkr ${data.binaryVersion}${FRAME_HEADER_GAP}${tabs} · zone ${localZoneLabel(data)}`;
145
+ const compactTabs = plan.tab === "watch"
146
+ ? `[WATCH]${DECISIONS_TAB_TITLE}`
147
+ : `WATCH[${DECISIONS_TAB_TITLE}]`;
148
+ const compact = `tickmarkr ${data.binaryVersion}${compactTabs} zone ${localZoneLabel(data)}`;
149
+ if (cellWidth(withVersion) <= columns)
150
+ return withVersion;
151
+ if (cellWidth(compact) <= columns)
152
+ return compact;
153
+ const markedTabs = plan.tab === "watch"
154
+ ? `>WATCH${DECISIONS_TAB_TITLE}`
155
+ : `WATCH>${DECISIONS_TAB_TITLE}`;
156
+ return `tickmarkr ${data.binaryVersion}${markedTabs} zone ${localZoneLabel(data)}`;
76
157
  }
77
158
  /** The one plain header line: brand, version and the two tab labels, the active one marked. */
78
159
  function PlannedHeader({ data, plan, columns, }) {
79
- return (_jsx(BodyText, { emphasis: "strong", children: fitCells(headerLead(data, plan), Math.max(1, columns)) }));
160
+ return (_jsx(BodyText, { emphasis: "strong", children: fitCells(headerLead(data, plan, columns), Math.max(1, columns)) }));
80
161
  }
81
162
  /** The header's caption sub-region: the run identity, right-aligned in it. */
82
163
  function PlannedCaption({ data, columns, }) {
@@ -114,7 +195,37 @@ function setupSectionLines(section, data) {
114
195
  const parked = data.taskRows.filter((row) => row.state === "human");
115
196
  if (parked.length === 0)
116
197
  return ["No parked decisions in this engagement"];
117
- return parked.map((row) => `${row.taskId} · parked · attempt ${row.attempts ?? 0}`);
198
+ return parked.map((row) => `${row.taskId} · parked · ${runAttemptLabel(row.attempts)}`);
199
+ }
200
+ /**
201
+ * The plan's item rows, and the ref the frame registers their painted node
202
+ * through. Every body panel draws its list into this one box, so the rectangle
203
+ * the conformance oracle measures off the paint is the rectangle planFrame
204
+ * planned — which is exactly the pin a field standing beside the plan could
205
+ * never carry. Absent on the unsized legacy surface, which has no plan.
206
+ */
207
+ const ItemRowsBand = createContext(undefined);
208
+ /**
209
+ * The identity of the row a live pointer is resting on, or none. It is drawn
210
+ * state and only drawn state: nothing reads it back, no interaction state
211
+ * carries it, and the one provider is fed by a `hover` prop the frame's own
212
+ * callers supply. A capture supplies none — `capture.ts` builds this frame from
213
+ * data, size and interaction alone — so every golden, anchor and colour capture
214
+ * records the pointerless appearance by construction rather than by a guard
215
+ * stripping a highlight back out afterwards.
216
+ */
217
+ const HoveredRow = createContext(null);
218
+ /**
219
+ * A body panel's list, drawn into the plan's own item rows and nowhere else.
220
+ * The band is consumed by the outermost panel that asks for it — a panel nested
221
+ * inside that one (the RUN view's own journal) is part of the list, not a second
222
+ * claim on the region, so the band is cleared for everything below.
223
+ */
224
+ function ItemRows({ children }) {
225
+ const band = useContext(ItemRowsBand);
226
+ if (band === undefined)
227
+ return _jsx(_Fragment, { children: children });
228
+ return (_jsx(Box, { ref: band.regionRef, flexDirection: "column", height: band.region.rows, width: band.region.columns, flexShrink: 0, children: _jsx(ItemRowsBand.Provider, { value: undefined, children: children }) }));
118
229
  }
119
230
  /**
120
231
  * The setup tab's body, drawn inside the same row budget the watch tab's views
@@ -156,20 +267,26 @@ export function deriveRunViewRows(data, viewId, filterQuery = "") {
156
267
  * drew. The overview owns no list of its own, and the journal is a tail.
157
268
  */
158
269
  function promotedViewRows(data, viewId) {
270
+ // Surrendered by identity, never rebuilt: the journal view shows the fold's own records, and
271
+ // rewriting their text here — even to name the ruler its attempt is counted on — would be this
272
+ // surface authoring a row. The counters the surface DOES compose (the caption, the task rows)
273
+ // name their rulers below.
159
274
  if (viewId === "journal")
160
275
  return data.journalRows;
161
276
  if (viewId === "tasks") {
162
- return data.taskRows.map((row) => ({
277
+ return data.taskRows.filter(recordedTaskRow).map((row) => ({
163
278
  id: row.id,
164
279
  time: fieldReading(row.lastEventTime),
165
- state: taskRowState(row.state),
166
- text: `${row.taskId} · ${fieldReading(row.state)} · attempt ${fieldReading(row.attempts)} · ${fieldReading(row.actor)}${row.title === undefined ? "" : ` · ${row.title}`}`,
280
+ ...(row.lastEventTimestamp === undefined ? {} : { timestamp: row.lastEventTimestamp }),
281
+ state: taskRowState(row),
282
+ text: `${row.taskId} · ${fieldReading(row.state)} · ${runAttemptLabel(row.attempts)} · ${fieldReading(row.actor)}${row.title === undefined ? "" : ` · ${row.title}`}`,
167
283
  }));
168
284
  }
169
285
  if (viewId === "gates") {
170
286
  return data.gateRows.map((row) => ({
171
287
  id: row.id,
172
288
  time: row.time,
289
+ ...(row.timestamp === undefined ? {} : { timestamp: row.timestamp }),
173
290
  state: row.state,
174
291
  text: `${fieldReading(row.taskId)} · ${fieldReading(row.gate)} · ${row.pass === undefined ? fieldReading(undefined) : row.pass ? "pass" : "fail"}${row.details === undefined ? "" : ` · ${row.details}`}`,
175
292
  }));
@@ -178,19 +295,29 @@ function promotedViewRows(data, viewId) {
178
295
  return data.fleetRows.map((row) => ({
179
296
  id: row.id,
180
297
  time: fieldReading(row.lastEventTime),
298
+ ...(row.lastEventTimestamp === undefined ? {} : { timestamp: row.lastEventTimestamp }),
181
299
  state: "neutral",
182
300
  text: `${row.adapter} · ${row.model} · dispatches ${fieldReading(row.dispatches)}`,
183
301
  }));
184
302
  }
185
303
  return [];
186
304
  }
305
+ /** A task row exists on an operator surface only when the journal fold recorded a task fact. */
306
+ function recordedTaskRow(row) {
307
+ return row.state !== undefined
308
+ || row.lastEventTime !== undefined
309
+ || row.attempts !== undefined
310
+ || row.actor !== undefined
311
+ || row.merged === true
312
+ || row.parkKind !== undefined;
313
+ }
187
314
  /** A task's recorded state read as the component vocabulary the rows draw in. */
188
- function taskRowState(state) {
189
- if (state === "done")
315
+ function taskRowState(row) {
316
+ if (row.state === "done" && row.merged === true)
190
317
  return "pass";
191
- if (state === "failed")
318
+ if (row.state === "failed")
192
319
  return "fail";
193
- if (state === "running" || state === "human")
320
+ if (row.state === "running" || row.state === "human")
194
321
  return "active";
195
322
  return "neutral";
196
323
  }
@@ -207,17 +334,47 @@ function windowedRows(rows, bodyRows, selection) {
207
334
  ...(selectedIndex < 0 ? {} : { selection: selectedIndex - offset }),
208
335
  };
209
336
  }
337
+ /**
338
+ * The identities the body's rows carry in the order the body draws them — the
339
+ * same window `SizedJournalPanel` paints, so a hit on a body row resolves to
340
+ * the row an operator is looking at rather than to a position in a collection
341
+ * the frame scrolled past. A body that draws no selectable list (the overview,
342
+ * the journal tail, an open detail, the help overlay, the decisions tab) owns
343
+ * no rows here.
344
+ */
345
+ export function drawnRunViewRowIds(data, interaction, bodyRows) {
346
+ if (interaction.tab === "setup"
347
+ || interaction.activeView === "run"
348
+ || interaction.opened !== null
349
+ || interaction.help)
350
+ return [];
351
+ const rows = deriveRunViewRows(data, interaction.activeView, interaction.filterQuery);
352
+ return selectableRunViewRowIds(interaction.activeView, windowedRows(rows, bodyRows, interaction.selection).rows.map((row) => row.id));
353
+ }
210
354
  function SizedJournalPanel({ rows, bodyRows, title, selection, focused, }) {
211
355
  const window = windowedRows(rows, bodyRows, selection);
356
+ // The hovered row is carried as its identity, never as a screen row: it was
357
+ // resolved through the plan once, and this panel finds it in the very window
358
+ // it is about to paint or does not draw it at all. A list this pointer cell
359
+ // acts on nothing in — the passive overview tail, the journal's unselectable
360
+ // rows — finds nothing here, so no row is decorated for being passed over.
361
+ const hovered = useContext(HoveredRow);
362
+ const hover = hovered === null
363
+ ? -1
364
+ : window.rows.findIndex((row) => row.id === hovered);
212
365
  const emptyRows = Math.max(0, bodyRows - window.rows.length);
213
366
  const journal = JournalRowPanel({
214
- rows: window.rows,
367
+ rows: window.rows.map((row) => ({
368
+ ...row,
369
+ time: localClockTime(row.time, row.timestamp),
370
+ })),
215
371
  title,
216
372
  selection: window.selection,
217
373
  focused,
374
+ ...(hover < 0 ? {} : { hover }),
218
375
  });
219
376
  const journalProps = journal.props;
220
- return cloneElement(journal, {}, journalProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "journal-space") : null);
377
+ return cloneElement(journal, {}, _jsxs(ItemRows, { children: [journalProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "journal-space") : null] }, "items"));
221
378
  }
222
379
  /** A body panel (help overlay, opened event) padded to the journal's rows. */
223
380
  function SizedLinesPanel({ title, lines, bodyRows, wrapColumns, focused, flexGrow, }) {
@@ -226,7 +383,7 @@ function SizedLinesPanel({ title, lines, bodyRows, wrapColumns, focused, flexGro
226
383
  : lines.flatMap((line) => composeBandLine(line, wrapColumns));
227
384
  const visible = drawnLines.slice(0, bodyRows);
228
385
  const emptyRows = Math.max(0, bodyRows - visible.length);
229
- return (_jsxs(Panel, { title: title, focused: focused, flexGrow: flexGrow, children: [visible.map((line, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: line }) }, `${index}:${line}`))), emptyRows > 0 && _jsx(Box, { height: emptyRows })] }));
386
+ return (_jsx(Panel, { title: title, focused: focused, flexGrow: flexGrow, children: _jsxs(ItemRows, { children: [visible.map((line, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: line }) }, `${index}:${line}`))), emptyRows > 0 && _jsx(Box, { height: emptyRows })] }) }));
230
387
  }
231
388
  /** The lines the help overlay draws: every advertised key and its promise. */
232
389
  function helpLines(entries) {
@@ -234,7 +391,7 @@ function helpLines(entries) {
234
391
  }
235
392
  /** The lines the opened-event view draws for one journal row. */
236
393
  function eventLines(row) {
237
- return [`${row.time} · ${row.state}`, row.text];
394
+ return [`${localClockTime(row.time, row.timestamp)} · ${row.state}`, row.text];
238
395
  }
239
396
  /**
240
397
  * A view drawn from the rows its own derivation produces: its detail while the
@@ -246,7 +403,7 @@ function RunViewPanel({ data, viewId, interaction, focused, keyEntries, bodyRows
246
403
  const view = runViewById(viewId);
247
404
  const rows = deriveRunViewRows(data, viewId, interaction.filterQuery);
248
405
  if (interaction.help) {
249
- return (_jsx(Panel, { title: "HELP", focused: focused, flexGrow: 1, children: helpLines(keyEntries).map((line) => _jsx(BodyText, { children: line }, line)) }));
406
+ return (_jsx(Panel, { title: "HELP", focused: focused, flexGrow: 1, children: _jsx(ItemRows, { children: helpLines(keyEntries).map((line) => _jsx(BodyText, { children: line }, line)) }) }));
250
407
  }
251
408
  const opened = interaction.opened === null
252
409
  ? undefined
@@ -255,7 +412,7 @@ function RunViewPanel({ data, viewId, interaction, focused, keyEntries, bodyRows
255
412
  return (_jsx(SizedLinesPanel, { title: view.detailTitle, lines: eventLines(opened), bodyRows: bodyRows, wrapColumns: bodyColumns, focused: focused, flexGrow: 1 }));
256
413
  }
257
414
  if (rows.length === 0) {
258
- return (_jsx(Panel, { title: view.title, focused: focused, flexGrow: 1, children: _jsxs(BodyText, { emphasis: "dim", children: ["No ", view.id, " in this engagement"] }) }));
415
+ return (_jsx(Panel, { title: view.title, focused: focused, flexGrow: 1, children: _jsx(ItemRows, { children: _jsxs(BodyText, { emphasis: "dim", children: ["No ", view.id, " in this engagement"] }) }) }));
259
416
  }
260
417
  return (_jsx(SizedJournalPanel, { rows: rows, bodyRows: bodyRows, title: interaction.filterPrompt
261
418
  ? `${view.title} /${interaction.filterQuery}`
@@ -359,15 +516,15 @@ function TailBand({ data, rows, columns, pendingWrites, }) {
359
516
  ? ["PENDING WRITES · none"]
360
517
  : deriveRunViewRows(data, "journal")
361
518
  .slice(-rows)
362
- .map((row) => `${row.time} ${row.text}`);
519
+ .map((row) => `${localClockTime(row.time, row.timestamp)} ${row.text}`);
363
520
  // A styled blank cell differs with colour on and off; these rows carry no dim style.
364
521
  return (_jsx(Box, { flexDirection: "column", height: rows, overflow: "hidden", children: Array.from({ length: rows }, (_, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: fitCells(lines[index] ?? "", columns) }) }, index))) }));
365
522
  }
366
- function ApprovedProgress({ data, caption, }) {
367
- return (_jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), caption && _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }));
523
+ function ApprovedProgress({ data, caption, columns, }) {
524
+ return (_jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), caption && _jsx(BodyText, { emphasis: "dim", children: engagementProgressCaption(data, columns) })] }));
368
525
  }
369
- function CompactProgress({ data, caption, meterWidth = 28, }) {
370
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", children: [_jsx(BodyText, { emphasis: "strong", children: "PROGRESS " }), _jsx(ProgressMeter, { value: data.progress, width: meterWidth })] }), caption && _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }));
526
+ function CompactProgress({ data, caption, meterWidth = 28, columns, }) {
527
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", children: [_jsx(BodyText, { emphasis: "strong", children: "PROGRESS " }), _jsx(ProgressMeter, { value: data.progress, width: meterWidth })] }), caption && _jsx(BodyText, { emphasis: "dim", children: engagementProgressCaption(data, columns) })] }));
371
528
  }
372
529
  /** The RUN view's interior, allocated out of the planned body span, each element granted whole or not at all. */
373
530
  function resolveRunViewContent(bodySpan, bodyColumns, data) {
@@ -390,8 +547,9 @@ function resolveRunViewContent(bodySpan, bodyColumns, data) {
390
547
  : 0;
391
548
  journal += tail;
392
549
  available -= tail;
550
+ const progressCaptionColumns = Math.max(1, bodyColumns - RUN_PANEL_HORIZONTAL_CHROME);
393
551
  const captionRows = progressBar
394
- ? wrapCells(data.progressCaption, Math.max(1, bodyColumns)).length
552
+ ? wrapCells(engagementProgressCaption(data, progressCaptionColumns), progressCaptionColumns).length
395
553
  : 0;
396
554
  const progressCaption = captionRows > 0 && available >= captionRows;
397
555
  if (progressCaption)
@@ -414,12 +572,13 @@ function resolveRunViewContent(bodySpan, bodyColumns, data) {
414
572
  }
415
573
  /** What fills the regions this paint draws, resolved from the plan's own spans. */
416
574
  function resolveRegionContent(plan, data, interaction) {
417
- const bodyRegion = requiredRegion(plan, "body");
418
- const bodySpan = bodyRegion.rows;
575
+ const bodySpan = requiredRegion(plan, "body").rows;
419
576
  const sideRails = plan.band === "sidebar";
420
- const body = Math.max(1, bodySpan - PANEL_CHROME_ROWS);
421
- const bodyColumns = Math.max(1, bodyRegion.columns - RUN_PANEL_HORIZONTAL_CHROME);
422
- const common = { body, bodyColumns, sideRails };
577
+ // The plan's own item rows, consumed unmodified: the paint draws its list at
578
+ // this region and the pointer resolves a hit through it — one geometry.
579
+ const items = requiredRegion(plan, "items");
580
+ const bodyColumns = items.columns;
581
+ const common = { items, sideRails };
423
582
  if (interaction.tab !== "setup" && interaction.activeView === "run") {
424
583
  return {
425
584
  ...common,
@@ -447,15 +606,15 @@ function resolveRegionContent(plan, data, interaction) {
447
606
  /** The RUN view drawn at its planned spans: every row budget arrives resolved. */
448
607
  function LadderRunPanel({ data, content, compactMeterWidth, interaction, keyEntries, focused = true, }) {
449
608
  if (interaction?.help === true) {
450
- return (_jsx(SizedLinesPanel, { title: "HELP", lines: helpLines(keyEntries), bodyRows: content.body, wrapColumns: content.bodyColumns, focused: focused, flexGrow: 1 }));
609
+ return (_jsx(SizedLinesPanel, { title: "HELP", lines: helpLines(keyEntries), bodyRows: content.items.rows, wrapColumns: content.items.columns, focused: focused, flexGrow: 1 }));
451
610
  }
452
- return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: content.stats }), content.progressBar && (content.approvedProgress
453
- ? _jsx(ApprovedProgress, { data: data, caption: content.progressCaption })
454
- : (_jsx(CompactProgress, { data: data, caption: content.progressCaption, meterWidth: compactMeterWidth }))), content.journal > 0 && (_jsx(JournalBody, { data: data, interaction: interaction, bodyRows: content.journal, keyEntries: keyEntries }))] }));
611
+ return (_jsx(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: _jsxs(ItemRows, { children: [_jsx(RunStats, { data: data, mode: content.stats }), content.progressBar && (content.approvedProgress
612
+ ? (_jsx(ApprovedProgress, { data: data, caption: content.progressCaption, columns: Math.max(1, content.items.columns - RUN_PANEL_HORIZONTAL_CHROME) }))
613
+ : (_jsx(CompactProgress, { data: data, caption: content.progressCaption, meterWidth: compactMeterWidth, columns: Math.max(1, content.items.columns - RUN_PANEL_HORIZONTAL_CHROME) }))), content.journal > 0 && (_jsx(JournalBody, { data: data, interaction: interaction, bodyRows: content.journal, keyEntries: keyEntries }))] }) }));
455
614
  }
456
615
  function LegacyRunPanel({ data, interaction, keyEntries, focused = true, }) {
457
616
  const rows = deriveRunViewRows(data, "journal");
458
- return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: "tiles" }), _jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }), _jsx(JournalBody, { data: data, interaction: interaction, bodyRows: rows.length, keyEntries: keyEntries })] }));
617
+ return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: "tiles" }), _jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), _jsx(BodyText, { emphasis: "dim", children: engagementProgressCaption(data) })] }), _jsx(JournalBody, { data: data, interaction: interaction, bodyRows: rows.length, keyEntries: keyEntries })] }));
459
618
  }
460
619
  /**
461
620
  * One planned band, drawn at exactly the span the plan gives it. Unsized (the
@@ -467,8 +626,11 @@ function FrameBand({ rows, row = false, gap, regionRef, children, }) {
467
626
  /** A sized sub-floor plan draws the plain fallback, clipped to the measured size. */
468
627
  function PlainFallbackPaint({ data, size, }) {
469
628
  const columns = Math.max(1, size.columns);
629
+ // This is an operator surface, so it names its zone once like every other one.
630
+ // The run.ci/run.non-tty machine anchors are drawn by capture.ts's
631
+ // defaultPlainRenderer, not by this component, so the label does not move them.
470
632
  const lines = [
471
- `tickmarkr ${data.binaryVersion}`,
633
+ `tickmarkr ${data.binaryVersion} · zone ${localZoneLabel(data)}`,
472
634
  `${data.runId} ${data.status} ${data.elapsed}`,
473
635
  ...data.statusItems.map((item) => item.text),
474
636
  ];
@@ -524,14 +686,14 @@ function RunCockpitPaint({ data, columns, plan, content, interaction, keyProject
524
686
  const setup = currentInteraction.tab === "setup";
525
687
  return (_jsxs(Box, { flexDirection: "column", width: plan.size.columns, height: plan.size.rows, overflow: "hidden", children: [_jsxs(FrameBand, { rows: header.rows, row: true, regionRef: regionRef("header"), children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, height: header.rows, overflow: "hidden", children: _jsx(PlannedHeader, { data: data, plan: plan, columns: plan.size.columns - (caption?.columns ?? 0) }) }), caption !== undefined && (_jsx(Box, { ref: regionRef("caption"), width: caption.columns, height: caption.rows, flexShrink: 0, overflow: "hidden", children: _jsx(PlannedCaption, { data: data, columns: caption.columns }) }))] }), _jsx(FrameBand, { rows: requiredRegion(plan, "rule").rows, regionRef: regionRef("rule"), children: _jsx(RuleBand, { columns: plan.size.columns }) }), strip !== undefined && (_jsx(FrameBand, { rows: strip.rows, regionRef: regionRef("strip"), children: _jsx(ViewStrip, { selection: railSelection, columns: strip.columns }) })), _jsxs(FrameBand, { rows: body.rows, row: true, children: [rail !== undefined && (_jsx(Box, { ref: regionRef("rail"), width: rail.columns, height: rail.rows, flexShrink: 0, overflow: "hidden", children: setup
526
688
  ? (_jsx(SectionsBand, { focused: railFocused, selection: railSelection, columns: rail.columns }))
527
- : (_jsx(RailBand, { data: data, focused: railFocused, selection: railSelection, columns: rail.columns, rows: rail.rows, sidebar: plan.sidebar })) })), _jsx(Box, { ref: regionRef("body"), width: body.columns, height: body.rows, flexShrink: 0, flexDirection: "column", overflow: "hidden", children: setup
528
- ? (_jsx(SetupTabPanel, { data: data, selection: railSelection, focused: contentFocused, keyEntries: keyEntries, help: currentInteraction.help, bodyRows: content.body, bodyColumns: content.bodyColumns }))
529
- : activeView === "run"
530
- ? (_jsx(LadderRunPanel, { data: data, content: content, compactMeterWidth: content.sideRails ? undefined : 24, interaction: currentInteraction, keyEntries: keyEntries, focused: contentFocused }))
531
- : (_jsx(RunViewPanel, { data: data, viewId: activeView, interaction: currentInteraction, focused: contentFocused, keyEntries: keyEntries, bodyRows: content.body, bodyColumns: content.bodyColumns })) })] }), _jsx(FrameBand, { rows: requiredRegion(plan, "rule2").rows, regionRef: regionRef("rule2"), children: _jsx(RuleBand, { columns: plan.size.columns }) }), tail !== undefined && (_jsx(FrameBand, { rows: tail.rows, regionRef: regionRef("tail"), children: _jsx(TailBand, { data: data, rows: tail.rows, columns: tail.columns, pendingWrites: plan.content.tail === "pending-writes" }) })), _jsx(FrameBand, { rows: requiredRegion(plan, "status").rows, regionRef: regionRef("status"), children: _jsx(StatusStrip, { items: reportedStatusItems, width: plan.size.columns }) }), _jsx(FrameBand, { rows: requiredRegion(plan, "keybar").rows, regionRef: regionRef("keybar"), children: _jsx(RunKeybar, { entries: keyEntries, width: plan.size.columns, help: currentInteraction.help }) })] }));
689
+ : (_jsx(RailBand, { data: data, focused: railFocused, selection: railSelection, columns: rail.columns, rows: rail.rows, sidebar: plan.sidebar })) })), _jsx(Box, { ref: regionRef("body"), width: body.columns, height: body.rows, flexShrink: 0, flexDirection: "column", overflow: "hidden", children: _jsx(ItemRowsBand.Provider, { value: { region: content.items, regionRef: regionRef("items") }, children: setup
690
+ ? (_jsx(SetupTabPanel, { data: data, selection: railSelection, focused: contentFocused, keyEntries: keyEntries, help: currentInteraction.help, bodyRows: content.items.rows, bodyColumns: content.items.columns }))
691
+ : activeView === "run"
692
+ ? (_jsx(LadderRunPanel, { data: data, content: content, compactMeterWidth: 24, interaction: currentInteraction, keyEntries: keyEntries, focused: contentFocused }))
693
+ : (_jsx(RunViewPanel, { data: data, viewId: activeView, interaction: currentInteraction, focused: contentFocused, keyEntries: keyEntries, bodyRows: content.items.rows, bodyColumns: content.items.columns })) }) })] }), _jsx(FrameBand, { rows: requiredRegion(plan, "rule2").rows, regionRef: regionRef("rule2"), children: _jsx(RuleBand, { columns: plan.size.columns }) }), tail !== undefined && (_jsx(FrameBand, { rows: tail.rows, regionRef: regionRef("tail"), children: _jsx(TailBand, { data: data, rows: tail.rows, columns: tail.columns, pendingWrites: plan.content.tail === "pending-writes" }) })), _jsx(FrameBand, { rows: requiredRegion(plan, "status").rows, regionRef: regionRef("status"), children: _jsx(StatusStrip, { items: reportedStatusItems, width: plan.size.columns }) }), _jsx(FrameBand, { rows: requiredRegion(plan, "keybar").rows, regionRef: regionRef("keybar"), children: _jsx(RunKeybar, { entries: keyEntries, width: plan.size.columns, help: currentInteraction.help }) })] }));
532
694
  }
533
695
  /** Resolve every draw-time input; the plan is planFrame's own return, consumed unmodified. */
534
- export function planRunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, }) {
696
+ export function planRunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, railColumns, }) {
535
697
  const suppliedInteraction = interaction ?? initialRunInteractionState();
536
698
  const suppliedRows = deriveRunViewRows(data, suppliedInteraction.activeView, suppliedInteraction.filterQuery);
537
699
  const rowIds = suppliedRows.map((row) => row.id);
@@ -547,6 +709,7 @@ export function planRunCockpitFrame({ data, columns, rows, interaction, keyProje
547
709
  tab: repaired.tab,
548
710
  detail: repaired.opened !== null,
549
711
  captionCells: cellWidth(frameCaption(data)),
712
+ ...(railColumns === undefined ? {} : { railColumns }),
550
713
  };
551
714
  const plan = planFrame(size, view, state);
552
715
  const content = rows === undefined || plan.kind !== "frame"
@@ -565,15 +728,56 @@ export function planRunCockpitFrame({ data, columns, rows, interaction, keyProje
565
728
  };
566
729
  }
567
730
  /** The public surface always enters paint through one measured plan. */
568
- export function RunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, }) {
731
+ export function RunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, onCommittedFrame, }) {
732
+ // The session's panel resize override, read from the pointer layer the live
733
+ // drag feeds and redrawn when the boundary moves. It enters the frame only
734
+ // as an input to this planning call — the drawn frame is the recomputed
735
+ // plan, so a resize can never paint beside the plan. Nothing persists it: a
736
+ // relaunch plans the default layout.
737
+ const railOverride = useSyncExternalStore(watchSessionRailOverride, sessionRailOverride, sessionRailOverride);
569
738
  const plannedFrame = useMemo(() => planRunCockpitFrame({
570
739
  data,
571
740
  columns,
572
741
  rows,
573
742
  interaction,
574
743
  keyProjection,
575
- }), [data, columns, rows, interaction, keyProjection]);
576
- return (_jsx(RunCockpitFrameFromPlan, { data: data, plannedFrame: plannedFrame }));
744
+ railColumns: railOverride ?? undefined,
745
+ }), [data, columns, rows, interaction, keyProjection, railOverride]);
746
+ useEffect(() => {
747
+ onCommittedFrame?.(plannedFrame, data);
748
+ }, [plannedFrame, data, onCommittedFrame]);
749
+ // Where the pointer is resting, read from the pointer layer that the live
750
+ // input path feeds and redrawn when it moves. There is no prop beside it: a
751
+ // hover cannot be handed to this frame at all, only reported to the terminal.
752
+ const resting = useSyncExternalStore(watchPointerRest, pointerRestingCell, pointerRestingCell);
753
+ // And only a frame that publishes its geometry for hit testing can be hovered
754
+ // — publishing it is what makes a pointer report resolve against this frame
755
+ // at all. A capture publishes none and resolves none, so no golden, anchor or
756
+ // colour capture can carry a highlight however the pointer came to rest: the
757
+ // pointerless appearance is what that path draws by construction, not what a
758
+ // guard strips back out of it.
759
+ const hover = onCommittedFrame === undefined ? null : resting;
760
+ const hoveredRow = useMemo(() => {
761
+ const plan = plannedFrame.plan;
762
+ if (hover === null || plan.kind !== "frame"
763
+ || plannedFrame.content === undefined)
764
+ return null;
765
+ const interaction = plannedFrame.interaction;
766
+ // The whole surface a click resolves through, not only the geometry: what a
767
+ // click at this cell ACTS ON is the highlight's answer, and that route
768
+ // passes through the focus the frame is drawn with and the rows a key may
769
+ // stand on. Deriving the highlight from the row lookup alone would advertise
770
+ // a row of the drawn view while the rail marker stands on another — where
771
+ // the click opens the marked view and selects nothing.
772
+ return pointerHoverRow({
773
+ plan,
774
+ interaction,
775
+ drawnRowIds: drawnRunViewRowIds(data, interaction, plannedFrame.content.items.rows),
776
+ rowIds: selectableRunViewRowIds(interaction.activeView, deriveRunViewRows(data, interaction.activeView, interaction.filterQuery)
777
+ .map((row) => row.id)),
778
+ }, hover) ?? null;
779
+ }, [plannedFrame, data, hover]);
780
+ return (_jsx(HoveredRow.Provider, { value: hoveredRow, children: _jsx(RunCockpitFrameFromPlan, { data: data, plannedFrame: plannedFrame }) }));
577
781
  }
578
782
  function requiredRegion(plan, id) {
579
783
  const region = plan.regions.find((candidate) => candidate.id === id);
@@ -588,6 +792,21 @@ function assertPlanSpans(plan) {
588
792
  let nextRow = 0;
589
793
  for (const [id, span] of Object.entries(plan.rowSpans)) {
590
794
  const region = requiredRegion(plan, id);
795
+ const host = FRAME_NESTED_ROW_BANDS[id];
796
+ if (host !== undefined) {
797
+ // A nested band refines its host's rows rather than tiling beside them,
798
+ // so it advances no row cursor — and is held to its span and to staying
799
+ // inside the band that hosts it.
800
+ const within = requiredRegion(plan, host);
801
+ if (region.rows !== span ||
802
+ region.row < within.row ||
803
+ region.row + region.rows > within.row + within.rows ||
804
+ region.column < within.column ||
805
+ region.column + region.columns > within.column + within.columns) {
806
+ throw new Error(`planned row region ${id} has offset/span ${region.row}/${region.rows} column ${region.column}/${region.columns}, expected span ${span} inside ${host} at ${within.row}/${within.rows} column ${within.column}/${within.columns}`);
807
+ }
808
+ continue;
809
+ }
591
810
  const fullWidth = id !== "body";
592
811
  if (region.row !== nextRow ||
593
812
  region.rows !== span ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.83.0",
3
+ "version": "1.85.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",