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,328 +0,0 @@
1
- /**
2
- * The in-TUI diagnostics channel.
3
- *
4
- * Pi renders every diagnostic inside its own report; text written to stdout/stderr at load time
5
- * appears in the terminal around Pi's startup, outside the TUI that owns the screen. So the bridge
6
- * never writes to the console: a message produced before a context exists is buffered here and
7
- * flushed ONCE through `ctx.ui.notify`, and `/bridge-status` keeps reporting it afterwards.
8
- *
9
- * These tests pin the whole contract: the bounded buffer, the dedupe, the flush-once rule, the
10
- * no-UI case, the status block, and — the reason the module exists — that loading the entry, the
11
- * example and an armed session produce ZERO console writes.
12
- */
13
-
14
- import assert from "node:assert/strict";
15
- import test from "node:test";
16
-
17
- import { BRIDGE_API_KEY, resetBridgeApiForTests } from "../src/api.ts";
18
- import { resetBusForTests } from "../src/bus.ts";
19
- import { resetCaptureStateForTests } from "../src/capture.ts";
20
- import gentleShellBridge, {
21
- type BridgeExtensionApi,
22
- type BridgeSessionHandler,
23
- } from "../src/extension.ts";
24
- import { resetHookStateForTests } from "../src/hook.ts";
25
- import {
26
- MAX_NOTICES,
27
- flushNotices,
28
- recordNotice,
29
- resetNoticesForTests,
30
- snapshotNotices,
31
- } from "../src/notices.ts";
32
- import { resetRowBuildStateForTests } from "../src/rows.ts";
33
- import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
34
- import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
35
- import { collectStatus, formatStatus } from "../src/status.ts";
36
- import { resetStoreStateForTests } from "../src/store.ts";
37
- import { createFakeHost } from "./fake-host.ts";
38
- import { resetSidebar } from "./fake-sidebar.ts";
39
-
40
- const hostSlot = (): Record<symbol, unknown> => globalThis as unknown as Record<symbol, unknown>;
41
-
42
- interface ConsoleCapture {
43
- writes: string[];
44
- restore: () => void;
45
- }
46
-
47
- /** Record every terminal write the process would show. */
48
- const captureConsole = (): ConsoleCapture => {
49
- const writes: string[] = [];
50
- const sink = (globalThis as unknown as { console: { log: unknown; warn: unknown; error: unknown } })
51
- .console;
52
- const original = { log: sink.log, warn: sink.warn, error: sink.error };
53
- const record =
54
- (method: string) =>
55
- (...args: unknown[]): void => {
56
- writes.push(`${method}: ${args.map((arg) => String(arg)).join(" ")}`);
57
- };
58
- sink.log = record("log");
59
- sink.warn = record("warn");
60
- sink.error = record("error");
61
- return {
62
- writes,
63
- restore: () => {
64
- sink.log = original.log;
65
- sink.warn = original.warn;
66
- sink.error = original.error;
67
- },
68
- };
69
- };
70
-
71
- interface RecordingUi {
72
- notify: (message: string, level?: "info" | "warning" | "error") => void;
73
- }
74
-
75
- const recordingUi = (): { ui: RecordingUi; calls: Array<[string, string | undefined]> } => {
76
- const calls: Array<[string, string | undefined]> = [];
77
- return {
78
- ui: { notify: (message, level) => calls.push([message, level]) },
79
- calls,
80
- };
81
- };
82
-
83
- const entryHandlers = (): BridgeSessionHandler[] => {
84
- const handlers: BridgeSessionHandler[] = [];
85
- const pi: BridgeExtensionApi = {
86
- // `gentle-pi` is loaded, so the runtime gate is open and the buffered notice is flushed.
87
- getCommands: () => [{ name: "gentle:customize" }],
88
- on(_event, handler) {
89
- handlers.push(handler);
90
- },
91
- registerCommand() {},
92
- };
93
- gentleShellBridge(pi);
94
- return handlers;
95
- };
96
-
97
- test.beforeEach(() => {
98
- resetBridgeApiForTests();
99
- resetNoticesForTests();
100
- resetBusForTests();
101
- resetSidebar();
102
- resetSidebarStateForTests();
103
- resetSectionFailuresForTests();
104
- resetStoreStateForTests();
105
- resetCaptureStateForTests();
106
- resetHookStateForTests();
107
- resetRowBuildStateForTests();
108
- disableSidebarBackend();
109
- });
110
-
111
- // ---------------------------------------------------------------------------------------------
112
- // The bounded buffer
113
- // ---------------------------------------------------------------------------------------------
114
-
115
- test("a notice is buffered as pending, with its level", () => {
116
- recordNotice("the slot holds a foreign object", "warning");
117
-
118
- assert.deepEqual(snapshotNotices(), {
119
- entries: [{ message: "the slot holds a foreign object", level: "warning", emitted: false }],
120
- dropped: 0,
121
- });
122
- });
123
-
124
- test("the default level is warning: every notice here is a problem report", () => {
125
- recordNotice("no registerToolRenderer");
126
-
127
- assert.equal(snapshotNotices().entries[0].level, "warning");
128
- });
129
-
130
- test("an identical message is recorded once, not once per load", () => {
131
- recordNotice("same fact", "warning");
132
- recordNotice("same fact", "warning");
133
- recordNotice("same fact", "error");
134
-
135
- assert.deepEqual(
136
- snapshotNotices().entries.map((entry) => [entry.message, entry.level]),
137
- [
138
- ["same fact", "warning"],
139
- ["same fact", "error"],
140
- ],
141
- );
142
- });
143
-
144
- test("the buffer is bounded and counts what it had to drop", () => {
145
- for (let index = 0; index < MAX_NOTICES + 3; index += 1) recordNotice(`notice ${index}`);
146
-
147
- const snapshot = snapshotNotices();
148
- assert.equal(snapshot.entries.length, MAX_NOTICES, "never unbounded");
149
- assert.equal(snapshot.dropped, 3, "and the loss is reported, not silent");
150
- assert.equal(snapshot.entries[0].message, "notice 3", "the newest messages survive");
151
- assert.equal(snapshot.entries.at(-1)?.message, `notice ${MAX_NOTICES + 2}`);
152
- });
153
-
154
- test("delivered entries are evicted before a pending one, and nothing is dropped", () => {
155
- for (let index = 0; index < MAX_NOTICES; index += 1) recordNotice(`delivered ${index}`);
156
- flushNotices(recordingUi().ui);
157
- recordNotice("still pending");
158
- recordNotice("later 1");
159
- recordNotice("later 2");
160
-
161
- const snapshot = snapshotNotices();
162
- assert.equal(snapshot.entries.length, MAX_NOTICES);
163
- assert.equal(snapshot.dropped, 0, "only already-delivered entries made room");
164
- assert.equal(
165
- snapshot.entries.some((entry) => entry.message === "still pending"),
166
- true,
167
- "a message the user never saw is never the first to go",
168
- );
169
- assert.equal(
170
- snapshot.entries.some((entry) => entry.message === "delivered 0"),
171
- false,
172
- "the oldest delivered notice made the room",
173
- );
174
- });
175
-
176
- // ---------------------------------------------------------------------------------------------
177
- // The flush: once, through the UI, and only when a UI exists
178
- // ---------------------------------------------------------------------------------------------
179
-
180
- test("the first UI session delivers every pending notice exactly once", () => {
181
- const { ui, calls } = recordingUi();
182
- recordNotice("first", "warning");
183
- recordNotice("second", "error");
184
-
185
- assert.equal(flushNotices(ui), 2);
186
- assert.equal(flushNotices(ui), 0, "a second session_start never repeats a delivered message");
187
- assert.deepEqual(calls, [
188
- ["first", "warning"],
189
- ["second", "error"],
190
- ]);
191
- assert.deepEqual(
192
- snapshotNotices().entries.map((entry) => entry.emitted),
193
- [true, true],
194
- "and they stay reported for /bridge-status",
195
- );
196
- });
197
-
198
- test("a session without a UI flushes nothing and keeps the message pending", () => {
199
- recordNotice("waiting for a UI");
200
-
201
- assert.equal(flushNotices(undefined), 0);
202
- assert.equal(flushNotices({}), 0, "a context without notify cannot deliver anything");
203
- assert.deepEqual(
204
- snapshotNotices().entries.map((entry) => entry.emitted),
205
- [false],
206
- "nothing was marked delivered",
207
- );
208
- });
209
-
210
- test("a notify that throws leaves the message pending instead of losing it", () => {
211
- recordNotice("retry me");
212
-
213
- assert.doesNotThrow(() =>
214
- flushNotices({
215
- notify: () => {
216
- throw new Error("ui bug");
217
- },
218
- }),
219
- );
220
- assert.equal(snapshotNotices().entries[0].emitted, false);
221
-
222
- const { ui, calls } = recordingUi();
223
- assert.equal(flushNotices(ui), 1, "the next session delivers it");
224
- assert.deepEqual(calls, [["retry me", "warning"]]);
225
- });
226
-
227
- // ---------------------------------------------------------------------------------------------
228
- // /bridge-status keeps the information
229
- // ---------------------------------------------------------------------------------------------
230
-
231
- test("status reports a pending notice, and the same notice once delivered", () => {
232
- recordNotice("incompatible bridge object", "warning");
233
-
234
- let lines = formatStatus(collectStatus());
235
- assert.ok(lines.includes("notices: 1"));
236
- assert.ok(lines.includes(" - warning (pending): incompatible bridge object"));
237
-
238
- flushNotices(recordingUi().ui);
239
-
240
- lines = formatStatus(collectStatus());
241
- assert.ok(lines.includes("notices: 1"), "still visible after the moment passed");
242
- assert.ok(lines.includes(" - warning (delivered): incompatible bridge object"));
243
- });
244
-
245
- test("status reports dropped notices when the bound was reached", () => {
246
- for (let index = 0; index < MAX_NOTICES + 2; index += 1) recordNotice(`notice ${index}`);
247
-
248
- const snapshot = collectStatus();
249
- assert.equal(snapshot.notices?.length, MAX_NOTICES);
250
- assert.equal(snapshot.noticesDropped, 2);
251
- assert.ok(formatStatus(snapshot).includes("notices dropped: 2"));
252
- });
253
-
254
- test("an empty notice state adds nothing to the status report", () => {
255
- const snapshot = collectStatus();
256
-
257
- assert.equal(snapshot.notices, undefined);
258
- assert.equal(snapshot.noticesDropped, undefined);
259
- assert.equal(
260
- formatStatus(snapshot).some((line) => line.startsWith("notices")),
261
- false,
262
- "the pre-change output stays exactly the same",
263
- );
264
- });
265
-
266
- // ---------------------------------------------------------------------------------------------
267
- // No console writes, ever
268
- // ---------------------------------------------------------------------------------------------
269
-
270
- test("loading a copy of the handshake with an incompatible slot writes nothing to the console", async () => {
271
- resetBridgeApiForTests();
272
- resetNoticesForTests();
273
- hostSlot()[BRIDGE_API_KEY] = { schema: "someone-else/api", version: 9 };
274
-
275
- const captured = captureConsole();
276
- try {
277
- await import(new URL("../src/api.ts?console-free-skew", import.meta.url).href);
278
- } finally {
279
- captured.restore();
280
- }
281
-
282
- assert.deepEqual(captured.writes, [], "the message must not land in the terminal");
283
- const entries = snapshotNotices().entries;
284
- assert.equal(entries.length, 1, "it is buffered instead");
285
- assert.equal(entries[0].level, "warning");
286
- assert.match(entries[0].message, /incompatible|not the|version/u);
287
- });
288
-
289
- test("loading the extension entry writes nothing to the console", async () => {
290
- const captured = captureConsole();
291
- try {
292
- await import(new URL("../src/extension.ts?console-free-load", import.meta.url).href);
293
- } finally {
294
- captured.restore();
295
- }
296
-
297
- assert.deepEqual(captured.writes, []);
298
- });
299
-
300
- test("an armed session flushes once through notify and writes nothing to the console", () => {
301
- const host = createFakeHost();
302
- const { ui, calls } = recordingUi();
303
- const handlers = entryHandlers();
304
- recordNotice("buffered before the UI existed", "warning");
305
-
306
- const captured = captureConsole();
307
- try {
308
- handlers[0]({}, { hasUI: true, ui: { ...host.ui, ...ui } });
309
- handlers[0]({}, { hasUI: true, ui: { ...host.ui, ...ui } });
310
- } finally {
311
- captured.restore();
312
- }
313
-
314
- assert.deepEqual(captured.writes, []);
315
- assert.deepEqual(calls, [["buffered before the UI existed", "warning"]]);
316
- });
317
-
318
- test("a no-UI session does not flush, and the same message arrives on the next UI session", () => {
319
- const { ui, calls } = recordingUi();
320
- const handlers = entryHandlers();
321
- recordNotice("waiting for a UI", "warning");
322
-
323
- handlers[0]({}, { hasUI: false });
324
- assert.deepEqual(calls, [], "nothing can be shown without a UI");
325
-
326
- handlers[0]({}, { hasUI: true, ui });
327
- assert.deepEqual(calls, [["waiting for a UI", "warning"]], "flushed exactly once, later");
328
- });