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,471 +0,0 @@
1
- /**
2
- * Rail interception (S4, S9), proven against the fake host.
3
- *
4
- * The host registers the `footer` part itself (`extensions/gentle-shell.ts:2681`), so our arming
5
- * may run before OR after it. This suite covers both orders, the pass-through of every field the
6
- * host's readers use (`digest` for the layout memo, `spend` for the Stats section), the mouse
7
- * delegation, idempotency across two installs / a rebind / a second package copy, the disable and
8
- * restore path, and the extension point T4 will use to contribute card groups.
9
- */
10
-
11
- import assert from "node:assert/strict";
12
- import test from "node:test";
13
-
14
- import {
15
- SIDEBAR_RAIL_KEY,
16
- disableSidebarBackend,
17
- getSidebarDiagnostics,
18
- installSidebarBackend,
19
- resetSidebarStateForTests,
20
- setRailContributor,
21
- } from "../src/sidebar.ts";
22
- import {
23
- SIDEBAR_STATE_KEY,
24
- cardBody,
25
- cardTitles,
26
- fakeState,
27
- fakeTerminal,
28
- fakeTui,
29
- registerNativeFooter,
30
- registerNativeRail,
31
- renderSidebar,
32
- resetSidebar,
33
- type Rail,
34
- } from "./fake-sidebar.ts";
35
-
36
- interface NativeOptions {
37
- title?: string;
38
- body?: string[];
39
- render?: (width: number) => string[];
40
- cardGroups?: Rail["cardGroups"];
41
- handleMouse?: (event: unknown) => unknown;
42
- }
43
-
44
- interface NativeRail {
45
- rail: Rail;
46
- calls: {
47
- render: number;
48
- cardGroups: number;
49
- digest: number;
50
- spend: number;
51
- handleMouse: number;
52
- open: number;
53
- };
54
- }
55
-
56
- /** The host's own footer part: a real rail shape with every field a reader in the host touches. */
57
- function nativeRail(options: NativeOptions = {}): NativeRail {
58
- const title = options.title ?? "FOOTER";
59
- const body = options.body ?? ["native row 1", "native row 2"];
60
- const calls = {
61
- render: 0,
62
- cardGroups: 0,
63
- digest: 0,
64
- spend: 0,
65
- handleMouse: 0,
66
- open: 0,
67
- };
68
-
69
- const rail: Rail = {
70
- render: (width) => {
71
- calls.render += 1;
72
- return options.render === undefined ? [`▾ ${title}`, ...body] : options.render(width);
73
- },
74
- cardGroups: (width) => {
75
- calls.cardGroups += 1;
76
- return options.cardGroups === undefined
77
- ? { groups: [{ start: 0, end: body.length + 1 }] }
78
- : options.cardGroups(width);
79
- },
80
- digest: () => {
81
- calls.digest += 1;
82
- return "footer:stable";
83
- },
84
- surface: "panel",
85
- summary: () => ({ row: "native summary" }),
86
- open: () => {
87
- calls.open += 1;
88
- },
89
- alert: () => ["needs you"],
90
- spend: () => {
91
- calls.spend += 1;
92
- return { runs: 2, tokens: 3, cost: 4 };
93
- },
94
- invalidate: () => {},
95
- };
96
- if (options.handleMouse !== undefined) {
97
- const handler = options.handleMouse;
98
- rail.handleMouse = (event) => {
99
- calls.handleMouse += 1;
100
- return handler(event);
101
- };
102
- }
103
- return { rail, calls };
104
- }
105
-
106
- const install = () => installSidebarBackend(() => fakeTui());
107
- const footer = () => fakeState().parts.get(SIDEBAR_RAIL_KEY);
108
-
109
- test.beforeEach(() => {
110
- resetSidebar();
111
- resetSidebarStateForTests();
112
- setRailContributor(undefined);
113
- });
114
-
115
- test("a footer the host registers after the bridge armed is captured", () => {
116
- fakeState(); // the host created its rail state at startup, with no footer part yet
117
- const result = install();
118
-
119
- assert.deepEqual(
120
- { installed: result.installed, outcome: result.outcome, rail: result.rail },
121
- { installed: true, outcome: "intercepted", rail: "pending" },
122
- "the map is intercepted even before the part exists",
123
- );
124
- assert.equal(footer(), undefined);
125
-
126
- const native = registerNativeFooter(nativeRail().rail);
127
- const wrapped = footer();
128
-
129
- assert.notEqual(wrapped, native, "the map hands out the wrapper, not the host's raw rail");
130
- assert.equal(wrapped, footer(), "one wrapper: a read never re-wraps");
131
- assert.deepEqual(wrapped?.render(40), native.render(40), "render delegates to the host's rail");
132
- assert.deepEqual(cardTitles(), ["FOOTER"]);
133
- });
134
-
135
- test("an already-registered footer is wrapped on install", () => {
136
- const { rail } = nativeRail();
137
- registerNativeFooter(rail);
138
-
139
- const result = install();
140
-
141
- assert.equal(result.outcome, "intercepted");
142
- assert.equal(result.rail, "wrapped");
143
- assert.notEqual(footer(), rail);
144
- assert.deepEqual(cardTitles(), ["FOOTER"]);
145
- assert.deepEqual(cardBody("FOOTER"), ["native row 1", "native row 2"]);
146
- });
147
-
148
- test("digest, spend, surface, summary, open, alert and invalidate reach their readers", () => {
149
- const { rail, calls } = nativeRail();
150
- registerNativeFooter(rail);
151
- install();
152
-
153
- const wrapped = footer();
154
- assert.ok(wrapped !== undefined);
155
- // The digest's VALUE is the host's own while nothing contributes rows (N14): the layout memo
156
- // compares values (`existing.digest === digest`), never function identity, so the wrapper may keep
157
- // forwarding the host's answer from its own function.
158
- const hostDigest = rail.digest?.();
159
- assert.equal(wrapped.digest?.(), hostDigest, "the host's digest value reaches its reader");
160
- assert.equal(calls.digest, 2, "each read calls the host's own function, once");
161
- assert.equal(wrapped.spend, rail.spend, "spend keeps its identity");
162
- assert.deepEqual(wrapped.spend?.(), { runs: 2, tokens: 3, cost: 4 });
163
- assert.equal(wrapped.surface, "panel");
164
- assert.equal(wrapped.summary, rail.summary);
165
- assert.deepEqual(wrapped.summary?.(), { row: "native summary" });
166
- assert.equal(wrapped.open, rail.open);
167
- wrapped.open?.();
168
- assert.equal(calls.open, 1);
169
- assert.equal(wrapped.alert, rail.alert);
170
- assert.deepEqual(wrapped.alert?.(40), ["needs you"]);
171
-
172
- // The host reads spend exactly like this (`gentle-shell.ts:2652`).
173
- assert.deepEqual(fakeState().parts.get("agents")?.spend?.(), undefined);
174
- registerNativeRail("agents", { ...rail });
175
- install();
176
- assert.deepEqual(fakeState().parts.get(SIDEBAR_RAIL_KEY)?.spend?.(), { runs: 2, tokens: 3, cost: 4 });
177
- });
178
-
179
- test("a throwing host render is rethrown and recorded, never swallowed", () => {
180
- registerNativeFooter(
181
- nativeRail({
182
- render: () => {
183
- throw new Error("host render exploded");
184
- },
185
- }).rail,
186
- );
187
- install();
188
-
189
- assert.throws(() => footer()?.render(40), /host render exploded/);
190
-
191
- const failures = getSidebarDiagnostics().lastFailures;
192
- assert.equal(failures.length, 1);
193
- assert.equal(failures[0].key, SIDEBAR_RAIL_KEY);
194
- assert.equal(failures[0].phase, "render");
195
- assert.equal(failures[0].message, "host render exploded");
196
- });
197
-
198
- test("a throwing host cardGroups is rethrown and recorded, never swallowed", () => {
199
- registerNativeFooter(
200
- nativeRail({
201
- cardGroups: () => {
202
- throw new Error("host groups exploded");
203
- },
204
- }).rail,
205
- );
206
- install();
207
-
208
- assert.throws(() => footer()?.cardGroups?.(40), /host groups exploded/);
209
- const failure = getSidebarDiagnostics().lastFailures.at(-1);
210
- assert.equal(failure?.phase, "cardGroups");
211
- assert.equal(failure?.message, "host groups exploded");
212
- });
213
-
214
- test("a second install on the same tui does not stack a wrapper or duplicate groups", () => {
215
- const { rail, calls } = nativeRail();
216
- registerNativeFooter(rail);
217
- assert.equal(install().outcome, "intercepted");
218
- const first = footer();
219
-
220
- const second = install();
221
-
222
- assert.equal(second.outcome, "already-intercepted");
223
- assert.equal(footer(), first, "the same wrapper object, not a wrapper of a wrapper");
224
-
225
- calls.render = 0;
226
- calls.cardGroups = 0;
227
- const rows = renderSidebar(40);
228
- assert.equal(calls.render, 1, "one frame renders the host's rail exactly once");
229
- assert.equal(calls.cardGroups, 1, "and reads its card groups exactly once");
230
- assert.deepEqual(
231
- rows.filter((row) => row.startsWith("╭")).length,
232
- 1,
233
- "one card, not the same card twice",
234
- );
235
- });
236
-
237
- test("a rebind onto a fresh ui object for the same terminal does not re-wrap", () => {
238
- const { rail } = nativeRail();
239
- registerNativeFooter(rail);
240
- install();
241
- const first = footer();
242
-
243
- const rebound = installSidebarBackend(() => ({ terminal: fakeTerminal() }));
244
-
245
- assert.equal(rebound.outcome, "already-intercepted");
246
- assert.equal(footer(), first);
247
- });
248
-
249
- test("a second copy of the package sees the interception and does not stack", async () => {
250
- const { rail } = nativeRail();
251
- registerNativeFooter(rail);
252
- install();
253
- const first = footer();
254
-
255
- const secondCopySpecifier = "../src/sidebar.ts?copy=2";
256
- const copyTwo: typeof import("../src/sidebar.ts") = await import(secondCopySpecifier);
257
- assert.notEqual(copyTwo, await import("../src/sidebar.ts"));
258
-
259
- const result = copyTwo.installSidebarBackend(() => fakeTui());
260
-
261
- assert.equal(result.outcome, "already-intercepted");
262
- assert.equal(footer(), first, "the marker on the map is process-global, like the slot");
263
- });
264
-
265
- test("disableSidebarBackend restores the host rail and the map's own methods", () => {
266
- const { rail } = nativeRail();
267
- registerNativeFooter(rail);
268
- install();
269
-
270
- const parts = fakeState().parts;
271
- const wrapped = footer();
272
- assert.ok(Object.hasOwn(parts, "set"), "the map is intercepted through an own property");
273
- assert.ok(Object.hasOwn(parts, "get"));
274
- assert.notEqual(footer(), rail);
275
-
276
- assert.equal(disableSidebarBackend(), 1);
277
-
278
- assert.equal(Object.hasOwn(parts, "set"), false, "the prototype's own set/get are in force again");
279
- assert.equal(Object.hasOwn(parts, "get"), false);
280
- assert.equal(fakeState().parts.get(SIDEBAR_RAIL_KEY), rail, "the host's rail is back in the map");
281
-
282
- // A wrapper retained by foreign code must be a plain pass-through now.
283
- assert.deepEqual(wrapped?.render(40), ["▾ FOOTER", "native row 1", "native row 2"]);
284
- });
285
-
286
- test("a foreign wrapper already on the rail is wrapped around, never clobbered", () => {
287
- const { rail } = nativeRail();
288
- let foreignCalls = 0;
289
- const foreign: Rail = {
290
- ...rail,
291
- render: (width) => {
292
- foreignCalls += 1;
293
- return rail.render(width);
294
- },
295
- };
296
- registerNativeFooter(foreign);
297
-
298
- install();
299
-
300
- const wrapped = footer();
301
- assert.ok(wrapped !== undefined);
302
- assert.notEqual(wrapped, foreign, "we wrap the foreign wrapper instead of replacing it");
303
- wrapped.render(40);
304
- assert.equal(foreignCalls, 1, "the foreign wrapper still runs");
305
- assert.equal(wrapped.digest?.(), rail.digest?.(), "and its fields still flow through ours");
306
- });
307
-
308
- test("an unknown tui shape is reported, never thrown", () => {
309
- const noTui = installSidebarBackend(() => undefined);
310
- assert.deepEqual(
311
- { installed: noTui.installed, outcome: noTui.outcome },
312
- { installed: false, outcome: "no-sidebar-state" },
313
- );
314
-
315
- const noTerminal = installSidebarBackend(() => ({}));
316
- assert.equal(noTerminal.outcome, "no-sidebar-state");
317
-
318
- const noState = installSidebarBackend(() => ({ terminal: fakeTerminal(), parts: new Map() }));
319
- assert.equal(noState.outcome, "no-sidebar-state", "a parts map outside the host's slot is not the host's");
320
-
321
- const badState = installSidebarBackend(() => ({
322
- terminal: { [SIDEBAR_STATE_KEY]: { parts: "not a map" } },
323
- }));
324
- assert.equal(badState.outcome, "no-sidebar-state");
325
-
326
- const threw = installSidebarBackend(() => {
327
- throw new Error("no tui yet");
328
- });
329
- assert.equal(threw.installed, false);
330
- assert.equal(threw.outcome, "resolver-threw");
331
- assert.equal(threw.reason, "no tui yet");
332
- });
333
-
334
- test("a resolver that starts returning a tui lets a later install succeed", () => {
335
- let available: { terminal: Record<string | symbol, unknown> } | undefined;
336
- const resolve = () => available;
337
-
338
- assert.equal(installSidebarBackend(resolve).outcome, "no-sidebar-state");
339
-
340
- // The wiring resolved a real tui in between (a widget factory fired, say).
341
- fakeState();
342
- registerNativeFooter(nativeRail().rail);
343
- available = fakeTui();
344
-
345
- const result = installSidebarBackend(resolve);
346
- assert.equal(result.outcome, "intercepted");
347
- assert.notEqual(footer(), undefined);
348
- });
349
-
350
- test("the contributor appends its lines and card groups after the host's own", () => {
351
- registerNativeFooter(nativeRail().rail);
352
- install();
353
- setRailContributor({
354
- render: () => ({ lines: ["▾ EXTRA", "extra row"], groups: [{ start: 0, end: 2 }] }),
355
- });
356
-
357
- const wrapped = footer();
358
- assert.ok(wrapped !== undefined);
359
- assert.deepEqual(wrapped.render(40), [
360
- "▾ FOOTER",
361
- "native row 1",
362
- "native row 2",
363
- "▾ EXTRA",
364
- "extra row",
365
- ]);
366
- assert.deepEqual(wrapped.cardGroups?.(40), {
367
- groups: [
368
- { start: 0, end: 3 },
369
- { start: 3, end: 5 },
370
- ],
371
- });
372
-
373
- assert.deepEqual(cardTitles(), ["FOOTER", "EXTRA"], "the host frames our rows as its own card");
374
- assert.deepEqual(cardBody("EXTRA"), ["extra row"]);
375
- });
376
-
377
- test("a host rail without cardGroups gets one synthesized group covering native and contributed rows", () => {
378
- registerNativeFooter(nativeRail({ cardGroups: undefined, body: ["only row"] }).rail);
379
- const parts = fakeState().parts;
380
- const native = parts.get(SIDEBAR_RAIL_KEY);
381
- assert.ok(native !== undefined);
382
- delete (native as { cardGroups?: unknown }).cardGroups;
383
- install();
384
- setRailContributor({
385
- render: () => ({ lines: ["▾ EXTRA", "extra row"], groups: [{ start: 0, end: 2 }] }),
386
- });
387
-
388
- assert.deepEqual(footer()?.cardGroups?.(40), {
389
- groups: [
390
- { start: 0, end: 2 },
391
- { start: 2, end: 4 },
392
- ],
393
- });
394
- assert.deepEqual(cardTitles(), ["FOOTER", "EXTRA"]);
395
- });
396
-
397
- test("no host frame means no contribution: an empty render stays empty", () => {
398
- registerNativeFooter(
399
- nativeRail({ render: () => [], cardGroups: () => ({ groups: [] }) }).rail,
400
- );
401
- install();
402
- setRailContributor({
403
- render: () => ({ lines: ["▾ EXTRA", "extra row"], groups: [{ start: 0, end: 2 }] }),
404
- });
405
-
406
- assert.deepEqual(footer()?.render(40), [], "the host painted nothing, so neither do we");
407
- assert.deepEqual(footer()?.cardGroups?.(40), { groups: [] });
408
- assert.deepEqual(renderSidebar(40), []);
409
- });
410
-
411
- test("mouse dispatch goes to the host rail first; our targets only after it declines", () => {
412
- const handled = { handled: true, render: true };
413
- registerNativeFooter(
414
- nativeRail({ handleMouse: (event) => (event === "hit" ? handled : undefined) }).rail,
415
- );
416
- install();
417
- const ours: unknown[] = [];
418
- setRailContributor({
419
- render: () => undefined,
420
- handleMouse: (event: unknown) => {
421
- ours.push(event);
422
- return { handled: true, render: true, ours: true };
423
- },
424
- });
425
-
426
- const wrapped = footer();
427
- assert.ok(wrapped?.handleMouse !== undefined);
428
- assert.deepEqual(wrapped.handleMouse("hit"), handled);
429
- assert.deepEqual(ours, [], "the host handled it, so our targets are not consulted");
430
-
431
- assert.deepEqual(wrapped.handleMouse("miss"), { handled: true, render: true, ours: true }, "our target answers");
432
- assert.deepEqual(ours, ["miss"]);
433
- });
434
-
435
- test("our mouse targets work even when the host rail has no mouse handler", () => {
436
- registerNativeFooter(nativeRail().rail); // no handleMouse on the host's rail
437
- install();
438
- setRailContributor({ render: () => undefined, handleMouse: () => ({ ours: true }) });
439
-
440
- assert.deepEqual(footer()?.handleMouse?.("event"), { ours: true });
441
- });
442
-
443
- test("a throwing contributor is recorded and leaves every host row rendering", () => {
444
- registerNativeFooter(nativeRail().rail);
445
- install();
446
- setRailContributor({
447
- render: () => {
448
- throw new Error("section renderer exploded");
449
- },
450
- });
451
-
452
- const wrapped = footer();
453
- assert.deepEqual(wrapped?.render(40), ["▾ FOOTER", "native row 1", "native row 2"]);
454
- assert.deepEqual(cardTitles(), ["FOOTER"], "the host's own card still draws");
455
-
456
- const failure = getSidebarDiagnostics().lastFailures.at(-1);
457
- assert.equal(failure?.message, "section renderer exploded");
458
- assert.equal(failure?.phase, "contribute");
459
- });
460
-
461
- test("the interception only wraps the footer part", () => {
462
- fakeState();
463
- install();
464
- const todo = nativeRail({ title: "TODO" }).rail;
465
- registerNativeRail("todo", todo);
466
- registerNativeFooter(nativeRail().rail);
467
-
468
- assert.equal(fakeState().parts.get("todo"), todo, "another rail keeps its own identity");
469
- assert.notEqual(footer(), undefined);
470
- assert.deepEqual(cardTitles(), ["TODO", "FOOTER"]);
471
- });