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,372 +0,0 @@
1
- /**
2
- * T10: the worked example's DESIGN for `bash`, driven through the real bus (S1, S5, S6, S8).
3
- *
4
- * The host heads a minimal bash card with the bare command and supplies the `$` prompt as the card's
5
- * glyph (`quiet-tools.ts:521-525`). The design in `examples/bash-card.ts` heads that card `bash`
6
- * instead, and changes nothing else: the frame, the ANSI, the padding, the right-aligned hint and
7
- * every body row are the host's own bytes.
8
- *
9
- * This file loads the real example, arms the real bridge (which installs the one resolver and the
10
- * `Cards` rows), and renders the components Pi keeps — the call component and the result component —
11
- * so the claim is made about the rows a user sees, not about a helper. `float` here; the `neon`
12
- * outline is `test/example-neon-transform.test.ts`.
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, setToolDesignSelection } 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_FLOAT_CALL,
46
- BASH_MINIMAL_FLOAT_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-identity-"));
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
- /** Pi's own component contract, as far as a card uses it. */
114
- interface FakeComponent {
115
- render(width: number): string[];
116
- }
117
-
118
- interface FakeRenderers {
119
- renderShell?: unknown;
120
- renderCall?: unknown;
121
- renderResult?: unknown;
122
- [member: string]: unknown;
123
- }
124
-
125
- /** A component that returns ONE array instance, so "the host's own rows" is observable by identity. */
126
- function fakeComponent(lines: readonly string[]): FakeComponent {
127
- const rows = [...lines];
128
- return { render: (): string[] => rows };
129
- }
130
-
131
- /** The renderers a tool advertises: a call component and a result component, like the host's. */
132
- function fakeRenderers(call: readonly string[], result: readonly string[]): FakeRenderers {
133
- // One component instance per renderer, like Pi: the renderer is called once and the SAME object
134
- // is re-rendered on every frame, which is what makes "the host's own array instance" observable.
135
- const callComponent = fakeComponent(call);
136
- const resultComponent = fakeComponent(result);
137
- return {
138
- renderShell: "default",
139
- renderCall: () => callComponent,
140
- renderResult: () => resultComponent,
141
- };
142
- }
143
-
144
- const THEME = { name: "fake-theme" };
145
-
146
- /** The component Pi keeps after resolving a renderer once, when the card is constructed. */
147
- function mount(
148
- renderers: FakeRenderers,
149
- member: "renderCall" | "renderResult",
150
- ): FakeComponent {
151
- const renderer = renderers[member];
152
- assert.equal(typeof renderer, "function");
153
- return member === "renderCall"
154
- ? (renderer as (args: unknown, theme: unknown, ctx: unknown) => FakeComponent)(
155
- { command: "npm test" },
156
- THEME,
157
- {},
158
- )
159
- : (renderer as (result: unknown, options: unknown, theme: unknown, ctx: unknown) => FakeComponent)(
160
- { content: [] },
161
- { expanded: false },
162
- THEME,
163
- {},
164
- );
165
- }
166
-
167
- interface World {
168
- /** The one resolver the bridge registered on this Pi. */
169
- readonly resolver: (tool: string, next: () => FakeRenderers) => FakeRenderers | undefined;
170
- /** The rows the customize menu builds right now. */
171
- rows(): CustomizeRow[];
172
- /** Press the value with this label on the `bash` design line. */
173
- pick(value: string): Promise<void>;
174
- dispose(): void;
175
- }
176
-
177
- /**
178
- * A session as Pi runs it: the bridge arms itself (installing the one resolver and the design rows),
179
- * then the example registers its design from its own `session_start`.
180
- */
181
- /** The tool-design line's setting, old bridge (`bash`) and current bridge (`Card bash`). */
182
- const isBashDesignSetting = (setting: string | undefined): boolean =>
183
- setting === "bash" || setting === "Card bash";
184
-
185
- async function startWorld(tag: string): Promise<World> {
186
- publishBridgeApi();
187
- const host = createFakeHost();
188
- const resolvers: unknown[] = [];
189
- const sessionHandlers: SessionHandler[] = [];
190
- const pi: FakePi = {
191
- // `gentle-pi` is present, so the bridge's `session_start` arms (S9/T12).
192
- getCommands: () => [{ name: "gentle:customize" }],
193
- on(_event, handler) {
194
- sessionHandlers.push(handler);
195
- },
196
- registerCommand() {},
197
- registerToolRenderer(resolver) {
198
- resolvers.push(resolver);
199
- },
200
- };
201
-
202
- gentleShellBridge(pi as BridgeExtensionApi);
203
- const dispose = (await loadExample(tag))(pi);
204
- for (const handler of sessionHandlers) handler({}, { hasUI: true, ui: host.ui });
205
-
206
- assert.equal(resolvers.length, 1, "the bridge registers exactly one resolver");
207
- const resolver = resolvers[0] as World["resolver"];
208
- assert.equal(typeof resolver, "function");
209
-
210
- const rows = (): CustomizeRow[] => buildRows({ theme: undefined, configHome }).rows;
211
-
212
- return {
213
- resolver,
214
- rows,
215
- pick: async (value) => {
216
- const row = rows().find(
217
- (candidate) =>
218
- isBashDesignSetting(candidate.choice?.setting) && candidate.choice?.value === value,
219
- );
220
- assert.ok(row !== undefined, `the Cards line for bash carries the value ${value}`);
221
- assert.ok(row.action !== undefined, `the ${value} value must have an action`);
222
- await row.action(inlineHelpers);
223
- },
224
- dispose,
225
- };
226
- }
227
-
228
- /**
229
- * The heading row with the host's `$` mark replaced by `bash`, and three fill columns fewer.
230
- * The host's bytes are `test/fake-transcript.ts`'s `BASH_MINIMAL_FLOAT_CALL[1]`: the mark sits inside
231
- * the title run the host painted, so the word replaces one character and the row's space fill — not
232
- * the hint, not the rail — gives the three extra columns back.
233
- */
234
- const DESIGNED_FLOAT_HEADING =
235
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[95mbash \u001b[95m\u001b[1mnpm test\u001b[22m\u001b[39m" +
236
- "\u001b[37m (timeout 30s)\u001b[39m\u001b[39m\u001b[95m \u001b[39m \u001b[2mctrl+o to expand\u001b[39m" +
237
- " \u001b[95m \u001b[39m\u001b[49m ";
238
-
239
- // ---------------------------------------------------------------------------------------------
240
- // The default: the host's own card, no intervention
241
- // ---------------------------------------------------------------------------------------------
242
-
243
- test("T10/S8: the design appears on the `bash` Cards line beside the host's own card", async () => {
244
- const world = await startWorld("cards-line");
245
-
246
- const values = world
247
- .rows()
248
- .filter((row) => isBashDesignSetting(row.choice?.setting))
249
- .map((row) => row.choice?.value);
250
- assert.deepEqual(values, ["native", "bash heading"], "the host's card is the default value");
251
-
252
- const native = world
253
- .rows()
254
- .find((row) => isBashDesignSetting(row.choice?.setting) && row.choice?.value === "native");
255
- assert.equal(native?.choice?.current(), true, "nothing is selected on install");
256
-
257
- world.dispose();
258
- });
259
-
260
- test("T10: with the host's own card selected, the resolver draws the host's rows unchanged", async () => {
261
- const world = await startWorld("native-bytes");
262
- const renderers = fakeRenderers(BASH_MINIMAL_FLOAT_CALL, BASH_MINIMAL_FLOAT_RESULT);
263
- const resolved = world.resolver("bash", () => renderers);
264
- assert.ok(resolved !== undefined);
265
-
266
- const call = mount(resolved, "renderCall");
267
- const result = mount(resolved, "renderResult");
268
-
269
- const hostCall = mount(renderers, "renderCall").render(60);
270
- const hostResult = mount(renderers, "renderResult").render(60);
271
-
272
- assert.equal(call.render(60), hostCall, "the host's own ARRAY instance, not a copy");
273
- assert.equal(result.render(60), hostResult, "both renderers, byte for byte");
274
- assert.deepEqual(call.render(60), [...BASH_MINIMAL_FLOAT_CALL]);
275
- assert.deepEqual(result.render(60), [...BASH_MINIMAL_FLOAT_RESULT]);
276
-
277
- world.dispose();
278
- });
279
-
280
- // ---------------------------------------------------------------------------------------------
281
- // The design: `bash` where the host prints `$`, and every other byte the host's
282
- // ---------------------------------------------------------------------------------------------
283
-
284
- test("T10: the `bash` design changes the heading mark and nothing else, byte for byte", async () => {
285
- const world = await startWorld("designed-bytes");
286
- const renderers = fakeRenderers(BASH_MINIMAL_FLOAT_CALL, BASH_MINIMAL_FLOAT_RESULT);
287
- const resolved = world.resolver("bash", () => renderers);
288
- assert.ok(resolved !== undefined);
289
- const call = mount(resolved, "renderCall");
290
- const result = mount(resolved, "renderResult");
291
-
292
- await world.pick("bash heading");
293
-
294
- const output = call.render(60);
295
-
296
- assert.equal(output.length, BASH_MINIMAL_FLOAT_CALL.length, "the frame keeps its row count");
297
- assert.equal(output[1], DESIGNED_FLOAT_HEADING, "the heading mark is `bash`");
298
- assert.equal(
299
- stripAnsi(output[1] ?? "").length,
300
- stripAnsi(BASH_MINIMAL_FLOAT_CALL[1] ?? "").length,
301
- "width preserved",
302
- );
303
- for (const index of [0]) {
304
- assert.equal(
305
- output[index],
306
- BASH_MINIMAL_FLOAT_CALL[index],
307
- `row ${index} is the host's byte for byte`,
308
- );
309
- }
310
- assert.notEqual(
311
- output,
312
- BASH_MINIMAL_FLOAT_CALL,
313
- "a new frame was produced, not the host's array mutated",
314
- );
315
-
316
- const resultOutput = result.render(60);
317
- assert.deepEqual(
318
- resultOutput,
319
- mount(renderers, "renderResult").render(60),
320
- "the body is the host's, row for row",
321
- );
322
- assert.deepEqual(resultOutput, [...BASH_MINIMAL_FLOAT_RESULT], "every result row is byte-identical");
323
- assert.ok(
324
- resultOutput.some((row) => row.includes("duration 1.2s")),
325
- "the host's body is what was drawn",
326
- );
327
-
328
- world.dispose();
329
- });
330
-
331
- test("T10: picking the host's own card again restores the host's rows", async () => {
332
- const world = await startWorld("back-to-native");
333
- const renderers = fakeRenderers(BASH_MINIMAL_FLOAT_CALL, BASH_MINIMAL_FLOAT_RESULT);
334
- const resolved = world.resolver("bash", () => renderers);
335
- assert.ok(resolved !== undefined);
336
- const call = mount(resolved, "renderCall");
337
-
338
- await world.pick("bash heading");
339
- assert.equal(call.render(60)[1], DESIGNED_FLOAT_HEADING);
340
-
341
- await world.pick("native");
342
- assert.deepEqual(call.render(60), [...BASH_MINIMAL_FLOAT_CALL], "the host's card, unchanged");
343
-
344
- world.dispose();
345
- });
346
-
347
- test("T10: the design is declared for the `bash` tool name, so another tool is untouched", async () => {
348
- const world = await startWorld("other-tool");
349
- const readRenderers = fakeRenderers(
350
- [" \u001b[90m▎\u001b[39m \u001b[2m≡ read src/api.ts\u001b[39m"],
351
- [" \u001b[90m▎\u001b[39m export function bridgeApi() {"],
352
- );
353
-
354
- const resolved = world.resolver("read", () => readRenderers);
355
-
356
- assert.equal(resolved, readRenderers, "a tool with no design keeps the host's own renderers");
357
- world.dispose();
358
- });
359
-
360
- test("T10: a design whose selection names nothing falls back to the host's card", async () => {
361
- const world = await startWorld("missing-selection");
362
- const renderers = fakeRenderers(BASH_MINIMAL_FLOAT_CALL, BASH_MINIMAL_FLOAT_RESULT);
363
- const resolved = world.resolver("bash", () => renderers);
364
- assert.ok(resolved !== undefined);
365
- const call = mount(resolved, "renderCall");
366
-
367
- setToolDesignSelection("bash", "bash-card.retired");
368
-
369
- assert.deepEqual(call.render(60), [...BASH_MINIMAL_FLOAT_CALL], "a design nobody owns draws the host's card");
370
-
371
- world.dispose();
372
- });