tickmarkr 1.81.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.
- package/README.md +4 -4
- package/dist/brand.d.ts +4 -1
- package/dist/brand.js +46 -7
- package/dist/cli/commands/approve.js +53 -8
- package/dist/cli/commands/plan.js +17 -2
- package/dist/cli/commands/report.js +15 -6
- package/dist/cli/commands/ui.js +13 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/compile/collateral.d.ts +19 -0
- package/dist/compile/collateral.js +90 -0
- package/dist/compile/index.js +18 -4
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +19 -0
- package/dist/drivers/types.d.ts +1 -0
- package/dist/gates/acceptance.js +50 -8
- package/dist/gates/llm.js +34 -19
- package/dist/gates/review.d.ts +9 -1
- package/dist/gates/review.js +173 -7
- package/dist/gates/run-gates.d.ts +1 -0
- package/dist/gates/run-gates.js +18 -1
- package/dist/report/bundle.d.ts +6 -1
- package/dist/report/bundle.js +13 -3
- package/dist/run/daemon.d.ts +5 -0
- package/dist/run/daemon.js +188 -27
- package/dist/run/journal.d.ts +5 -0
- package/dist/run/journal.js +71 -1
- package/dist/tui/cockpit/capture.d.ts +107 -1
- package/dist/tui/cockpit/capture.js +287 -10
- package/dist/tui/cockpit/components.d.ts +40 -47
- package/dist/tui/cockpit/components.js +66 -50
- package/dist/tui/cockpit/derive.d.ts +73 -1
- package/dist/tui/cockpit/derive.js +211 -15
- package/dist/tui/cockpit/keys.d.ts +114 -19
- package/dist/tui/cockpit/keys.js +413 -44
- package/dist/tui/cockpit/layout.d.ts +231 -0
- package/dist/tui/cockpit/layout.js +307 -0
- package/dist/tui/cockpit/live.d.ts +65 -3
- package/dist/tui/cockpit/live.js +418 -48
- package/dist/tui/cockpit/pointer.d.ts +261 -0
- package/dist/tui/cockpit/pointer.js +610 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +93 -6
- package/dist/tui/cockpit/run-cockpit.js +805 -164
- package/dist/tui/cockpit/setup-cockpit.d.ts +146 -1
- package/dist/tui/cockpit/setup-cockpit.js +337 -5
- package/dist/tui/cockpit/views.d.ts +72 -0
- package/dist/tui/cockpit/views.js +56 -0
- package/dist/tui/cockpit/width.d.ts +105 -0
- package/dist/tui/cockpit/width.js +276 -0
- package/fixtures/speckit-sample/tasks.md +2 -2
- package/package.json +3 -1
|
@@ -72,5 +72,236 @@ export interface PlainCockpitLayout {
|
|
|
72
72
|
arrangement: "plain";
|
|
73
73
|
}
|
|
74
74
|
export type CockpitLayout = FrameCockpitLayout | PlainCockpitLayout;
|
|
75
|
+
export interface MeasuredSize {
|
|
76
|
+
columns: number;
|
|
77
|
+
rows: number;
|
|
78
|
+
}
|
|
79
|
+
/** The five sidebar views, in jump-key order (1–5). */
|
|
80
|
+
export declare const FRAME_VIEWS: readonly ["run", "tasks", "gates", "journal", "fleet"];
|
|
81
|
+
export type FrameView = (typeof FRAME_VIEWS)[number];
|
|
82
|
+
export type FrameTab = "watch" | "setup";
|
|
83
|
+
export interface FrameState {
|
|
84
|
+
/** Active top-level tab; SETUP turns the journal tail into PENDING WRITES. */
|
|
85
|
+
tab?: FrameTab;
|
|
86
|
+
/** A row's full detail is open in the body (sidebar and tail stay). */
|
|
87
|
+
detail?: boolean;
|
|
88
|
+
/** Header caption, pre-measured in terminal cells by the caller. */
|
|
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;
|
|
100
|
+
}
|
|
101
|
+
export declare const FRAME_WIDTH_FLOOR = 40;
|
|
102
|
+
export declare const FRAME_HEIGHT_FLOOR = 14;
|
|
103
|
+
export declare const SIDEBAR_COLUMN_FLOOR = 64;
|
|
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;
|
|
124
|
+
export declare const STANDARD_TAIL_HEIGHT = 16;
|
|
125
|
+
export declare const FULL_TAIL_HEIGHT = 24;
|
|
126
|
+
export declare const TAIL_STANDARD_ROWS = 1;
|
|
127
|
+
export declare const TAIL_FULL_ROWS = 3;
|
|
128
|
+
/**
|
|
129
|
+
* The header's fixed parts beside the caption — brand and version, then the
|
|
130
|
+
* two tab labels — joined by the contracted gap. Composed from named parts so
|
|
131
|
+
* a header change moves the caption fit check with it; the suite pins the
|
|
132
|
+
* composed text so an unreviewed header change fails a test. Measured in
|
|
133
|
+
* display cells by the cockpit's width authority, never String length.
|
|
134
|
+
*/
|
|
135
|
+
export declare const FRAME_HEADER_PARTS: readonly ["tickmarkr 1.83.0", "[ WATCH ]", "SETUP"];
|
|
136
|
+
export declare const FRAME_HEADER_GAP = " ";
|
|
137
|
+
export declare const HEADER_FIXED_CELLS: number;
|
|
138
|
+
/** Every (columns, rows) pair the sweep holds the plan to. */
|
|
139
|
+
export declare const FRAME_CONTRACT_DOMAIN: {
|
|
140
|
+
readonly minColumns: 40;
|
|
141
|
+
readonly maxColumns: 220;
|
|
142
|
+
readonly minRows: 14;
|
|
143
|
+
readonly maxRows: 50;
|
|
144
|
+
};
|
|
145
|
+
/** The fixed order in which the fit check surrenders regions, first to last. */
|
|
146
|
+
export declare const FRAME_SURRENDER_ORDER: readonly ["caption", "tail"];
|
|
147
|
+
export type SurrenderedRegionId = (typeof FRAME_SURRENDER_ORDER)[number];
|
|
148
|
+
export interface FrameFit {
|
|
149
|
+
columns: number;
|
|
150
|
+
tailRows: number;
|
|
151
|
+
captionCells: number;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The fit predicate for each surrenderable region. The fit check walks
|
|
155
|
+
* FRAME_SURRENDER_ORDER and applies these predicates, so the constant is the
|
|
156
|
+
* ordering decision — not a comment about one.
|
|
157
|
+
*/
|
|
158
|
+
export declare const FRAME_SURRENDER_FIT: Readonly<Record<SurrenderedRegionId, (fit: FrameFit) => boolean>>;
|
|
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>>>;
|
|
177
|
+
/** The vitals elements in block order, top first; a short rail surrenders from the top, so the meter is the last to go. */
|
|
178
|
+
export declare const SIDEBAR_VITALS_ORDER: readonly ["tasks", "gates", "meter"];
|
|
179
|
+
export type SidebarVitalsElement = (typeof SIDEBAR_VITALS_ORDER)[number];
|
|
180
|
+
/** The rail rows at and above which the menu carries its label row. */
|
|
181
|
+
export declare const SIDEBAR_MENU_LABEL_ROWS = 10;
|
|
182
|
+
/** Rows one vitals element occupies in each mode: label plus sparkline, or one inline row. */
|
|
183
|
+
export declare const SIDEBAR_VITALS_FULL_ELEMENT_ROWS = 2;
|
|
184
|
+
export declare const SIDEBAR_VITALS_COMPACT_ELEMENT_ROWS = 1;
|
|
185
|
+
export interface SidebarPlan {
|
|
186
|
+
/** Rows the menu block occupies at the rail's top: the five views, plus the label row when it fits. */
|
|
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>>;
|
|
195
|
+
/**
|
|
196
|
+
* Blank rows between the last menu row and the first vitals row. Never zero
|
|
197
|
+
* while any vitals element draws — a short rail surrenders whole vitals
|
|
198
|
+
* elements rather than closing the gap.
|
|
199
|
+
*/
|
|
200
|
+
gapRows: number;
|
|
201
|
+
/** Two rows per element, or one when the rail cannot pay for the full block. */
|
|
202
|
+
vitalsMode: "full" | "compact";
|
|
203
|
+
/** The vitals elements that draw, top to bottom, anchored to the rail's bottom. */
|
|
204
|
+
vitals: readonly SidebarVitalsElement[];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Compose the sidebar band for a rail of `railRows` rows — pure. The menu sits
|
|
208
|
+
* at the top; the vitals block sits at the bottom; the gap between them absorbs
|
|
209
|
+
* every surplus row and is never traded away: what remains after the menu and
|
|
210
|
+
* one gap row is the vitals budget, spent on the full two-row block when it
|
|
211
|
+
* fits whole, else on the one-row elements, surrendered from the top of the
|
|
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.
|
|
219
|
+
*/
|
|
220
|
+
export declare function planSidebar(railRows: number, railRow: number): SidebarPlan;
|
|
221
|
+
export interface FrameRegion {
|
|
222
|
+
id: FrameRegionId;
|
|
223
|
+
row: number;
|
|
224
|
+
rows: number;
|
|
225
|
+
column: number;
|
|
226
|
+
columns: number;
|
|
227
|
+
}
|
|
228
|
+
export interface FrameBandContent {
|
|
229
|
+
/** Body band: view rows, an open row detail, or the full-height journal tail. */
|
|
230
|
+
body: "rows" | "detail" | "journal";
|
|
231
|
+
/** Tail band: the compact journal tail, or PENDING WRITES on the SETUP tab. */
|
|
232
|
+
tail: "journal" | "pending-writes";
|
|
233
|
+
}
|
|
234
|
+
export interface PlannedFrame {
|
|
235
|
+
kind: "frame";
|
|
236
|
+
size: MeasuredSize;
|
|
237
|
+
view: FrameView;
|
|
238
|
+
tab: FrameTab;
|
|
239
|
+
band: "strip" | "sidebar";
|
|
240
|
+
/** What the view and state draw into each flexible band. */
|
|
241
|
+
content: FrameBandContent;
|
|
242
|
+
regions: readonly FrameRegion[];
|
|
243
|
+
/**
|
|
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.
|
|
249
|
+
*/
|
|
250
|
+
rowSpans: Readonly<Record<string, number>>;
|
|
251
|
+
/**
|
|
252
|
+
* Column bands of the body band; their spans tile the width exactly. A
|
|
253
|
+
* band map: the nested caption sub-region is deliberately excluded.
|
|
254
|
+
*/
|
|
255
|
+
columnSpans: Readonly<Record<string, number>>;
|
|
256
|
+
/** The single surplus-absorbing element on each axis. */
|
|
257
|
+
flexible: {
|
|
258
|
+
readonly row: "body";
|
|
259
|
+
readonly column: "body";
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* The rail band's composition — menu, gap and vitals — planned when the
|
|
263
|
+
* band is the sidebar, null on the strip band. The renderer draws it
|
|
264
|
+
* unmodified.
|
|
265
|
+
*/
|
|
266
|
+
sidebar: SidebarPlan | null;
|
|
267
|
+
/** Regions the fit check surrendered, in FRAME_SURRENDER_ORDER. */
|
|
268
|
+
surrendered: readonly SurrenderedRegionId[];
|
|
269
|
+
}
|
|
270
|
+
export interface PlainFallbackPlan {
|
|
271
|
+
kind: "plain";
|
|
272
|
+
size: MeasuredSize;
|
|
273
|
+
}
|
|
274
|
+
export type FramePlan = PlannedFrame | PlainFallbackPlan;
|
|
275
|
+
/**
|
|
276
|
+
* Plan the frame for a measured size, a view and a state — pure: no
|
|
277
|
+
* rendering, no I/O, no read of the live terminal. Below either floor the
|
|
278
|
+
* plan is the plain fallback. Otherwise the frame is row bands that tile the
|
|
279
|
+
* height exactly — header, rule, the view strip (40–63 columns), the body
|
|
280
|
+
* band, a second rule, the journal tail surrendered whole by height tier,
|
|
281
|
+
* status, keybar — and, at ≥64 columns, column bands that tile the width
|
|
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
|
|
285
|
+
* flexible element on both axes. The view and state decide what the flexible
|
|
286
|
+
* bands draw (the Journal view is the full-height tail; SETUP turns the tail
|
|
287
|
+
* into PENDING WRITES; an open detail owns the body) without moving any
|
|
288
|
+
* span. The caption is a nested sub-region sharing row 0 with the header
|
|
289
|
+
* band, so cells on that row are owned by the header band and refinable by
|
|
290
|
+
* the caption; it is surrendered first when it does not fit beside the fixed
|
|
291
|
+
* header text, never squeezed to a zero or negative span. On the sidebar band
|
|
292
|
+
* the plan also owns the rail's composition: the menu at the top, the vitals
|
|
293
|
+
* anchored to the bottom, and at least one blank row between them whenever
|
|
294
|
+
* both draw — a too-short rail surrenders whole vitals elements from the top
|
|
295
|
+
* of the block, the meter last, rather than closing the gap. rowSpans and
|
|
296
|
+
* columnSpans are band maps: they deliberately exclude the nested caption,
|
|
297
|
+
* and column bands are reported for the body band only, so each map's spans
|
|
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.
|
|
304
|
+
*/
|
|
305
|
+
export declare function planFrame(size: MeasuredSize, view?: FrameView, state?: FrameState): FramePlan;
|
|
75
306
|
export declare function resolveCockpitLayout(columns: number, rows: number): CockpitLayout;
|
|
76
307
|
export {};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PANEL_CHROME_ROWS, READABLE_PANEL_COLUMNS } from "./components.js";
|
|
2
|
+
import { cellWidth } from "./width.js";
|
|
1
3
|
export const COCKPIT_ROW_FLOOR = 14;
|
|
2
4
|
export const COCKPIT_COLUMN_FLOOR = 40;
|
|
3
5
|
export const FULL_JOURNAL_ROWS = 3;
|
|
@@ -50,6 +52,311 @@ function allocateRows(rows) {
|
|
|
50
52
|
allocation.journal += available;
|
|
51
53
|
return allocation;
|
|
52
54
|
}
|
|
55
|
+
/** The five sidebar views, in jump-key order (1–5). */
|
|
56
|
+
export const FRAME_VIEWS = ["run", "tasks", "gates", "journal", "fleet"];
|
|
57
|
+
export const FRAME_WIDTH_FLOOR = COCKPIT_COLUMN_FLOOR;
|
|
58
|
+
export const FRAME_HEIGHT_FLOOR = COCKPIT_ROW_FLOOR;
|
|
59
|
+
export const SIDEBAR_COLUMN_FLOOR = 64;
|
|
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
|
+
}
|
|
85
|
+
export const STANDARD_TAIL_HEIGHT = 16;
|
|
86
|
+
export const FULL_TAIL_HEIGHT = 24;
|
|
87
|
+
export const TAIL_STANDARD_ROWS = 1;
|
|
88
|
+
export const TAIL_FULL_ROWS = FULL_JOURNAL_ROWS;
|
|
89
|
+
/**
|
|
90
|
+
* The header's fixed parts beside the caption — brand and version, then the
|
|
91
|
+
* two tab labels — joined by the contracted gap. Composed from named parts so
|
|
92
|
+
* a header change moves the caption fit check with it; the suite pins the
|
|
93
|
+
* composed text so an unreviewed header change fails a test. Measured in
|
|
94
|
+
* display cells by the cockpit's width authority, never String length.
|
|
95
|
+
*/
|
|
96
|
+
export const FRAME_HEADER_PARTS = [
|
|
97
|
+
"tickmarkr 1.83.0",
|
|
98
|
+
"[ WATCH ]",
|
|
99
|
+
"SETUP",
|
|
100
|
+
];
|
|
101
|
+
export const FRAME_HEADER_GAP = " ";
|
|
102
|
+
export const HEADER_FIXED_CELLS = cellWidth(FRAME_HEADER_PARTS.join(FRAME_HEADER_GAP));
|
|
103
|
+
/** Every (columns, rows) pair the sweep holds the plan to. */
|
|
104
|
+
export const FRAME_CONTRACT_DOMAIN = {
|
|
105
|
+
minColumns: 40,
|
|
106
|
+
maxColumns: 220,
|
|
107
|
+
minRows: 14,
|
|
108
|
+
maxRows: 50,
|
|
109
|
+
};
|
|
110
|
+
/** The fixed order in which the fit check surrenders regions, first to last. */
|
|
111
|
+
export const FRAME_SURRENDER_ORDER = ["caption", "tail"];
|
|
112
|
+
/**
|
|
113
|
+
* The fit predicate for each surrenderable region. The fit check walks
|
|
114
|
+
* FRAME_SURRENDER_ORDER and applies these predicates, so the constant is the
|
|
115
|
+
* ordering decision — not a comment about one.
|
|
116
|
+
*/
|
|
117
|
+
export const FRAME_SURRENDER_FIT = {
|
|
118
|
+
caption: ({ captionCells, columns }) => captionCells >= 1 && captionCells <= columns - HEADER_FIXED_CELLS,
|
|
119
|
+
tail: ({ tailRows }) => tailRows > 0,
|
|
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" };
|
|
138
|
+
/* ------------------------------------------------------------------ */
|
|
139
|
+
/* The sidebar band's composition: the menu at the rail's top, the */
|
|
140
|
+
/* vitals anchored to the rail's bottom, and at least one blank row */
|
|
141
|
+
/* between them whenever both draw. The plan owns this geometry; the */
|
|
142
|
+
/* band composition draws it unmodified. */
|
|
143
|
+
/* ------------------------------------------------------------------ */
|
|
144
|
+
/** The vitals elements in block order, top first; a short rail surrenders from the top, so the meter is the last to go. */
|
|
145
|
+
export const SIDEBAR_VITALS_ORDER = ["tasks", "gates", "meter"];
|
|
146
|
+
/** The rail rows at and above which the menu carries its label row. */
|
|
147
|
+
export const SIDEBAR_MENU_LABEL_ROWS = 10;
|
|
148
|
+
/** Rows one vitals element occupies in each mode: label plus sparkline, or one inline row. */
|
|
149
|
+
export const SIDEBAR_VITALS_FULL_ELEMENT_ROWS = 2;
|
|
150
|
+
export const SIDEBAR_VITALS_COMPACT_ELEMENT_ROWS = 1;
|
|
151
|
+
/**
|
|
152
|
+
* Compose the sidebar band for a rail of `railRows` rows — pure. The menu sits
|
|
153
|
+
* at the top; the vitals block sits at the bottom; the gap between them absorbs
|
|
154
|
+
* every surplus row and is never traded away: what remains after the menu and
|
|
155
|
+
* one gap row is the vitals budget, spent on the full two-row block when it
|
|
156
|
+
* fits whole, else on the one-row elements, surrendered from the top of the
|
|
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.
|
|
164
|
+
*/
|
|
165
|
+
export function planSidebar(railRows, railRow) {
|
|
166
|
+
const menuRows = railRows >= SIDEBAR_MENU_LABEL_ROWS
|
|
167
|
+
? 1 + FRAME_VIEWS.length
|
|
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]));
|
|
173
|
+
const fullRows = SIDEBAR_VITALS_ORDER.length * SIDEBAR_VITALS_FULL_ELEMENT_ROWS;
|
|
174
|
+
// The gap is funded before the vitals are: the budget is what remains after it.
|
|
175
|
+
const budget = railRows - menuRows - 1;
|
|
176
|
+
if (budget >= fullRows) {
|
|
177
|
+
return {
|
|
178
|
+
menuRows,
|
|
179
|
+
viewRows,
|
|
180
|
+
gapRows: railRows - menuRows - fullRows,
|
|
181
|
+
vitalsMode: "full",
|
|
182
|
+
vitals: SIDEBAR_VITALS_ORDER,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const kept = Math.max(0, Math.min(SIDEBAR_VITALS_ORDER.length, Math.floor(budget)));
|
|
186
|
+
return {
|
|
187
|
+
menuRows,
|
|
188
|
+
viewRows,
|
|
189
|
+
gapRows: kept > 0 ? railRows - menuRows - kept : 0,
|
|
190
|
+
vitalsMode: "compact",
|
|
191
|
+
vitals: SIDEBAR_VITALS_ORDER.slice(SIDEBAR_VITALS_ORDER.length - kept),
|
|
192
|
+
};
|
|
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
|
+
}
|
|
211
|
+
function tailRowsFor(rows) {
|
|
212
|
+
if (rows >= FULL_TAIL_HEIGHT)
|
|
213
|
+
return TAIL_FULL_ROWS;
|
|
214
|
+
if (rows >= STANDARD_TAIL_HEIGHT)
|
|
215
|
+
return TAIL_STANDARD_ROWS;
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Plan the frame for a measured size, a view and a state — pure: no
|
|
220
|
+
* rendering, no I/O, no read of the live terminal. Below either floor the
|
|
221
|
+
* plan is the plain fallback. Otherwise the frame is row bands that tile the
|
|
222
|
+
* height exactly — header, rule, the view strip (40–63 columns), the body
|
|
223
|
+
* band, a second rule, the journal tail surrendered whole by height tier,
|
|
224
|
+
* status, keybar — and, at ≥64 columns, column bands that tile the width
|
|
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
|
|
228
|
+
* flexible element on both axes. The view and state decide what the flexible
|
|
229
|
+
* bands draw (the Journal view is the full-height tail; SETUP turns the tail
|
|
230
|
+
* into PENDING WRITES; an open detail owns the body) without moving any
|
|
231
|
+
* span. The caption is a nested sub-region sharing row 0 with the header
|
|
232
|
+
* band, so cells on that row are owned by the header band and refinable by
|
|
233
|
+
* the caption; it is surrendered first when it does not fit beside the fixed
|
|
234
|
+
* header text, never squeezed to a zero or negative span. On the sidebar band
|
|
235
|
+
* the plan also owns the rail's composition: the menu at the top, the vitals
|
|
236
|
+
* anchored to the bottom, and at least one blank row between them whenever
|
|
237
|
+
* both draw — a too-short rail surrenders whole vitals elements from the top
|
|
238
|
+
* of the block, the meter last, rather than closing the gap. rowSpans and
|
|
239
|
+
* columnSpans are band maps: they deliberately exclude the nested caption,
|
|
240
|
+
* and column bands are reported for the body band only, so each map's spans
|
|
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.
|
|
247
|
+
*/
|
|
248
|
+
export function planFrame(size, view = "tasks", state = {}) {
|
|
249
|
+
const columns = Math.floor(size.columns);
|
|
250
|
+
const rows = Math.floor(size.rows);
|
|
251
|
+
if (!Number.isFinite(columns) ||
|
|
252
|
+
!Number.isFinite(rows) ||
|
|
253
|
+
columns < FRAME_WIDTH_FLOOR ||
|
|
254
|
+
rows < FRAME_HEIGHT_FLOOR) {
|
|
255
|
+
return { kind: "plain", size: { columns, rows } };
|
|
256
|
+
}
|
|
257
|
+
const tab = state.tab ?? "watch";
|
|
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);
|
|
262
|
+
const stripRows = band === "strip" ? 1 : 0;
|
|
263
|
+
const tailRows = tailRowsFor(rows);
|
|
264
|
+
// Fixed chrome is five rows: header, rule, a second rule below the body
|
|
265
|
+
// band, status and keybar.
|
|
266
|
+
const bodyRows = rows - (5 + stripRows + tailRows);
|
|
267
|
+
const fit = {
|
|
268
|
+
columns,
|
|
269
|
+
tailRows,
|
|
270
|
+
captionCells: state.captionCells ?? 0,
|
|
271
|
+
};
|
|
272
|
+
// The fit check walks the declared order; the constant is what orders the
|
|
273
|
+
// surrender decisions.
|
|
274
|
+
const surrendered = FRAME_SURRENDER_ORDER.filter((id) => !FRAME_SURRENDER_FIT[id](fit));
|
|
275
|
+
const captionFits = FRAME_SURRENDER_FIT.caption(fit);
|
|
276
|
+
const captionCells = fit.captionCells;
|
|
277
|
+
const regions = [];
|
|
278
|
+
let row = 0;
|
|
279
|
+
regions.push({ id: "header", row, rows: 1, column: 0, columns });
|
|
280
|
+
row += 1;
|
|
281
|
+
regions.push({ id: "rule", row, rows: 1, column: 0, columns });
|
|
282
|
+
row += 1;
|
|
283
|
+
if (stripRows > 0) {
|
|
284
|
+
regions.push({ id: "strip", row, rows: stripRows, column: 0, columns });
|
|
285
|
+
row += stripRows;
|
|
286
|
+
}
|
|
287
|
+
const bodyRow = row;
|
|
288
|
+
row += bodyRows;
|
|
289
|
+
regions.push({ id: "rule2", row, rows: 1, column: 0, columns });
|
|
290
|
+
row += 1;
|
|
291
|
+
if (tailRows > 0) {
|
|
292
|
+
regions.push({ id: "tail", row, rows: tailRows, column: 0, columns });
|
|
293
|
+
row += tailRows;
|
|
294
|
+
}
|
|
295
|
+
regions.push({ id: "status", row, rows: 1, column: 0, columns });
|
|
296
|
+
row += 1;
|
|
297
|
+
regions.push({ id: "keybar", row, rows: 1, column: 0, columns });
|
|
298
|
+
row += 1;
|
|
299
|
+
if (band === "sidebar") {
|
|
300
|
+
regions.push({
|
|
301
|
+
id: "rail",
|
|
302
|
+
row: bodyRow,
|
|
303
|
+
rows: bodyRows,
|
|
304
|
+
column: 0,
|
|
305
|
+
columns: railColumns,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const bodyRegion = band === "sidebar"
|
|
309
|
+
? {
|
|
310
|
+
id: "body",
|
|
311
|
+
row: bodyRow,
|
|
312
|
+
rows: bodyRows,
|
|
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);
|
|
320
|
+
if (captionFits) {
|
|
321
|
+
regions.push({
|
|
322
|
+
id: "caption",
|
|
323
|
+
row: 0,
|
|
324
|
+
rows: 1,
|
|
325
|
+
column: columns - captionCells,
|
|
326
|
+
columns: captionCells,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
const rowSpans = { header: 1, rule: 1 };
|
|
330
|
+
if (stripRows > 0)
|
|
331
|
+
rowSpans.strip = stripRows;
|
|
332
|
+
rowSpans.body = bodyRows;
|
|
333
|
+
rowSpans.items = itemsRegion.rows;
|
|
334
|
+
rowSpans.rule2 = 1;
|
|
335
|
+
if (tailRows > 0)
|
|
336
|
+
rowSpans.tail = tailRows;
|
|
337
|
+
rowSpans.status = 1;
|
|
338
|
+
rowSpans.keybar = 1;
|
|
339
|
+
const columnSpans = band === "sidebar"
|
|
340
|
+
? { rail: railColumns, body: columns - railColumns }
|
|
341
|
+
: { body: columns };
|
|
342
|
+
return {
|
|
343
|
+
kind: "frame",
|
|
344
|
+
size: { columns, rows },
|
|
345
|
+
view,
|
|
346
|
+
tab,
|
|
347
|
+
band,
|
|
348
|
+
content: {
|
|
349
|
+
body: view === "journal" ? "journal" : state.detail ? "detail" : "rows",
|
|
350
|
+
tail: tab === "setup" ? "pending-writes" : "journal",
|
|
351
|
+
},
|
|
352
|
+
regions,
|
|
353
|
+
rowSpans,
|
|
354
|
+
columnSpans,
|
|
355
|
+
flexible: { row: "body", column: "body" },
|
|
356
|
+
sidebar: band === "sidebar" ? planSidebar(bodyRows, bodyRow) : null,
|
|
357
|
+
surrendered,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
53
360
|
export function resolveCockpitLayout(columns, rows) {
|
|
54
361
|
if (!Number.isFinite(columns) ||
|
|
55
362
|
!Number.isFinite(rows) ||
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { RunCockpitData, RunCockpitSource } from "./derive.js";
|
|
1
|
+
import type { CockpitGraph, RunCockpitData, RunCockpitSource } from "./derive.js";
|
|
2
|
+
import { type RunInteractionState, type RunKeyEvent } from "./keys.js";
|
|
3
|
+
import { type PointerReport, type PointerSurface } from "./pointer.js";
|
|
2
4
|
/**
|
|
3
5
|
* THE engagement selection rule — stated once, here, so it can change in one
|
|
4
6
|
* place without touching the renderer or the command: an explicit engagement
|
|
@@ -11,6 +13,18 @@ export declare function selectEngagementRunId(cwd: string, explicit?: string): s
|
|
|
11
13
|
* failure throws: the caller refuses rather than drawing from nothing.
|
|
12
14
|
*/
|
|
13
15
|
export declare function loadEngagementSource(cwd: string, runId: string): RunCockpitSource;
|
|
16
|
+
/**
|
|
17
|
+
* The compiled graph, when the repository has one. It lends the tasks view its
|
|
18
|
+
* identities and nothing else, so an engagement with no compiled graph at all
|
|
19
|
+
* still draws every task the journal mentions rather than refusing to open.
|
|
20
|
+
*
|
|
21
|
+
* Absence is the only fallback. A graph that exists and cannot be read, parsed
|
|
22
|
+
* or validated is a fault, and swallowing it would quietly demote the surface:
|
|
23
|
+
* every task the graph alone knows about would vanish from the rows, and a
|
|
24
|
+
* selection standing on one of them would be cleared as if the engagement had
|
|
25
|
+
* dropped it. So it throws, and the caller keeps its last good frame instead.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadEngagementGraph(cwd: string): CockpitGraph | undefined;
|
|
14
28
|
export declare function formatEventAge(ageMs: number): string;
|
|
15
29
|
/**
|
|
16
30
|
* Live derivation over a real engagement's journal bytes. The header's
|
|
@@ -19,8 +33,52 @@ export declare function formatEventAge(ageMs: number): string;
|
|
|
19
33
|
* than as current. Throws on unreadable bytes (empty capture, no run-start) —
|
|
20
34
|
* the caller refuses instead of rendering a plausible surface from nothing.
|
|
21
35
|
*/
|
|
22
|
-
export declare function deriveLiveRunCockpitData(source: RunCockpitSource, binaryVersion: string, now?: () => number): RunCockpitData;
|
|
23
|
-
|
|
36
|
+
export declare function deriveLiveRunCockpitData(source: RunCockpitSource, binaryVersion: string, now?: () => number, graph?: CockpitGraph): RunCockpitData;
|
|
37
|
+
/**
|
|
38
|
+
* Repair the interaction state against the rows a refreshed view now owns.
|
|
39
|
+
* Selection is held by the row's source identity, so it survives rows landing
|
|
40
|
+
* above it and is dropped — never slid onto whoever inherited its index —
|
|
41
|
+
* when the refreshed engagement no longer carries it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function reconcileLiveRunInteraction(interaction: RunInteractionState, data: RunCockpitData): RunInteractionState;
|
|
44
|
+
/**
|
|
45
|
+
* The rows the live surface hands its keys — the engagement's own identities,
|
|
46
|
+
* minus the journal's, which are never selectable in either tab. This is the
|
|
47
|
+
* one place rows enter the dispatcher, so no key can reach a journal row.
|
|
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;
|
|
61
|
+
export type LiveCockpitSurface = {
|
|
62
|
+
readonly data: RunCockpitData;
|
|
63
|
+
readonly interaction: RunInteractionState;
|
|
64
|
+
/** The tab waiting behind the drawn one, holding the state it was left in. */
|
|
65
|
+
readonly stashed: RunInteractionState;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The live owner's delivery boundary. A refresh is the real journal read and
|
|
69
|
+
* state transition used by the timer; snapshot exposes the state subsequent
|
|
70
|
+
* deliveries will read. Batch keeps applying transitions synchronously while
|
|
71
|
+
* deferring only the redraw notification, so multiple sources can be ordered
|
|
72
|
+
* in one turn without substituting a helper state machine for the live one.
|
|
73
|
+
*/
|
|
74
|
+
export type LiveCockpitDelivery = {
|
|
75
|
+
readonly snapshot: () => LiveCockpitSurface;
|
|
76
|
+
readonly refresh: () => boolean;
|
|
77
|
+
readonly key: (event: RunKeyEvent) => boolean;
|
|
78
|
+
readonly pointer: (report: PointerReport) => boolean;
|
|
79
|
+
readonly batch: <Result>(deliver: () => Result) => Result;
|
|
80
|
+
};
|
|
81
|
+
export declare function runLiveCockpit({ input, output, cwd, runId, binaryVersion, refreshMs, now, debug, onDelivery, }: {
|
|
24
82
|
input: NodeJS.ReadStream;
|
|
25
83
|
output: NodeJS.WriteStream;
|
|
26
84
|
cwd: string;
|
|
@@ -28,4 +86,8 @@ export declare function runLiveCockpit({ input, output, cwd, runId, binaryVersio
|
|
|
28
86
|
binaryVersion: string;
|
|
29
87
|
refreshMs?: number;
|
|
30
88
|
now?: () => number;
|
|
89
|
+
/** Emit complete frames for diagnostics instead of terminal diffs. */
|
|
90
|
+
debug?: boolean;
|
|
91
|
+
/** Observe and control the production delivery boundary. */
|
|
92
|
+
onDelivery?: (delivery: LiveCockpitDelivery) => void;
|
|
31
93
|
}): Promise<void>;
|