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.
- package/LICENSE +202 -0
- package/README.md +91 -722
- package/docs/contract.md +92 -35
- package/package.json +13 -1
- package/src/api.ts +1 -1
- package/.codegraph/.gitignore +0 -5
- package/PRD.md +0 -144
- package/examples/bash-card.ts +0 -1694
- package/gentle-shell-bridge-0.3.0.tgz +0 -0
- package/odd/tasks/palette-sections.md +0 -30
- package/odd/tasks/plugin-keymap.md +0 -32
- package/odd/tasks/plugin-surface-contract.md +0 -55
- package/test/api.test.ts +0 -395
- package/test/apply-on-move.test.ts +0 -477
- package/test/arm.test.ts +0 -220
- package/test/bridge-ready.test.ts +0 -309
- package/test/bus.test.ts +0 -165
- package/test/button-rows.test.ts +0 -409
- package/test/capture.test.ts +0 -356
- package/test/card-style-rows.test.ts +0 -510
- package/test/card-style-transform.test.ts +0 -379
- package/test/card-style-wiring.test.ts +0 -258
- package/test/card-styles-e2e.test.ts +0 -491
- package/test/card-styles.test.ts +0 -324
- package/test/card-transcript-path.test.ts +0 -178
- package/test/config.test.ts +0 -93
- package/test/e2e.test.ts +0 -443
- package/test/example-bash-identity.test.ts +0 -372
- package/test/example-bash-matrix.test.ts +0 -856
- package/test/example-designs.test.ts +0 -734
- package/test/example-neon-transform.test.ts +0 -283
- package/test/example-plain-style.test.ts +0 -1268
- package/test/extension.test.ts +0 -160
- package/test/fake-events.ts +0 -58
- package/test/fake-host.ts +0 -211
- package/test/fake-sidebar.ts +0 -516
- package/test/fake-transcript.ts +0 -486
- package/test/frame-telemetry.test.ts +0 -417
- package/test/hook.test.ts +0 -196
- package/test/manifest.test.ts +0 -105
- package/test/new-category.test.ts +0 -979
- package/test/notices.test.ts +0 -328
- package/test/plugin-integration-lines.test.ts +0 -979
- package/test/plugin-surface.test.ts +0 -152
- package/test/presence.test.ts +0 -459
- package/test/rail-surfaces.test.ts +0 -457
- package/test/rail-width.test.ts +0 -423
- package/test/regression.test.ts +0 -483
- package/test/repaint-api.test.ts +0 -200
- package/test/reserved-categories.test.ts +0 -361
- package/test/row-labels.test.ts +0 -284
- package/test/row-names.test.ts +0 -473
- package/test/row-origin.test.ts +0 -395
- package/test/section-interaction.test.ts +0 -364
- package/test/section-panel.test.ts +0 -403
- package/test/sections-rows.test.ts +0 -291
- package/test/sidebar-backend.test.ts +0 -471
- package/test/sidebar-e2e.test.ts +0 -331
- package/test/sidebar-hover.test.ts +0 -276
- package/test/sidebar-independent.test.ts +0 -349
- package/test/sidebar-render.test.ts +0 -281
- package/test/sidebar-sections.test.ts +0 -245
- package/test/status-card-styles.test.ts +0 -201
- package/test/status-section-frame.test.ts +0 -181
- package/test/status-sections.test.ts +0 -243
- package/test/status.test.ts +0 -209
- package/test/store.test.ts +0 -192
- package/test/surface-repaint-cost.test.ts +0 -520
- package/test/tool-design-chain.test.ts +0 -483
- package/test/tool-design-cost.test.ts +0 -473
- package/test/tool-design-rows.test.ts +0 -359
- package/test/tool-designs.test.ts +0 -745
- package/test/transform-pipeline.test.ts +0 -465
- package/test/transforms-bus.test.ts +0 -233
- package/test/verifier-width-regressions.test.ts +0 -164
- package/tsconfig.json +0 -18
package/test/rail-width.test.ts
DELETED
|
@@ -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
|
-
});
|