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