gentle-shell-bridge 0.3.0 → 0.3.2

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 (76) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +91 -722
  3. package/docs/contract.md +92 -35
  4. package/package.json +13 -1
  5. package/src/api.ts +1 -1
  6. package/.codegraph/.gitignore +0 -5
  7. package/PRD.md +0 -144
  8. package/examples/bash-card.ts +0 -1694
  9. package/gentle-shell-bridge-0.3.0.tgz +0 -0
  10. package/odd/tasks/palette-sections.md +0 -30
  11. package/odd/tasks/plugin-keymap.md +0 -32
  12. package/odd/tasks/plugin-surface-contract.md +0 -55
  13. package/test/api.test.ts +0 -395
  14. package/test/apply-on-move.test.ts +0 -477
  15. package/test/arm.test.ts +0 -220
  16. package/test/bridge-ready.test.ts +0 -309
  17. package/test/bus.test.ts +0 -165
  18. package/test/button-rows.test.ts +0 -409
  19. package/test/capture.test.ts +0 -356
  20. package/test/card-style-rows.test.ts +0 -510
  21. package/test/card-style-transform.test.ts +0 -379
  22. package/test/card-style-wiring.test.ts +0 -258
  23. package/test/card-styles-e2e.test.ts +0 -491
  24. package/test/card-styles.test.ts +0 -324
  25. package/test/card-transcript-path.test.ts +0 -178
  26. package/test/config.test.ts +0 -93
  27. package/test/e2e.test.ts +0 -443
  28. package/test/example-bash-identity.test.ts +0 -372
  29. package/test/example-bash-matrix.test.ts +0 -856
  30. package/test/example-designs.test.ts +0 -734
  31. package/test/example-neon-transform.test.ts +0 -283
  32. package/test/example-plain-style.test.ts +0 -1268
  33. package/test/extension.test.ts +0 -160
  34. package/test/fake-events.ts +0 -58
  35. package/test/fake-host.ts +0 -211
  36. package/test/fake-sidebar.ts +0 -516
  37. package/test/fake-transcript.ts +0 -486
  38. package/test/frame-telemetry.test.ts +0 -417
  39. package/test/hook.test.ts +0 -196
  40. package/test/manifest.test.ts +0 -105
  41. package/test/new-category.test.ts +0 -979
  42. package/test/notices.test.ts +0 -328
  43. package/test/plugin-integration-lines.test.ts +0 -979
  44. package/test/plugin-surface.test.ts +0 -152
  45. package/test/presence.test.ts +0 -459
  46. package/test/rail-surfaces.test.ts +0 -457
  47. package/test/rail-width.test.ts +0 -423
  48. package/test/regression.test.ts +0 -483
  49. package/test/repaint-api.test.ts +0 -200
  50. package/test/reserved-categories.test.ts +0 -361
  51. package/test/row-labels.test.ts +0 -284
  52. package/test/row-names.test.ts +0 -473
  53. package/test/row-origin.test.ts +0 -395
  54. package/test/section-interaction.test.ts +0 -364
  55. package/test/section-panel.test.ts +0 -403
  56. package/test/sections-rows.test.ts +0 -291
  57. package/test/sidebar-backend.test.ts +0 -471
  58. package/test/sidebar-e2e.test.ts +0 -331
  59. package/test/sidebar-hover.test.ts +0 -276
  60. package/test/sidebar-independent.test.ts +0 -349
  61. package/test/sidebar-render.test.ts +0 -281
  62. package/test/sidebar-sections.test.ts +0 -245
  63. package/test/status-card-styles.test.ts +0 -201
  64. package/test/status-section-frame.test.ts +0 -181
  65. package/test/status-sections.test.ts +0 -243
  66. package/test/status.test.ts +0 -209
  67. package/test/store.test.ts +0 -192
  68. package/test/surface-repaint-cost.test.ts +0 -520
  69. package/test/tool-design-chain.test.ts +0 -483
  70. package/test/tool-design-cost.test.ts +0 -473
  71. package/test/tool-design-rows.test.ts +0 -359
  72. package/test/tool-designs.test.ts +0 -745
  73. package/test/transform-pipeline.test.ts +0 -465
  74. package/test/transforms-bus.test.ts +0 -233
  75. package/test/verifier-width-regressions.test.ts +0 -164
  76. package/tsconfig.json +0 -18
@@ -1,423 +0,0 @@
1
- /**
2
- * Width discipline on the host's rail (N17–N19), measured the way the host measures it.
3
- *
4
- * ## What decides the width of the main column
5
- *
6
- * The rail is the SECOND entry of the host's fullscreen `hstack`, with a FIXED basis:
7
- *
8
- * ```ts
9
- * { component: scroll, basis: RAIL_WIDTH, grow: 0, shrink: 0, minSize: RAIL_WIDTH }
10
- * ```
11
- * (`lib/shell-sidebar-layout.ts:493-496`; `RAIL_WIDTH = 50`, `GAP = 3` at `:9,:12`), so a part's
12
- * rows can never widen the rail column: pi-tui allocates a numeric `basis` without measuring the
13
- * child (`pi-tui/dist/layout.js:124`, `allocateStackSizes` in `dist/components/stack.js`). What a
14
- * part CAN change is whether the rail is painted at all:
15
- *
16
- * ```ts
17
- * const active = railLines.length > 0 && railLines.every((line) => visibleWidth(line) <= contentWidth);
18
- * ```
19
- * (`lib/shell-sidebar-layout.ts:410`). When a single rail row is wider than the content width the
20
- * host drops the whole rail and the main column gets every column back — the layout's widths DID
21
- * change, from the part's output alone.
22
- *
23
- * ## How the host measures a row
24
- *
25
- * `visibleWidth` (`pi-tui/dist/utils.js:212-268`, pi-tui 1.5.x) strips complete CSI/OSC/APC
26
- * sequences, expands a TAB to THREE columns (`:229-231`), segments the rest into grapheme clusters
27
- * and charges each one the way `graphemeWidth` charges it (`:143-207`): a lone printable ASCII or
28
- * TAB by its own rule, a cluster of terminal-spacing marks by its code point count, zero for a
29
- * cluster of ignorables/controls/marks, two for an RGI emoji (after the host's own `couldBeEmoji`
30
- * prefilter, so a bare `©`, `☺` or `▶` is NOT an emoji and stays one column), two for a
31
- * regional-indicator pair, otherwise the base code point's East Asian `W|F` width plus every
32
- * terminal-spacing mark, mark-following visible code point (an Indic consonant after a virama),
33
- * halfwidth/fullwidth form and Thai/Lao AM vowel the cluster carries. This package's own
34
- * `visibleColumns` counts a tab as one column and every code point separately, so a row this
35
- * package believes is 48 columns wide can measure 51 to the host. The port below is the host's
36
- * model, kept here independently of `src/` — no production width helper is imported — so the
37
- * assertions do not test the implementation against itself; a self-check test pins the values
38
- * measured from the installed `visibleWidth`.
39
- *
40
- * ## The rail path, as the host paints it
41
- *
42
- * On the tinted panel surface a part's row becomes `" " + row + " "` (`:414`), and the width a part
43
- * is handed is `contentWidth - RAIL_PADDING * 2` (`:355`): 48 columns at `RAIL_WIDTH` 50. On a
44
- * transparent rail the host re-frames the rows itself through the same `< contentWidth` budget
45
- * (`sectionCardRows` → `cardLine`, `:36-47`), so the budget is the same number.
46
- */
47
-
48
- import assert from "node:assert/strict";
49
- import { mkdtempSync, rmSync } from "node:fs";
50
- import { tmpdir } from "node:os";
51
- import { join } from "node:path";
52
- import test from "node:test";
53
-
54
- import { registerSection, resetBusForTests, resetSectionsForTests } from "../src/bus.ts";
55
- import { resetSectionPanelForTests, setSectionTheme } from "../src/section-panel.ts";
56
- import { buildSectionContribution, installSectionRail, resetSectionFailuresForTests } from "../src/sidebar-render.ts";
57
- import { installSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
58
- import { collectStatus, formatStatus } from "../src/status.ts";
59
- import { resetStoreStateForTests } from "../src/store.ts";
60
- import type { SectionContext, SidebarSection } from "../src/types.ts";
61
- import { resetSidebar } from "./fake-sidebar.ts";
62
-
63
- // ---------------------------------------------------------------------------------------------
64
- // The host's numbers, quoted from `lib/shell-sidebar-layout.ts`
65
- // ---------------------------------------------------------------------------------------------
66
-
67
- /** The rail column's basis (`:9`). */
68
- const RAIL_WIDTH = 50;
69
- /** One column of rail padding on each side of a panel row (`:10`). */
70
- const RAIL_PADDING = 1;
71
- /** The gap between the conversation and the rail (`:12`). */
72
- const GAP = 3;
73
- /** What the rail takes from the editor column (`:15`). */
74
- const RAIL_COLUMNS = RAIL_WIDTH + GAP;
75
- const TERMINAL_COLUMNS = 200;
76
-
77
- /** The width the host hands a part on the tinted panel surface (`:355`). */
78
- const GIVEN = RAIL_WIDTH - RAIL_PADDING * 2;
79
-
80
- /** The main column the host allocates when the rail paints: no basis of ours is measured. */
81
- const MAIN_WITH_RAIL = TERMINAL_COLUMNS - RAIL_COLUMNS;
82
- /** The main column when the rail does not paint at all. */
83
- const MAIN_WITHOUT_RAIL = TERMINAL_COLUMNS;
84
-
85
- /**
86
- * One complete escape sequence's length at `index`, read as the host's `extractAnsiCode` reads it
87
- * (`pi-tui/dist/utils.js:265-320`): CSI up to its final byte, OSC/APC up to BEL or ST. Zero when
88
- * there is none, so the walk copies the character.
89
- */
90
- function escapeLengthAt(line: string, index: number): number {
91
- if (line[index] !== "\u001b") return 0;
92
- const kind = line[index + 1];
93
- if (kind === "[") {
94
- let end = index + 2;
95
- while (end < line.length && !/[mGKHJ]/u.test(line[end] ?? "")) end += 1;
96
- return end < line.length ? end + 1 - index : 0;
97
- }
98
- if (kind === "]" || kind === "_") {
99
- let end = index + 2;
100
- while (end < line.length) {
101
- if (line[end] === "\u0007") return end + 1 - index;
102
- if (line[end] === "\u001b" && line[end + 1] === "\\") return end + 2 - index;
103
- end += 1;
104
- }
105
- }
106
- return 0;
107
- }
108
-
109
- /** A cluster of nothing but ignorables, controls, marks or surrogates: no columns (`:34`). */
110
- const ZERO_WIDTH = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Mark}|\p{Surrogate})+$/v;
111
- /** What a cluster may carry before its base (`leadingNonPrintingRegex`, `:35`). */
112
- const LEADING_NON_PRINTING =
113
- /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v;
114
- /** One character the host's trailing loop skips (`nonPrintingCharRegex`, `:36`). */
115
- const NON_PRINTING_CHAR =
116
- /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Format}|\p{Mark}|\p{Surrogate})$/v;
117
- /** One combining mark (`markCharRegex`, `:37`). */
118
- const MARK_CHAR = /^\p{Mark}$/v;
119
- /**
120
- * Marks terminals allocate cells for, with or without a base (`terminalSpacingMarkRegex`, `:38-40`):
121
- * spacing marks except a few zero-width exceptions, plus the scripts whose spacing marks legacy
122
- * wcwidth tables charge.
123
- */
124
- const TERMINAL_SPACING_MARK =
125
- /^(?:[\p{Spacing_Mark}--[\u1734\u302E\u302F]]|[\u065F\u0F7F\u102B\u102C\u1031\u1033-\u1035\u1038\u103A-\u103E])+$/v;
126
- /** The emoji set the host tests AFTER its prefilter (`rgiEmojiRegex`, `:41`). */
127
- const RGI_EMOJI = /^\p{RGI_Emoji}$/v;
128
-
129
- /** The host's cheap emoji prefilter (`couldBeEmoji`, `:16-26`). */
130
- function couldBeEmoji(segment: string): boolean {
131
- const codePoint = segment.codePointAt(0) ?? 0;
132
- return (
133
- (codePoint >= 0x1f000 && codePoint <= 0x1fbff) ||
134
- (codePoint >= 0x2300 && codePoint <= 0x23ff) ||
135
- (codePoint >= 0x2600 && codePoint <= 0x27bf) ||
136
- (codePoint >= 0x2b50 && codePoint <= 0x2b55) ||
137
- segment.includes("\ufe0f") ||
138
- segment.length > 2
139
- );
140
- }
141
-
142
- /**
143
- * The East Asian Wide/Fullwidth blocks: a base code point in one of them is two columns
144
- * (`get-east-asian-width`'s `eastAsianWidth`, which `graphemeWidth` calls at `:151`). These are the
145
- * blocks a rail row can carry — CJK, Kana, Hangul, the fullwidth forms, enclosed ideographs and the
146
- * supplementary ideographic planes; a wide EMOJI is charged by the RGI rule above instead.
147
- */
148
- const WIDE: ReadonlyArray<readonly [number, number]> = [
149
- [0x1100, 0x115f],
150
- [0x2329, 0x232a],
151
- [0x2e80, 0x303e],
152
- [0x3041, 0xa4cf],
153
- [0xa960, 0xa97f],
154
- [0xac00, 0xd7a3],
155
- [0xf900, 0xfaff],
156
- [0xfe10, 0xfe19],
157
- [0xfe30, 0xfe6f],
158
- [0xff01, 0xff60],
159
- [0xffe0, 0xffe6],
160
- [0x16fe0, 0x16fe4],
161
- [0x17000, 0x18d08],
162
- [0x1b000, 0x1b2ff],
163
- [0x1d300, 0x1d376],
164
- [0x1f200, 0x1f2ff],
165
- [0x20000, 0x3fffd],
166
- ];
167
-
168
- function isWide(codePoint: number): boolean {
169
- return WIDE.some(([start, end]) => codePoint >= start && codePoint <= end);
170
- }
171
-
172
- /**
173
- * pi-tui's `visibleWidth`: escapes dropped, a tab three columns, and each grapheme cluster charged
174
- * as the host charges it. Kept independent of `src/` on purpose.
175
- */
176
- function hostWidth(line: string): number {
177
- let clean = "";
178
- let index = 0;
179
- while (index < line.length) {
180
- const length = escapeLengthAt(line, index);
181
- if (length > 0) {
182
- index += length;
183
- continue;
184
- }
185
- clean += line[index];
186
- index += 1;
187
- }
188
- // A TAB is three columns (`:229-231`); the host substitutes before segmenting.
189
- clean = clean.replace(/\t/gu, " ");
190
- // Fast path, like the host's: printable ASCII is one column per byte.
191
- if (/^[\x20-\x7e]*$/u.test(clean)) return clean.length;
192
- let total = 0;
193
- for (const { segment } of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(clean)) {
194
- total += clusterWidth(segment);
195
- }
196
- return total;
197
- }
198
-
199
- /** One grapheme cluster's columns, in the host's own order (`graphemeWidth`, `:143-207`). */
200
- function clusterWidth(segment: string): number {
201
- // The host's own fast path: a lone printable ASCII byte is one column, a lone TAB three.
202
- if (segment.length === 1) {
203
- const code = segment.charCodeAt(0);
204
- if (code >= 0x20 && code <= 0x7e) return 1;
205
- if (code === 0x09) return 3;
206
- }
207
- // Some marks occupy cells even without a base character.
208
- if (TERMINAL_SPACING_MARK.test(segment)) return [...segment].length;
209
- if (ZERO_WIDTH.test(segment)) return 0;
210
- if (couldBeEmoji(segment) && RGI_EMOJI.test(segment)) return 2;
211
-
212
- const base = segment.replace(LEADING_NON_PRINTING, "");
213
- const codePoint = base.codePointAt(0);
214
- if (codePoint === undefined) return 0;
215
- if (codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff) return 2;
216
-
217
- let width = isWide(codePoint) ? 2 : 1;
218
- let followsMark = false;
219
- for (const char of [...base].slice(1)) {
220
- if (TERMINAL_SPACING_MARK.test(char)) {
221
- width += 1;
222
- followsMark = false;
223
- } else if (MARK_CHAR.test(char)) {
224
- followsMark = true;
225
- } else if (!NON_PRINTING_CHAR.test(char)) {
226
- const trailing = char.codePointAt(0) ?? 0;
227
- if (followsMark) width += isWide(trailing) ? 2 : 1;
228
- else if (trailing >= 0xff00 && trailing <= 0xffef) width += isWide(trailing) ? 2 : 1;
229
- else if (trailing === 0x0e33 || trailing === 0x0eb3) width += 1;
230
- followsMark = false;
231
- }
232
- }
233
- return width;
234
- }
235
-
236
- // ---------------------------------------------------------------------------------------------
237
- // The oracle itself, pinned to numbers measured from the installed `visibleWidth`
238
- // ---------------------------------------------------------------------------------------------
239
-
240
- test("the host-width oracle agrees with the values pi-tui's own visibleWidth produces", () => {
241
- const measured: Array<[string, number]> = [
242
- ["a", 1],
243
- ["©", 1],
244
- ["©️", 2],
245
- ["1️⃣", 2],
246
- ["▶", 1],
247
- ["🎵", 2],
248
- ["ガ", 2],
249
- ["क्‍ष", 2],
250
- ["नमस्ते", 4],
251
- ["ि", 1],
252
- ["👨‍👩‍👧‍👦", 2],
253
- ["🇯🇵", 2],
254
- ["e\u0301", 1],
255
- ["\u0301", 0],
256
- ["a\tb", 5],
257
- ["中文", 4],
258
- ["한국어", 6],
259
- ["A", 2],
260
- ["👨\u001b[31m‍👩‍👧‍👦AB", 4],
261
- ["\u001b]8;;http://example.com\u0007a\u001b]8;;\u0007", 1],
262
- ];
263
- for (const [row, columns] of measured) {
264
- assert.equal(hostWidth(row), columns, `${JSON.stringify(row)} measures ${columns} to the host`);
265
- }
266
- });
267
-
268
- /**
269
- * The width the layout gives the main column, given what a part returned: the host pads each row by
270
- * one column on both sides (`:414`) and keeps the rail only while every rail row fits the content
271
- * width (`:410`). Otherwise the rail is dropped and the conversation gets the full terminal.
272
- */
273
- function mainColumnFor(partLines: readonly string[]): number {
274
- const railLines = partLines.map((line) => " ".repeat(RAIL_PADDING) + line + " ".repeat(RAIL_PADDING));
275
- const active = railLines.length > 0 && railLines.every((line) => hostWidth(line) <= RAIL_WIDTH);
276
- return active ? MAIN_WITH_RAIL : MAIN_WITHOUT_RAIL;
277
- }
278
-
279
- // ---------------------------------------------------------------------------------------------
280
- // Fixtures
281
- // ---------------------------------------------------------------------------------------------
282
-
283
- let configHome: string;
284
- let previousConfigHome: string | undefined;
285
-
286
- test.before(() => {
287
- configHome = mkdtempSync(join(tmpdir(), "gsb-rail-width-"));
288
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
289
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
290
- });
291
-
292
- test.after(() => {
293
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
294
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
295
- rmSync(configHome, { recursive: true, force: true });
296
- });
297
-
298
- test.beforeEach(() => {
299
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
300
- resetBusForTests();
301
- resetSectionsForTests();
302
- resetSidebar();
303
- resetSidebarStateForTests();
304
- resetSectionPanelForTests();
305
- resetSectionFailuresForTests();
306
- resetStoreStateForTests();
307
- setSectionTheme(undefined);
308
- installSectionRail();
309
- installSidebarBackend(() => ({ terminal: { columns: TERMINAL_COLUMNS, rows: 40 } }));
310
- });
311
-
312
- const ctx: SectionContext = { theme: undefined, configHome: "" };
313
-
314
- function contribute(section: SidebarSection, width = GIVEN): string[] {
315
- registerSection(section);
316
- return buildSectionContribution(width, ctx)?.lines ?? [];
317
- }
318
-
319
- /** The widest row, as the host reads it, with the row itself for the failure message. */
320
- function widest(lines: readonly string[]): { width: number; text: string } {
321
- return lines.reduce(
322
- (worst, line) => (hostWidth(line) > worst.width ? { width: hostWidth(line), text: line } : worst),
323
- { width: 0, text: "" },
324
- );
325
- }
326
-
327
- // ---------------------------------------------------------------------------------------------
328
- // N17/N19 — a contributed part never asks for more room than it was given
329
- // ---------------------------------------------------------------------------------------------
330
-
331
- test("a section's rows never exceed the width the host handed it, measured the host's way", () => {
332
- const rows: Array<[string, SidebarSection]> = [
333
- ["plain ASCII", {
334
- id: "width.ascii",
335
- title: "ASCII",
336
- lines: () => ["transform registered", "second row"],
337
- }],
338
- // `clipRow` measures with `String#length`, so a row of wide characters is left untouched while
339
- // the host measures twice as many columns.
340
- ["a wide-character body row", {
341
- id: "width.wide",
342
- title: "WIDE",
343
- lines: () => ["日本語のテキストをここに書いて、幅を超えさせる行です。もっと長くします。"],
344
- }],
345
- // A tab is ONE column to this package and THREE to the host (`utils.js:153-155`), so a padded
346
- // row measures two columns wider than the width it was fitted to.
347
- ["a tab in a structured row", {
348
- id: "width.tab",
349
- title: "TAB",
350
- lines: () => [{ glyph: "○", label: "rendering\tmode", meta: "default", tone: "muted" }],
351
- }],
352
- ["an emoji cluster", {
353
- id: "width.emoji",
354
- title: "EMOJI",
355
- lines: () => [{ glyph: "🎵", label: "now playing 👨‍👩‍👧‍👦", meta: "▶", tone: "accent" }],
356
- }],
357
- ];
358
-
359
- // Every case is measured before the first failure, so one RED run names each violation and its
360
- // numbers instead of only the first.
361
- const violations: string[] = [];
362
- for (const [label, section] of rows) {
363
- resetSectionsForTests();
364
- const lines = contribute(section);
365
- assert.ok(lines.length > 0, `${label}: the section contributed no rows`);
366
- const worst = widest(lines);
367
- if (worst.width > GIVEN) {
368
- violations.push(
369
- `${label}: the host measures ${worst.width} columns for a row fitted to ${GIVEN} (over by ${worst.width - GIVEN}): ${JSON.stringify(worst.text)}`,
370
- );
371
- }
372
- }
373
- assert.deepEqual(violations, [], violations.join("\n"));
374
- });
375
-
376
- test("a contributed section leaves the main column exactly where the host put it", () => {
377
- // The host's own footer part, as the host paints it: one column of padding on both sides (`:414`).
378
- const hostFooter = ["▾ STATUS 2", " model claude", " cost $0.12"];
379
- assert.equal(mainColumnFor(hostFooter), MAIN_WITH_RAIL);
380
-
381
- const sections: SidebarSection[] = [
382
- { id: "width.plain", title: "ASCII", lines: () => ["transform registered"] },
383
- { id: "width.cjk", title: "WIDE", lines: () => ["日本語のテキストをここに書いて、幅を超えさせる行です。もっと長くします。"] },
384
- { id: "width.tabbed", title: "TAB", lines: () => [{ glyph: "○", label: "rendering\tmode", meta: "default" }] },
385
- ];
386
-
387
- for (const section of sections) {
388
- resetSectionsForTests();
389
- const lines = contribute(section);
390
- const rail = [...hostFooter, ...lines];
391
- assert.equal(
392
- mainColumnFor(rail),
393
- MAIN_WITH_RAIL,
394
- `section ${section.id}: the main column moved to ${mainColumnFor(rail)} (rail columns ${RAIL_COLUMNS}, terminal ${TERMINAL_COLUMNS}); ` +
395
- `the widest contributed row is ${widest(lines).width} columns for a ${GIVEN}-column budget`,
396
- );
397
- }
398
- });
399
-
400
- // ---------------------------------------------------------------------------------------------
401
- // N20 — the report carries the last render's budget and what the bridge did with it
402
- // ---------------------------------------------------------------------------------------------
403
-
404
- test("/bridge-status reports the last render's given width and the widest row the bridge produced", () => {
405
- contribute({
406
- id: "width.report",
407
- title: "REPORT",
408
- // A row fitted to the budget, and one that has to give way to it.
409
- lines: () => [{ glyph: "○", label: "rendering", meta: "default" }],
410
- });
411
- const report = formatStatus(collectStatus());
412
- const line = report.find((entry) => entry.startsWith("section width:"));
413
- assert.equal(line, `section width: ${GIVEN} columns given, widest row ${GIVEN}`);
414
- });
415
-
416
- test("a session whose rail never rendered a section keeps its report unchanged", () => {
417
- registerSection({ id: "width.noframe", title: "NOFRAME", lines: () => ["never rendered"] });
418
- const report = formatStatus(collectStatus());
419
- assert.deepEqual(
420
- report.filter((entry) => entry.startsWith("section width:")),
421
- [],
422
- );
423
- });