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,283 +0,0 @@
1
- /**
2
- * T10: the worked example's bash DESIGN inside the host's `neon` outline (S1, S5).
3
- *
4
- * The user's live frame: the host's own `Card style` line is on `neon`, and the bash card must come
5
- * back with `bash` where it printed `$` — inside the SAME outline. A design never draws chrome: the
6
- * host's frame is already there when it is handed the rows, so `neon` (and `float`) survive by
7
- * construction, and the design only changes the mark and re-balances the rule's fill to keep the row
8
- * exactly as wide as the host made it.
9
- *
10
- * The frames are host-derived fixtures (`test/fake-transcript.ts`: `HOST_BASH_NEON_*`, built from
11
- * `lib/shell-card.ts` — `cardTop`/`cardLine`/`cardBottom` at width 60), split the way Pi builds a
12
- * card: the top rule is the call component, the body and closing rule the result component.
13
- */
14
-
15
- import assert from "node:assert/strict";
16
- import { mkdtempSync, rmSync } from "node:fs";
17
- import { tmpdir } from "node:os";
18
- import { join } from "node:path";
19
- import test from "node:test";
20
-
21
- import { publishBridgeApi, resetBridgeApiForTests } from "../src/api.ts";
22
- import {
23
- resetBusForTests,
24
- resetCardStylesForTests,
25
- resetSectionsForTests,
26
- } from "../src/bus.ts";
27
- import { resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
28
- import { disableCardStyleTransform, resetCardStyleWiringForTests } from "../src/card-style-wiring.ts";
29
- import { disableCaptureBackend, resetCaptureStateForTests } from "../src/capture.ts";
30
- import gentleShellBridge, { type BridgeExtensionApi } from "../src/extension.ts";
31
- import { resetHookStateForTests } from "../src/hook.ts";
32
- import { resetNoticesForTests } from "../src/notices.ts";
33
- import { resetPluginLinesForTests } from "../src/plugin-lines.ts";
34
- import { resetRepaintForTests } from "../src/repaint.ts";
35
- import { buildRows, resetRowBuildStateForTests } from "../src/rows.ts";
36
- import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
37
- import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
38
- import { resetStoreStateForTests } from "../src/store.ts";
39
- import { resetToolDesignsForTests } from "../src/tool-designs.ts";
40
- import { resetToolDesignRowStateForTests } from "../src/tool-design-rows.ts";
41
- import { resetToolDesignWiringForTests } from "../src/tool-design-wiring.ts";
42
- import type { CustomizeRow, InlineHelpers } from "../src/types.ts";
43
- import { createFakeHost } from "./fake-host.ts";
44
- import {
45
- BASH_MINIMAL_NEON_CALL,
46
- BASH_MINIMAL_NEON_RESULT,
47
- stripAnsi,
48
- } from "./fake-transcript.ts";
49
-
50
- let configHome: string;
51
- let previousConfigHome: string | undefined;
52
-
53
- test.before(() => {
54
- configHome = mkdtempSync(join(tmpdir(), "gsb-example-neon-"));
55
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
56
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
57
- });
58
-
59
- test.after(() => {
60
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
61
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
62
- rmSync(configHome, { recursive: true, force: true });
63
- });
64
-
65
- test.beforeEach(() => {
66
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
67
- resetBridgeApiForTests();
68
- resetBusForTests();
69
- resetSectionsForTests();
70
- resetCardStylesForTests();
71
- resetCardStyleTransformForTests();
72
- resetCardStyleWiringForTests();
73
- resetCaptureStateForTests();
74
- resetHookStateForTests();
75
- resetRowBuildStateForTests();
76
- resetSidebarStateForTests();
77
- resetSectionFailuresForTests();
78
- resetNoticesForTests();
79
- resetPluginLinesForTests();
80
- resetRepaintForTests();
81
- resetStoreStateForTests();
82
- resetToolDesignsForTests();
83
- resetToolDesignRowStateForTests();
84
- resetToolDesignWiringForTests();
85
- disableCaptureBackend();
86
- disableSidebarBackend();
87
- disableCardStyleTransform();
88
- });
89
-
90
- type SessionHandler = (event: unknown, ctx: { hasUI?: boolean; ui?: unknown }) => void;
91
-
92
- interface FakePi {
93
- on?: (event: string, handler: SessionHandler) => void;
94
- registerCommand?: () => void;
95
- registerToolRenderer?: (resolver: unknown) => void;
96
- /** Present because the bridge checks the REQUIRED host package at runtime (S9/T12). */
97
- getCommands?: () => Array<{ name: string }>;
98
- }
99
-
100
- type ExampleInit = (pi: FakePi) => () => void;
101
-
102
- const loadExample = async (tag: string): Promise<ExampleInit> => {
103
- const specifier = new URL(`../examples/bash-card.ts?${tag}`, import.meta.url).href;
104
- return ((await import(specifier)) as { default: ExampleInit }).default;
105
- };
106
-
107
- const inlineHelpers: InlineHelpers = {
108
- input: async () => undefined,
109
- confirm: async () => true,
110
- capture: async () => undefined,
111
- };
112
-
113
- interface FakeComponent {
114
- render(width: number): string[];
115
- }
116
-
117
- interface FakeRenderers {
118
- renderShell?: unknown;
119
- renderCall?: unknown;
120
- renderResult?: unknown;
121
- [member: string]: unknown;
122
- }
123
-
124
- function fakeComponent(lines: readonly string[]): FakeComponent {
125
- const rows = [...lines];
126
- return { render: (): string[] => rows };
127
- }
128
-
129
- function fakeRenderers(call: readonly string[], result: readonly string[]): FakeRenderers {
130
- // One component instance per renderer, like Pi: the renderer is called once and the SAME object
131
- // is re-rendered on every frame, which is what makes "the host's own array instance" observable.
132
- const callComponent = fakeComponent(call);
133
- const resultComponent = fakeComponent(result);
134
- return {
135
- renderShell: "default",
136
- renderCall: () => callComponent,
137
- renderResult: () => resultComponent,
138
- };
139
- }
140
-
141
- const THEME = { name: "fake-theme" };
142
-
143
- function mount(renderers: FakeRenderers, member: "renderCall" | "renderResult"): FakeComponent {
144
- const renderer = renderers[member];
145
- assert.equal(typeof renderer, "function");
146
- return member === "renderCall"
147
- ? (renderer as (args: unknown, theme: unknown, ctx: unknown) => FakeComponent)(
148
- { command: "npm test" },
149
- THEME,
150
- {},
151
- )
152
- : (renderer as (result: unknown, options: unknown, theme: unknown, ctx: unknown) => FakeComponent)(
153
- { content: [] },
154
- { expanded: false },
155
- THEME,
156
- {},
157
- );
158
- }
159
-
160
- interface World {
161
- readonly resolver: (tool: string, next: () => FakeRenderers) => FakeRenderers | undefined;
162
- rows(): CustomizeRow[];
163
- pick(value: string): Promise<void>;
164
- dispose(): void;
165
- }
166
-
167
- async function startWorld(tag: string): Promise<World> {
168
- publishBridgeApi();
169
- const host = createFakeHost();
170
- const resolvers: unknown[] = [];
171
- const sessionHandlers: SessionHandler[] = [];
172
- const pi: FakePi = {
173
- // `gentle-pi` is present, so the bridge's `session_start` arms (S9/T12).
174
- getCommands: () => [{ name: "gentle:customize" }],
175
- on(_event, handler) {
176
- sessionHandlers.push(handler);
177
- },
178
- registerCommand() {},
179
- registerToolRenderer(resolver) {
180
- resolvers.push(resolver);
181
- },
182
- };
183
-
184
- gentleShellBridge(pi as BridgeExtensionApi);
185
- const dispose = (await loadExample(tag))(pi);
186
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui: host.ui });
187
-
188
- assert.equal(resolvers.length, 1, "one resolver, the bridge's");
189
- const resolver = resolvers[0] as World["resolver"];
190
- const rows = (): CustomizeRow[] => buildRows({ theme: undefined, configHome }).rows;
191
- return {
192
- resolver,
193
- rows,
194
- pick: async (value) => {
195
- const row = rows().find(
196
- (candidate) =>
197
- (candidate.choice?.setting === "bash" || candidate.choice?.setting === "Card bash") &&
198
- candidate.choice?.value === value,
199
- );
200
- assert.ok(row?.action !== undefined, `the Cards line carries the value ${value}`);
201
- await row.action(inlineHelpers);
202
- },
203
- dispose,
204
- };
205
- }
206
-
207
- /**
208
- * The host's neon top rule with `bash` in place of `$`, three rule columns fewer: the host's bytes are
209
- * `test/fake-transcript.ts`'s `BASH_MINIMAL_NEON_CALL[0]`, and the rule's own `─` fill is what gives
210
- * the word its three extra columns back.
211
- */
212
- const DESIGNED_NEON_TOP =
213
- "\u001b[95m╭\u001b[39m\u001b[95m─ \u001b[39m\u001b[95mbash \u001b[95m\u001b[1mnpm test\u001b[22m\u001b[39m" +
214
- "\u001b[37m (timeout 30s)\u001b[39m\u001b[39m\u001b[95m ──────────\u001b[39m \u001b[2mctrl+o to expand\u001b[39m" +
215
- " \u001b[95m╮\u001b[39m";
216
-
217
- test("T10: the neon outline stays neon, and only the mark changes", async () => {
218
- const world = await startWorld("neon-design");
219
- const renderers = fakeRenderers(BASH_MINIMAL_NEON_CALL, BASH_MINIMAL_NEON_RESULT);
220
- const resolved = world.resolver("bash", () => renderers);
221
- assert.ok(resolved !== undefined);
222
- const call = mount(resolved, "renderCall");
223
- const result = mount(resolved, "renderResult");
224
-
225
- assert.deepEqual(call.render(60), [...BASH_MINIMAL_NEON_CALL], "the host's neon rule, unchanged");
226
-
227
- await world.pick("bash heading");
228
- const output = call.render(60);
229
-
230
- assert.equal(output.length, 1, "the call component is still one rule");
231
- assert.equal(output[0], DESIGNED_NEON_TOP, "`bash` where the host printed `$`");
232
- assert.equal(
233
- stripAnsi(output[0]).length,
234
- stripAnsi(BASH_MINIMAL_NEON_CALL[0]).length,
235
- "the rule is exactly as wide as the host made it",
236
- );
237
- assert.ok(stripAnsi(output[0]).startsWith("╭─ bash npm test"), "the outline is still the outline");
238
- assert.ok(stripAnsi(output[0]).endsWith(" ctrl+o to expand ╮"), "and the hint is still the hint");
239
-
240
- const body = result.render(60);
241
- assert.deepEqual(body, [...BASH_MINIMAL_NEON_RESULT], "every body row is byte-identical");
242
- assert.equal(
243
- body[body.length - 1],
244
- BASH_MINIMAL_NEON_RESULT[BASH_MINIMAL_NEON_RESULT.length - 1],
245
- "the closing rule is the host's own",
246
- );
247
-
248
- world.dispose();
249
- });
250
-
251
- test("T10: the design leaves a body row that carries `$` inside text alone", async () => {
252
- const world = await startWorld("neon-content-dollar");
253
- const result = [
254
- "\u001b[95m│\u001b[39m \u001b[37mexport PATH=$HOME/bin\u001b[39m \u001b[95m│\u001b[39m",
255
- BASH_MINIMAL_NEON_RESULT[1],
256
- BASH_MINIMAL_NEON_RESULT[2],
257
- ];
258
- const renderers = fakeRenderers(BASH_MINIMAL_NEON_CALL, result);
259
- const resolved = world.resolver("bash", () => renderers);
260
- assert.ok(resolved !== undefined);
261
- const mounted = mount(resolved, "renderResult");
262
-
263
- await world.pick("bash heading");
264
-
265
- assert.deepEqual(mounted.render(60), result, "output is never rewritten as if it were the mark");
266
- world.dispose();
267
- });
268
-
269
- test("T10: the host's own card restores the neon rule byte for byte", async () => {
270
- const world = await startWorld("neon-native");
271
- const renderers = fakeRenderers(BASH_MINIMAL_NEON_CALL, BASH_MINIMAL_NEON_RESULT);
272
- const resolved = world.resolver("bash", () => renderers);
273
- assert.ok(resolved !== undefined);
274
- const call = mount(resolved, "renderCall");
275
-
276
- await world.pick("bash heading");
277
- assert.equal(call.render(60)[0], DESIGNED_NEON_TOP);
278
-
279
- await world.pick("native");
280
- assert.deepEqual(call.render(60), [...BASH_MINIMAL_NEON_CALL]);
281
-
282
- world.dispose();
283
- });