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,395 +0,0 @@
1
- /**
2
- * Row origin (T1–T3 of `odd/tasks/bash-card-identity.md`): WHICH host child drew each drawn row.
3
- *
4
- * The host heads a bash card with the inferred activity (`quiet-tools.ts:672`), never with the
5
- * literal `bash` the example scans for, so identity has to come from the component that produced
6
- * the row — the way `pi-cinlodev-cute` reads `child.toolName === "bash"`
7
- * (`src/transcript/transcript-dispatcher.ts:30-40`).
8
- *
9
- * What is real here and what is modelled: pi-tui is not a dependency of this package, so
10
- * `FakeContainer` copies `Container.render` (`pi-tui/dist/tui.js:136-149`) — render each child,
11
- * record `{ component, height }`, publish `mouseLayout`, concatenate the rows. Everything else is
12
- * the bridge's own code: `src/row-origin.ts` reads that published layout, `./card-style-wiring.ts`
13
- * builds the map after the host's render, and `./transform-pipeline.ts` hands it to the transform.
14
- * The rows are asserted through the public surfaces (the transform's `ctx`, the drawn frame), never
15
- * through an internal field.
16
- */
17
-
18
- import assert from "node:assert/strict";
19
- import { mkdtempSync, rmSync } from "node:fs";
20
- import { tmpdir } from "node:os";
21
- import { join } from "node:path";
22
- import test from "node:test";
23
-
24
- import { registerTransform, resetBusForTests } from "../src/bus.ts";
25
- import { resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
26
- import {
27
- disableCardStyleTransform,
28
- installCardStyleTransform,
29
- installRailCardStyleTransform,
30
- resetCardStyleWiringForTests,
31
- } from "../src/card-style-wiring.ts";
32
- import { setCardStyleSelection } from "../src/card-styles.ts";
33
- import { resetNoticesForTests } from "../src/notices.ts";
34
- import { rowOrigins } from "../src/row-origin.ts";
35
- import { resetStoreStateForTests } from "../src/store.ts";
36
- import type { RowOrigin } from "../src/types.ts";
37
- import { createRailTui } from "./fake-sidebar.ts";
38
-
39
- let configHome: string;
40
- let previousConfigHome: string | undefined;
41
-
42
- test.before(() => {
43
- configHome = mkdtempSync(join(tmpdir(), "gsb-row-origin-"));
44
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
45
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
46
- });
47
-
48
- test.after(() => {
49
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
50
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
51
- rmSync(configHome, { recursive: true, force: true });
52
- });
53
-
54
- test.beforeEach(() => {
55
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
56
- resetBusForTests();
57
- resetCardStyleTransformForTests();
58
- resetCardStyleWiringForTests();
59
- resetNoticesForTests();
60
- resetStoreStateForTests();
61
- disableCardStyleTransform();
62
- setCardStyleSelection("native");
63
- });
64
-
65
- // ---------------------------------------------------------------------------------------------
66
- // The fixture: the shapes the host's own render pass leaves behind
67
- // ---------------------------------------------------------------------------------------------
68
-
69
- const WIDTH = 40;
70
-
71
- /** A component, in Pi's sense: `render(width)` returns one row per line. */
72
- interface FakeComponent {
73
- render(width: number): string[];
74
- }
75
-
76
- /** A bash tool row: the `toolName` the host's own component carries (`pi-cinlodev-cute` reads it). */
77
- class BashRow {
78
- readonly toolName = "bash";
79
- private readonly height: number;
80
-
81
- constructor(height: number) {
82
- this.height = height;
83
- }
84
-
85
- render(): string[] {
86
- return Array.from({ length: this.height }, (_, row) => `$ npm run check ${String(row)}`);
87
- }
88
- }
89
-
90
- /** A read tool row: a different tool on the same host shape. */
91
- class ReadRow {
92
- readonly toolName = "read";
93
- private readonly height: number;
94
-
95
- constructor(height: number) {
96
- this.height = height;
97
- }
98
-
99
- render(): string[] {
100
- return Array.from({ length: this.height }, (_, row) => `≡ config.ts ${String(row)}`);
101
- }
102
- }
103
-
104
- /** A drawn block that is not a tool at all: prose, a banner, a spacer. It carries no `toolName`. */
105
- class ProseBlock {
106
- private readonly rows: readonly string[];
107
-
108
- constructor(rows: readonly string[]) {
109
- this.rows = rows;
110
- }
111
-
112
- render(): string[] {
113
- return [...this.rows];
114
- }
115
- }
116
-
117
- /** A `toolName` that is not a string is not a tool name: only a string is one (S1). */
118
- class OddRow {
119
- readonly toolName = 42;
120
-
121
- render(): string[] {
122
- return ["odd row"];
123
- }
124
- }
125
-
126
- interface MouseChild {
127
- component: FakeComponent;
128
- height: number;
129
- }
130
-
131
- interface MouseLayout {
132
- width: number;
133
- children: MouseChild[];
134
- }
135
-
136
- /**
137
- * `pi-tui`'s `Container`: the host's conversation component is one, so its `render(width)` publishes
138
- * `mouseLayout` — `{ width, children: [{ component, height }] }` — on every pass
139
- * (`pi-tui/dist/tui.js:136-149`). Copied rather than imported: `pi-tui` is not a dependency.
140
- */
141
- class FakeContainer {
142
- readonly children: FakeComponent[] = [];
143
- mouseLayout?: MouseLayout;
144
-
145
- addChild(child: FakeComponent): void {
146
- this.children.push(child);
147
- }
148
-
149
- render(width: number): string[] {
150
- const lines: string[] = [];
151
- const mouseChildren: MouseChild[] = [];
152
- for (const child of this.children) {
153
- const childLines = child.render(width);
154
- mouseChildren.push({ component: child, height: childLines.length });
155
- lines.push(...childLines);
156
- }
157
- this.mouseLayout = { width, children: mouseChildren };
158
- return lines;
159
- }
160
- }
161
-
162
- /** Prose(1) · bash(3) · read(2) · a non-string tool name(1) · prose(1) = 8 rows. */
163
- function conversationFixture(): FakeContainer {
164
- const conversation = new FakeContainer();
165
- conversation.addChild(new ProseBlock(["I will check the config first."]));
166
- conversation.addChild(new BashRow(3));
167
- conversation.addChild(new ReadRow(2));
168
- conversation.addChild(new OddRow());
169
- conversation.addChild(new ProseBlock([""]));
170
- return conversation;
171
- }
172
-
173
- const ORIGINS = {
174
- toolName: [undefined, "bash", "bash", "bash", "read", "read", undefined, undefined],
175
- component: [
176
- "ProseBlock",
177
- "BashRow",
178
- "BashRow",
179
- "BashRow",
180
- "ReadRow",
181
- "ReadRow",
182
- "OddRow",
183
- "ProseBlock",
184
- ],
185
- };
186
-
187
- /** The `tui` path `./card-style-wiring.ts` walks: `children[0]` → document → `children[2]`. */
188
- function tuiFor(conversation: FakeComponent): Record<string, unknown> {
189
- return {
190
- terminal: {},
191
- children: [
192
- {
193
- children: [
194
- { render: () => ["header"] },
195
- { render: () => ["resources"] },
196
- conversation,
197
- ],
198
- },
199
- ],
200
- };
201
- }
202
-
203
- // ---------------------------------------------------------------------------------------------
204
- // T1 — the map
205
- // ---------------------------------------------------------------------------------------------
206
-
207
- test("T1: every drawn row names the child that produced it, with its tool name and constructor", () => {
208
- const conversation = conversationFixture();
209
- const lines = conversation.render(WIDTH);
210
-
211
- const map = rowOrigins(conversation, WIDTH, lines.length);
212
-
213
- assert.equal(map.reason, undefined, "a layout consistent with the frame is a complete map");
214
- assert.equal(map.rows.length, lines.length, "one entry per drawn row");
215
- assert.deepEqual(
216
- map.rows.map((row) => row.index),
217
- lines.map((_, index) => index),
218
- "the entry says which row it is about",
219
- );
220
- assert.deepEqual(map.rows.map((row) => row.toolName), ORIGINS.toolName);
221
- assert.deepEqual(
222
- map.rows.map((row) => row.component),
223
- ORIGINS.component,
224
- "a row whose child carries no tool name still names its constructor",
225
- );
226
- });
227
-
228
- test("T1: the map is derived from the host's own render pass, with no second render", () => {
229
- const conversation = conversationFixture();
230
- let renders = 0;
231
- const counting = new Proxy(conversation, {
232
- get(target, key, receiver): unknown {
233
- if (key === "render") {
234
- return (width: number): string[] => {
235
- renders += 1;
236
- return target.render(width);
237
- };
238
- }
239
- return Reflect.get(target, key, receiver);
240
- },
241
- });
242
-
243
- const lines = counting.render(WIDTH);
244
- assert.equal(renders, 1);
245
-
246
- const map = rowOrigins(counting, WIDTH, lines.length);
247
-
248
- assert.equal(renders, 1, "reading the layout never renders a child again");
249
- assert.equal(map.rows.length, lines.length);
250
- });
251
-
252
- // ---------------------------------------------------------------------------------------------
253
- // T1 — every absence is an empty map with a reason, never a guess and never a throw
254
- // ---------------------------------------------------------------------------------------------
255
-
256
- test("T1: a component that published no layout yields an empty map and a reason", () => {
257
- const map = rowOrigins({ render: () => ["one row"] }, WIDTH, 1);
258
-
259
- assert.deepEqual(map.rows, []);
260
- assert.equal(map.reason, "no-layout");
261
- });
262
-
263
- test("T1: a layout measured at another width yields an empty map and a reason", () => {
264
- const conversation = conversationFixture();
265
- const lines = conversation.render(WIDTH);
266
-
267
- const map = rowOrigins(conversation, WIDTH + 20, lines.length);
268
-
269
- assert.deepEqual(map.rows, []);
270
- assert.equal(map.reason, "width-mismatch");
271
- });
272
-
273
- test("T1: a layout whose heights do not add up to the frame yields an empty map, never a partial one", () => {
274
- const conversation = conversationFixture();
275
- const lines = conversation.render(WIDTH);
276
-
277
- const map = rowOrigins(conversation, WIDTH, lines.length + 1);
278
-
279
- assert.deepEqual(map.rows, [], "a partial map would attribute rows to the wrong child");
280
- assert.equal(map.reason, "height-mismatch");
281
- });
282
-
283
- test("T1: a malformed layout entry yields an empty map and a reason", () => {
284
- const broken = {
285
- mouseLayout: { width: WIDTH, children: [{ component: {}, height: -2 }] },
286
- };
287
-
288
- const map = rowOrigins(broken, WIDTH, -2);
289
-
290
- assert.deepEqual(map.rows, []);
291
- assert.equal(map.reason, "bad-layout");
292
- });
293
-
294
- test("T1: nothing it can be handed makes it throw", () => {
295
- for (const value of [null, undefined, 42, "container", {}, { mouseLayout: "nope" }]) {
296
- const map = rowOrigins(value, WIDTH, 3);
297
- assert.deepEqual(map.rows, []);
298
- assert.notEqual(map.reason, undefined);
299
- }
300
- });
301
-
302
- // ---------------------------------------------------------------------------------------------
303
- // T3 — the wrapper builds the map after the host render and hands it over
304
- // ---------------------------------------------------------------------------------------------
305
-
306
- test("T3: the transform receives one origin per row, built after the host's own render", () => {
307
- const conversation = conversationFixture();
308
- assert.deepEqual(installCardStyleTransform(tuiFor(conversation)), {
309
- installed: true,
310
- outcome: "wrapped",
311
- component: "chat",
312
- });
313
- const frames: Array<readonly RowOrigin[] | undefined> = [];
314
- registerTransform({
315
- id: "probe.rows",
316
- transform: (lines, ctx) => {
317
- frames.push(ctx.rows);
318
- return lines;
319
- },
320
- });
321
-
322
- const drawn = conversation.render(WIDTH);
323
-
324
- assert.equal(frames.length, 1, "the transform ran once per frame, inside the wrapper");
325
- const rows = frames[0] ?? [];
326
- assert.equal(rows.length, drawn.length, "one origin entry per drawn row");
327
- assert.deepEqual(rows.map((row) => row.toolName), ORIGINS.toolName);
328
- assert.deepEqual(rows.map((row) => row.component), ORIGINS.component);
329
- });
330
-
331
- test("T3: a host render that publishes no layout hands the transform an empty map, never an error", () => {
332
- const conversation: FakeComponent = { render: () => ["plain row", "second row"] };
333
- installCardStyleTransform(tuiFor(conversation));
334
- const frames: Array<readonly RowOrigin[] | undefined> = [];
335
- registerTransform({
336
- id: "probe.no-layout",
337
- transform: (lines, ctx) => {
338
- frames.push(ctx.rows);
339
- return lines;
340
- },
341
- });
342
-
343
- const drawn = conversation.render(WIDTH);
344
-
345
- assert.deepEqual(drawn, ["plain row", "second row"], "the frame is drawn untouched");
346
- assert.deepEqual(frames, [[]], "an unknown origin is an empty array, not a guess");
347
- });
348
-
349
- test("T3: the rail runs no transcript transform, so no origin map is ever needed there", () => {
350
- const railLines = ["rail row one", "rail row two"];
351
- const tui = createRailTui({ rail: { render: () => [...railLines] }, contentWidth: WIDTH });
352
- assert.deepEqual(installRailCardStyleTransform(tui), {
353
- installed: true,
354
- outcome: "wrapped",
355
- component: "rail",
356
- });
357
- let calls = 0;
358
- registerTransform({
359
- id: "probe.rail",
360
- transform: (lines) => {
361
- calls += 1;
362
- return lines;
363
- },
364
- });
365
-
366
- const drawn = tui.renderRail();
367
-
368
- assert.deepEqual(drawn, railLines, "the rail's own rows are drawn untouched");
369
- assert.equal(calls, 0, "a plugin's conversation transform never sees the rail");
370
- });
371
-
372
- // ---------------------------------------------------------------------------------------------
373
- // T2 — additive: a transform that ignores `rows` is byte-identical
374
- // ---------------------------------------------------------------------------------------------
375
-
376
- test("T2: a transform that ignores ctx.rows leaves the frame byte-identical", () => {
377
- const conversation = conversationFixture();
378
- const hostFrame = conversation.render(WIDTH);
379
- installCardStyleTransform(tuiFor(conversation));
380
- const seen: Array<readonly RowOrigin[] | undefined> = [];
381
- registerTransform({
382
- id: "probe.identity",
383
- transform: (lines, ctx) => {
384
- seen.push(ctx.rows);
385
- return lines;
386
- },
387
- });
388
-
389
- const drawn = conversation.render(WIDTH);
390
-
391
- assert.equal(seen.length, 1);
392
- assert.ok((seen[0] ?? []).length > 0, "the new field was delivered, so identity is not vacuous");
393
- assert.deepEqual(drawn, hostFrame);
394
- assert.equal(drawn.join("\n"), hostFrame.join("\n"), "the same bytes, row for row");
395
- });