@marimo-team/islands 0.23.9-dev9 → 0.23.9

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 (101) hide show
  1. package/dist/{ConnectedDataExplorerComponent-OzrfMM5L.js → ConnectedDataExplorerComponent-CyV83R2m.js} +4 -4
  2. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +1 -0
  3. package/dist/assets/{worker-CpBbwbQo.js → worker-ip3AI_sN.js} +2 -2
  4. package/dist/{chat-ui-BDI3FMI8.js → chat-ui-ChD4VvCo.js} +3060 -3033
  5. package/dist/{code-visibility-DgHF4q8X.js → code-visibility-BkuwTYAm.js} +1368 -1204
  6. package/dist/{formats-DQ5qjo_Q.js → formats-DHxc-FdY.js} +1 -1
  7. package/dist/{glide-data-editor-DqRY9naW.js → glide-data-editor-BOmK9ETQ.js} +2 -2
  8. package/dist/{html-to-image-CiSinpSR.js → html-to-image-BHv7CEU_.js} +2145 -2153
  9. package/dist/{input-CZD2z6X2.js → input-_2sjvfne.js} +1 -1
  10. package/dist/main.js +680 -705
  11. package/dist/{mermaid-IU93XzmY.js → mermaid-lXOw5Py9.js} +2 -2
  12. package/dist/{process-output-5qJjMRKh.js → process-output-BvySRgli.js} +33 -25
  13. package/dist/{reveal-component-qpHJES_u.js → reveal-component-DeBkkDcg.js} +312 -291
  14. package/dist/{spec-a6DaqW__.js → spec-B96zNUEA.js} +1 -1
  15. package/dist/style.css +1 -1
  16. package/dist/{toDate-ZVVIBmdk.js → toDate-x-WRDCH7.js} +1 -1
  17. package/dist/{useAsyncData-C008zUPi.js → useAsyncData-iRgKDT5s.js} +1 -1
  18. package/dist/{useDeepCompareMemoize-BrA3_n61.js → useDeepCompareMemoize-CkQ57VS2.js} +1 -1
  19. package/dist/{useLifecycle-BNaoJ5a4.js → useLifecycle-BBO9PIph.js} +1 -1
  20. package/dist/{useTheme-7O0YWlE5.js → useTheme-DHIrRQOe.js} +34 -21
  21. package/dist/{vega-component-DJNmOdUj.js → vega-component-Dq-SH463.js} +5 -5
  22. package/package.json +1 -1
  23. package/src/components/ai/__tests__/ai-utils.test.ts +43 -38
  24. package/src/components/ai/ai-model-dropdown.tsx +2 -2
  25. package/src/components/app-config/ai-config.tsx +147 -16
  26. package/src/components/app-config/user-config-form.tsx +37 -1
  27. package/src/components/chat/__tests__/chat-utils.test.ts +269 -0
  28. package/src/components/chat/chat-panel.tsx +38 -5
  29. package/src/components/chat/chat-utils.ts +14 -58
  30. package/src/components/data-table/TableBottomBar.tsx +5 -8
  31. package/src/components/data-table/__tests__/column-explorer.test.tsx +128 -0
  32. package/src/components/data-table/__tests__/header-items.test.tsx +220 -10
  33. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +95 -29
  34. package/src/components/data-table/column-header.tsx +17 -12
  35. package/src/components/data-table/data-table.tsx +4 -0
  36. package/src/components/data-table/export-actions.tsx +19 -12
  37. package/src/components/data-table/header-items.tsx +40 -16
  38. package/src/components/data-table/hooks/use-column-visibility.ts +14 -0
  39. package/src/components/data-table/schemas.ts +2 -2
  40. package/src/components/data-table/table-explorer-panel/table-explorer-panel.tsx +16 -6
  41. package/src/components/databases/display.tsx +2 -0
  42. package/src/components/datasources/__tests__/utils.test.ts +82 -0
  43. package/src/components/datasources/utils.ts +16 -15
  44. package/src/components/editor/Disconnected.tsx +1 -60
  45. package/src/components/editor/__tests__/viewer-banner.test.tsx +89 -0
  46. package/src/components/editor/actions/pair-with-agent-modal.tsx +1 -0
  47. package/src/components/editor/actions/useCellActionButton.tsx +3 -3
  48. package/src/components/editor/actions/useNotebookActions.tsx +5 -2
  49. package/src/components/editor/cell/code/cell-editor.tsx +25 -5
  50. package/src/components/editor/chrome/types.ts +13 -6
  51. package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
  52. package/src/components/editor/chrome/wrapper/footer-items/ai-status.tsx +10 -1
  53. package/src/components/editor/chrome/wrapper/sidebar.tsx +7 -5
  54. package/src/components/editor/errors/auto-fix.tsx +3 -3
  55. package/src/components/editor/header/__tests__/status.test.tsx +0 -15
  56. package/src/components/editor/header/app-header.tsx +1 -4
  57. package/src/components/editor/header/status.tsx +4 -13
  58. package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -0
  59. package/src/components/editor/navigation/navigation.ts +5 -0
  60. package/src/components/editor/output/MarimoErrorOutput.tsx +103 -25
  61. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -39
  62. package/src/components/editor/renderers/cell-array.tsx +27 -24
  63. package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +30 -17
  64. package/src/components/editor/renderers/slides-layout/compute-slide-cells.ts +17 -8
  65. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +10 -12
  66. package/src/components/editor/viewer-banner.tsx +82 -0
  67. package/src/components/slides/minimap.tsx +45 -9
  68. package/src/components/slides/reveal-component.tsx +82 -37
  69. package/src/components/slides/slide-cell-view.tsx +12 -1
  70. package/src/components/slides/slide-form.tsx +11 -3
  71. package/src/components/static-html/static-banner.tsx +28 -22
  72. package/src/core/ai/__tests__/model-registry.test.ts +72 -60
  73. package/src/core/ai/model-registry.ts +33 -28
  74. package/src/core/cells/__tests__/actions.test.ts +48 -0
  75. package/src/core/cells/actions.ts +5 -6
  76. package/src/core/codemirror/__tests__/setup.test.ts +29 -0
  77. package/src/core/codemirror/cells/traceback-decorations.ts +1 -1
  78. package/src/core/codemirror/cm.ts +50 -3
  79. package/src/core/codemirror/completion/hints.ts +4 -1
  80. package/src/core/codemirror/format.ts +1 -0
  81. package/src/core/codemirror/keymaps/vim.ts +63 -0
  82. package/src/core/codemirror/language/languages/sql/sql.ts +1 -0
  83. package/src/core/codemirror/language/languages/sql/utils.ts +2 -0
  84. package/src/core/config/__tests__/config-schema.test.ts +4 -0
  85. package/src/core/config/config-schema.ts +4 -0
  86. package/src/core/config/config.ts +16 -0
  87. package/src/core/edit-app.tsx +3 -0
  88. package/src/core/islands/bootstrap.ts +2 -0
  89. package/src/core/kernel/__tests__/handlers.test.ts +5 -0
  90. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +0 -13
  91. package/src/core/websocket/types.ts +0 -6
  92. package/src/core/websocket/useMarimoKernelConnection.tsx +3 -12
  93. package/src/css/app/Cell.css +0 -1
  94. package/src/plugins/impl/DataTablePlugin.tsx +48 -22
  95. package/src/plugins/impl/chat/ChatPlugin.tsx +7 -1
  96. package/src/plugins/impl/chat/__tests__/chat-ui.test.ts +278 -0
  97. package/src/plugins/impl/chat/chat-ui.tsx +106 -59
  98. package/src/plugins/impl/chat/types.ts +5 -0
  99. package/src/utils/__tests__/json-parser.test.ts +1 -69
  100. package/src/utils/json/json-parser.ts +0 -30
  101. package/dist/assets/__vite-browser-external-CAdMKBac.js +0 -1
@@ -29,10 +29,13 @@ import { formattingExample } from "./column-formatting/feature";
29
29
  import { formatOptions } from "./column-formatting/types";
30
30
  import { NAMELESS_COLUMN_PREFIX } from "./columns";
31
31
 
32
- export function renderFormatOptions<TData, TValue>(
33
- column: Column<TData, TValue>,
34
- locale: string,
35
- ) {
32
+ export function FormatOptions<TData, TValue>({
33
+ column,
34
+ locale,
35
+ }: {
36
+ column: Column<TData, TValue>;
37
+ locale: string;
38
+ }) {
36
39
  const dataType: DataType | undefined = column.columnDef.meta?.dataType;
37
40
  const columnFormatOptions = dataType ? formatOptions[dataType] : [];
38
41
 
@@ -83,9 +86,11 @@ export function renderFormatOptions<TData, TValue>(
83
86
  );
84
87
  }
85
88
 
86
- export function renderColumnWrapping<TData, TValue>(
87
- column: Column<TData, TValue>,
88
- ) {
89
+ export function ColumnWrapping<TData, TValue>({
90
+ column,
91
+ }: {
92
+ column: Column<TData, TValue>;
93
+ }) {
89
94
  if (!column.getCanWrap?.() || !column.getColumnWrapping) {
90
95
  return null;
91
96
  }
@@ -108,9 +113,11 @@ export function renderColumnWrapping<TData, TValue>(
108
113
  );
109
114
  }
110
115
 
111
- export function renderColumnPinning<TData, TValue>(
112
- column: Column<TData, TValue>,
113
- ) {
116
+ export function ColumnPinning<TData, TValue>({
117
+ column,
118
+ }: {
119
+ column: Column<TData, TValue>;
120
+ }) {
114
121
  if (!column.getCanPin?.() || !column.getIsPinned) {
115
122
  return null;
116
123
  }
@@ -157,7 +164,11 @@ export function HideColumn<TData, TValue>({
157
164
  );
158
165
  }
159
166
 
160
- export function renderCopyColumn<TData, TValue>(column: Column<TData, TValue>) {
167
+ export function CopyColumn<TData, TValue>({
168
+ column,
169
+ }: {
170
+ column: Column<TData, TValue>;
171
+ }) {
161
172
  if (!column.getCanCopy?.()) {
162
173
  return null;
163
174
  }
@@ -177,10 +188,19 @@ export function renderCopyColumn<TData, TValue>(column: Column<TData, TValue>) {
177
188
  const AscIcon = ArrowUpNarrowWideIcon;
178
189
  const DescIcon = ArrowDownWideNarrowIcon;
179
190
 
180
- export function renderSorts<TData, TValue>(
181
- column: Column<TData, TValue>,
182
- table?: Table<TData>,
183
- ) {
191
+ /**
192
+ * `table` is optional: it is only needed to detect multi-column sorting and
193
+ * offer "Clear all sorts". Call sites that build their header inside column
194
+ * definitions (where the table instance isn't yet in scope) omit it and fall
195
+ * back to single-column "Clear sort".
196
+ */
197
+ export function Sorts<TData, TValue>({
198
+ column,
199
+ table,
200
+ }: {
201
+ column: Column<TData, TValue>;
202
+ table?: Table<TData>;
203
+ }) {
184
204
  if (!column.getCanSort()) {
185
205
  return null;
186
206
  }
@@ -271,7 +291,11 @@ export function renderSortIcon<TData, TValue>(column: Column<TData, TValue>) {
271
291
  return <Icon className="h-3 w-3" />;
272
292
  }
273
293
 
274
- export function renderDataType<TData, TValue>(column: Column<TData, TValue>) {
294
+ export function DataType<TData, TValue>({
295
+ column,
296
+ }: {
297
+ column: Column<TData, TValue>;
298
+ }) {
275
299
  const dtype: string | undefined = column.columnDef.meta?.dtype;
276
300
  if (!dtype) {
277
301
  return null;
@@ -40,3 +40,17 @@ export function getUserColumnVisibilityCounts<TData>(
40
40
  hidden: userColumns.length - visible,
41
41
  };
42
42
  }
43
+
44
+ // When columns are clipped server-side, the TanStack instance only holds the
45
+ // rendered subset, so visible/hidden math must use that subset's total. The
46
+ // dataset-wide value is still correct for the no-hidden "N columns" label.
47
+ export function getColumnCountForDisplay<TData>(
48
+ table: Table<TData>,
49
+ datasetTotalColumns: number,
50
+ ): { totalColumns: number; hiddenColumns: number } {
51
+ const counts = getUserColumnVisibilityCounts(table);
52
+ return {
53
+ totalColumns: counts.hidden > 0 ? counts.total : datasetTotalColumns,
54
+ hiddenColumns: counts.hidden,
55
+ };
56
+ }
@@ -4,7 +4,7 @@ import z from "zod";
4
4
  import { rpc } from "@/plugins/core/rpc";
5
5
 
6
6
  export type DownloadAsArgs = (req: {
7
- format: "csv" | "json" | "parquet";
7
+ format: "csv" | "json" | "parquet" | "tsv";
8
8
  }) => Promise<{
9
9
  url: string;
10
10
  filename: string;
@@ -15,7 +15,7 @@ export type DownloadAsArgs = (req: {
15
15
  export const DownloadAsSchema = rpc
16
16
  .input(
17
17
  z.object({
18
- format: z.enum(["csv", "json", "parquet"]),
18
+ format: z.enum(["csv", "json", "parquet", "tsv"]),
19
19
  }),
20
20
  )
21
21
  .output(
@@ -1,8 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
+ "use no memo";
3
+
4
+ // tanstack/table is not compatible with React compiler
5
+ // https://github.com/TanStack/table/issues/5567
2
6
 
3
7
  import { Fill } from "@marimo-team/react-slotz";
4
- import type { OnChangeFn, RowSelectionState } from "@tanstack/react-table";
5
- import type React from "react";
8
+ import type {
9
+ OnChangeFn,
10
+ RowSelectionState,
11
+ Table,
12
+ } from "@tanstack/react-table";
6
13
  import { Button } from "@/components/ui/button";
7
14
  import { Tabs, TabsContent } from "@/components/ui/tabs";
8
15
  import { SlotNames } from "@/core/slots/slots";
@@ -19,7 +26,7 @@ import { ColumnExplorerPanel } from "../column-explorer-panel/column-explorer";
19
26
  import { RowViewerPanel } from "../row-viewer-panel/row-viewer";
20
27
  import type { FieldTypesWithExternalType, TooManyRows } from "../types";
21
28
 
22
- export interface TableExplorerPanelProps {
29
+ export interface TableExplorerPanelProps<TData> {
23
30
  // Row viewer props
24
31
  rowIdx: number;
25
32
  setRowIdx: (rowIdx: number) => void;
@@ -33,6 +40,7 @@ export interface TableExplorerPanelProps {
33
40
  previewColumn?: PreviewColumn;
34
41
  totalColumns: number;
35
42
  tableId: string;
43
+ table: Table<TData>;
36
44
  // Visibility flags
37
45
  showRowExplorer: boolean;
38
46
  showColumnExplorer: boolean;
@@ -46,7 +54,7 @@ const tabTriggerClassName =
46
54
  const activeClassName = "text-primary";
47
55
  const inactiveClassName = "hover:text-foreground";
48
56
 
49
- export const TableExplorerPanel: React.FC<TableExplorerPanelProps> = ({
57
+ export function TableExplorerPanel<TData>({
50
58
  // Row viewer
51
59
  rowIdx,
52
60
  setRowIdx,
@@ -60,13 +68,14 @@ export const TableExplorerPanel: React.FC<TableExplorerPanelProps> = ({
60
68
  previewColumn,
61
69
  totalColumns,
62
70
  tableId,
71
+ table,
63
72
  // Visibility
64
73
  showRowExplorer,
65
74
  showColumnExplorer,
66
75
  // Tab state
67
76
  activeTab,
68
77
  onTabChange,
69
- }) => {
78
+ }: TableExplorerPanelProps<TData>) {
70
79
  const showTabs = showRowExplorer && showColumnExplorer;
71
80
 
72
81
  const rowViewer = (
@@ -89,6 +98,7 @@ export const TableExplorerPanel: React.FC<TableExplorerPanelProps> = ({
89
98
  totalRows={totalRows}
90
99
  totalColumns={totalColumns}
91
100
  tableId={tableId}
101
+ table={table}
92
102
  />
93
103
  );
94
104
 
@@ -158,4 +168,4 @@ export const TableExplorerPanel: React.FC<TableExplorerPanelProps> = ({
158
168
  </TabsContent>
159
169
  </Tabs>
160
170
  );
161
- };
171
+ }
@@ -53,6 +53,8 @@ export function dbDisplayName(name: string) {
53
53
  return "MongoDB";
54
54
  case "iceberg":
55
55
  return "Apache Iceberg";
56
+ case "dremio":
57
+ return "Dremio";
56
58
  default:
57
59
  return name;
58
60
  }
@@ -313,6 +313,26 @@ describe("sqlCode", () => {
313
313
  );
314
314
  });
315
315
 
316
+ it("should preserve dots inside quoted schema names", () => {
317
+ const sqlTableContext: SQLTableContext = {
318
+ engine: "postgres",
319
+ schema: "analytics.events",
320
+ defaultSchema: "public",
321
+ defaultDatabase: "mydb",
322
+ database: "remote",
323
+ dialect: "postgres",
324
+ };
325
+
326
+ const result = sqlCode({
327
+ table: mockTable,
328
+ columnName: mockColumn.name,
329
+ sqlTableContext,
330
+ });
331
+ expect(result).toBe(
332
+ '_df = mo.sql(f"""\nSELECT "email" FROM "remote"."analytics.events"."users" LIMIT 100\n""", engine=postgres)',
333
+ );
334
+ });
335
+
316
336
  it("should not quote * column name", () => {
317
337
  const sqlTableContext: SQLTableContext = {
318
338
  engine: "postgres",
@@ -334,6 +354,68 @@ describe("sqlCode", () => {
334
354
  });
335
355
  });
336
356
 
357
+ describe("Dremio dialect", () => {
358
+ it("should quote reserved column names and table path parts", () => {
359
+ const sqlTableContext: SQLTableContext = {
360
+ engine: "dremio_conn",
361
+ schema: "operations",
362
+ defaultSchema: "",
363
+ defaultDatabase: "",
364
+ database: "lakehouse",
365
+ dialect: "dremio",
366
+ };
367
+
368
+ const result = sqlCode({
369
+ table: { ...mockTable, name: "shipments" as const },
370
+ columnName: "order",
371
+ sqlTableContext,
372
+ });
373
+ expect(result).toBe(
374
+ '_df = mo.sql(f"""\nSELECT "order" FROM "lakehouse"."operations"."shipments" LIMIT 100\n""", engine=dremio_conn)',
375
+ );
376
+ });
377
+
378
+ it("should not quote * column name", () => {
379
+ const sqlTableContext: SQLTableContext = {
380
+ engine: "dremio_conn",
381
+ schema: "operations",
382
+ defaultSchema: "",
383
+ defaultDatabase: "",
384
+ database: "lakehouse",
385
+ dialect: "dremio",
386
+ };
387
+
388
+ const result = sqlCode({
389
+ table: { ...mockTable, name: "customers" as const },
390
+ columnName: "*",
391
+ sqlTableContext,
392
+ });
393
+ expect(result).toBe(
394
+ '_df = mo.sql(f"""\nSELECT * FROM "lakehouse"."operations"."customers" LIMIT 100\n""", engine=dremio_conn)',
395
+ );
396
+ });
397
+
398
+ it("should preserve dots inside quoted schema names", () => {
399
+ const sqlTableContext: SQLTableContext = {
400
+ engine: "dremio_conn",
401
+ schema: "samples.dremio.com",
402
+ defaultSchema: "",
403
+ defaultDatabase: "",
404
+ database: "Samples",
405
+ dialect: "dremio",
406
+ };
407
+
408
+ const result = sqlCode({
409
+ table: { ...mockTable, name: "airlines" as const },
410
+ columnName: "*",
411
+ sqlTableContext,
412
+ });
413
+ expect(result).toBe(
414
+ '_df = mo.sql(f"""\nSELECT * FROM "Samples"."samples.dremio.com"."airlines" LIMIT 100\n""", engine=dremio_conn)',
415
+ );
416
+ });
417
+ });
418
+
337
419
  describe("fallback behavior", () => {
338
420
  it("should use default formatter for unknown dialect", () => {
339
421
  const sqlTableContext: SQLTableContext = {
@@ -15,9 +15,9 @@ export function isSchemaless(schemaName: string) {
15
15
 
16
16
  interface SqlCodeFormatter {
17
17
  /**
18
- * Format the table name based on dialect-specific rules
18
+ * Format the table path based on dialect-specific rules
19
19
  */
20
- formatTableName: (tableName: string) => string;
20
+ formatTablePath: (tablePath: string[]) => string;
21
21
  /**
22
22
  * Format the SELECT clause
23
23
  */
@@ -25,7 +25,7 @@ interface SqlCodeFormatter {
25
25
  }
26
26
 
27
27
  const defaultFormatter: SqlCodeFormatter = {
28
- formatTableName: (tableName: string) => tableName,
28
+ formatTablePath: (tablePath: string[]) => tablePath.join("."),
29
29
  formatSelectClause: (columnName: string, tableName: string) =>
30
30
  `SELECT ${columnName} FROM ${tableName} LIMIT 100`,
31
31
  };
@@ -41,7 +41,8 @@ function getFormatter(dialect: string): SqlCodeFormatter {
41
41
  const quote = BigQueryDialect.spec.identifierQuotes;
42
42
  return {
43
43
  // BigQuery uses backticks for identifiers
44
- formatTableName: (tableName: string) => `${quote}${tableName}${quote}`,
44
+ formatTablePath: (tablePath: string[]) =>
45
+ `${quote}${tablePath.join(".")}${quote}`,
45
46
  formatSelectClause: defaultFormatter.formatSelectClause,
46
47
  };
47
48
  }
@@ -49,7 +50,7 @@ function getFormatter(dialect: string): SqlCodeFormatter {
49
50
  case "sqlserver":
50
51
  case "microsoft sql server":
51
52
  return {
52
- formatTableName: defaultFormatter.formatTableName,
53
+ formatTablePath: defaultFormatter.formatTablePath,
53
54
  formatSelectClause: (columnName: string, tableName: string) =>
54
55
  `SELECT TOP 100 ${columnName} FROM ${tableName}`,
55
56
  };
@@ -57,12 +58,11 @@ function getFormatter(dialect: string): SqlCodeFormatter {
57
58
  case "postgres":
58
59
  case "postgresql":
59
60
  case "duckdb":
61
+ case "dremio":
60
62
  // Quote column and table names to avoid raising errors on weird characters
61
63
  return {
62
- formatTableName: (tableName: string) => {
63
- const parts = tableName.split(".");
64
- return parts.map((part) => `"${part}"`).join(".");
65
- },
64
+ formatTablePath: (tablePath: string[]) =>
65
+ tablePath.map((part) => `"${part}"`).join("."),
66
66
  formatSelectClause: (columnName: string, tableName: string) =>
67
67
  `SELECT ${columnName === "*" ? "*" : `"${columnName}"`} FROM ${tableName} LIMIT 100`,
68
68
  };
@@ -114,26 +114,27 @@ export function sqlCode({
114
114
  database,
115
115
  dialect,
116
116
  } = sqlTableContext;
117
- let tableName = table.name;
117
+ const tablePath = [table.name];
118
118
 
119
119
  // Set the fully qualified table name based on schema and database
120
120
  if (isSchemaless(schema)) {
121
- tableName =
122
- database === defaultDatabase ? tableName : `${database}.${tableName}`;
121
+ if (database !== defaultDatabase) {
122
+ tablePath.unshift(database);
123
+ }
123
124
  } else {
124
125
  // Include schema if it's not the default schema
125
126
  if (schema !== defaultSchema) {
126
- tableName = `${schema}.${tableName}`;
127
+ tablePath.unshift(schema);
127
128
  }
128
129
 
129
130
  // Include database if it's not the default database
130
131
  if (database !== defaultDatabase) {
131
- tableName = `${database}.${tableName}`;
132
+ tablePath.unshift(database);
132
133
  }
133
134
  }
134
135
 
135
136
  const formatter = getFormatter(dialect);
136
- const formattedTableName = formatter.formatTableName(tableName);
137
+ const formattedTableName = formatter.formatTablePath(tablePath);
137
138
  const selectClause = formatter.formatSelectClause(
138
139
  columnName,
139
140
  formattedTableName,
@@ -1,69 +1,10 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { ArrowRightSquareIcon } from "lucide-react";
4
- import { API } from "@/core/network/api";
5
- import { Banner } from "@/plugins/impl/common/error-banner";
6
- import { prettyError } from "@/utils/errors";
7
- import { reloadSafe } from "@/utils/reload-safe";
8
- import { Button } from "../ui/button";
9
- import { toast } from "../ui/use-toast";
10
-
11
3
  interface DisconnectedProps {
12
4
  reason: string;
13
- canTakeover: boolean | undefined;
14
5
  }
15
6
 
16
- export const Disconnected = ({
17
- reason,
18
- canTakeover = false,
19
- }: DisconnectedProps) => {
20
- const handleTakeover = async () => {
21
- try {
22
- const searchParams = new URL(window.location.href).searchParams;
23
- await API.post(`/kernel/takeover?${searchParams.toString()}`, {});
24
-
25
- // Refresh the page to reconnect
26
- reloadSafe();
27
- } catch (error) {
28
- toast({
29
- title: "Failed to take over session",
30
- description: prettyError(error),
31
- variant: "danger",
32
- });
33
- }
34
- };
35
-
36
- if (canTakeover) {
37
- return (
38
- <div className="flex justify-center">
39
- <Banner
40
- kind="info"
41
- className="mt-10 flex flex-col rounded p-3 max-w-[800px] mx-4"
42
- >
43
- <div className="flex justify-between">
44
- <span className="font-bold text-xl flex items-center mb-2">
45
- Notebook already connected
46
- </span>
47
- </div>
48
- <div className="flex justify-between items-end text-base gap-20">
49
- <span>{reason}</span>
50
- {canTakeover && (
51
- <Button
52
- onClick={handleTakeover}
53
- variant="outline"
54
- data-testid="takeover-button"
55
- className="shrink-0"
56
- >
57
- <ArrowRightSquareIcon className="w-4 h-4 mr-2" />
58
- Take over session
59
- </Button>
60
- )}
61
- </div>
62
- </Banner>
63
- </div>
64
- );
65
- }
66
-
7
+ export const Disconnected = ({ reason }: DisconnectedProps) => {
67
8
  return (
68
9
  <div className="font-mono text-center text-base text-(--red-11)">
69
10
  <p>{reason}</p>
@@ -0,0 +1,89 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { render, screen } from "@testing-library/react";
3
+ import { createStore, Provider } from "jotai";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { TooltipProvider } from "@/components/ui/tooltip";
6
+ import { layoutStateAtom } from "@/core/layout/layout";
7
+ import { kioskModeAtom, viewStateAtom } from "@/core/mode";
8
+ import { API } from "@/core/network/api";
9
+ import { ViewerBanner } from "../viewer-banner";
10
+
11
+ describe("ViewerBanner", () => {
12
+ it("renders nothing when not in kiosk mode", () => {
13
+ const store = createStore();
14
+ store.set(kioskModeAtom, false);
15
+ const { container } = render(
16
+ <Provider store={store}>
17
+ <TooltipProvider>
18
+ <ViewerBanner />
19
+ </TooltipProvider>
20
+ </Provider>,
21
+ );
22
+ expect(container).toBeEmptyDOMElement();
23
+ });
24
+
25
+ it("renders nothing for an intentional kiosk client (?kiosk=true)", () => {
26
+ const store = createStore();
27
+ store.set(kioskModeAtom, true);
28
+ window.history.pushState({}, "", "/?kiosk=true");
29
+ try {
30
+ const { container } = render(
31
+ <Provider store={store}>
32
+ <TooltipProvider>
33
+ <ViewerBanner />
34
+ </TooltipProvider>
35
+ </Provider>,
36
+ );
37
+ expect(container).toBeEmptyDOMElement();
38
+ } finally {
39
+ window.history.pushState({}, "", "/");
40
+ }
41
+ });
42
+
43
+ it("renders nothing in a non-vertical layout (grid/slides)", () => {
44
+ const store = createStore();
45
+ store.set(kioskModeAtom, true);
46
+ store.set(layoutStateAtom, { selectedLayout: "grid", layoutData: {} });
47
+ const { container } = render(
48
+ <Provider store={store}>
49
+ <TooltipProvider>
50
+ <ViewerBanner />
51
+ </TooltipProvider>
52
+ </Provider>,
53
+ );
54
+ expect(container).toBeEmptyDOMElement();
55
+ });
56
+
57
+ it("renders nothing in present mode", () => {
58
+ const store = createStore();
59
+ store.set(kioskModeAtom, true);
60
+ store.set(viewStateAtom, { mode: "present", cellAnchor: null });
61
+ const { container } = render(
62
+ <Provider store={store}>
63
+ <TooltipProvider>
64
+ <ViewerBanner />
65
+ </TooltipProvider>
66
+ </Provider>,
67
+ );
68
+ expect(container).toBeEmptyDOMElement();
69
+ });
70
+
71
+ it("shows take over and posts without reload when viewing", () => {
72
+ const store = createStore();
73
+ store.set(kioskModeAtom, true);
74
+ const post = vi.spyOn(API, "post").mockResolvedValue({} as never);
75
+ render(
76
+ <Provider store={store}>
77
+ <TooltipProvider>
78
+ <ViewerBanner />
79
+ </TooltipProvider>
80
+ </Provider>,
81
+ );
82
+ const button = screen.getByTestId("takeover-button");
83
+ button.click();
84
+ expect(post).toHaveBeenCalledWith(
85
+ expect.stringContaining("/kernel/takeover"),
86
+ {},
87
+ );
88
+ });
89
+ });
@@ -113,6 +113,7 @@ export const PairWithAgentModal: React.FC<{
113
113
  className="underline"
114
114
  >
115
115
  Learn more
116
+ <span className="sr-only"> about pairing marimo with an agent</span>
116
117
  </a>
117
118
  .
118
119
  </DialogDescription>
@@ -48,7 +48,7 @@ import {
48
48
  import { switchLanguage } from "@/core/codemirror/language/extension";
49
49
  import { MARKDOWN_INITIAL_HIDE_CODE } from "@/core/codemirror/language/languages/markdown";
50
50
  import {
51
- aiEnabledAtom,
51
+ aiFeaturesEnabledAtom,
52
52
  appWidthAtom,
53
53
  autoInstantiateAtom,
54
54
  } from "@/core/config/config";
@@ -100,7 +100,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
100
100
  const deleteCell = useDeleteCellCallback();
101
101
  const { openModal } = useImperativeModal();
102
102
  const setAiCompletionCell = useSetAtom(aiCompletionCellAtom);
103
- const aiEnabled = useAtomValue(aiEnabledAtom);
103
+ const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom);
104
104
  const autoInstantiate = useAtomValue(autoInstantiateAtom);
105
105
  const kioskMode = useAtomValue(kioskModeAtom);
106
106
  const appWidth = useAtomValue(appWidthAtom);
@@ -162,7 +162,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
162
162
  {
163
163
  icon: <SparklesIcon size={13} strokeWidth={1.5} />,
164
164
  label: "Refactor with AI",
165
- hidden: !aiEnabled,
165
+ hidden: !aiFeaturesEnabled,
166
166
  handle: () => {
167
167
  setAiCompletionCell((current) =>
168
168
  current?.cellId === cellId ? null : { cellId },
@@ -142,9 +142,10 @@ export function useNotebookActions() {
142
142
  const { selectedLayout } = useLayoutState();
143
143
  const { setLayoutView } = useLayoutActions();
144
144
  const togglePresenting = useTogglePresenting();
145
- // Fallback: if sharing is undefined, both are enabled by default
145
+ // Fallback: if sharing is undefined, all options are enabled by default
146
146
  const sharingHtmlEnabled = resolvedConfig.sharing?.html ?? true;
147
147
  const sharingWasmEnabled = resolvedConfig.sharing?.wasm ?? true;
148
+ const sharingMolabEnabled = resolvedConfig.sharing?.molab ?? true;
148
149
 
149
150
  // Server-side PDF export is always available outside WASM.
150
151
  // Browser print fallback is used in WASM.
@@ -360,7 +361,8 @@ export function useNotebookActions() {
360
361
  icon: <Share2Icon size={14} strokeWidth={1.5} />,
361
362
  label: "Share",
362
363
  handle: NOOP_HANDLER,
363
- hidden: !sharingHtmlEnabled && !sharingWasmEnabled,
364
+ hidden:
365
+ !sharingHtmlEnabled && !sharingWasmEnabled && !sharingMolabEnabled,
364
366
  dropdown: [
365
367
  {
366
368
  icon: <GlobeIcon size={14} strokeWidth={1.5} />,
@@ -387,6 +389,7 @@ export function useNotebookActions() {
387
389
  {
388
390
  icon: <MarimoPlusIcon size={14} strokeWidth={1.5} />,
389
391
  label: "Create molab notebook",
392
+ hidden: !sharingMolabEnabled,
390
393
  handle: async () => {
391
394
  const code = await readCode();
392
395
  const url = createShareableLink({