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