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,409 +0,0 @@
1
- /**
2
- * T27 / S16: a BUTTON row is a first-class kind, not an accident of a missing field.
3
- *
4
- * The host's row type (`lib/visual-customize-view.ts:107-130`) makes `choice` optional and
5
- * `action(inline)` required, and Enter/Space runs `action` (`:575-577`) exactly as it does for the
6
- * host's own `Profiles` and `Reset` rows (`extensions/gentle-shell.ts:3541,3550`). So a row with
7
- * `category`, `label`, `action` and NO `choice` is the second supported kind, and this suite pins
8
- * everything about it through the real entry (`publishBridgeApi().registerRows`) and the real
9
- * capture path:
10
- *
11
- * - it is accepted, reaches the host's array with its category untouched, and renders as its own
12
- * row — never a value of another row's setting;
13
- * - `preview`, `keyhint`, `visible` and a function-valued `label` are reference-identical, so the
14
- * bridge passes them through instead of re-deriving them;
15
- * - its label is the WHOLE visible text of the row, so the bridge's plain-word row-name rule applies
16
- * to it exactly as it applies to a toggle's label: a mark, or more than three words, is refused
17
- * and reported verbatim, and the row is absent rather than rewritten;
18
- * - `confirmOnEnter` never touches it: no `choice` appears, no `applyOnMove` is injected, and the
19
- * provider's own object is not mutated;
20
- * - a throwing (or rejecting) `action` is contained by the bridge and reported ONCE through the
21
- * notice channel that `/bridge-status` lists, so the menu and every other row stay usable;
22
- * - a provider may mix toggles and buttons in one category, and the order it gave is kept.
23
- */
24
-
25
- import assert from "node:assert/strict";
26
- import { mkdtempSync, rmSync } from "node:fs";
27
- import { tmpdir } from "node:os";
28
- import { join } from "node:path";
29
- import test from "node:test";
30
-
31
- import { publishBridgeApi, resetBridgeApiForTests } from "../src/api.ts";
32
- import { armBridge } from "../src/arm.ts";
33
- import {
34
- registerRows,
35
- resetBusForTests,
36
- resetCardStylesForTests,
37
- resetSectionsForTests,
38
- resetTransformsForTests,
39
- } from "../src/bus.ts";
40
- import { disableCaptureBackend, resetCaptureStateForTests } from "../src/capture.ts";
41
- import { resetHookStateForTests } from "../src/hook.ts";
42
- import { resetNoticesForTests, snapshotNotices } from "../src/notices.ts";
43
- import { getRowNameRejections, resetRowNameRejectionsForTests } from "../src/row-names.ts";
44
- import { getLastRowBuild, resetRowBuildStateForTests } from "../src/rows.ts";
45
- import { resetStoreStateForTests } from "../src/store.ts";
46
- import type { CustomizeRow, RowLabel } from "../src/types.ts";
47
- import { createFakeHost, type FakeHost } from "./fake-host.ts";
48
-
49
- const MARK_RULE =
50
- "a row name is one to three plain words with no arrow, colon, dot, pipe, slash or bracket";
51
- const WORD_RULE = "a row name is one to three plain words";
52
-
53
- const labelText = (label: RowLabel | undefined): string => {
54
- if (typeof label === "function") return label();
55
- return label ?? "";
56
- };
57
-
58
- const loonLabels = (host: FakeHost): string[] =>
59
- host
60
- .liveRows()
61
- .map((row) => labelText(row.label))
62
- .filter((label) => label.startsWith("Loon"));
63
-
64
- const rowsOfLastBuild = (providerId: string): CustomizeRow[] =>
65
- getLastRowBuild()?.find((record) => record.providerId === providerId)?.rows ?? [];
66
-
67
- /** The one visible row a provider contributed, as the host holds it right now. */
68
- const liveRow = (host: FakeHost, label: string): CustomizeRow => {
69
- const row = host.liveRows().find((candidate) => labelText(candidate.label) === label);
70
- assert.ok(row !== undefined, `the host holds no row labelled ${JSON.stringify(label)}`);
71
- return row;
72
- };
73
-
74
- let configHome: string;
75
- let previousConfigHome: string | undefined;
76
-
77
- test.before(() => {
78
- configHome = mkdtempSync(join(tmpdir(), "gsb-button-rows-"));
79
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
80
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
81
- });
82
-
83
- test.after(() => {
84
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
85
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
86
- rmSync(configHome, { recursive: true, force: true });
87
- });
88
-
89
- test.beforeEach(() => {
90
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
91
- resetBridgeApiForTests();
92
- resetBusForTests();
93
- resetSectionsForTests();
94
- resetCardStylesForTests();
95
- resetTransformsForTests();
96
- resetRowNameRejectionsForTests();
97
- resetCaptureStateForTests();
98
- resetHookStateForTests();
99
- resetRowBuildStateForTests();
100
- resetNoticesForTests();
101
- resetStoreStateForTests();
102
- disableCaptureBackend();
103
- });
104
-
105
- const menuHost = (): FakeHost => {
106
- const host = createFakeHost();
107
- armBridge(host.ui);
108
- return host;
109
- };
110
-
111
- /** A toggle line: consecutive rows sharing one `choice.setting`, the existing kind. */
112
- const toggles = (setting: string, label: string, values: [string, string]): CustomizeRow[] =>
113
- values.map((value) => ({
114
- category: "Cards",
115
- label,
116
- choice: { setting, value, current: () => value === values[0] },
117
- action: () => {},
118
- }));
119
-
120
- // ---------------------------------------------------------------------------------------------
121
- // Accepted, rendered, and never a value of somebody else's setting
122
- // ---------------------------------------------------------------------------------------------
123
-
124
- test("a button row is accepted in an existing category and Enter runs its action once", async () => {
125
- const calls: string[] = [];
126
- publishBridgeApi().registerRows({
127
- id: "loon.buttons",
128
- category: "Cards",
129
- rows: () => [
130
- ...toggles("Bash rendering", "Bash rendering", ["default", "tool"]),
131
- { category: "Cards", label: "Loon refresh", action: () => void calls.push("refresh") },
132
- ],
133
- });
134
-
135
- const host = menuHost();
136
- host.buildMenu();
137
-
138
- assert.ok(host.categories().includes("Cards"), "an existing category is reused");
139
- assert.deepEqual(loonLabels(host), ["Loon refresh"], "the row is drawn as its own line");
140
- assert.deepEqual(
141
- host.toggleLines(),
142
- ["Bash rendering ● default ○ tool"],
143
- "and it is not a value of the toggle line it sits after",
144
- );
145
-
146
- const row = liveRow(host, "Loon refresh");
147
- assert.equal(row.choice, undefined, "a button carries no choice");
148
- assert.equal("choice" in row, false, "and no choice is invented for it");
149
-
150
- await host.press("Loon refresh");
151
- assert.deepEqual(calls, ["refresh"], "Enter (or Space) runs the action exactly once");
152
- });
153
-
154
- test("a button row in a new category keeps that category untouched", () => {
155
- publishBridgeApi().registerRows({
156
- id: "loon.buttons",
157
- category: "Loon",
158
- rows: () => [{ label: "Loon refresh", action: () => {} }],
159
- });
160
-
161
- const host = menuHost();
162
- host.buildMenu();
163
-
164
- const row = liveRow(host, "Loon refresh");
165
- assert.equal(row.category, "Loon", "the provider's own category name is not rewritten");
166
- assert.ok(
167
- host.liveRows().some((candidate) => candidate.category === "Loon"),
168
- "the row is in the array the host derives its categories from (visual-customize-view.ts:387)",
169
- );
170
- // Fixed by S18: the bridge appends the providers' rows BEFORE the view's constructor runs
171
- // `options.rows.map(...)` (`visual-customize-view.ts:387`), so the page the row names exists to be
172
- // selected. A late push — after the factory returned — could only join a page the list already
173
- // held, which is why a brand-new category was invisible before (E8).
174
- assert.equal(
175
- host.categories().includes("Loon"),
176
- true,
177
- "the new page is in the category list the view computed",
178
- );
179
- });
180
-
181
- // ---------------------------------------------------------------------------------------------
182
- // Pass-through: nothing is re-derived
183
- // ---------------------------------------------------------------------------------------------
184
-
185
- test("preview, keyhint, visible and a function label pass through untouched", () => {
186
- const preview = () => ({ title: "Loon · refresh", sample: "redraws the rail" });
187
- const visible = () => true;
188
- const label = () => "Loon refresh";
189
- const action = () => {};
190
- publishBridgeApi().registerRows({
191
- id: "loon.buttons",
192
- category: "Cards",
193
- rows: () => [{ category: "Cards", label, preview, keyhint: "Enter refresh · Esc close", visible, action }],
194
- });
195
-
196
- const host = menuHost();
197
- host.buildMenu();
198
- const row = liveRow(host, "Loon refresh");
199
-
200
- assert.equal(row.label, label, "a function label is handed over as the same function");
201
- assert.equal(row.preview, preview, "preview is the provider's function");
202
- assert.equal(row.keyhint, "Enter refresh · Esc close", "keyhint is verbatim");
203
- assert.equal(row.visible, visible, "visible is the provider's function");
204
- assert.deepEqual(row.preview?.(), { title: "Loon · refresh", sample: "redraws the rail" });
205
- assert.equal(row.visible?.(), true);
206
- assert.notEqual(row.action, action, "the action is the ONE thing the bridge wraps (see below)");
207
- assert.deepEqual(
208
- Object.keys(row).sort(),
209
- ["action", "category", "keyhint", "label", "preview", "visible"],
210
- "no field is added, dropped or renamed on the way through",
211
- );
212
- });
213
-
214
- // ---------------------------------------------------------------------------------------------
215
- // The label is the row's whole visible text, so the row-name rule applies to it unchanged
216
- // ---------------------------------------------------------------------------------------------
217
-
218
- test("a button label with a structural mark is refused and reported, and its siblings stay", () => {
219
- publishBridgeApi().registerRows({
220
- id: "loon.buttons",
221
- category: "Cards",
222
- rows: () => [
223
- { category: "Cards", label: "Loon: refresh", action: () => {} },
224
- { category: "Cards", label: "Loon refresh the rail now", action: () => {} },
225
- { category: "Cards", label: () => "Loon: functional label", action: () => {} },
226
- { category: "Cards", label: "Loon refresh", action: () => {} },
227
- ],
228
- });
229
-
230
- const host = menuHost();
231
- host.buildMenu();
232
-
233
- assert.deepEqual(loonLabels(host), ["Loon refresh"], "only the conforming row reaches the menu");
234
- assert.deepEqual(
235
- getRowNameRejections(),
236
- [
237
- {
238
- surface: "rows",
239
- id: "loon.buttons",
240
- field: "row label",
241
- value: "Loon: refresh",
242
- rule: "mark",
243
- reason: `it contains ":"; ${MARK_RULE}`,
244
- },
245
- {
246
- surface: "rows",
247
- id: "loon.buttons",
248
- field: "row label",
249
- value: "Loon refresh the rail now",
250
- rule: "words",
251
- reason: `it has 5 words; ${WORD_RULE}`,
252
- },
253
- {
254
- surface: "rows",
255
- id: "loon.buttons",
256
- field: "row label",
257
- value: "Loon: functional label",
258
- rule: "mark",
259
- reason: `it contains ":"; ${MARK_RULE}`,
260
- },
261
- ],
262
- "the same rule as a toggle's label, and a function label is evaluated to check it",
263
- );
264
- assert.deepEqual(
265
- rowsOfLastBuild("loon.buttons").map((row) => labelText(row.label)),
266
- ["Loon refresh"],
267
- "the refused rows are absent from the effective set, not merely hidden",
268
- );
269
- });
270
-
271
- // ---------------------------------------------------------------------------------------------
272
- // confirmOnEnter leaves a button row alone
273
- // ---------------------------------------------------------------------------------------------
274
-
275
- test("a button row keeps no choice and no applyOnMove, and the provider's object is not mutated", () => {
276
- const providerRow: CustomizeRow = { category: "Cards", label: "Loon refresh", action: () => {} };
277
- const toggleRow: CustomizeRow = {
278
- category: "Cards",
279
- label: "Loon rendering",
280
- choice: { setting: "Loon rendering", value: "on", current: () => true },
281
- action: () => {},
282
- };
283
- publishBridgeApi().registerRows({
284
- id: "loon.buttons",
285
- category: "Cards",
286
- rows: () => [providerRow, toggleRow],
287
- });
288
-
289
- const host = menuHost();
290
- host.buildMenu();
291
-
292
- assert.deepEqual(
293
- Object.keys(providerRow).sort(),
294
- ["action", "category", "label"],
295
- "the bridge never writes to the row the provider keeps",
296
- );
297
- const row = liveRow(host, "Loon refresh");
298
- assert.equal("choice" in row, false);
299
- assert.equal("applyOnMove" in row, false);
300
-
301
- // PRESERVE: the existing kind is unchanged — a choice row still defers its value to Enter.
302
- const toggle = liveRow(host, "Loon rendering");
303
- assert.equal(toggle.choice?.applyOnMove, false);
304
- assert.equal(toggle.action, toggleRow.action, "and its action is handed over untouched");
305
- });
306
-
307
- // ---------------------------------------------------------------------------------------------
308
- // A failing action is contained and reported once
309
- // ---------------------------------------------------------------------------------------------
310
-
311
- const failureNotice = (label: string, message: string): string =>
312
- `gentle-shell-bridge: the button row ${JSON.stringify(label)} from "loon.buttons" failed: ` +
313
- `${message}. The menu stays open; fix the row's action.`;
314
-
315
- test("a synchronously throwing action is contained and reported once", async () => {
316
- const ran: string[] = [];
317
- publishBridgeApi().registerRows({
318
- id: "loon.buttons",
319
- category: "Cards",
320
- rows: () => [
321
- {
322
- category: "Cards",
323
- label: "Loon explode",
324
- action: () => {
325
- ran.push("explode");
326
- throw new Error("row action exploded");
327
- },
328
- },
329
- { category: "Cards", label: "Loon refresh", action: () => void ran.push("refresh") },
330
- ],
331
- });
332
-
333
- const host = menuHost();
334
- host.buildMenu();
335
- assert.deepEqual(loonLabels(host), ["Loon explode", "Loon refresh"]);
336
-
337
- await host.press("Loon explode");
338
- await host.press("Loon explode");
339
-
340
- assert.deepEqual(ran, ["explode", "explode"], "the row ran; the throw did not escape the bridge");
341
- assert.deepEqual(
342
- snapshotNotices().entries.map((entry) => `${entry.level}: ${entry.message}`),
343
- [`error: ${failureNotice("Loon explode", "row action exploded")}`],
344
- "one report for one repeated failure, not one per press",
345
- );
346
-
347
- // The menu is still usable: the other row renders and its action still runs.
348
- assert.deepEqual(loonLabels(host), ["Loon explode", "Loon refresh"]);
349
- await host.press("Loon refresh");
350
- assert.deepEqual(ran, ["explode", "explode", "refresh"]);
351
- });
352
-
353
- test("a rejecting action is contained and reported once", async () => {
354
- publishBridgeApi().registerRows({
355
- id: "loon.buttons",
356
- category: "Cards",
357
- rows: () => [
358
- {
359
- category: "Cards",
360
- label: "Loon explode",
361
- action: async () => {
362
- throw new Error("row action rejected");
363
- },
364
- },
365
- ],
366
- });
367
-
368
- const host = menuHost();
369
- host.buildMenu();
370
-
371
- await host.press("Loon explode");
372
- await host.press("Loon explode");
373
-
374
- assert.deepEqual(
375
- snapshotNotices().entries.map((entry) => entry.message),
376
- [failureNotice("Loon explode", "row action rejected")],
377
- "a rejected promise is a failure too, and it is reported once",
378
- );
379
- });
380
-
381
- // ---------------------------------------------------------------------------------------------
382
- // One provider, both kinds, in its own order
383
- // ---------------------------------------------------------------------------------------------
384
-
385
- test("toggles and buttons in one category keep the provider's order", () => {
386
- publishBridgeApi().registerRows({
387
- id: "loon.buttons",
388
- category: "Cards",
389
- rows: () => [
390
- ...toggles("Loon alpha", "Loon alpha", ["on", "off"]),
391
- { category: "Cards", label: "Loon refresh", action: () => {} },
392
- ...toggles("Loon beta", "Loon beta", ["low", "high"]),
393
- ],
394
- });
395
-
396
- const host = menuHost();
397
- host.buildMenu();
398
-
399
- assert.deepEqual(
400
- rowsOfLastBuild("loon.buttons").map((row) => labelText(row.label)),
401
- ["Loon alpha", "Loon alpha", "Loon refresh", "Loon beta", "Loon beta"],
402
- "the build keeps the provider's order across both kinds",
403
- );
404
- assert.deepEqual(
405
- host.toggleLines(),
406
- ["Loon alpha ● on ○ off", "Loon beta ● low ○ high"],
407
- "each toggle line is still one line; the button is its own row between them",
408
- );
409
- });