@marimo-team/islands 0.19.5-dev9 → 0.19.6-dev0

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 (39) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DjQ_E5BA.js → ConnectedDataExplorerComponent-Dr9GhOQj.js} +10 -10
  2. package/dist/main.js +269 -225
  3. package/dist/style.css +1 -1
  4. package/package.json +1 -1
  5. package/src/__mocks__/requests.ts +1 -0
  6. package/src/components/ai/__tests__/ai-utils.test.ts +276 -0
  7. package/src/components/ai/ai-utils.ts +101 -0
  8. package/src/components/app-config/ai-config.tsx +56 -16
  9. package/src/components/app-config/user-config-form.tsx +63 -1
  10. package/src/components/chat/chat-panel.tsx +4 -4
  11. package/src/components/data-table/cell-utils.ts +10 -0
  12. package/src/components/editor/Output.tsx +21 -14
  13. package/src/components/editor/actions/useCellActionButton.tsx +2 -2
  14. package/src/components/editor/actions/useNotebookActions.tsx +61 -21
  15. package/src/components/editor/cell/cell-actions.tsx +6 -1
  16. package/src/components/editor/controls/Controls.tsx +1 -8
  17. package/src/components/editor/navigation/navigation.ts +39 -1
  18. package/src/components/editor/renderMimeIcon.tsx +2 -0
  19. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
  20. package/src/core/ai/model-registry.ts +21 -3
  21. package/src/core/codemirror/language/panel/panel.tsx +3 -0
  22. package/src/core/codemirror/language/panel/sql.tsx +6 -2
  23. package/src/core/config/feature-flag.tsx +2 -0
  24. package/src/core/export/__tests__/hooks.test.ts +120 -1
  25. package/src/core/export/hooks.ts +48 -18
  26. package/src/core/islands/bridge.ts +1 -0
  27. package/src/core/lsp/__tests__/transport.test.ts +149 -0
  28. package/src/core/lsp/transport.ts +48 -0
  29. package/src/core/network/requests-lazy.ts +1 -0
  30. package/src/core/network/requests-network.ts +9 -0
  31. package/src/core/network/requests-static.ts +1 -0
  32. package/src/core/network/requests-toasting.tsx +1 -0
  33. package/src/core/network/types.ts +2 -0
  34. package/src/core/wasm/bridge.ts +1 -0
  35. package/src/plugins/impl/data-explorer/ConnectedDataExplorerComponent.tsx +1 -1
  36. package/src/utils/__tests__/download.test.tsx +492 -0
  37. package/src/utils/download.ts +161 -6
  38. package/src/utils/filenames.ts +3 -0
  39. package/src/components/export/export-output-button.tsx +0 -14
@@ -262,7 +262,7 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
262
262
 
263
263
  return (
264
264
  <TooltipProvider>
265
- <div className="px-3 py-2 border-t border-border/20 flex flex-row items-center justify-between">
265
+ <div className="px-3 py-2 border-t border-border/20 flex flex-row flex-wrap items-center justify-between gap-1">
266
266
  <div className="flex items-center gap-2">
267
267
  <FeatureFlagged feature="chat_modes">
268
268
  <Select value={currentMode} onValueChange={saveModeChange}>
@@ -426,14 +426,14 @@ const ChatInput: React.FC<ChatInputProps> = memo(
426
426
  ChatInput.displayName = "ChatInput";
427
427
 
428
428
  const ChatPanel = () => {
429
- const aiEnabled = useAtomValue(aiEnabledAtom);
429
+ const aiConfigured = useAtomValue(aiEnabledAtom);
430
430
  const { handleClick } = useOpenSettingsToTab();
431
431
 
432
- if (!aiEnabled) {
432
+ if (!aiConfigured) {
433
433
  return (
434
434
  <PanelEmptyState
435
435
  title="Chat with AI"
436
- description="AI is currently disabled. Add your API key to enable."
436
+ description="No AI provider configured or model selected"
437
437
  action={
438
438
  <Button variant="outline" size="sm" onClick={() => handleClick("ai")}>
439
439
  Edit AI settings
@@ -1,5 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type { CellId } from "@/core/cells/ids";
4
+
3
5
  export const DATA_CELL_ID = "data-cell-id";
4
6
 
5
7
  export function getCellDomProps(cellId: string) {
@@ -7,3 +9,11 @@ export function getCellDomProps(cellId: string) {
7
9
  [DATA_CELL_ID]: cellId,
8
10
  };
9
11
  }
12
+
13
+ export const DATA_FOR_CELL_ID = "data-for-cell-id";
14
+
15
+ export function getCellForDomProps(cellId: CellId) {
16
+ return {
17
+ [DATA_FOR_CELL_ID]: cellId,
18
+ };
19
+ }
@@ -20,6 +20,7 @@ import { TextOutput } from "./output/TextOutput";
20
20
  import { VideoOutput } from "./output/VideoOutput";
21
21
 
22
22
  import "./output/Outputs.css";
23
+ import { useAtomValue } from "jotai";
23
24
  import {
24
25
  ChevronsDownUpIcon,
25
26
  ChevronsUpDownIcon,
@@ -27,6 +28,7 @@ import {
27
28
  } from "lucide-react";
28
29
  import { tooltipHandler } from "@/components/charts/tooltip";
29
30
  import { useExpandedOutput } from "@/core/cells/outputs";
31
+ import { viewStateAtom } from "@/core/mode";
30
32
  import { useIframeCapabilities } from "@/hooks/useIframeCapabilities";
31
33
  import { renderHTML } from "@/plugins/core/RenderHTML";
32
34
  import { Banner } from "@/plugins/impl/common/error-banner";
@@ -46,10 +48,9 @@ import { renderMimeIcon } from "./renderMimeIcon";
46
48
 
47
49
  const METADATA_KEY = "__metadata__";
48
50
 
49
- type MimeBundleWithoutMetadata = Record<
50
- OutputMessage["mimetype"],
51
- { [key: string]: unknown }
52
- >;
51
+ export type MimeType = OutputMessage["mimetype"];
52
+
53
+ type MimeBundleWithoutMetadata = Record<MimeType, { [key: string]: unknown }>;
53
54
 
54
55
  type MimeBundle = MimeBundleWithoutMetadata & {
55
56
  [METADATA_KEY]?: Record<string, { width?: number; height?: number }>;
@@ -70,7 +71,7 @@ export const OutputRenderer: React.FC<{
70
71
  onRefactorWithAI?: OnRefactorWithAI;
71
72
  wrapText?: boolean;
72
73
  metadata?: { width?: number; height?: number };
73
- renderFallback?: (mimetype: OutputMessage["mimetype"]) => React.ReactNode;
74
+ renderFallback?: (mimetype: MimeType) => React.ReactNode;
74
75
  }> = memo((props) => {
75
76
  const {
76
77
  message,
@@ -90,6 +91,8 @@ export const OutputRenderer: React.FC<{
90
91
  case "application/vnd.marimo+mimebundle":
91
92
  case "application/vnd.vegalite.v5+json":
92
93
  case "application/vnd.vega.v5+json":
94
+ case "application/vnd.vegalite.v6+json":
95
+ case "application/vnd.vega.v6+json":
93
96
  return typeof data === "string" ? JSON.parse(data) : data;
94
97
  default:
95
98
  return;
@@ -199,6 +202,8 @@ export const OutputRenderer: React.FC<{
199
202
  );
200
203
  case "application/vnd.vegalite.v5+json":
201
204
  case "application/vnd.vega.v5+json":
205
+ case "application/vnd.vegalite.v6+json":
206
+ case "application/vnd.vega.v6+json":
202
207
  return (
203
208
  <Suspense fallback={<ChartLoadingState />}>
204
209
  <LazyVegaEmbed
@@ -216,9 +221,7 @@ export const OutputRenderer: React.FC<{
216
221
  return (
217
222
  <MimeBundleOutputRenderer
218
223
  channel={channel}
219
- data={
220
- parsedJsonData as Record<OutputMessage["mimetype"], OutputMessage>
221
- }
224
+ data={parsedJsonData as Record<MimeType, OutputMessage>}
222
225
  />
223
226
  );
224
227
  case "application/vnd.jupyter.widget-view+json":
@@ -258,6 +261,8 @@ const MimeBundleOutputRenderer: React.FC<{
258
261
  cellId?: CellId;
259
262
  }> = memo(({ data, channel, cellId }) => {
260
263
  const mimebundle = Array.isArray(data) ? data[0] : data;
264
+ const { mode } = useAtomValue(viewStateAtom);
265
+ const appView = mode === "present" || mode === "read";
261
266
 
262
267
  // Extract metadata if present (e.g., for retina image rendering)
263
268
  const metadata = mimebundle[METADATA_KEY];
@@ -265,10 +270,7 @@ const MimeBundleOutputRenderer: React.FC<{
265
270
  // Filter out metadata from the mime entries and type narrow
266
271
  const mimeEntries = Objects.entries(mimebundle as Record<string, unknown>)
267
272
  .filter(([key]) => key !== METADATA_KEY)
268
- .map(
269
- ([mime, data]) =>
270
- [mime, data] as [OutputMessage["mimetype"], CellOutput["data"]],
271
- );
273
+ .map(([mime, data]) => [mime, data] as [MimeType, CellOutput["data"]]);
272
274
 
273
275
  // If there is none, return null
274
276
  const first = mimeEntries[0]?.[0];
@@ -302,7 +304,12 @@ const MimeBundleOutputRenderer: React.FC<{
302
304
  return (
303
305
  <Tabs defaultValue={first} orientation="vertical">
304
306
  <div className="flex">
305
- <TabsList className="self-start max-h-none flex flex-col gap-2 mr-4 shrink-0">
307
+ <TabsList
308
+ className={cn(
309
+ "self-start max-h-none flex flex-col gap-2 mr-3 shrink-0",
310
+ appView && "mt-4",
311
+ )}
312
+ >
306
313
  {mimeEntries.map(([mime]) => (
307
314
  <TabsTrigger
308
315
  key={mime}
@@ -315,7 +322,7 @@ const MimeBundleOutputRenderer: React.FC<{
315
322
  </TabsTrigger>
316
323
  ))}
317
324
  </TabsList>
318
- <div className="flex-1">
325
+ <div className="flex-1 w-full">
319
326
  {mimeEntries.map(([mime, output]) => (
320
327
  <TabsContent key={mime} value={mime}>
321
328
  <ErrorBoundary>
@@ -26,7 +26,6 @@ import {
26
26
  ZapIcon,
27
27
  ZapOffIcon,
28
28
  } from "lucide-react";
29
- import { downloadCellOutput } from "@/components/export/export-output-button";
30
29
  import { MultiIcon } from "@/components/icons/multi-icon";
31
30
  import { useImperativeModal } from "@/components/modal/ImperativeModal";
32
31
  import {
@@ -54,6 +53,7 @@ import { useRequestClient } from "@/core/network/requests";
54
53
  import type { CellConfig, RuntimeState } from "@/core/network/types";
55
54
  import { canLinkToCell, createCellLink } from "@/utils/cell-urls";
56
55
  import { copyToClipboard } from "@/utils/copy";
56
+ import { downloadCellOutputAsImage } from "@/utils/download";
57
57
  import { MarkdownIcon, PythonIcon } from "../cell/code/icons";
58
58
  import { useDeleteCellCallback } from "../cell/useDeleteCell";
59
59
  import { useRunCell } from "../cell/useRunCells";
@@ -341,7 +341,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
341
341
  icon: <ImageIcon size={13} strokeWidth={1.5} />,
342
342
  label: "Export output as PNG",
343
343
  hidden: !hasOutput,
344
- handle: () => downloadCellOutput(cellId),
344
+ handle: () => downloadCellOutputAsImage(cellId, "result"),
345
345
  },
346
346
  {
347
347
  icon: <XCircleIcon size={13} strokeWidth={1.5} />,
@@ -54,7 +54,12 @@ import {
54
54
  } from "@/core/cells/cells";
55
55
  import { disabledCellIds } from "@/core/cells/utils";
56
56
  import { useResolvedMarimoConfig } from "@/core/config/config";
57
+ import { getFeatureFlag } from "@/core/config/feature-flag";
57
58
  import { Constants } from "@/core/constants";
59
+ import {
60
+ updateCellOutputsWithScreenshots,
61
+ useEnrichCellOutputs,
62
+ } from "@/core/export/hooks";
58
63
  import { useLayoutActions, useLayoutState } from "@/core/layout/layout";
59
64
  import { useTogglePresenting } from "@/core/layout/useTogglePresenting";
60
65
  import { kioskModeAtom, viewStateAtom } from "@/core/mode";
@@ -64,7 +69,12 @@ import { downloadAsHTML } from "@/core/static/download-html";
64
69
  import { createShareableLink } from "@/core/wasm/share";
65
70
  import { isWasm } from "@/core/wasm/utils";
66
71
  import { copyToClipboard } from "@/utils/copy";
67
- import { downloadBlob, downloadHTMLAsImage } from "@/utils/download";
72
+ import {
73
+ downloadAsPDF,
74
+ downloadBlob,
75
+ downloadHTMLAsImage,
76
+ withLoadingToast,
77
+ } from "@/utils/download";
68
78
  import { Filenames } from "@/utils/filenames";
69
79
  import { Objects } from "@/utils/objects";
70
80
  import { newNotebookURL } from "@/utils/urls";
@@ -110,18 +120,26 @@ export function useNotebookActions() {
110
120
  const setCommandPaletteOpen = useSetAtom(commandPaletteAtom);
111
121
  const setSettingsDialogOpen = useSetAtom(settingDialogAtom);
112
122
  const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
113
- const { exportAsMarkdown, readCode, saveCellConfig } = useRequestClient();
123
+ const { exportAsMarkdown, readCode, saveCellConfig, updateCellOutputs } =
124
+ useRequestClient();
114
125
 
115
126
  const hasDisabledCells = useAtomValue(hasDisabledCellsAtom);
116
127
  const canUndoDeletes = useAtomValue(canUndoDeletesAtom);
117
128
  const { selectedLayout } = useLayoutState();
118
129
  const { setLayoutView } = useLayoutActions();
119
130
  const togglePresenting = useTogglePresenting();
131
+ const takeScreenshots = useEnrichCellOutputs();
120
132
 
121
133
  // Fallback: if sharing is undefined, both are enabled by default
122
134
  const sharingHtmlEnabled = resolvedConfig.sharing?.html ?? true;
123
135
  const sharingWasmEnabled = resolvedConfig.sharing?.wasm ?? true;
124
136
 
137
+ const isServerSidePdfExportEnabled = getFeatureFlag("server_side_pdf_export");
138
+ // With server side pdf export, it doesn't matter what mode we are in,
139
+ // Default export uses browser print, which is better in present mode
140
+ const pdfDownloadEnabled =
141
+ isServerSidePdfExportEnabled || viewState.mode === "present";
142
+
125
143
  const renderCheckboxElement = (checked: boolean) => (
126
144
  <div className="w-8 flex justify-end">
127
145
  {checked && <CheckIcon size={14} />}
@@ -139,11 +157,7 @@ export function useNotebookActions() {
139
157
  label: "Download as HTML",
140
158
  handle: async () => {
141
159
  if (!filename) {
142
- toast({
143
- variant: "danger",
144
- title: "Error",
145
- description: "Notebooks must be named to be exported.",
146
- });
160
+ toastNotebookMustBeNamed();
147
161
  return;
148
162
  }
149
163
  await downloadAsHTML({ filename, includeCode: true });
@@ -154,11 +168,7 @@ export function useNotebookActions() {
154
168
  label: "Download as HTML (exclude code)",
155
169
  handle: async () => {
156
170
  if (!filename) {
157
- toast({
158
- variant: "danger",
159
- title: "Error",
160
- description: "Notebooks must be named to be exported.",
161
- });
171
+ toastNotebookMustBeNamed();
162
172
  return;
163
173
  }
164
174
  await downloadAsHTML({ filename, includeCode: false });
@@ -205,21 +215,43 @@ export function useNotebookActions() {
205
215
  if (!app) {
206
216
  return;
207
217
  }
208
- await downloadHTMLAsImage(app, document.title);
218
+ await downloadHTMLAsImage({
219
+ element: app,
220
+ filename: document.title,
221
+ });
209
222
  },
210
223
  },
211
224
  {
212
225
  icon: <FileIcon size={14} strokeWidth={1.5} />,
213
226
  label: "Download as PDF",
214
- disabled: viewState.mode !== "present",
215
- tooltip:
216
- viewState.mode === "present" ? undefined : (
217
- <span>
218
- Only available in app view. <br />
219
- Toggle with: {renderShortcut("global.hideCode", false)}
220
- </span>
221
- ),
227
+ disabled: !pdfDownloadEnabled,
228
+ tooltip: pdfDownloadEnabled ? undefined : (
229
+ <span>
230
+ Only available in app view. <br />
231
+ Toggle with: {renderShortcut("global.hideCode", false)}
232
+ </span>
233
+ ),
222
234
  handle: async () => {
235
+ if (isServerSidePdfExportEnabled) {
236
+ if (!filename) {
237
+ toastNotebookMustBeNamed();
238
+ return;
239
+ }
240
+
241
+ const downloadPDF = async () => {
242
+ await updateCellOutputsWithScreenshots(
243
+ takeScreenshots,
244
+ updateCellOutputs,
245
+ );
246
+ await downloadAsPDF({
247
+ filename: filename,
248
+ webpdf: false,
249
+ });
250
+ };
251
+ await withLoadingToast("Downloading PDF...", downloadPDF);
252
+ return;
253
+ }
254
+
223
255
  const beforeprint = new Event("export-beforeprint");
224
256
  const afterprint = new Event("export-afterprint");
225
257
  function print() {
@@ -527,3 +559,11 @@ export function useNotebookActions() {
527
559
  return action;
528
560
  });
529
561
  }
562
+
563
+ function toastNotebookMustBeNamed() {
564
+ toast({
565
+ title: "Error",
566
+ description: "Notebooks must be named to be exported.",
567
+ variant: "danger",
568
+ });
569
+ }
@@ -12,6 +12,7 @@ import React, {
12
12
  useState,
13
13
  } from "react";
14
14
  import useEvent from "react-use-event-hook";
15
+ import { getCellForDomProps } from "@/components/data-table/cell-utils";
15
16
  import {
16
17
  renderMinimalShortcut,
17
18
  renderShortcut,
@@ -104,7 +105,11 @@ const CellActionsDropdownInternal = (
104
105
  {...restoreFocus}
105
106
  >
106
107
  <Command>
107
- <CommandInput placeholder="Search actions..." className="h-6 m-1" />
108
+ <CommandInput
109
+ placeholder="Search actions..."
110
+ className="h-6 m-1"
111
+ {...getCellForDomProps(props.cellId)}
112
+ />
108
113
  <CommandList>
109
114
  <CommandEmpty>No results</CommandEmpty>
110
115
  {actions.map((group, i) => (
@@ -54,9 +54,7 @@ export const Controls = ({
54
54
  onRun,
55
55
  connectionState,
56
56
  running,
57
- appConfig,
58
57
  }: ControlsProps): JSX.Element => {
59
- const appWidth = appConfig.width;
60
58
  const undoAvailable = useAtomValue(canUndoDeletesAtom);
61
59
  const needsRun = useAtomValue(needsRunAtom);
62
60
  const { undoDeleteCell } = useCellActions();
@@ -103,12 +101,7 @@ export const Controls = ({
103
101
  </div>
104
102
  )}
105
103
 
106
- <div
107
- className={cn(
108
- bottomRightControls,
109
- appWidth === "compact" && "xl:flex-row items-end",
110
- )}
111
- >
104
+ <div className={cn(bottomRightControls)}>
112
105
  <HideInKioskMode>
113
106
  <SaveComponent kioskMode={false} />
114
107
  </HideInKioskMode>
@@ -10,6 +10,7 @@ import {
10
10
  import { useAtomValue, useSetAtom, useStore } from "jotai";
11
11
  import { useMemo } from "react";
12
12
  import { mergeProps, useFocusWithin, useKeyboard } from "react-aria";
13
+ import { DATA_FOR_CELL_ID } from "@/components/data-table/cell-utils";
13
14
  import { aiCompletionCellAtom } from "@/core/ai/state";
14
15
  import { cellIdsAtom, notebookAtom, useCellActions } from "@/core/cells/cells";
15
16
  import { useCellFocusActions } from "@/core/cells/focus";
@@ -101,7 +102,23 @@ function useCellFocusProps(
101
102
  // On focus, set the last focused cell id.
102
103
  focusActions.focusCell({ cellId });
103
104
  },
104
- onBlurWithin: () => {
105
+ onBlurWithin: (e) => {
106
+ // Check if blur is happening because of vim search panel interaction
107
+ if (isInVimPanel(e.relatedTarget) || isInVimPanel(e.target)) {
108
+ // Don't hide code if we're just interacting with vim search
109
+ return;
110
+ }
111
+
112
+ // If the related target is for a cell id (data-for-cell-id), then we don't want to hide the code, otherwise it might
113
+ // close the dropdown.
114
+ if (
115
+ getDataForCellId(e.relatedTarget) === cellId ||
116
+ getDataForCellId(e.relatedTarget?.closest(`[${DATA_FOR_CELL_ID}]`)) ===
117
+ cellId
118
+ ) {
119
+ return;
120
+ }
121
+
105
122
  // On blur, hide the code if it was temporarily shown.
106
123
  temporarilyShownCodeActions.remove(cellId);
107
124
  actions.markTouched({ cellId });
@@ -116,6 +133,27 @@ function useCellFocusProps(
116
133
  return focusWithinProps;
117
134
  }
118
135
 
136
+ function isInVimPanel(element: Element | null): boolean {
137
+ if (!element) {
138
+ return false;
139
+ }
140
+ if (element instanceof HTMLElement) {
141
+ return element.closest(".cm-vim-panel") !== null;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ function getDataForCellId(element: Element | null | undefined): CellId | null {
147
+ if (!element) {
148
+ return null;
149
+ }
150
+ const cellId = element.getAttribute(DATA_FOR_CELL_ID);
151
+ if (!cellId) {
152
+ return null;
153
+ }
154
+ return cellId as CellId;
155
+ }
156
+
119
157
  type KeymapHandlers = Record<string, () => boolean>;
120
158
 
121
159
  /**
@@ -29,6 +29,8 @@ export function renderMimeIcon(mime: string) {
29
29
  return "📝";
30
30
  case "application/vnd.vegalite.v5+json":
31
31
  case "application/vnd.vega.v5+json":
32
+ case "application/vnd.vegalite.v6+json":
33
+ case "application/vnd.vega.v6+json":
32
34
  return "📊";
33
35
  case "application/vnd.marimo+mimebundle":
34
36
  return "📦";
@@ -185,7 +185,7 @@ const ActionButtons: React.FC<{
185
185
  if (!app) {
186
186
  return;
187
187
  }
188
- await downloadHTMLAsImage(app, document.title);
188
+ await downloadHTMLAsImage({ element: app, filename: document.title });
189
189
  };
190
190
 
191
191
  const handleDownloadAsHTML = async () => {
@@ -21,8 +21,17 @@ export interface AiModel extends AiModelType {
21
21
  custom: boolean;
22
22
  }
23
23
 
24
- const getKnownModelMap = once((): ReadonlyMap<QualifiedModelId, AiModel> => {
24
+ interface KnownModelMaps {
25
+ /** Map of qualified model ID to model info */
26
+ modelMap: ReadonlyMap<QualifiedModelId, AiModel>;
27
+ /** Map of provider ID to first default model (supports chat or edit) */
28
+ defaultModelByProvider: ReadonlyMap<ProviderId, QualifiedModelId>;
29
+ }
30
+
31
+ export const getKnownModelMaps = once((): KnownModelMaps => {
25
32
  const modelMap = new Map<QualifiedModelId, AiModel>();
33
+ const defaultModelByProvider = new Map<ProviderId, QualifiedModelId>();
34
+
26
35
  for (const model of models) {
27
36
  const modelId = model.model as ShortModelId;
28
37
  const modelInfo: AiModel = {
@@ -33,12 +42,21 @@ const getKnownModelMap = once((): ReadonlyMap<QualifiedModelId, AiModel> => {
33
42
  custom: false,
34
43
  };
35
44
 
45
+ const supportsChatOrEdit =
46
+ modelInfo.roles.includes("chat") || modelInfo.roles.includes("edit");
47
+
36
48
  for (const provider of modelInfo.providers) {
37
49
  const qualifiedModelId: QualifiedModelId = `${provider}/${modelId}`;
38
50
  modelMap.set(qualifiedModelId, modelInfo);
51
+
52
+ // Track first model per provider that supports chat or edit
53
+ if (supportsChatOrEdit && !defaultModelByProvider.has(provider)) {
54
+ defaultModelByProvider.set(provider, qualifiedModelId);
55
+ }
39
56
  }
40
57
  }
41
- return modelMap;
58
+
59
+ return { modelMap, defaultModelByProvider };
42
60
  });
43
61
 
44
62
  const getProviderMap = once(
@@ -125,7 +143,7 @@ export class AiModelRegistry {
125
143
  }) {
126
144
  const { displayedModels, customModels } = opts;
127
145
  const hasDisplayedModels = displayedModels.size > 0;
128
- const knownModelMap = getKnownModelMap();
146
+ const knownModelMap = getKnownModelMaps().modelMap;
129
147
  const customModelsMap = new Map<QualifiedModelId, AiModel>();
130
148
 
131
149
  let modelsMap = new Map<QualifiedModelId, AiModel>();
@@ -8,6 +8,7 @@ import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
8
8
  import { normalizeName } from "@/core/cells/names";
9
9
  import { type ConnectionName, DUCKDB_ENGINE } from "@/core/datasets/engines";
10
10
  import { useAutoGrowInputProps } from "@/hooks/useAutoGrowInputProps";
11
+ import { cellIdState } from "../../cells/state";
11
12
  import { formatSQL } from "../../format";
12
13
  import { languageAdapterState } from "../extension";
13
14
  import { MarkdownLanguageAdapter } from "../languages/markdown";
@@ -31,6 +32,7 @@ export const LanguagePanelComponent: React.FC<{
31
32
  }> = ({ view }) => {
32
33
  const { spanProps, inputProps } = useAutoGrowInputProps({ minWidth: 50 });
33
34
  const languageAdapter = view.state.field(languageAdapterState);
35
+ const cellId = view.state.facet(cellIdState);
34
36
 
35
37
  let actions: React.ReactNode = <div />;
36
38
  let showDivider = false;
@@ -93,6 +95,7 @@ export const LanguagePanelComponent: React.FC<{
93
95
  <SQLEngineSelect
94
96
  selectedEngine={metadata.engine}
95
97
  onChange={switchEngine}
98
+ cellId={cellId}
96
99
  />
97
100
  <div className="flex items-center gap-2 ml-auto">
98
101
  {metadata.engine === DUCKDB_ENGINE && <SQLModeSelect />}
@@ -8,6 +8,7 @@ import {
8
8
  DatabaseBackup,
9
9
  SearchCheck,
10
10
  } from "lucide-react";
11
+ import { getCellForDomProps } from "@/components/data-table/cell-utils";
11
12
  import { transformDisplayName } from "@/components/databases/display";
12
13
  import { DatabaseLogo } from "@/components/databases/icon";
13
14
  import { Button } from "@/components/ui/button";
@@ -22,6 +23,7 @@ import {
22
23
  SelectValue,
23
24
  } from "@/components/ui/select";
24
25
  import { Tooltip } from "@/components/ui/tooltip";
26
+ import type { CellId } from "@/core/cells/ids";
25
27
  import {
26
28
  dataConnectionsMapAtom,
27
29
  setLatestEngineSelected,
@@ -38,11 +40,13 @@ import { type SQLMode, useSQLMode } from "../languages/sql/sql-mode";
38
40
  interface SelectProps {
39
41
  selectedEngine: ConnectionName;
40
42
  onChange: (engine: ConnectionName) => void;
43
+ cellId: CellId;
41
44
  }
42
45
 
43
46
  export const SQLEngineSelect: React.FC<SelectProps> = ({
44
47
  selectedEngine,
45
48
  onChange,
49
+ cellId,
46
50
  }) => {
47
51
  const connectionsMap = useAtomValue(dataConnectionsMapAtom);
48
52
 
@@ -98,10 +102,10 @@ export const SQLEngineSelect: React.FC<SelectProps> = ({
98
102
  return (
99
103
  <div className="flex flex-row gap-1 items-center">
100
104
  <Select value={selectedEngine} onValueChange={handleSelectEngine}>
101
- <SQLSelectTrigger>
105
+ <SQLSelectTrigger {...getCellForDomProps(cellId)}>
102
106
  <SelectValue placeholder="Select an engine" />
103
107
  </SQLSelectTrigger>
104
- <SelectContent>
108
+ <SelectContent {...getCellForDomProps(cellId)}>
105
109
  <SelectGroup>
106
110
  <SelectLabel>Database connections</SelectLabel>
107
111
  {engineIsDisconnected && (
@@ -13,6 +13,7 @@ export interface ExperimentalFeatures {
13
13
  chat_modes: boolean;
14
14
  cache_panel: boolean;
15
15
  external_agents: boolean;
16
+ server_side_pdf_export: boolean;
16
17
  // Add new feature flags here
17
18
  }
18
19
 
@@ -24,6 +25,7 @@ const defaultValues: ExperimentalFeatures = {
24
25
  chat_modes: false,
25
26
  cache_panel: false,
26
27
  external_agents: import.meta.env.DEV,
28
+ server_side_pdf_export: false,
27
29
  };
28
30
 
29
31
  export function getFeatureFlag<T extends keyof ExperimentalFeatures>(