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,245 +0,0 @@
1
- /**
2
- * The sidebar section surface, proven against the fake host in `test/fake-sidebar.ts`.
3
- *
4
- * T1 (S9, S4): the fake host models the host's process-global rail state, its `parts` map and the
5
- * card frame the layout draws from a rail's `cardGroups`. T2 (S1, S2): the section registry on the
6
- * same process-global bus as the providers — one ordered snapshot, `id` replacement, a disposer
7
- * that removes exactly one section.
8
- *
9
- * The first two tests are the scaffold's own: the fake is not the host, so it must be shown to
10
- * behave like it (state slot, later registration, framed output) before any section test leans on
11
- * it.
12
- */
13
-
14
- import assert from "node:assert/strict";
15
- import test from "node:test";
16
-
17
- import {
18
- registerRows,
19
- registerSection,
20
- resetBusForTests,
21
- resetSectionsForTests,
22
- snapshotProviders,
23
- snapshotSections,
24
- } from "../src/bus.ts";
25
- import type { RowProvider, SidebarSection } from "../src/types.ts";
26
- import {
27
- SIDEBAR_STATE_KEY,
28
- cardTitles,
29
- fakeState,
30
- registerNativeFooter,
31
- renderSidebar,
32
- resetSidebar,
33
- } from "./fake-sidebar.ts";
34
-
35
- const section = (id: string, extra: Partial<SidebarSection> = {}): SidebarSection => ({
36
- id,
37
- title: id.toUpperCase(),
38
- lines: () => [`body ${id}`],
39
- ...extra,
40
- });
41
-
42
- const provider = (id: string): RowProvider => ({ id, category: "Layout", rows: () => [] });
43
-
44
- const ids = (): string[] => snapshotSections().map((entry) => entry.id);
45
-
46
- test.beforeEach(() => {
47
- resetBusForTests();
48
- resetSectionsForTests();
49
- resetSidebar();
50
- });
51
-
52
- test("the fake host carries the rail map on the host's process-global state symbol", () => {
53
- assert.equal(SIDEBAR_STATE_KEY, Symbol.for("gentle-pi.experimental-sidebar.state"));
54
- assert.equal(renderSidebar(40).join("\n"), "", "an empty host paints nothing");
55
-
56
- registerNativeFooter();
57
- const footer = fakeState().parts.get("footer");
58
- assert.ok(footer !== undefined, "the native footer sits in the host's rail map");
59
- assert.deepEqual(cardTitles(), ["FOOTER"], "the host draws the rail as one framed card");
60
- });
61
-
62
- test("the host can register its footer rail after the first frame was painted", () => {
63
- assert.deepEqual(cardTitles(), []);
64
- registerNativeFooter();
65
- assert.deepEqual(cardTitles(), ["FOOTER"]);
66
- });
67
-
68
- test("registerSection returns a disposer that removes exactly that section", () => {
69
- registerSection(section("a"));
70
- const disposeB = registerSection(section("b"));
71
- registerSection(section("c"));
72
-
73
- assert.deepEqual(ids(), ["a", "b", "c"]);
74
-
75
- disposeB();
76
-
77
- assert.deepEqual(ids(), ["a", "c"], "the other sections keep their relative order");
78
- });
79
-
80
- test("registerSection stores the caller's object unchanged", () => {
81
- const registered = section("a", {
82
- title: "MUSIC",
83
- meta: () => "1 playing",
84
- lines: () => ["track -", "album -"],
85
- defaultVisible: false,
86
- order: 3,
87
- });
88
-
89
- registerSection(registered);
90
-
91
- assert.equal(snapshotSections()[0], registered, "the snapshot hands back the registered object");
92
- assert.equal(snapshotSections()[0].title, "MUSIC");
93
- assert.equal(snapshotSections()[0].defaultVisible, false);
94
- assert.equal(snapshotSections()[0].order, 3);
95
- });
96
-
97
- test("a second registration with the same id replaces in place, keeping its position", () => {
98
- registerSection(section("a"));
99
- registerSection(section("b"));
100
- registerSection(section("c"));
101
-
102
- const replacement = section("b", { title: "REPLACED" });
103
- registerSection(replacement);
104
-
105
- assert.deepEqual(ids(), ["a", "b", "c"], "one entry, original position");
106
- assert.equal(snapshotSections().length, 3);
107
- assert.equal(snapshotSections()[1], replacement, "the new registration is the live one");
108
- assert.equal(snapshotSections()[1].title, "REPLACED");
109
- });
110
-
111
- test("the replaced section's disposer does not remove its replacement", () => {
112
- const disposeFirst = registerSection(section("a"));
113
- const replacement = section("a", { title: "REPLACED" });
114
- registerSection(replacement);
115
-
116
- disposeFirst();
117
-
118
- assert.deepEqual(ids(), ["a"], "the stale disposer is a no-op, not a removal of the replacement");
119
- assert.equal(snapshotSections()[0], replacement);
120
- });
121
-
122
- test("a disposer called twice is a no-op the second time", () => {
123
- registerSection(section("a"));
124
- const disposeB = registerSection(section("b"));
125
- registerSection(section("c"));
126
-
127
- disposeB();
128
- disposeB();
129
-
130
- assert.deepEqual(ids(), ["a", "c"]);
131
- });
132
-
133
- test("sections with an order come first, ascending and ties by registration order", () => {
134
- registerSection(section("un-ordered-first"));
135
- registerSection(section("ten", { order: 10 }));
136
- registerSection(section("one", { order: 1 }));
137
- registerSection(section("tie-a", { order: 5 }));
138
- registerSection(section("tie-b", { order: 5 }));
139
- registerSection(section("un-ordered-second"));
140
-
141
- assert.deepEqual(
142
- ids(),
143
- ["one", "tie-a", "tie-b", "ten", "un-ordered-first", "un-ordered-second"],
144
- "ordered sections sort ascending ahead of the unordered ones, which keep registration order",
145
- );
146
- });
147
-
148
- test("a replacement may move a section by changing its order", () => {
149
- registerSection(section("a", { order: 2 }));
150
- registerSection(section("b", { order: 1 }));
151
-
152
- registerSection(section("a", { order: 0 }));
153
-
154
- assert.deepEqual(ids(), ["a", "b"]);
155
- });
156
-
157
- test("only numbered orders sort; a section without one goes to the tail", () => {
158
- registerSection(section("zero", { order: 0 }));
159
- registerSection(section("unordered", { order: undefined }));
160
- registerSection(section("negative", { order: -3 }));
161
- registerSection(section("not-a-number", { order: Number.NaN }));
162
-
163
- assert.deepEqual(ids(), ["negative", "zero", "unordered", "not-a-number"]);
164
- });
165
-
166
- test("a section snapshot is a stable copy taken at call time", () => {
167
- registerSection(section("a"));
168
-
169
- const during = snapshotSections();
170
- const seen: string[] = [];
171
- for (const entry of during) {
172
- seen.push(entry.id);
173
- registerSection(section("late"));
174
- }
175
-
176
- assert.deepEqual(seen, ["a"]);
177
- assert.deepEqual(during.map((entry) => entry.id), ["a"]);
178
- assert.deepEqual(ids(), ["a", "late"]);
179
- });
180
-
181
- test("two independent module copies in one process share one section registry", async () => {
182
- // A query string yields a second module instance with its own module scope — the closest local
183
- // model of jiti's `moduleCache: false` loading two package copies.
184
- const secondCopySpecifier = "../src/bus.ts?copy=2";
185
- const copyTwo: typeof import("../src/bus.ts") = await import(secondCopySpecifier);
186
-
187
- registerSection(section("from-copy-one"));
188
- copyTwo.registerSection(section("from-copy-two"));
189
-
190
- assert.deepEqual(ids(), ["from-copy-one", "from-copy-two"]);
191
- assert.deepEqual(
192
- copyTwo.snapshotSections().map((entry) => entry.id),
193
- ["from-copy-one", "from-copy-two"],
194
- );
195
-
196
- const dispose = copyTwo.registerSection(section("shared"));
197
- assert.deepEqual(ids(), ["from-copy-one", "from-copy-two", "shared"]);
198
- dispose();
199
- assert.deepEqual(ids(), ["from-copy-one", "from-copy-two"], "the global registry, not a local one");
200
- });
201
-
202
- test("the section list is reachable behind the same bus slot as the providers", () => {
203
- registerSection(section("a"));
204
- const slot = (globalThis as unknown as Record<symbol, unknown>)[
205
- Symbol.for("gentle-shell-bridge.providers")
206
- ];
207
- assert.ok(slot !== undefined, "the bus is the provider bus, not a second registry");
208
- });
209
-
210
- test("a bus slot created before sections existed keeps its providers and grows a section list", () => {
211
- // A copy of this package that predates sections writes `{ entries }` only. Reading that slot
212
- // must not throw away the providers already registered in it.
213
- const earlierProvider = provider("earlier");
214
- const slot = globalThis as unknown as Record<symbol, unknown>;
215
- slot[Symbol.for("gentle-shell-bridge.providers")] = { entries: [{ provider: earlierProvider }] };
216
-
217
- registerSection(section("a"));
218
-
219
- assert.deepEqual(ids(), ["a"], "the section list was added in place");
220
- assert.deepEqual(
221
- snapshotProviders().map((entry) => entry.id),
222
- ["earlier"],
223
- "the providers the earlier copy registered are still there",
224
- );
225
- });
226
-
227
- test("resetSectionsForTests clears sections and leaves providers registered", () => {
228
- registerRows(provider("p"));
229
- registerSection(section("a"));
230
-
231
- resetSectionsForTests();
232
-
233
- assert.deepEqual(ids(), []);
234
- assert.deepEqual(
235
- snapshotProviders().map((entry) => entry.id),
236
- ["p"],
237
- "provider state is not the section state",
238
- );
239
- });
240
-
241
- test("the internal section seams are not on the public entry", async () => {
242
- const entry = await import("../src/index.ts");
243
- assert.equal("resetSectionsForTests" in entry, false);
244
- assert.equal("snapshotSections" in entry, false);
245
- });
@@ -1,201 +0,0 @@
1
- /**
2
- * `/bridge-status` for the card-styles surface (S7, S12): the line mode, the registered styles in
3
- * order, the live selection (including a stale one), what the last frame did, the per-style failures,
4
- * and the one warning the bridge owes the user — a later post-processor can override this frame.
5
- *
6
- * The guard that matters most: a session with no card style registered and no card state prints the
7
- * pre-feature report byte for byte, because every block below is emitted only when it has something
8
- * to say.
9
- */
10
-
11
- import assert from "node:assert/strict";
12
- import { mkdtempSync, rmSync } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import { join } from "node:path";
15
- import test from "node:test";
16
-
17
- import { armBridge } from "../src/arm.ts";
18
- import { registerCardStyle, resetBusForTests, resetCardStylesForTests } from "../src/bus.ts";
19
- import { installCardStyleRows, resetCardStyleRowStateForTests } from "../src/card-style-rows.ts";
20
- import { getCardStyleTransformFailures } from "../src/card-style-transform.ts";
21
- import {
22
- disableCardStyleTransform,
23
- installCardStyleTransform,
24
- resetCardStyleWiringForTests,
25
- } from "../src/card-style-wiring.ts";
26
- import { resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
27
- import { setCardStyleSelection } from "../src/card-styles.ts";
28
- import { disableCaptureBackend, resetCaptureStateForTests } from "../src/capture.ts";
29
- import { resetHookStateForTests } from "../src/hook.ts";
30
- import { resetNoticesForTests } from "../src/notices.ts";
31
- import { resetRowBuildStateForTests } from "../src/rows.ts";
32
- import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
33
- import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
34
- import { resetSectionsForTests } from "../src/bus.ts";
35
- import { collectStatus, formatStatus } from "../src/status.ts";
36
- import { resetStoreStateForTests } from "../src/store.ts";
37
- import type { CardStyleRegistration } from "../src/types.ts";
38
- import { createFakeHost } from "./fake-host.ts";
39
- import { READ_CARD, createFakeTranscriptTui } from "./fake-transcript.ts";
40
-
41
- let configHome: string;
42
- let previousConfigHome: string | undefined;
43
-
44
- test.before(() => {
45
- configHome = mkdtempSync(join(tmpdir(), "gsb-status-cards-"));
46
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
47
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
48
- });
49
-
50
- test.after(() => {
51
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
52
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
53
- rmSync(configHome, { recursive: true, force: true });
54
- });
55
-
56
- test.beforeEach(() => {
57
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
58
- resetBusForTests();
59
- resetSectionsForTests();
60
- resetCardStylesForTests();
61
- resetCardStyleRowStateForTests();
62
- resetCardStyleTransformForTests();
63
- resetCardStyleWiringForTests();
64
- resetCaptureStateForTests();
65
- resetHookStateForTests();
66
- resetRowBuildStateForTests();
67
- resetSidebarStateForTests();
68
- resetSectionFailuresForTests();
69
- resetNoticesForTests();
70
- resetStoreStateForTests();
71
- disableCaptureBackend();
72
- disableSidebarBackend();
73
- disableCardStyleTransform();
74
- });
75
-
76
- const style = (id: string, label: string, order?: number): CardStyleRegistration => ({
77
- id,
78
- label,
79
- ...(order === undefined ? {} : { order }),
80
- frame: { top: () => "+", line: (ctx) => `> ${ctx.text}`, bottom: () => "-" },
81
- });
82
-
83
- const statusLines = (): string[] => formatStatus(collectStatus());
84
-
85
- const cardBlock = (lines: readonly string[]): string[] => {
86
- const start = lines.findIndex((line) => line.startsWith("card styles: "));
87
- if (start === -1) return [];
88
- const rest = lines.slice(start);
89
- const end = rest.findIndex((line) => line.startsWith("card style note: "));
90
- return end === -1 ? rest : rest.slice(0, end + 1);
91
- };
92
-
93
- test("a session with no style and no card state keeps the historical output byte for byte", () => {
94
- assert.deepEqual(statusLines(), [
95
- "gentle-shell-bridge status",
96
- "providers: none",
97
- "backend: none",
98
- "last capture: none",
99
- "last failures: none",
100
- "store problems: none",
101
- "duplicate settings: no build yet",
102
- ]);
103
- assert.equal(collectStatus().cardStyles, undefined);
104
- });
105
-
106
- test("the block reports mode, styles in order, selection, last frame and the downstream warning", () => {
107
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
108
- installCardStyleTransform(tui);
109
- registerCardStyle(style("loon.cute", "cute", 10));
110
- registerCardStyle(style("pix.mono", "mono"));
111
- registerCardStyle({
112
- id: "loon.broken",
113
- label: "broken",
114
- frame: {
115
- top: () => {
116
- throw new Error("top exploded");
117
- },
118
- line: (ctx) => ctx.text,
119
- bottom: () => "-",
120
- },
121
- });
122
-
123
- setCardStyleSelection("loon.broken");
124
- tui.chat.render(40);
125
- setCardStyleSelection("loon.cute");
126
- tui.chat.render(40);
127
-
128
- assert.deepEqual(
129
- getCardStyleTransformFailures(),
130
- [{ styleId: "loon.broken", message: "top exploded" }],
131
- "the failing style is remembered",
132
- );
133
- assert.deepEqual(cardBlock(statusLines()), [
134
- "card styles: separate, 3 registered",
135
- ' - loon.cute "cute" (order 10)',
136
- ' - pix.mono "mono" (registration order)',
137
- ' - loon.broken "broken" (registration order)',
138
- 'card style selection: loon.cute "cute"',
139
- "card style frame: transformed 1 card",
140
- "card style surface conversation: reached (chat wrapped), last frame transformed 1 card",
141
- "card style surface rail: not reached (not attempted), last frame none",
142
- "card style wiring: chat wrapped",
143
- "card style failures: 1",
144
- " - loon.broken: top exploded",
145
- "card style note: a downstream post-processor can strip or override this frame; " +
146
- "the bridge cannot prevent it.",
147
- ]);
148
- });
149
-
150
- test("a stale selection is reported as missing, never rendered (S12)", () => {
151
- const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
152
- installCardStyleTransform(tui);
153
- const dispose = registerCardStyle(style("loon.cute", "cute"));
154
- setCardStyleSelection("loon.cute");
155
- tui.chat.render(40);
156
- dispose();
157
-
158
- tui.chat.render(40);
159
-
160
- assert.deepEqual(cardBlock(statusLines()), [
161
- "card styles: separate, 0 registered",
162
- "card style selection: loon.cute (missing: not registered)",
163
- "card style frame: pass-through (missing: a disposed style)",
164
- "card style surface conversation: reached (chat wrapped), last frame pass-through (missing: a disposed style)",
165
- "card style surface rail: not reached (not attempted), last frame none",
166
- "card style wiring: chat wrapped",
167
- "card style note: a downstream post-processor can strip or override this frame; " +
168
- "the bridge cannot prevent it.",
169
- ]);
170
- });
171
-
172
- test("an unarmed transform reports why, so a missing repaint has an answer", () => {
173
- registerCardStyle(style("loon.cute", "cute"));
174
- installCardStyleTransform({ terminal: {}, children: [] });
175
-
176
- const lines = statusLines();
177
-
178
- assert.ok(lines.includes("card style wiring: not wrapped (no-children)"));
179
- assert.ok(lines.includes("card style frame: none"));
180
- });
181
-
182
- test("the extended mode is reported once the host's own line was found", () => {
183
- const host = createFakeHost({
184
- rows: [
185
- { category: "Cards", label: "Card bash: builtin", action: () => {} },
186
- {
187
- category: "Cards",
188
- label: () => "Card style: float",
189
- action: () => {},
190
- choice: { setting: "Card style", value: "float", current: () => true },
191
- },
192
- { category: "Layout", label: "Layout: builtin", action: () => {} },
193
- ],
194
- });
195
- armBridge(host.ui);
196
- installCardStyleRows();
197
- registerCardStyle(style("loon.cute", "cute"));
198
- host.buildMenu();
199
-
200
- assert.ok(statusLines().includes("card styles: extended, 1 registered"));
201
- });
@@ -1,181 +0,0 @@
1
- /**
2
- * `/bridge-status` for the section frame (N11, N15): which drawing the header and rows use, whether
3
- * the theme was captured, how the pointer reaches them, and — when the host's own builders cannot be
4
- * reached — the gap, named.
5
- *
6
- * Every fact asserted here is a measurement of the run: the source comes from looking for the host's
7
- * builders on the objects the bridge is handed, so the "host" case is proved by publishing them on a
8
- * fake rail and watching the drawing change.
9
- */
10
-
11
- import assert from "node:assert/strict";
12
- import { mkdtempSync, rmSync } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import { join } from "node:path";
15
- import test from "node:test";
16
-
17
- import { registerSection, resetBusForTests, resetSectionsForTests } from "../src/bus.ts";
18
- import { setRepaintHost, resetRepaintForTests } from "../src/repaint.ts";
19
- import { resetSectionPanelForTests, setSectionTheme } from "../src/section-panel.ts";
20
- import { resetSectionPointerForTests } from "../src/section-pointer.ts";
21
- import {
22
- SIDEBAR_RAIL_KEY,
23
- disableSidebarBackend,
24
- installSidebarBackend,
25
- resetSidebarStateForTests,
26
- } from "../src/sidebar.ts";
27
- import {
28
- installSectionRail,
29
- resetSectionFailuresForTests,
30
- } from "../src/sidebar-render.ts";
31
- import { collectStatus, formatStatus } from "../src/status.ts";
32
- import { resetStoreStateForTests } from "../src/store.ts";
33
- import type { SidebarSection } from "../src/types.ts";
34
- import { setSectionCollapsed } from "../src/visibility.ts";
35
- import {
36
- fakeState,
37
- fakeTerminal,
38
- fakeTui,
39
- registerNativeFooter,
40
- renderSidebar,
41
- resetSidebar,
42
- } from "./fake-sidebar.ts";
43
-
44
- let configHome: string;
45
- let previousConfigHome: string | undefined;
46
-
47
- test.before(() => {
48
- configHome = mkdtempSync(join(tmpdir(), "gsb-frame-status-"));
49
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
50
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
51
- });
52
-
53
- test.after(() => {
54
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
55
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
56
- rmSync(configHome, { recursive: true, force: true });
57
- });
58
-
59
- test.beforeEach(() => {
60
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
61
- resetBusForTests();
62
- resetSectionsForTests();
63
- resetSidebar();
64
- resetSidebarStateForTests();
65
- resetSectionFailuresForTests();
66
- resetSectionPanelForTests();
67
- resetSectionPointerForTests();
68
- resetRepaintForTests();
69
- resetStoreStateForTests();
70
- disableSidebarBackend();
71
- });
72
-
73
- const section = (id: string, extra: Partial<SidebarSection> = {}): SidebarSection => ({
74
- id,
75
- title: id.toUpperCase(),
76
- lines: () => [`body ${id}`],
77
- ...extra,
78
- });
79
-
80
- function arm(): void {
81
- setRepaintHost({ terminal: fakeTerminal(), requestRender: () => {} });
82
- registerNativeFooter();
83
- installSidebarBackend(() => fakeTui());
84
- installSectionRail();
85
- }
86
-
87
- const frameLines = (): string[] => formatStatus(collectStatus()).filter((line) => line.startsWith("section "));
88
-
89
- /** A theme shaped like Pi's, so the drawing has something to paint with. */
90
- const theme = { fg: (role: string, text: string) => `<${role}>${text}`, bold: (text: string) => text };
91
-
92
- test("with no section there is no frame block: a row-only session keeps its output", () => {
93
- assert.deepEqual(frameLines(), []);
94
- assert.equal(collectStatus().sectionFrame, undefined);
95
- });
96
-
97
- test("the fallback reports what it looked for and found, and names the gap", () => {
98
- registerSection(section("music"));
99
- arm();
100
-
101
- assert.deepEqual(frameLines(), [
102
- "section frame: fallback (looked for renderPanelHeader/renderPanelRow on: the terminal sidebar state, the rail part; found none; no theme captured yet, rows drawn uncoloured)",
103
- "section interaction: rail handleMouse (the host's own dispatch), hover not used yet",
104
- "section frame gap: gentle-shell's lib/shell-panel.ts cannot be imported (Pi loads each extension in its own module graph) and publishes no builder on the rail part, the Pi theme or the terminal's sidebar state, so the header and rows are drawn by a byte-identical port",
105
- ]);
106
- const frame = collectStatus().sectionFrame;
107
- assert.equal(frame?.source, "fallback");
108
- assert.deepEqual(frame?.lookedAt, ["the terminal sidebar state", "the rail part"]);
109
- assert.equal(frame?.theme, "missing");
110
- });
111
-
112
- test("a host that publishes its own builders is used, and reported as the source", () => {
113
- setSectionTheme(theme);
114
- registerSection(section("music"));
115
- const rail = registerNativeFooter();
116
- const host = rail as unknown as Record<string, unknown>;
117
- host.renderPanelHeader = (_theme: unknown, _width: number, header: { title: string }) =>
118
- `HOST HEADER ${header.title.toUpperCase()}`;
119
- host.renderPanelRow = (_width: number, left: string) => `HOST ROW ${left}`;
120
- installSidebarBackend(() => fakeTui());
121
- installSectionRail();
122
-
123
- assert.deepEqual(frameLines(), [
124
- "section frame: host (the host's own renderPanelHeader/renderPanelRow, found on: the Pi theme, the terminal sidebar state, the rail part; theme captured, rows painted in the host's roles)",
125
- "section interaction: rail handleMouse (the host's own dispatch), hover not used yet",
126
- ]);
127
- assert.deepEqual(collectStatus().sectionFrame?.missing, []);
128
- assert.deepEqual((fakeState().parts.get(SIDEBAR_RAIL_KEY)?.render(20) ?? []).slice(3), [
129
- "HOST HEADER MUSIC",
130
- // A plain string row stays the bytes the author wrote, even when the host draws structured rows.
131
- "body music",
132
- ]);
133
- });
134
-
135
- test("the theme is reported captured once Pi hands it over", () => {
136
- setSectionTheme(theme);
137
- registerSection(section("music"));
138
- arm();
139
-
140
- assert.ok(
141
- frameLines()[0].includes("theme captured, rows painted in the host's roles"),
142
- frameLines()[0],
143
- );
144
- assert.equal(collectStatus().sectionFrame?.theme, "captured");
145
- });
146
-
147
- test("a collapsed section says so in the sections table", () => {
148
- registerSection(section("music", { title: "MUSIC" }));
149
- arm();
150
- setSectionCollapsed("music", true);
151
-
152
- assert.deepEqual(
153
- formatStatus(collectStatus()).filter((line) => line.includes('"MUSIC"')),
154
- [' - music "MUSIC": visible, collapsed, registration order, row "music"'],
155
- );
156
- assert.equal(collectStatus().sections?.[0].collapsed, true);
157
- });
158
-
159
- test("the pointer's mechanism is reported once it has been used", () => {
160
- setSectionTheme(theme);
161
- registerSection(
162
- section("music", { lines: () => [{ label: "play", onClick: () => {} }] }),
163
- );
164
- arm();
165
- renderSidebar();
166
-
167
- fakeState().parts.get(SIDEBAR_RAIL_KEY)?.handleMouse?.({
168
- type: "move",
169
- button: "none",
170
- x: 4,
171
- y: 4,
172
- width: 40,
173
- height: 20,
174
- });
175
-
176
- assert.equal(collectStatus().sectionFrame?.hover, "host-state");
177
- assert.ok(
178
- frameLines()[1].includes("hover owner claimed on the host's sidebar state"),
179
- frameLines()[1],
180
- );
181
- });