@marimo-team/frontend 0.22.1-dev2 → 0.22.1-dev20

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 (70) hide show
  1. package/dist/assets/{ConnectedDataExplorerComponent-BhOfvmd2.js → ConnectedDataExplorerComponent-Bgd8MpO7.js} +1 -1
  2. package/dist/assets/{JsonOutput-DK6QFpxq.js → JsonOutput-CYRewW_n.js} +10 -10
  3. package/dist/assets/{add-cell-with-ai-C9CXMDL-.js → add-cell-with-ai-CJL5NwHh.js} +24 -24
  4. package/dist/assets/{add-connection-dialog-uUw1BwGd.js → add-connection-dialog-_oI2JBU2.js} +1 -1
  5. package/dist/assets/{agent-panel-CKXYOD10.js → agent-panel-frCoZBMf.js} +1 -1
  6. package/dist/assets/ai-model-dropdown-CMj49xMg.js +5 -0
  7. package/dist/assets/{app-config-button-B4_Fzop6.js → app-config-button-DCVf5Azv.js} +1 -1
  8. package/dist/assets/{cell-editor-DsXpgc4r.js → cell-editor-nJeqD5Xq.js} +1 -1
  9. package/dist/assets/{chat-display-CRKEYeok.js → chat-display-DnlV-Vjp.js} +1 -1
  10. package/dist/assets/chat-panel-BrH9Bycd.js +3 -0
  11. package/dist/assets/{chat-ui-CQB2SbzK.js → chat-ui-gEEIyfNV.js} +1 -1
  12. package/dist/assets/{column-preview-CWOsM3UD.js → column-preview-PiH54hAW.js} +1 -1
  13. package/dist/assets/{command-palette-BXM1GCVc.js → command-palette-CrC7ohqO.js} +1 -1
  14. package/dist/assets/{edit-page-BXQVe56n.js → edit-page-idaINdSX.js} +7 -7
  15. package/dist/assets/{file-explorer-panel-QFaPxpp8.js → file-explorer-panel-sS4HTA69.js} +1 -1
  16. package/dist/assets/{form-CoRP5wCe.js → form-B-BLUzr3.js} +1 -1
  17. package/dist/assets/{formats-C_TavbEL.js → formats-N7VZhahg.js} +1 -1
  18. package/dist/assets/{home-page-GHJ3-S70.js → home-page-Lm5CjxXF.js} +1 -1
  19. package/dist/assets/{hooks-CfqzfU-0.js → hooks-ChIFqb9W.js} +1 -1
  20. package/dist/assets/index-CCazW2UV.css +2 -0
  21. package/dist/assets/{index-e_bkIEdq.js → index-DjrkoUpB.js} +20 -20
  22. package/dist/assets/{layout-FSEZfgnG.js → layout-BlYB96ph.js} +1 -1
  23. package/dist/assets/{markdown-renderer-wLZT8no0.js → markdown-renderer-CYb9pckL.js} +1 -1
  24. package/dist/assets/{packages-panel-CwbHm9uC.js → packages-panel-C7RTPIUf.js} +1 -1
  25. package/dist/assets/{panels-BhaV1xQQ.js → panels-qPMPLlzN.js} +1 -1
  26. package/dist/assets/{run-page-BtSGYy1m.js → run-page-Cqmrl9SW.js} +1 -1
  27. package/dist/assets/{scratchpad-panel-riBRfLg4.js → scratchpad-panel-Ddw1KV8R.js} +1 -1
  28. package/dist/assets/{session-panel-B2ux2cYO.js → session-panel-DTp3QX73.js} +1 -1
  29. package/dist/assets/{state-LRco7VGF.js → state-Nag9RDED.js} +1 -1
  30. package/dist/assets/{useNotebookActions-C2iPqhjB.js → useNotebookActions-CG6L_-9T.js} +1 -1
  31. package/dist/assets/{utils-BDlGlVyF.js → utils-DWFl4LEP.js} +3 -3
  32. package/dist/assets/{vega-component-DiFt6ZG4.js → vega-component-BAuy1PqH.js} +1 -1
  33. package/dist/index.html +9 -9
  34. package/package.json +1 -1
  35. package/src/__tests__/branded.ts +6 -0
  36. package/src/components/chat/chat-panel.tsx +2 -1
  37. package/src/components/data-table/TableBottomBar.tsx +12 -1
  38. package/src/components/data-table/TableTopBar.tsx +31 -35
  39. package/src/components/data-table/charts/charts.tsx +40 -11
  40. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +1 -1
  41. package/src/components/data-table/data-table.tsx +6 -1
  42. package/src/components/data-table/loading-table.tsx +4 -1
  43. package/src/components/data-table/range-focus/cell-selection-stats.tsx +3 -1
  44. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +1 -1
  45. package/src/components/data-table/table-explorer-panel/table-explorer-panel.tsx +2 -2
  46. package/src/components/editor/ai/add-cell-with-ai.tsx +2 -1
  47. package/src/components/editor/chrome/panels/context-aware-panel/context-aware-panel.tsx +1 -1
  48. package/src/components/editor/chrome/wrapper/footer-items/lsp-status.tsx +2 -1
  49. package/src/core/cells/__tests__/apply-transaction.test.ts +12 -11
  50. package/src/core/cells/document-changes.ts +9 -9
  51. package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -2
  52. package/src/core/codemirror/copilot/client.ts +9 -2
  53. package/src/core/codemirror/copilot/language-server.ts +11 -0
  54. package/src/core/codemirror/copilot/transport.ts +32 -6
  55. package/src/core/islands/__tests__/bridge.test.ts +20 -10
  56. package/src/core/network/__tests__/requests-lazy.test.ts +30 -14
  57. package/src/core/websocket/useMarimoKernelConnection.tsx +5 -11
  58. package/src/core/websocket/useWebSocket.tsx +3 -1
  59. package/src/css/app/Cell.css +22 -1
  60. package/src/css/table.css +17 -0
  61. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  62. package/src/plugins/impl/vega/__tests__/utils.test.ts +68 -0
  63. package/src/plugins/impl/vega/utils.ts +14 -5
  64. package/src/plugins/impl/vega/vega.css +2 -1
  65. package/src/utils/json/base64.ts +2 -5
  66. package/src/utils/time.ts +4 -2
  67. package/src/utils/typed.ts +2 -2
  68. package/dist/assets/ai-model-dropdown-Ck6NNlZH.js +0 -5
  69. package/dist/assets/chat-panel-CYkuZYzq.js +0 -3
  70. package/dist/assets/index-BFY3jw7I.css +0 -2
@@ -365,7 +365,7 @@ describe("LazyWebsocketTransport", () => {
365
365
  expect(delegate.connect).toHaveBeenCalled();
366
366
  });
367
367
 
368
- it("should clamp timeout to maxTimeoutMs", async () => {
368
+ it("should use maxTimeoutMs as default when no timeout is provided", async () => {
369
369
  const transport = new LazyWebsocketTransport({
370
370
  getWsUrl: mockGetWsUrl,
371
371
  waitForReady: mockWaitForReady,
@@ -376,12 +376,29 @@ describe("LazyWebsocketTransport", () => {
376
376
  await transport.connect();
377
377
 
378
378
  const data: any = { method: "test", params: [] };
379
- await transport.sendData(data, 10_000);
379
+ await transport.sendData(data, undefined);
380
380
 
381
381
  const delegate = (transport as any).delegate;
382
382
  expect(delegate.sendData).toHaveBeenCalledWith(data, 5000);
383
383
  });
384
384
 
385
+ it("should respect caller-provided timeout without clamping", async () => {
386
+ const transport = new LazyWebsocketTransport({
387
+ getWsUrl: mockGetWsUrl,
388
+ waitForReady: mockWaitForReady,
389
+ showError: mockShowError,
390
+ maxTimeoutMs: 5000,
391
+ });
392
+
393
+ await transport.connect();
394
+
395
+ const data: any = { method: "test", params: [] };
396
+ await transport.sendData(data, 30_000);
397
+
398
+ const delegate = (transport as any).delegate;
399
+ expect(delegate.sendData).toHaveBeenCalledWith(data, 30_000);
400
+ });
401
+
385
402
  it("should throw error if reconnection fails", async () => {
386
403
  const connectionError = new Error("Connection failed");
387
404
  (WebSocketTransport as any).mockImplementation(function (this: any) {
@@ -407,6 +424,115 @@ describe("LazyWebsocketTransport", () => {
407
424
  });
408
425
  });
409
426
 
427
+ describe("onReconnect", () => {
428
+ it("should call onReconnect after close + sendData reconnection", async () => {
429
+ const transport = new LazyWebsocketTransport({
430
+ getWsUrl: mockGetWsUrl,
431
+ waitForReady: mockWaitForReady,
432
+ showError: mockShowError,
433
+ });
434
+
435
+ const onReconnect = vi.fn().mockResolvedValue(undefined);
436
+ transport.onReconnect = onReconnect;
437
+
438
+ // Initial connect
439
+ await transport.connect();
440
+ expect(onReconnect).not.toHaveBeenCalled();
441
+
442
+ // Close the transport (simulates failure handling)
443
+ transport.close();
444
+
445
+ // sendData should reconnect and call onReconnect
446
+ const data: any = { method: "test", params: [] };
447
+ await transport.sendData(data, 5000);
448
+
449
+ expect(onReconnect).toHaveBeenCalledTimes(1);
450
+ });
451
+
452
+ it("should NOT call onReconnect on initial sendData connection", async () => {
453
+ const transport = new LazyWebsocketTransport({
454
+ getWsUrl: mockGetWsUrl,
455
+ waitForReady: mockWaitForReady,
456
+ showError: mockShowError,
457
+ });
458
+
459
+ const onReconnect = vi.fn().mockResolvedValue(undefined);
460
+ transport.onReconnect = onReconnect;
461
+
462
+ // sendData without prior connect — this is the initial connection
463
+ const data: any = { method: "test", params: [] };
464
+ await transport.sendData(data, 5000);
465
+
466
+ expect(onReconnect).not.toHaveBeenCalled();
467
+ });
468
+
469
+ it("should call onReconnect after tryConnect failure + retry via sendData", async () => {
470
+ let connectAttempt = 0;
471
+ (WebSocketTransport as any).mockImplementation(function (this: any) {
472
+ this.connect = vi.fn().mockImplementation(() => {
473
+ connectAttempt++;
474
+ // First 2 attempts fail (retries=2 exhausted), then succeed on next sendData
475
+ if (connectAttempt <= 2) {
476
+ return Promise.reject(new Error("Connection failed"));
477
+ }
478
+ return Promise.resolve(undefined);
479
+ });
480
+ this.close = vi.fn();
481
+ this.sendData = vi.fn().mockResolvedValue({ result: "success" });
482
+ this.subscribe = vi.fn();
483
+ this.unsubscribe = vi.fn();
484
+ });
485
+
486
+ const transport = new LazyWebsocketTransport({
487
+ getWsUrl: mockGetWsUrl,
488
+ waitForReady: mockWaitForReady,
489
+ showError: mockShowError,
490
+ retries: 2,
491
+ retryDelayMs: 10,
492
+ });
493
+
494
+ const onReconnect = vi.fn().mockResolvedValue(undefined);
495
+ transport.onReconnect = onReconnect;
496
+
497
+ // Initial connect fails (all retries exhausted)
498
+ await expect(transport.connect()).rejects.toThrow("Connection failed");
499
+ expect(onReconnect).not.toHaveBeenCalled();
500
+
501
+ // Now sendData reconnects successfully
502
+ const data: any = { method: "test", params: [] };
503
+ await transport.sendData(data, 5000);
504
+
505
+ expect(onReconnect).toHaveBeenCalledTimes(1);
506
+ });
507
+
508
+ it("should propagate onReconnect rejection and allow retry", async () => {
509
+ const transport = new LazyWebsocketTransport({
510
+ getWsUrl: mockGetWsUrl,
511
+ waitForReady: mockWaitForReady,
512
+ showError: mockShowError,
513
+ });
514
+
515
+ const reconnectError = new Error("Re-initialization failed");
516
+ const onReconnect = vi.fn().mockRejectedValueOnce(reconnectError);
517
+ transport.onReconnect = onReconnect;
518
+
519
+ await transport.connect();
520
+ transport.close();
521
+
522
+ const data: any = { method: "test", params: [] };
523
+ await expect(transport.sendData(data, 5000)).rejects.toThrow(
524
+ "Re-initialization failed",
525
+ );
526
+ expect(onReconnect).toHaveBeenCalledTimes(1);
527
+
528
+ // needsReInitialization should still be true, so a subsequent retry
529
+ // will attempt onReconnect again
530
+ onReconnect.mockResolvedValueOnce(undefined);
531
+ await transport.sendData(data, 5000);
532
+ expect(onReconnect).toHaveBeenCalledTimes(2);
533
+ });
534
+ });
535
+
410
536
  describe("close", () => {
411
537
  it("should close delegate and clear it", async () => {
412
538
  const transport = new LazyWebsocketTransport({
@@ -37,13 +37,20 @@ export const createWSTransport = once(() => {
37
37
  export const getCopilotClient = once(() => {
38
38
  const userConfig = store.get(resolvedMarimoConfigAtom);
39
39
  const copilotSettings = userConfig.ai?.github?.copilot_settings ?? {};
40
+ const transport = createWSTransport();
40
41
 
41
- return new CopilotLanguageServerClient({
42
+ const client = new CopilotLanguageServerClient({
42
43
  rootUri: FILE_URI,
43
44
  workspaceFolders: null,
44
- transport: createWSTransport(),
45
+ transport,
45
46
  copilotSettings,
46
47
  });
48
+
49
+ // Re-run the LSP initialize handshake when the transport reconnects
50
+ // after a close or connection failure.
51
+ transport.onReconnect = () => client.reInitialize();
52
+
53
+ return client;
47
54
  });
48
55
 
49
56
  export function copilotServer() {
@@ -86,6 +86,17 @@ export class CopilotLanguageServerClient extends LanguageServerClient {
86
86
  });
87
87
  }
88
88
 
89
+ /**
90
+ * Re-run the LSP initialize handshake and send configuration.
91
+ * Called by the transport's onReconnect callback after reconnecting.
92
+ */
93
+ async reInitialize(): Promise<void> {
94
+ logger.log("#reInitialize: Re-initializing LSP connection");
95
+ this.initializePromise = this.initialize();
96
+ await this.initializePromise;
97
+ await this.sendConfiguration();
98
+ }
99
+
89
100
  private async sendConfiguration() {
90
101
  const settings = this.copilotSettings;
91
102
  // Skip if no settings are provided
@@ -36,7 +36,8 @@ export interface LazyWebsocketTransportOptions {
36
36
  retryDelayMs?: number;
37
37
 
38
38
  /**
39
- * Maximum timeout for sendData operations in milliseconds.
39
+ * Default timeout for sendData operations in milliseconds.
40
+ * Used when the caller does not provide an explicit timeout.
40
41
  * @default 5000
41
42
  */
42
43
  maxTimeoutMs?: number;
@@ -60,6 +61,13 @@ export class LazyWebsocketTransport extends Transport {
60
61
  private delegate: WebSocketTransport | undefined;
61
62
  private pendingSubscriptions: Subscription[] = [];
62
63
  private readonly options: Required<LazyWebsocketTransportOptions>;
64
+ private needsReInitialization = false;
65
+
66
+ /**
67
+ * Callback invoked after the transport reconnects following a close or connection failure.
68
+ * Used by the LSP client to re-run the initialize handshake on the new connection.
69
+ */
70
+ onReconnect?: () => Promise<void>;
63
71
 
64
72
  constructor(options: LazyWebsocketTransportOptions) {
65
73
  super();
@@ -157,6 +165,7 @@ export class LazyWebsocketTransport extends Transport {
157
165
  );
158
166
  if (attempt === this.options.retries) {
159
167
  this.delegate = undefined;
168
+ this.needsReInitialization = true;
160
169
  // Show error toast on final retry
161
170
  this.options.showError(
162
171
  "GitHub Copilot Connection Error",
@@ -183,6 +192,7 @@ export class LazyWebsocketTransport extends Transport {
183
192
  override close(): void {
184
193
  this.delegate?.close();
185
194
  this.delegate = undefined;
195
+ this.needsReInitialization = true;
186
196
  }
187
197
 
188
198
  override async sendData(
@@ -202,6 +212,25 @@ export class LazyWebsocketTransport extends Transport {
202
212
  "Unable to connect to GitHub Copilot. Please check your settings and try again.",
203
213
  );
204
214
  }
215
+
216
+ // Re-run LSP initialization handshake after reconnecting
217
+ if (this.needsReInitialization && this.onReconnect) {
218
+ Logger.log(
219
+ "Copilot#sendData: Re-initializing LSP after reconnection...",
220
+ );
221
+ try {
222
+ await this.onReconnect();
223
+ this.needsReInitialization = false;
224
+ } catch (error) {
225
+ // Close the uninitialized connection so the next attempt starts fresh
226
+ this.close();
227
+ Logger.error(
228
+ "Copilot#sendData: LSP re-initialization after reconnection failed",
229
+ error,
230
+ );
231
+ throw error;
232
+ }
233
+ }
205
234
  }
206
235
 
207
236
  // After reconnection, delegate should be initialized
@@ -211,11 +240,8 @@ export class LazyWebsocketTransport extends Transport {
211
240
  );
212
241
  }
213
242
 
214
- // Clamp timeout to maxTimeoutMs
215
- timeout = Math.min(
216
- timeout ?? this.options.maxTimeoutMs,
217
- this.options.maxTimeoutMs,
218
- );
243
+ // Use maxTimeoutMs as default when no timeout is provided
244
+ timeout = timeout ?? this.options.maxTimeoutMs;
219
245
  return this.delegate.sendData(data, timeout);
220
246
  }
221
247
  }
@@ -1,5 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { components } from "@marimo-team/marimo-api";
2
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import {
6
+ cellId,
7
+ requestId,
8
+ uiElementId,
9
+ widgetModelId,
10
+ } from "@/__tests__/branded";
11
+
12
+ type Base64String = components["schemas"]["Base64String"];
3
13
 
4
14
  // Mock browser APIs before any imports
5
15
  vi.stubGlobal(
@@ -89,7 +99,7 @@ describe("IslandsPyodideBridge", () => {
89
99
  describe("sendComponentValues", () => {
90
100
  it("should include type field and token in control request", async () => {
91
101
  const request = {
92
- objectIds: ["Hbol-0"],
102
+ objectIds: [uiElementId("Hbol-0")],
93
103
  values: [58],
94
104
  };
95
105
 
@@ -108,7 +118,7 @@ describe("IslandsPyodideBridge", () => {
108
118
 
109
119
  it("should preserve all request properties", async () => {
110
120
  const request = {
111
- objectIds: ["slider-1", "slider-2"],
121
+ objectIds: [uiElementId("slider-1"), uiElementId("slider-2")],
112
122
  values: [10, 20],
113
123
  };
114
124
 
@@ -128,7 +138,7 @@ describe("IslandsPyodideBridge", () => {
128
138
  describe("sendFunctionRequest", () => {
129
139
  it("should include type field in control request", async () => {
130
140
  const request = {
131
- functionCallId: "call-123",
141
+ functionCallId: requestId("call-123"),
132
142
  namespace: "test_namespace",
133
143
  functionName: "my_function",
134
144
  args: { x: 1, y: 2 },
@@ -152,7 +162,7 @@ describe("IslandsPyodideBridge", () => {
152
162
  describe("sendRun", () => {
153
163
  it("should include type field in control request", async () => {
154
164
  const request = {
155
- cellIds: ["cell-1", "cell-2"],
165
+ cellIds: [cellId("cell-1"), cellId("cell-2")],
156
166
  codes: ["print('hello')", "print('world')"],
157
167
  };
158
168
 
@@ -170,7 +180,7 @@ describe("IslandsPyodideBridge", () => {
170
180
 
171
181
  it("should call loadPackages before putControlRequest", async () => {
172
182
  const request = {
173
- cellIds: ["cell-1"],
183
+ cellIds: [cellId("cell-1")],
174
184
  codes: ["import pandas"],
175
185
  };
176
186
 
@@ -190,13 +200,13 @@ describe("IslandsPyodideBridge", () => {
190
200
  describe("sendModelValue", () => {
191
201
  it("should include type field in control request", async () => {
192
202
  const request = {
193
- modelId: "widget-1",
203
+ modelId: widgetModelId("widget-1"),
194
204
  message: {
195
205
  method: "update" as const,
196
206
  state: { value: 42 },
197
207
  bufferPaths: [],
198
208
  },
199
- buffers: [],
209
+ buffers: [] as Base64String[],
200
210
  };
201
211
 
202
212
  await bridge.sendModelValue(request);
@@ -222,16 +232,16 @@ describe("IslandsPyodideBridge", () => {
222
232
  // Test all methods to ensure they include the type field
223
233
  await bridge.sendComponentValues({ objectIds: [], values: [] });
224
234
  await bridge.sendFunctionRequest({
225
- functionCallId: "",
235
+ functionCallId: requestId(""),
226
236
  namespace: "",
227
237
  functionName: "",
228
238
  args: {},
229
239
  });
230
240
  await bridge.sendRun({ cellIds: [], codes: [] });
231
241
  await bridge.sendModelValue({
232
- modelId: "",
242
+ modelId: widgetModelId(""),
233
243
  message: { method: "update", state: {}, bufferPaths: [] },
234
- buffers: [],
244
+ buffers: [] as Base64String[],
235
245
  });
236
246
 
237
247
  // All calls should have the type field
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { cellId, requestId, uiElementId } from "@/__tests__/branded";
4
5
  import type { RuntimeManager } from "../../runtime/runtime";
5
6
  import { createLazyRequests } from "../requests-lazy";
6
7
  import type { EditRequests, RunRequests } from "../types";
@@ -59,7 +60,7 @@ describe("createLazyRequests", () => {
59
60
  mockGetRuntimeManager,
60
61
  );
61
62
 
62
- await lazyRequests.sendRun({ cellIds: ["cell1"], codes: ["code"] });
63
+ await lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] });
63
64
 
64
65
  expect(mockInit).toHaveBeenCalledTimes(1);
65
66
  });
@@ -70,10 +71,13 @@ describe("createLazyRequests", () => {
70
71
  mockGetRuntimeManager,
71
72
  );
72
73
 
73
- await lazyRequests.sendRun({ cellIds: ["cell1"], codes: ["code"] });
74
- await lazyRequests.sendInstantiate({ objectIds: ["obj1"], values: [] });
74
+ await lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] });
75
+ await lazyRequests.sendInstantiate({
76
+ objectIds: [uiElementId("obj1")],
77
+ values: [],
78
+ });
75
79
  await lazyRequests.sendFunctionRequest({
76
- functionCallId: "func1",
80
+ functionCallId: requestId("func1"),
77
81
  functionName: "testFunc",
78
82
  args: {},
79
83
  namespace: "test",
@@ -89,7 +93,7 @@ describe("createLazyRequests", () => {
89
93
  mockGetRuntimeManager,
90
94
  );
91
95
 
92
- await lazyRequests.sendRun({ cellIds: ["cell1"], codes: ["code"] });
96
+ await lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] });
93
97
 
94
98
  expect(waitForConnectionOpen).toHaveBeenCalled();
95
99
  });
@@ -100,7 +104,7 @@ describe("createLazyRequests", () => {
100
104
  mockGetRuntimeManager,
101
105
  );
102
106
 
103
- const args = { cellIds: ["cell1"], codes: ["code"] };
107
+ const args = { cellIds: [cellId("cell1")], codes: ["code"] };
104
108
  await lazyRequests.sendRun(args);
105
109
 
106
110
  expect(mockDelegate.sendRun).toHaveBeenCalledWith(args);
@@ -113,7 +117,7 @@ describe("createLazyRequests", () => {
113
117
  );
114
118
 
115
119
  const result = await lazyRequests.sendFunctionRequest({
116
- functionCallId: "func1",
120
+ functionCallId: requestId("func1"),
117
121
  functionName: "testFunc",
118
122
  args: {},
119
123
  namespace: "test",
@@ -143,7 +147,7 @@ describe("createLazyRequests", () => {
143
147
  );
144
148
 
145
149
  await expect(
146
- lazyRequests.sendRun({ cellIds: ["cell1"], codes: ["code"] }),
150
+ lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] }),
147
151
  ).rejects.toThrow("Init failed");
148
152
  });
149
153
 
@@ -157,7 +161,7 @@ describe("createLazyRequests", () => {
157
161
  );
158
162
 
159
163
  await expect(
160
- lazyRequests.sendRun({ cellIds: ["cell1"], codes: ["code"] }),
164
+ lazyRequests.sendRun({ cellIds: [cellId("cell1")], codes: ["code"] }),
161
165
  ).rejects.toThrow("Request failed");
162
166
  });
163
167
 
@@ -169,7 +173,10 @@ describe("createLazyRequests", () => {
169
173
  );
170
174
 
171
175
  // First request with first runtime manager
172
- await lazyRequests.sendRun({ cellIds: ["cell1"], codes: ["code"] });
176
+ await lazyRequests.sendRun({
177
+ cellIds: [cellId("cell1")],
178
+ codes: ["code"],
179
+ });
173
180
  expect(mockInit).toHaveBeenCalledTimes(1);
174
181
 
175
182
  // Create a new runtime manager
@@ -187,7 +194,10 @@ describe("createLazyRequests", () => {
187
194
  );
188
195
 
189
196
  // Second request with second runtime manager
190
- await lazyRequests2.sendRun({ cellIds: ["cell2"], codes: ["code2"] });
197
+ await lazyRequests2.sendRun({
198
+ cellIds: [cellId("cell2")],
199
+ codes: ["code2"],
200
+ });
191
201
 
192
202
  // Both inits should have been called
193
203
  expect(mockInit).toHaveBeenCalledTimes(1);
@@ -201,9 +211,15 @@ describe("createLazyRequests", () => {
201
211
  );
202
212
 
203
213
  // Multiple requests
204
- await lazyRequests.sendRun({ cellIds: ["cell1"], codes: ["code"] });
205
- await lazyRequests.sendDeleteCell({ cellId: "cell2" });
206
- await lazyRequests.sendInstantiate({ objectIds: ["obj1"], values: [] });
214
+ await lazyRequests.sendRun({
215
+ cellIds: [cellId("cell1")],
216
+ codes: ["code"],
217
+ });
218
+ await lazyRequests.sendDeleteCell({ cellId: cellId("cell2") });
219
+ await lazyRequests.sendInstantiate({
220
+ objectIds: [uiElementId("obj1")],
221
+ values: [],
222
+ });
207
223
 
208
224
  // Init should only be called once
209
225
  expect(mockInit).toHaveBeenCalledTimes(1);
@@ -186,7 +186,7 @@ export function useMarimoKernelConnection(opts: {
186
186
  return;
187
187
 
188
188
  case "completion-result":
189
- AUTOCOMPLETER.resolve(msg.data.completion_id as RequestId, msg.data);
189
+ AUTOCOMPLETER.resolve(msg.data.completion_id, msg.data);
190
190
  return;
191
191
  case "function-call-result":
192
192
  FUNCTIONS_REGISTRY.resolve(msg.data.function_call_id, msg.data);
@@ -207,20 +207,14 @@ export function useMarimoKernelConnection(opts: {
207
207
  case "variables":
208
208
  setVariables(
209
209
  msg.data.variables.map((v) => ({
210
- name: v.name as VariableName,
210
+ name: v.name,
211
211
  declaredBy: v.declared_by,
212
212
  usedBy: v.used_by,
213
213
  })),
214
214
  );
215
- filterDatasetsFromVariables(
216
- msg.data.variables.map((v) => v.name as VariableName),
217
- );
218
- filterDataSourcesFromVariables(
219
- msg.data.variables.map((v) => v.name as VariableName),
220
- );
221
- filterStorageFromVariables(
222
- msg.data.variables.map((v) => v.name as VariableName),
223
- );
215
+ filterDatasetsFromVariables(msg.data.variables.map((v) => v.name));
216
+ filterDataSourcesFromVariables(msg.data.variables.map((v) => v.name));
217
+ filterStorageFromVariables(msg.data.variables.map((v) => v.name));
224
218
  return;
225
219
  case "variable-values":
226
220
  setMetadata(
@@ -30,6 +30,8 @@ function createConnectionTransport(
30
30
  // Create a connection transport using the ReconnectingWebSocket from partysocket
31
31
  // This handles reconnecting when the connection is lost.
32
32
  const urlProvider = options.url; // We don't call the URL provider now since it may change (i.e. if the runtime redirects)
33
+ // Cast needed: ReconnectingWebSocket types readyState as `number`
34
+ // but IConnectionTransport expects `0 | 1 | 2 | 3`
33
35
  return new ReconnectingWebSocket(urlProvider, undefined, {
34
36
  // We don't want Infinity retries
35
37
  maxRetries: 10,
@@ -38,7 +40,7 @@ function createConnectionTransport(
38
40
  // long timeout -- the server can become slow when many notebooks
39
41
  // are open.
40
42
  connectionTimeout: 10_000,
41
- });
43
+ }) as unknown as IConnectionTransport;
42
44
  }
43
45
 
44
46
  /**
@@ -98,9 +98,30 @@
98
98
 
99
99
  /* Special case for particular components */
100
100
 
101
- .output-area:has(> .output > marimo-ui-element > marimo-table) {
101
+ .output-area:has(
102
+ > .output:only-child > marimo-ui-element:only-child > marimo-table
103
+ ) {
104
+ padding: 0 0 5px;
102
105
  max-height: none;
103
106
  overflow: hidden;
107
+
108
+ /* Flush table: remove border and configure edge padding via CSS variable */
109
+ --marimo-table-edge-padding: 0.75rem;
110
+
111
+ marimo-table::part(table-tabs) {
112
+ margin-top: 0.25rem;
113
+ border: none;
114
+ border-radius: 0;
115
+ }
116
+
117
+ marimo-table::part(table-wrapper) {
118
+ border: none;
119
+ border-radius: 0;
120
+ }
121
+
122
+ marimo-table::part(table-footer) {
123
+ padding-inline: 0.25rem;
124
+ }
104
125
  }
105
126
 
106
127
  & > :first-child {
package/src/css/table.css CHANGED
@@ -1,5 +1,22 @@
1
1
  @reference "../css/globals.css";
2
2
 
3
+ /* Edge padding for flush tables (--marimo-table-edge-padding inherits through shadow DOM) */
4
+ [part="table-wrapper"] th:first-child {
5
+ padding-left: var(--marimo-table-edge-padding, 0.5rem);
6
+ }
7
+
8
+ [part="table-wrapper"] th:last-child {
9
+ padding-right: var(--marimo-table-edge-padding, 0.5rem);
10
+ }
11
+
12
+ [part="table-wrapper"] td:first-child {
13
+ padding-left: var(--marimo-table-edge-padding, 0.375rem);
14
+ }
15
+
16
+ [part="table-wrapper"] td:last-child {
17
+ padding-right: var(--marimo-table-edge-padding, 0.375rem);
18
+ }
19
+
3
20
  .markdown table,
4
21
  table.dataframe {
5
22
  display: block;
@@ -763,6 +763,7 @@ export const LoadingDataTableComponent = memo(
763
763
  {props.showChartBuilder ? (
764
764
  <TablePanel
765
765
  displayHeader={displayHeader}
766
+ onCloseChartBuilder={() => setDisplayHeader(false)}
766
767
  data={data?.rows || []}
767
768
  columns={props.totalColumns}
768
769
  totalRows={props.totalRows}
@@ -33,4 +33,72 @@ describe("getContainerWidth", () => {
33
33
  it("should return undefined when width is explicitly undefined", () => {
34
34
  expect(getContainerWidth({ width: undefined })).toBeUndefined();
35
35
  });
36
+
37
+ it("should find width in nested facet spec", () => {
38
+ expect(
39
+ getContainerWidth({
40
+ $schema: "https://vega.github.io/schema/vega-lite/v6.json",
41
+ facet: { column: { field: "Origin", type: "nominal" } },
42
+ spec: {
43
+ mark: "point",
44
+ encoding: {},
45
+ width: "container",
46
+ },
47
+ }),
48
+ ).toBe("container");
49
+ });
50
+
51
+ it("should find width in nested repeat spec", () => {
52
+ expect(
53
+ getContainerWidth({
54
+ $schema: "https://vega.github.io/schema/vega-lite/v6.json",
55
+ repeat: { row: ["A", "B"] },
56
+ spec: {
57
+ mark: "point",
58
+ encoding: {},
59
+ width: "container",
60
+ },
61
+ }),
62
+ ).toBe("container");
63
+ });
64
+
65
+ it("should return undefined for nested spec without width", () => {
66
+ expect(
67
+ getContainerWidth({
68
+ facet: { column: { field: "Origin" } },
69
+ spec: { mark: "point", encoding: {} },
70
+ }),
71
+ ).toBeUndefined();
72
+ });
73
+
74
+ it("should return undefined for hconcat (width on sub-specs)", () => {
75
+ expect(
76
+ getContainerWidth({
77
+ hconcat: [{ width: "container" }, { width: "container" }],
78
+ }),
79
+ ).toBeUndefined();
80
+ });
81
+
82
+ it("should return undefined for vconcat (width on sub-specs)", () => {
83
+ expect(
84
+ getContainerWidth({
85
+ vconcat: [{ width: "container" }, { width: "container" }],
86
+ }),
87
+ ).toBeUndefined();
88
+ });
89
+
90
+ it("should return undefined for compiled Vega spec (width as signal)", () => {
91
+ expect(
92
+ getContainerWidth({
93
+ $schema: "https://vega.github.io/schema/vega/v6.json",
94
+ autosize: { contains: "padding", type: "fit-x" },
95
+ signals: [
96
+ {
97
+ name: "width",
98
+ init: "isFinite(containerSize()[0]) ? containerSize()[0] : 300",
99
+ },
100
+ ],
101
+ }),
102
+ ).toBeUndefined();
103
+ });
36
104
  });