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