gentle-shell-bridge 0.3.0 → 0.3.1

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 (75) hide show
  1. package/LICENSE +202 -0
  2. package/docs/contract.md +1 -1
  3. package/package.json +13 -1
  4. package/src/api.ts +1 -1
  5. package/.codegraph/.gitignore +0 -5
  6. package/PRD.md +0 -144
  7. package/examples/bash-card.ts +0 -1694
  8. package/gentle-shell-bridge-0.3.0.tgz +0 -0
  9. package/odd/tasks/palette-sections.md +0 -30
  10. package/odd/tasks/plugin-keymap.md +0 -32
  11. package/odd/tasks/plugin-surface-contract.md +0 -55
  12. package/test/api.test.ts +0 -395
  13. package/test/apply-on-move.test.ts +0 -477
  14. package/test/arm.test.ts +0 -220
  15. package/test/bridge-ready.test.ts +0 -309
  16. package/test/bus.test.ts +0 -165
  17. package/test/button-rows.test.ts +0 -409
  18. package/test/capture.test.ts +0 -356
  19. package/test/card-style-rows.test.ts +0 -510
  20. package/test/card-style-transform.test.ts +0 -379
  21. package/test/card-style-wiring.test.ts +0 -258
  22. package/test/card-styles-e2e.test.ts +0 -491
  23. package/test/card-styles.test.ts +0 -324
  24. package/test/card-transcript-path.test.ts +0 -178
  25. package/test/config.test.ts +0 -93
  26. package/test/e2e.test.ts +0 -443
  27. package/test/example-bash-identity.test.ts +0 -372
  28. package/test/example-bash-matrix.test.ts +0 -856
  29. package/test/example-designs.test.ts +0 -734
  30. package/test/example-neon-transform.test.ts +0 -283
  31. package/test/example-plain-style.test.ts +0 -1268
  32. package/test/extension.test.ts +0 -160
  33. package/test/fake-events.ts +0 -58
  34. package/test/fake-host.ts +0 -211
  35. package/test/fake-sidebar.ts +0 -516
  36. package/test/fake-transcript.ts +0 -486
  37. package/test/frame-telemetry.test.ts +0 -417
  38. package/test/hook.test.ts +0 -196
  39. package/test/manifest.test.ts +0 -105
  40. package/test/new-category.test.ts +0 -979
  41. package/test/notices.test.ts +0 -328
  42. package/test/plugin-integration-lines.test.ts +0 -979
  43. package/test/plugin-surface.test.ts +0 -152
  44. package/test/presence.test.ts +0 -459
  45. package/test/rail-surfaces.test.ts +0 -457
  46. package/test/rail-width.test.ts +0 -423
  47. package/test/regression.test.ts +0 -483
  48. package/test/repaint-api.test.ts +0 -200
  49. package/test/reserved-categories.test.ts +0 -361
  50. package/test/row-labels.test.ts +0 -284
  51. package/test/row-names.test.ts +0 -473
  52. package/test/row-origin.test.ts +0 -395
  53. package/test/section-interaction.test.ts +0 -364
  54. package/test/section-panel.test.ts +0 -403
  55. package/test/sections-rows.test.ts +0 -291
  56. package/test/sidebar-backend.test.ts +0 -471
  57. package/test/sidebar-e2e.test.ts +0 -331
  58. package/test/sidebar-hover.test.ts +0 -276
  59. package/test/sidebar-independent.test.ts +0 -349
  60. package/test/sidebar-render.test.ts +0 -281
  61. package/test/sidebar-sections.test.ts +0 -245
  62. package/test/status-card-styles.test.ts +0 -201
  63. package/test/status-section-frame.test.ts +0 -181
  64. package/test/status-sections.test.ts +0 -243
  65. package/test/status.test.ts +0 -209
  66. package/test/store.test.ts +0 -192
  67. package/test/surface-repaint-cost.test.ts +0 -520
  68. package/test/tool-design-chain.test.ts +0 -483
  69. package/test/tool-design-cost.test.ts +0 -473
  70. package/test/tool-design-rows.test.ts +0 -359
  71. package/test/tool-designs.test.ts +0 -745
  72. package/test/transform-pipeline.test.ts +0 -465
  73. package/test/transforms-bus.test.ts +0 -233
  74. package/test/verifier-width-regressions.test.ts +0 -164
  75. 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
- });