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,359 +0,0 @@
1
- /**
2
- * The customize rows for tool designs (S6, T9): one line per tool that HAS designs, in the `Cards`
3
- * category, with the tool's own card as one value and each design as another. The selection is
4
- * persisted per tool in the bridge's own store, and changing it repaints the cards already drawn
5
- * through `requestRepaint()` — one differential frame, because the wrapper re-reads the live selection
6
- * on every render pass (S13/T23: dropping the caches was the freeze).
7
- *
8
- * Asserted through the row contract the host receives — `buildRows` — and the store file, so the
9
- * test reads what the menu reads.
10
- */
11
-
12
- import assert from "node:assert/strict";
13
- import { mkdtempSync, rmSync } from "node:fs";
14
- import { tmpdir } from "node:os";
15
- import { join } from "node:path";
16
- import test from "node:test";
17
-
18
- import { snapshotProviders } from "../src/bus.ts";
19
- import { resetNoticesForTests } from "../src/notices.ts";
20
- import { resetPluginLinesForTests } from "../src/plugin-lines.ts";
21
- import { getRepaintDiagnostics, resetRepaintForTests, setRepaintHost } from "../src/repaint.ts";
22
- import { buildRows, resetRowBuildStateForTests } from "../src/rows.ts";
23
- import { createStore, resetStoreStateForTests } from "../src/store.ts";
24
- import {
25
- TOOL_DESIGN_NATIVE,
26
- getToolDesignSelection,
27
- registerToolDesign,
28
- resetToolDesignsForTests,
29
- toolNamesWithDesigns,
30
- } from "../src/tool-designs.ts";
31
- import {
32
- TOOL_DESIGN_ROWS_PROVIDER_ID,
33
- installToolDesignRows,
34
- resetToolDesignRowStateForTests,
35
- } from "../src/tool-design-rows.ts";
36
- import {
37
- resetToolDesignWiringForTests,
38
- toolDesignResolver,
39
- } from "../src/tool-design-wiring.ts";
40
- import type { CustomizeRow, ToolDesignRegistration } from "../src/types.ts";
41
-
42
- let configHome: string;
43
- let previousConfigHome: string | undefined;
44
-
45
- test.before(() => {
46
- configHome = mkdtempSync(join(tmpdir(), "gsb-tool-design-rows-"));
47
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
48
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
49
- });
50
-
51
- test.after(() => {
52
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
53
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
54
- rmSync(configHome, { recursive: true, force: true });
55
- });
56
-
57
- test.beforeEach(() => {
58
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
59
- resetToolDesignsForTests();
60
- resetToolDesignRowStateForTests();
61
- resetToolDesignWiringForTests();
62
- resetRepaintForTests();
63
- resetStoreStateForTests();
64
- resetNoticesForTests();
65
- resetPluginLinesForTests();
66
- resetRowBuildStateForTests();
67
- });
68
-
69
- const bashHeading: ToolDesignRegistration = {
70
- tool: "bash",
71
- id: "loon.bash-heading",
72
- label: "bash heading",
73
- description: "heads the card with bash instead of the host's $ prompt",
74
- preview: () => ({ title: "Cards · bash heading", sample: "bash ls -la" }),
75
- apply: (lines) => [...lines],
76
- };
77
-
78
- const bashFramed: ToolDesignRegistration = {
79
- tool: "bash",
80
- id: "loon.bash-framed",
81
- label: "bash framed",
82
- apply: (lines) => [...lines],
83
- };
84
-
85
- const writeBadge: ToolDesignRegistration = {
86
- tool: "write",
87
- id: "loon.write-badge",
88
- label: "write badge",
89
- apply: (lines) => [...lines],
90
- };
91
-
92
- /** The rows one menu build receives, with the design bus installed. */
93
- const rowsForBuild = (): CustomizeRow[] => {
94
- installToolDesignRows();
95
- return buildRows({ theme: undefined, configHome }).rows;
96
- };
97
-
98
- /** The lines the host would draw for one setting, reading `current()` at render time. */
99
- const toggleLines = (rows: readonly CustomizeRow[]): string[] => {
100
- const lines: string[] = [];
101
- let index = 0;
102
- while (index < rows.length) {
103
- const setting = rows[index].choice?.setting;
104
- if (setting === undefined) {
105
- index += 1;
106
- continue;
107
- }
108
- let end = index;
109
- while (end + 1 < rows.length && rows[end + 1].choice?.setting === setting) end += 1;
110
- const marks = rows
111
- .slice(index, end + 1)
112
- .map((row) => `${row.choice?.current() === true ? "●" : "○"} ${row.choice?.value ?? ""}`);
113
- lines.push(`${setting} ${marks.join(" ")}`);
114
- index = end + 1;
115
- }
116
- return lines;
117
- };
118
-
119
- const recordingTui = (): { calls: string[]; host: Record<string, unknown> } => {
120
- const calls: string[] = [];
121
- return {
122
- calls,
123
- host: {
124
- terminal: {},
125
- invalidate: () => calls.push("invalidate"),
126
- requestRender: () => calls.push("requestRender"),
127
- },
128
- };
129
- };
130
-
131
- test("T9: the bridge owns one rows provider, and it is internal plumbing", () => {
132
- installToolDesignRows();
133
- assert.deepEqual(
134
- snapshotProviders().map((provider) => provider.id),
135
- [TOOL_DESIGN_ROWS_PROVIDER_ID],
136
- );
137
- installToolDesignRows();
138
- assert.equal(snapshotProviders().length, 1, "installing twice replaces the provider in place");
139
- });
140
-
141
- test("T9: no design means NO line: a session that never registers one is unchanged", () => {
142
- assert.deepEqual(rowsForBuild(), []);
143
- });
144
-
145
- test("T9: one Cards line per tool that has designs, in Cards, values are the tool's card + designs", () => {
146
- registerToolDesign(bashHeading);
147
- registerToolDesign(bashFramed);
148
- registerToolDesign(writeBadge);
149
-
150
- const rows = rowsForBuild();
151
-
152
- assert.ok(rows.every((row) => row.category === "Cards"), "every row belongs to Cards");
153
- const lines = toggleLines(rows);
154
- assert.equal(lines.length, 2, "one line per tool, never one line per design");
155
- assert.match(lines[0], /^Card bash /u, "the line is named after the Cards category, then the tool");
156
- assert.match(lines[0], /● native/u, "the tool's own card is the default selection");
157
- assert.match(lines[0], /○ bash heading/u);
158
- assert.match(lines[0], /○ bash framed/u);
159
- assert.match(lines[1], /^Card write /u);
160
- assert.match(lines[1], /○ write badge/u);
161
- });
162
-
163
- test("T9: the selection persists PER TOOL and moves the bullet on the next build", async () => {
164
- registerToolDesign(bashHeading);
165
- registerToolDesign(writeBadge);
166
-
167
- const rows = rowsForBuild();
168
- const pick = rows.find((row) => row.choice?.value === "bash heading");
169
- assert.ok(pick?.action !== undefined);
170
- await pick.action({ input: async () => undefined, confirm: async () => true, capture: async () => undefined });
171
-
172
- assert.equal(getToolDesignSelection("bash"), "loon.bash-heading");
173
- assert.equal(getToolDesignSelection("write"), TOOL_DESIGN_NATIVE, "the other tool did not move");
174
-
175
- const after = toggleLines(rowsForBuild());
176
- assert.match(after[0], /○ native ● bash heading/u);
177
- assert.match(after[1], /● native ○ write badge/u);
178
-
179
- // And the way back to the host's own card.
180
- const native = rowsForBuild().find(
181
- (row) => row.choice?.value === TOOL_DESIGN_NATIVE && row.choice?.setting === "Card bash",
182
- );
183
- assert.ok(native?.action !== undefined);
184
- await native.action({ input: async () => undefined, confirm: async () => true, capture: async () => undefined });
185
- assert.equal(getToolDesignSelection("bash"), TOOL_DESIGN_NATIVE);
186
- });
187
-
188
- test("T9: changing the value repaints the drawn cards through requestRepaint()", async () => {
189
- registerToolDesign(bashHeading);
190
- const tui = recordingTui();
191
- setRepaintHost(tui.host);
192
-
193
- const rows = rowsForBuild();
194
- const pick = rows.find((row) => row.choice?.value === "bash heading");
195
- assert.ok(pick?.action !== undefined);
196
- await pick.action({ input: async () => undefined, confirm: async () => true, capture: async () => undefined });
197
-
198
- assert.deepEqual(
199
- tui.calls,
200
- ["requestRender"],
201
- "one differential frame; no cache is dropped and no forced full redraw is asked for",
202
- );
203
- assert.equal(getRepaintDiagnostics().lastTrigger?.outcome, "repainted");
204
-
205
- // Pressing the value that is already selected costs no frame.
206
- tui.calls.length = 0;
207
- await pick.action({ input: async () => undefined, confirm: async () => true, capture: async () => undefined });
208
- assert.deepEqual(tui.calls, []);
209
- });
210
-
211
- test("T9: a design's own preview is the preview its value shows", () => {
212
- registerToolDesign(bashHeading);
213
- registerToolDesign(writeBadge);
214
-
215
- const rows = rowsForBuild();
216
- const heading = rows.find((row) => row.choice?.value === "bash heading");
217
- assert.ok(heading?.preview !== undefined);
218
- assert.deepEqual(heading.preview(), { title: "Cards · bash heading", sample: "bash ls -la" });
219
-
220
- const badge = rows.find((row) => row.choice?.value === "write badge");
221
- assert.ok(badge?.preview !== undefined);
222
- assert.match(badge.preview().title, /write badge/u);
223
-
224
- const native = rows.find(
225
- (row) => row.choice?.value === TOOL_DESIGN_NATIVE && row.choice?.setting === "Card bash",
226
- );
227
- assert.ok(native?.preview !== undefined);
228
- assert.match(native.preview().sample, /own card|host/u);
229
- });
230
-
231
- test("T9: the line name starts with `Card `, then the tool's readable words, like the host's rows", () => {
232
- registerToolDesign({ ...writeBadge, tool: "bash", id: "loon.bash-heading" });
233
- registerToolDesign({ ...writeBadge, tool: "lsp_navigation", id: "loon.lsp" });
234
- registerToolDesign({ ...writeBadge, tool: "mcp__engram__mem_save", id: "loon.mcp" });
235
- registerToolDesign({ ...writeBadge, tool: "···", id: "loon.nowords" });
236
-
237
- const settings = [...new Set(rowsForBuild().map((row) => row.choice?.setting))];
238
-
239
- assert.deepEqual(settings, [
240
- "Card bash",
241
- "Card lsp navigation",
242
- // The TAIL of a long name is the specific part: `Card mcp engram` would say nothing.
243
- "Card mem save",
244
- // A tool name that derives to no words keeps the fallback word.
245
- "Card tool",
246
- ]);
247
- for (const setting of settings) {
248
- assert.ok(setting !== undefined);
249
- const words = setting.trim().split(/\s+/u);
250
- assert.ok(words.length >= 1 && words.length <= 3, `${setting} must stay within the word limit`);
251
- assert.equal(words[0], "Card", `${setting} must start with the Cards category's word`);
252
- assert.match(setting, /^Card( [a-z0-9]+){1,2}$/u, `${setting} is capitalised like Card style`);
253
- }
254
- });
255
-
256
- test("T9: two tools that clamp to the same name stay distinct and pass the shape rule", () => {
257
- registerToolDesign({ ...writeBadge, tool: "mcp__alpha__mem_save", id: "loon.alpha" });
258
- registerToolDesign({ ...writeBadge, tool: "mcp__beta__mem_save", id: "loon.beta" });
259
-
260
- const settings = [...new Set(rowsForBuild().map((row) => row.choice?.setting))];
261
-
262
- assert.deepEqual(settings, ["Card mem save", "Card mem 2"], "the second gets the lowest free ordinal");
263
- for (const setting of settings) {
264
- assert.ok(setting !== undefined);
265
- assert.ok(
266
- setting.trim().split(/\s+/u).length <= 3,
267
- `${setting} must stay within the row-name word limit`,
268
- );
269
- assert.doesNotMatch(setting, /[.:|/\\[\](){}→]/u, `${setting} must carry no structural mark`);
270
- }
271
- });
272
-
273
- test("T9: the persisted value and the stored selection are unchanged by the line's name", async () => {
274
- registerToolDesign(bashHeading);
275
-
276
- const pick = rowsForBuild().find((row) => row.choice?.value === "bash heading");
277
- assert.ok(pick?.action !== undefined);
278
- assert.equal(pick.choice?.setting, "Card bash", "the line is named after the card, not after the store");
279
- await pick.action({ input: async () => undefined, confirm: async () => true, capture: async () => undefined });
280
-
281
- // The VALUE is the design's id and the store is still keyed by the TOOL name: only the visible
282
- // line name moved.
283
- assert.equal(getToolDesignSelection("bash"), "loon.bash-heading");
284
- const stored = createStore("bridge-tool-designs").get<Record<string, unknown>>("selections", {});
285
- assert.deepEqual(stored, { bash: "loon.bash-heading" });
286
- });
287
-
288
- test("T9: a tool name keeps a row name that passes the shape rule, even a long MCP one", () => {
289
- registerToolDesign({ ...writeBadge, tool: "mcp__engram__mem_save", id: "loon.mcp" });
290
- registerToolDesign({ ...writeBadge, tool: "mcp__engram__mem_search", id: "loon.mcp2" });
291
-
292
- installToolDesignRows();
293
- const result = buildRows({ theme: undefined, configHome });
294
- const rows = result.rows;
295
-
296
- assert.deepEqual(result.failures, []);
297
- const settings = new Set(rows.map((row) => row.choice?.setting));
298
- assert.equal(settings.size, 2, "two long tool names still produce two distinct lines");
299
- for (const setting of settings) {
300
- assert.ok(setting !== undefined);
301
- assert.ok(
302
- setting.trim().split(/\s+/u).length <= 3,
303
- `${setting} must stay within the row-name word limit`,
304
- );
305
- assert.doesNotMatch(setting, /[.:|/\\[\](){}→]/u, `${setting} must carry no structural mark`);
306
- }
307
- });
308
-
309
- test("T9: tools are listed in a deterministic order, and only tools with designs appear", () => {
310
- registerToolDesign(writeBadge);
311
- registerToolDesign(bashHeading);
312
-
313
- const rows = rowsForBuild();
314
- assert.deepEqual(toolNamesWithDesigns(), ["write", "bash"], "registration order");
315
- assert.deepEqual(
316
- [...new Set(rows.map((row) => row.choice?.setting))].length,
317
- 2,
318
- );
319
- assert.equal(
320
- rows.some((row) => row.choice?.setting.toLowerCase().includes("edit")),
321
- false,
322
- "a tool with no design has no line",
323
- );
324
- });
325
-
326
- test("S2: picking a design in the menu repaints the card ALREADY on screen", async () => {
327
- // The user's words: the choice has to change "tanto las antiguas como posteriores", not only new
328
- // cards. Pi resolves the renderers once, when the card is constructed, and re-renders that same
329
- // component on every frame — so this is the card the menu action must reach.
330
- const hostCard = ["│ $ ls -la", "│ total 4"];
331
- const component = { render: (_width: number): string[] => [...hostCard] };
332
- const renderers = { renderShell: "default", renderCall: () => component };
333
-
334
- registerToolDesign({
335
- ...bashHeading,
336
- apply: (lines) => lines.map((line) => line.replace("$ ", "bash ")),
337
- });
338
- const resolved = toolDesignResolver("bash", () => renderers);
339
- assert.ok(resolved !== undefined);
340
- const card = (resolved.renderCall as () => typeof component)();
341
- assert.deepEqual(card.render(80), hostCard, "no design selected: the host's own bytes");
342
-
343
- const pick = rowsForBuild().find((row) => row.choice?.value === "bash heading");
344
- assert.ok(pick?.action !== undefined);
345
- await pick.action({ input: async () => undefined, confirm: async () => true, capture: async () => undefined });
346
-
347
- assert.deepEqual(
348
- card.render(80),
349
- ["│ bash ls -la", "│ total 4"],
350
- "the SAME, already-drawn card now shows the design the menu selected",
351
- );
352
- });
353
-
354
- test("PRESERVE: the provider's rows stay empty after the last design is disposed", () => {
355
- const dispose = registerToolDesign(bashHeading);
356
- assert.equal(rowsForBuild().length, 2, "native + one design");
357
- dispose();
358
- assert.deepEqual(rowsForBuild(), []);
359
- });