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,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
- });