@marimo-team/frontend 0.15.1-dev38 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/assets/{ConnectedDataExplorerComponent-D8km1NYJ.js → ConnectedDataExplorerComponent-BmuPnAsd.js} +1 -1
  2. package/dist/assets/{ImageComparisonComponent-CERR01UB.js → ImageComparisonComponent-uf-9TFG8.js} +1 -1
  3. package/dist/assets/{VegaLite-DzZIYwlo.js → VegaLite-CQyOAWzz.js} +1 -1
  4. package/dist/assets/{_baseEach-DO-XcSIV.js → _baseEach-2yxz4QPK.js} +1 -1
  5. package/dist/assets/_baseMap-Uhjhm4ni.js +1 -0
  6. package/dist/assets/{_baseUniq-urG2vBXB.js → _baseUniq-kkBJp3jc.js} +1 -1
  7. package/dist/assets/{_createAggregator-DBxX1xI5.js → _createAggregator-ByV0zOLy.js} +1 -1
  8. package/dist/assets/{any-language-editor-BhBzti0Q.js → any-language-editor-B-cvHGmQ.js} +1 -1
  9. package/dist/assets/{architectureDiagram-KFL7JDKH-mgZE9P9E.js → architectureDiagram-KFL7JDKH-CrRiJlSM.js} +1 -1
  10. package/dist/assets/{blockDiagram-ZYB65J3Q-DhlvqBBN.js → blockDiagram-ZYB65J3Q-BARtxPtR.js} +1 -1
  11. package/dist/assets/{c4Diagram-AAMF2YG6-WgF7TmcX.js → c4Diagram-AAMF2YG6-C1rZ_aGX.js} +1 -1
  12. package/dist/assets/channel-BFLyOCHP.js +1 -0
  13. package/dist/assets/{chunk-ANTBXLJU-Cc0FLBv4.js → chunk-ANTBXLJU-CXz5OLFO.js} +1 -1
  14. package/dist/assets/{chunk-FHKO5MBM-TDVJkINs.js → chunk-FHKO5MBM-BIrdB1fE.js} +1 -1
  15. package/dist/assets/{chunk-GLLZNHP4-BFZQsgmM.js → chunk-GLLZNHP4-jO0hO6S-.js} +1 -1
  16. package/dist/assets/{chunk-JBRWN2VN-jewL7EcA.js → chunk-JBRWN2VN-CqtMDVwa.js} +1 -1
  17. package/dist/assets/{chunk-LXBSTHXV-DtMvensd.js → chunk-LXBSTHXV-68wVSxsz.js} +1 -1
  18. package/dist/assets/{chunk-NRVI72HA-B7LxIi5N.js → chunk-NRVI72HA-6jfSceAv.js} +1 -1
  19. package/dist/assets/{chunk-OMD6QJNC-D77lwHsv.js → chunk-OMD6QJNC-DItPiESC.js} +1 -1
  20. package/dist/assets/{chunk-WVR4S24B-BdzaJi_f.js → chunk-WVR4S24B-Dk_4BMrf.js} +1 -1
  21. package/dist/assets/{circle-play-DbLJhgWd.js → circle-play-DlcxXne6.js} +1 -1
  22. package/dist/assets/classDiagram-3BZAVTQC-DmaMTSt3.js +1 -0
  23. package/dist/assets/classDiagram-v2-QTMF73CY-DmaMTSt3.js +1 -0
  24. package/dist/assets/clone-D8_RSzqX.js +1 -0
  25. package/dist/assets/{compile-ittiAa-p.js → compile-rRtwsAuM.js} +1 -1
  26. package/dist/assets/{dagre-2BBEFEWP-woDg8Awj.js → dagre-2BBEFEWP-BRQiUAjN.js} +1 -1
  27. package/dist/assets/{data-grid-overlay-editor-ncLMoVIb.js → data-grid-overlay-editor-D69G4ntu.js} +1 -1
  28. package/dist/assets/{diagram-4IRLE6MV-PAN7AATi.js → diagram-4IRLE6MV-dbf1VDp2.js} +1 -1
  29. package/dist/assets/{diagram-GUPCWM2R-Ce2AHqMP.js → diagram-GUPCWM2R-03cBW_UX.js} +1 -1
  30. package/dist/assets/{diagram-RP2FKANI-2WiKea8D.js → diagram-RP2FKANI-izLn5JA6.js} +1 -1
  31. package/dist/assets/edit-page-KQGIuOho.css +1 -0
  32. package/dist/assets/{edit-page-CZn2bhY3.js → edit-page-zgOEBr1X.js} +48 -48
  33. package/dist/assets/{erDiagram-HZWUO2LU-D5yTZwmt.js → erDiagram-HZWUO2LU-DvzfndiW.js} +1 -1
  34. package/dist/assets/{flowDiagram-THRYKUMA-CgN86sZ8.js → flowDiagram-THRYKUMA-DvMVXvqb.js} +1 -1
  35. package/dist/assets/{ganttDiagram-WV7ZQ7D5-5skQ2H3l.js → ganttDiagram-WV7ZQ7D5-BzN8gykN.js} +1 -1
  36. package/dist/assets/{gitGraphDiagram-OJR772UL-C75De8fN.js → gitGraphDiagram-OJR772UL-wxIdjcsZ.js} +1 -1
  37. package/dist/assets/{glide-data-editor-x0d0Up5U.js → glide-data-editor-D6Yhpu80.js} +4 -4
  38. package/dist/assets/{graph-Die-6Bfl.js → graph-DLqrcAlu.js} +1 -1
  39. package/dist/assets/{home-page-XrEKfDMd.js → home-page-CDKRxIoa.js} +1 -1
  40. package/dist/assets/{index-D_ebOE-e.js → index-0xwlfUxT.js} +1 -1
  41. package/dist/assets/{index-CXwX4pUI.js → index-BB71mG_d.js} +182 -182
  42. package/dist/assets/{index-BfqMljbw.js → index-BZeyS4ec.js} +1 -1
  43. package/dist/assets/{index-CoAopzxc.js → index-Bs6OAQYb.js} +1 -1
  44. package/dist/assets/{index-Dlrab0dK.js → index-BsnxGFdv.js} +1 -1
  45. package/dist/assets/{index-Dh8C39yO.js → index-C0OGBvcf.js} +1 -1
  46. package/dist/assets/{index-BSJet7Zq.js → index-CK28dYOC.js} +1 -1
  47. package/dist/assets/{index-DmHX1Rlv.js → index-CQZ2Knp2.js} +1 -1
  48. package/dist/assets/{index-De7juNGF.js → index-Cdz_z5nt.js} +1 -1
  49. package/dist/assets/{index-BWBg8Lhk.js → index-ClD_znk5.js} +1 -1
  50. package/dist/assets/{index-kraNHKP1.js → index-D-b5sgmz.js} +1 -1
  51. package/dist/assets/{index-CCZkMG5j.js → index-D2UJqE4Q.js} +1 -1
  52. package/dist/assets/{index-v546YXgW.js → index-D2fV3sDg.js} +1 -1
  53. package/dist/assets/{index-UAPuSZUk.js → index-DUN68f-4.js} +1 -1
  54. package/dist/assets/{index-B3-zxsj0.js → index-DZKpeoa1.js} +1 -1
  55. package/dist/assets/{index-F531CohR.js → index-DgoUxLCo.js} +1 -1
  56. package/dist/assets/{index-B1-fG89N.js → index-Dh_3Mb5d.js} +1 -1
  57. package/dist/assets/{index-D9ot8WPl.js → index-DroyKRLH.js} +1 -1
  58. package/dist/assets/index-DryPQDfA.css +1 -0
  59. package/dist/assets/{index-jwhWh6uS.js → index-bUE-XbFV.js} +1 -1
  60. package/dist/assets/{index-B7JwzNrx.js → index-yz44yADt.js} +1 -1
  61. package/dist/assets/infoDiagram-6WOFNB3A-CvNjv0rh.js +2 -0
  62. package/dist/assets/{journeyDiagram-FFXJYRFH-CrWxkiXG.js → journeyDiagram-FFXJYRFH-BON6WB5Z.js} +1 -1
  63. package/dist/assets/{kanban-definition-KOZQBZVT-D5haTqpz.js → kanban-definition-KOZQBZVT-DENjuTrn.js} +1 -1
  64. package/dist/assets/{layout-BEc7rmra.js → layout-3QJH3hkO.js} +1 -1
  65. package/dist/assets/{linear-BLmy5c_P.js → linear-BFvzcy10.js} +1 -1
  66. package/dist/assets/links-B7kV7dQW.js +17 -0
  67. package/dist/assets/{mermaid-Bu2fp8N3.js → mermaid-0ClPHFIM.js} +4 -4
  68. package/dist/assets/{min-BF5a70Ha.js → min-CO20nzic.js} +1 -1
  69. package/dist/assets/{mindmap-definition-LNHGMQRG-jD2ylGBG.js → mindmap-definition-LNHGMQRG-0ssdD2sg.js} +1 -1
  70. package/dist/assets/{number-overlay-editor-BadGHMrr.js → number-overlay-editor-CVVXgCAp.js} +1 -1
  71. package/dist/assets/{pieDiagram-DBDJKBY4-B11UeqxS.js → pieDiagram-DBDJKBY4-DI0JNhUn.js} +1 -1
  72. package/dist/assets/{quadrantDiagram-YPSRARAO-DznrQXs-.js → quadrantDiagram-YPSRARAO-Bqg0IAoq.js} +1 -1
  73. package/dist/assets/{react-plotly-B6W1vV_l.js → react-plotly-yplDi2oo.js} +1 -1
  74. package/dist/assets/{requirementDiagram-EGVEC5DT-BuWp2ov9.js → requirementDiagram-EGVEC5DT-C_YIxDr_.js} +1 -1
  75. package/dist/assets/{run-page-BD4YehOu.js → run-page-Bv2C3uJ1.js} +1 -1
  76. package/dist/assets/{sankeyDiagram-HRAUVNP4-BmDCzHkG.js → sankeyDiagram-HRAUVNP4-Dm-w1V71.js} +1 -1
  77. package/dist/assets/{sequenceDiagram-WFGC7UMF-B6aCHwAd.js → sequenceDiagram-WFGC7UMF-uox9Aix1.js} +1 -1
  78. package/dist/assets/{slides-component-CkHAJhvv.js → slides-component-BBkS68D5.js} +1 -1
  79. package/dist/assets/{sortBy-BgSF6Z3p.js → sortBy-yoFvjOas.js} +1 -1
  80. package/dist/assets/{stateDiagram-UUKSUZ4H-DcGKoXUB.js → stateDiagram-UUKSUZ4H-DVHFynm4.js} +1 -1
  81. package/dist/assets/stateDiagram-v2-EYPG3UTE-_z_RwTAL.js +1 -0
  82. package/dist/assets/{storage-BX5GfYeC.js → storage-C__Jd9kD.js} +3 -3
  83. package/dist/assets/{terminal-CpmTRzDD.js → terminal-BpOKN-Gr.js} +1 -1
  84. package/dist/assets/{time-igUPFhda.js → time-q7el0-6s.js} +1 -1
  85. package/dist/assets/{timeline-definition-3HZDQTIS-7b4K28uW.js → timeline-definition-3HZDQTIS-K-bheyBf.js} +1 -1
  86. package/dist/assets/{tracing-BWeRyJmf.js → tracing-Cv9nIL_Y.js} +2 -2
  87. package/dist/assets/{trash-Bv9cD3VE.js → trash-Izx1PgIJ.js} +1 -1
  88. package/dist/assets/{treemap-75Q7IDZK-DPkOPTcz.js → treemap-75Q7IDZK-mGt3Vxl-.js} +1 -1
  89. package/dist/assets/{vega-component-CGVDFtwG.js → vega-component-B1UZ_5Pg.js} +1 -1
  90. package/dist/assets/{xychartDiagram-FDP5SA34-BbvKlNzf.js → xychartDiagram-FDP5SA34-DGx9I_mu.js} +1 -1
  91. package/dist/index.html +2 -2
  92. package/package.json +7 -7
  93. package/src/components/ai/ai-model-dropdown.tsx +3 -3
  94. package/src/components/app-config/ai-config.tsx +244 -8
  95. package/src/components/chat/markdown-renderer.css +15 -0
  96. package/src/components/chat/markdown-renderer.tsx +2 -1
  97. package/src/components/editor/ai/completion-utils.ts +6 -2
  98. package/src/components/editor/cell/cell-context-menu.tsx +35 -13
  99. package/src/core/ai/__tests__/model-registry.test.ts +28 -0
  100. package/src/core/ai/model-registry.ts +44 -8
  101. package/src/core/codemirror/language/languages/sql/utils.ts +6 -1
  102. package/src/plugins/impl/FileBrowserPlugin.tsx +14 -20
  103. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +19 -2
  104. package/src/plugins/impl/common/error-banner.tsx +3 -0
  105. package/dist/assets/_baseMap-Dg1mkEJY.js +0 -1
  106. package/dist/assets/channel-D1aHTD78.js +0 -1
  107. package/dist/assets/classDiagram-3BZAVTQC-C8Rjm-tQ.js +0 -1
  108. package/dist/assets/classDiagram-v2-QTMF73CY-C8Rjm-tQ.js +0 -1
  109. package/dist/assets/clone-BWlvmMhq.js +0 -1
  110. package/dist/assets/edit-page-jgH_jXgs.css +0 -1
  111. package/dist/assets/index-B6q_Qknx.css +0 -1
  112. package/dist/assets/infoDiagram-6WOFNB3A-mJ455ogl.js +0 -2
  113. package/dist/assets/links-BGVSWGyv.js +0 -17
  114. package/dist/assets/stateDiagram-v2-EYPG3UTE-BW5Lhcrb.js +0 -1
@@ -1,7 +1,14 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
- import { BrainIcon, ChevronRightIcon, InfoIcon } from "lucide-react";
4
- import React, { useMemo } from "react";
3
+ import {
4
+ BotIcon,
5
+ BrainIcon,
6
+ ChevronRightIcon,
7
+ InfoIcon,
8
+ PlusIcon,
9
+ Trash2Icon,
10
+ } from "lucide-react";
11
+ import React, { useId, useMemo, useState } from "react";
5
12
  import {
6
13
  Button as AriaButton,
7
14
  Tree,
@@ -26,8 +33,10 @@ import { NativeSelect } from "@/components/ui/native-select";
26
33
  import { Textarea } from "@/components/ui/textarea";
27
34
  import {
28
35
  AiModelId,
36
+ PROVIDERS,
29
37
  type ProviderId,
30
38
  type QualifiedModelId,
39
+ type ShortModelId,
31
40
  } from "@/core/ai/ids/ids";
32
41
  import { type AiModel, AiModelRegistry } from "@/core/ai/model-registry";
33
42
  import { CopilotConfig } from "@/core/codemirror/copilot/copilot-config";
@@ -36,7 +45,7 @@ import { isWasm } from "@/core/wasm/utils";
36
45
  import { cn } from "@/utils/cn";
37
46
  import { Events } from "@/utils/events";
38
47
  import { Strings } from "@/utils/strings";
39
- import { AIModelDropdown } from "../ai/ai-model-dropdown";
48
+ import { AIModelDropdown, getProviderLabel } from "../ai/ai-model-dropdown";
40
49
  import {
41
50
  AiProviderIcon,
42
51
  type AiProviderIconProps,
@@ -48,9 +57,18 @@ import {
48
57
  AccordionItem,
49
58
  AccordionTrigger,
50
59
  } from "../ui/accordion";
60
+ import { Button } from "../ui/button";
51
61
  import { Checkbox } from "../ui/checkbox";
52
62
  import { DropdownMenuSeparator } from "../ui/dropdown-menu";
63
+ import { Label } from "../ui/label";
53
64
  import { ExternalLink } from "../ui/links";
65
+ import {
66
+ Select,
67
+ SelectContent,
68
+ SelectGroup,
69
+ SelectItem,
70
+ SelectTrigger,
71
+ } from "../ui/select";
54
72
  import { Switch } from "../ui/switch";
55
73
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
56
74
  import { Tooltip } from "../ui/tooltip";
@@ -424,6 +442,7 @@ interface ModelListItemProps {
424
442
  model: AiModel;
425
443
  isEnabled: boolean;
426
444
  onToggle: (modelId: QualifiedModelId) => void;
445
+ onDelete: (modelId: QualifiedModelId) => void;
427
446
  }
428
447
 
429
448
  const ModelListItem: React.FC<ModelListItemProps> = ({
@@ -431,11 +450,18 @@ const ModelListItem: React.FC<ModelListItemProps> = ({
431
450
  model,
432
451
  isEnabled,
433
452
  onToggle,
453
+ onDelete,
434
454
  }) => {
435
455
  const handleToggle = () => {
436
456
  onToggle(qualifiedId);
437
457
  };
438
458
 
459
+ const handleDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
460
+ e.stopPropagation();
461
+ e.preventDefault();
462
+ onDelete(qualifiedId);
463
+ };
464
+
439
465
  return (
440
466
  <TreeItem
441
467
  id={qualifiedId}
@@ -446,6 +472,16 @@ const ModelListItem: React.FC<ModelListItemProps> = ({
446
472
  <TreeItemContent>
447
473
  <div className="flex items-center justify-between px-4 py-3 border-b last:border-b-0 cursor-pointer outline-none">
448
474
  <ModelInfoCard model={model} qualifiedId={qualifiedId} />
475
+ {model.custom && (
476
+ <Button
477
+ variant="ghost"
478
+ size="icon"
479
+ onClick={handleDelete}
480
+ className="mr-2 hover:bg-transparent"
481
+ >
482
+ <Trash2Icon className="h-3.5 w-3.5 text-muted-foreground" />
483
+ </Button>
484
+ )}
449
485
  <Switch checked={isEnabled} onClick={handleToggle} size="sm" />
450
486
  </div>
451
487
  </TreeItemContent>
@@ -465,6 +501,9 @@ const ModelInfoCard = ({
465
501
  <div className="flex flex-col flex-1">
466
502
  <div className="flex items-center gap-2">
467
503
  <h3 className="font-medium">{model.name}</h3>
504
+ <Tooltip content="Custom model">
505
+ {model.custom && <BotIcon className="h-4 w-4" />}
506
+ </Tooltip>
468
507
  </div>
469
508
  <span className="text-xs text-muted-foreground font-mono">
470
509
  {qualifiedId}
@@ -932,6 +971,7 @@ interface ProviderTreeItemProps {
932
971
  enabledModels: Set<QualifiedModelId>;
933
972
  onToggleModel: (modelId: QualifiedModelId) => void;
934
973
  onToggleProvider: (providerId: ProviderId, enable: boolean) => void;
974
+ onDeleteModel: (modelId: QualifiedModelId) => void;
935
975
  }
936
976
 
937
977
  const ProviderTreeItem: React.FC<ProviderTreeItemProps> = ({
@@ -940,6 +980,7 @@ const ProviderTreeItem: React.FC<ProviderTreeItemProps> = ({
940
980
  enabledModels,
941
981
  onToggleModel,
942
982
  onToggleProvider,
983
+ onDeleteModel,
943
984
  }) => {
944
985
  const enabledCount = models.filter((model) =>
945
986
  enabledModels.has(new AiModelId(providerId, model.model).id),
@@ -996,6 +1037,7 @@ const ProviderTreeItem: React.FC<ProviderTreeItemProps> = ({
996
1037
  model={model}
997
1038
  isEnabled={enabledModels.has(qualifiedId)}
998
1039
  onToggle={onToggleModel}
1040
+ onDelete={onDeleteModel}
999
1041
  />
1000
1042
  );
1001
1043
  })}
@@ -1007,13 +1049,18 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
1007
1049
  form,
1008
1050
  onSubmit,
1009
1051
  }) => {
1052
+ const customModels = useWatch({
1053
+ control: form.control,
1054
+ name: "ai.models.custom_models",
1055
+ }) as QualifiedModelId[];
1056
+
1010
1057
  const aiModelRegistry = useMemo(
1011
1058
  () =>
1012
1059
  AiModelRegistry.create({
1013
1060
  displayedModels: [],
1014
- customModels: ["openrouter/deepseek-r1-distill-llama-70b"],
1061
+ customModels: customModels,
1015
1062
  }),
1016
- [],
1063
+ [customModels],
1017
1064
  );
1018
1065
  const currentDisplayedModels = useWatch({
1019
1066
  control: form.control,
@@ -1022,6 +1069,7 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
1022
1069
  }) as QualifiedModelId[];
1023
1070
  const currentDisplayedModelsSet = new Set(currentDisplayedModels);
1024
1071
  const modelsByProvider = aiModelRegistry.getGroupedModelsByProvider();
1072
+ const listModelsByProvider = aiModelRegistry.getListModelsByProvider();
1025
1073
 
1026
1074
  const toggleModelDisplay = useEvent((modelId: QualifiedModelId) => {
1027
1075
  const newModels = currentDisplayedModelsSet.has(modelId)
@@ -1050,9 +1098,15 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
1050
1098
  },
1051
1099
  );
1052
1100
 
1101
+ const deleteModel = useEvent((modelId: QualifiedModelId) => {
1102
+ const newModels = customModels.filter((id) => id !== modelId);
1103
+ form.setValue("ai.models.custom_models", newModels);
1104
+ onSubmit(form.getValues());
1105
+ });
1106
+
1053
1107
  return (
1054
- <SettingGroup>
1055
- <p className="text-sm text-muted-secondary mb-4">
1108
+ <SettingGroup className="gap-2">
1109
+ <p className="text-sm text-muted-secondary mb-6">
1056
1110
  Control which AI models are displayed in model selection dropdowns. When
1057
1111
  no models are selected, all available models will be shown.
1058
1112
  </p>
@@ -1063,7 +1117,7 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
1063
1117
  className="flex-1 overflow-auto outline-none focus-visible:outline-none"
1064
1118
  selectionMode="none"
1065
1119
  >
1066
- {[...modelsByProvider.entries()].map(([providerId, models]) => (
1120
+ {listModelsByProvider.map(([providerId, models]) => (
1067
1121
  <ProviderTreeItem
1068
1122
  key={providerId}
1069
1123
  providerId={providerId}
@@ -1071,14 +1125,196 @@ export const AiModelDisplayConfig: React.FC<AiConfigProps> = ({
1071
1125
  enabledModels={currentDisplayedModelsSet}
1072
1126
  onToggleModel={toggleModelDisplay}
1073
1127
  onToggleProvider={toggleProviderModels}
1128
+ onDeleteModel={deleteModel}
1074
1129
  />
1075
1130
  ))}
1076
1131
  </Tree>
1077
1132
  </div>
1133
+ <AddModelForm form={form} customModels={customModels} />
1078
1134
  </SettingGroup>
1079
1135
  );
1080
1136
  };
1081
1137
 
1138
+ export const AddModelForm: React.FC<{
1139
+ form: UseFormReturn<UserConfig>;
1140
+ customModels: QualifiedModelId[];
1141
+ }> = ({ form, customModels }) => {
1142
+ const [isFormOpen, setIsFormOpen] = useState(false);
1143
+ const [modelAdded, setModelAdded] = useState(false);
1144
+ const [provider, setProvider] = useState<ProviderId | "custom" | null>(null);
1145
+ const [customProviderName, setCustomProviderName] = useState("");
1146
+ const [modelName, setModelName] = useState("");
1147
+
1148
+ const providerSelectId = useId();
1149
+ const customProviderInputId = useId();
1150
+ const modelNameInputId = useId();
1151
+
1152
+ const isCustomProvider = provider === "custom";
1153
+ const providerName = isCustomProvider ? customProviderName : provider;
1154
+ const hasValidValues = providerName?.trim() && modelName?.trim();
1155
+
1156
+ const resetForm = () => {
1157
+ setProvider(null);
1158
+ setCustomProviderName("");
1159
+ setModelName("");
1160
+ setIsFormOpen(false);
1161
+ };
1162
+
1163
+ const handleAddModel = () => {
1164
+ if (!hasValidValues) {
1165
+ return;
1166
+ }
1167
+
1168
+ const newModel = new AiModelId(
1169
+ providerName as ProviderId,
1170
+ modelName as ShortModelId,
1171
+ );
1172
+
1173
+ form.setValue("ai.models.custom_models", [newModel.id, ...customModels]);
1174
+ resetForm();
1175
+
1176
+ // Show model added message for 2 seconds
1177
+ setModelAdded(true);
1178
+ setTimeout(() => setModelAdded(false), 2000);
1179
+ };
1180
+
1181
+ const providerClassName = "w-40 truncate";
1182
+
1183
+ const providerSelect = (
1184
+ <div className="flex flex-col gap-2">
1185
+ <div className="flex items-center gap-2">
1186
+ <Label
1187
+ htmlFor={providerSelectId}
1188
+ className="text-sm font-medium text-muted-foreground min-w-12"
1189
+ >
1190
+ Provider
1191
+ </Label>
1192
+ <Select
1193
+ value={provider || ""}
1194
+ onValueChange={(v) => setProvider(v as ProviderId | "custom")}
1195
+ >
1196
+ <SelectTrigger id={providerSelectId} className={providerClassName}>
1197
+ {provider ? (
1198
+ <div className="flex items-center gap-1.5">
1199
+ <AiProviderIcon
1200
+ provider={provider as ProviderId}
1201
+ className="h-3.5 w-3.5"
1202
+ />
1203
+ <span>{getProviderLabel(provider as ProviderId)}</span>
1204
+ </div>
1205
+ ) : (
1206
+ <span className="text-muted-foreground">Select...</span>
1207
+ )}
1208
+ </SelectTrigger>
1209
+ <SelectContent>
1210
+ <SelectGroup>
1211
+ <SelectItem value="custom">
1212
+ <div className="flex items-center gap-2">
1213
+ <AiProviderIcon
1214
+ provider="openai-compatible"
1215
+ className="h-4 w-4"
1216
+ />
1217
+ <span>Custom</span>
1218
+ </div>
1219
+ </SelectItem>
1220
+ {PROVIDERS.filter((p) => p !== "marimo").map((p) => (
1221
+ <SelectItem key={p} value={p}>
1222
+ <div className="flex items-center gap-2">
1223
+ <AiProviderIcon provider={p} className="h-4 w-4" />
1224
+ <span>{getProviderLabel(p)}</span>
1225
+ </div>
1226
+ </SelectItem>
1227
+ ))}
1228
+ </SelectGroup>
1229
+ </SelectContent>
1230
+ </Select>
1231
+ </div>
1232
+
1233
+ {isCustomProvider && (
1234
+ <div className="flex items-center gap-2">
1235
+ <Label
1236
+ htmlFor={customProviderInputId}
1237
+ className="text-sm font-medium text-muted-foreground min-w-12"
1238
+ >
1239
+ Name
1240
+ </Label>
1241
+ <Input
1242
+ id={customProviderInputId}
1243
+ value={customProviderName}
1244
+ onChange={(e) => setCustomProviderName(e.target.value)}
1245
+ placeholder="openrouter"
1246
+ className={providerClassName}
1247
+ />
1248
+ </div>
1249
+ )}
1250
+ </div>
1251
+ );
1252
+
1253
+ const modelInput = (
1254
+ <div
1255
+ className={cn(
1256
+ "flex items-center gap-2",
1257
+ isCustomProvider && "self-start",
1258
+ )}
1259
+ >
1260
+ <Label
1261
+ htmlFor={modelNameInputId}
1262
+ className="text-sm font-medium text-muted-foreground"
1263
+ >
1264
+ Model
1265
+ </Label>
1266
+ <Input
1267
+ id={modelNameInputId}
1268
+ value={modelName}
1269
+ onChange={(e) => setModelName(e.target.value)}
1270
+ placeholder="gpt-4"
1271
+ className="text-xs mb-0"
1272
+ />
1273
+ </div>
1274
+ );
1275
+
1276
+ const inputForm = (
1277
+ <div className="flex items-center gap-3 p-3 border border-border rounded-md">
1278
+ {providerSelect}
1279
+ {modelInput}
1280
+ <div
1281
+ className={cn("flex gap-1.5 ml-auto", isCustomProvider && "self-end")}
1282
+ >
1283
+ <Button onClick={handleAddModel} disabled={!hasValidValues} size="xs">
1284
+ Add
1285
+ </Button>
1286
+ <Button variant="outline" onClick={resetForm} size="xs">
1287
+ Cancel
1288
+ </Button>
1289
+ </div>
1290
+ </div>
1291
+ );
1292
+
1293
+ return (
1294
+ <div>
1295
+ {isFormOpen && inputForm}
1296
+ <div className="flex flex-row text-sm">
1297
+ <Button
1298
+ onClick={(e) => {
1299
+ e.preventDefault();
1300
+ setIsFormOpen(true);
1301
+ }}
1302
+ variant="link"
1303
+ disabled={isFormOpen}
1304
+ >
1305
+ <PlusIcon className="h-4 w-4 mr-2 mb-0.5" />
1306
+ Add Model
1307
+ </Button>
1308
+ {modelAdded && (
1309
+ <div className="flex items-center gap-1 text-green-700 bg-green-500/10 px-2 py-1 rounded-md ml-auto">
1310
+ ✓ Model added
1311
+ </div>
1312
+ )}
1313
+ </div>
1314
+ </div>
1315
+ );
1316
+ };
1317
+
1082
1318
  export const AiConfig: React.FC<AiConfigProps> = ({
1083
1319
  form,
1084
1320
  config,
@@ -0,0 +1,15 @@
1
+ .mo-markdown-renderer {
2
+ h1,
3
+ h2,
4
+ h3,
5
+ h4,
6
+ h5,
7
+ h6 {
8
+ margin-top: 10px;
9
+ }
10
+
11
+ hr {
12
+ margin-top: 8px;
13
+ margin-bottom: 12px;
14
+ }
15
+ }
@@ -17,6 +17,7 @@ import { autoInstantiateAtom } from "@/core/config/config";
17
17
  import { LazyAnyLanguageCodeMirror } from "@/plugins/impl/code/LazyAnyLanguageCodeMirror";
18
18
  import { useTheme } from "@/theme/useTheme";
19
19
  import { copyToClipboard } from "@/utils/copy";
20
+ import "./markdown-renderer.css";
20
21
 
21
22
  const extensions = [EditorView.lineWrapping];
22
23
 
@@ -202,7 +203,7 @@ const MemoizedMarkdownBlock = memo(
202
203
  <Markdown
203
204
  components={COMPONENTS}
204
205
  remarkPlugins={PLUGINS}
205
- className="prose dark:prose-invert max-w-none prose-pre:pl-0"
206
+ className="mo-markdown-renderer prose dark:prose-invert max-w-none prose-pre:pl-0"
206
207
  >
207
208
  {content}
208
209
  </Markdown>
@@ -19,8 +19,12 @@ export function getAICompletionBody({
19
19
  }: {
20
20
  input: string;
21
21
  }): Omit<AiCompletionRequest, "language" | "prompt" | "code"> {
22
- const contextString = extractDatasetsAndVariables(input);
23
- Logger.debug("Included context", contextString);
22
+ let contextString = "";
23
+ // Skip if no '@' in the input
24
+ if (input.includes("@")) {
25
+ contextString = extractDatasetsAndVariables(input);
26
+ Logger.debug("Included context", contextString);
27
+ }
24
28
 
25
29
  return {
26
30
  includeOtherCode: getCodes(""),
@@ -17,6 +17,7 @@ import {
17
17
  ContextMenuSeparator,
18
18
  ContextMenuTrigger,
19
19
  } from "@/components/ui/context-menu";
20
+ import { menuItemVariants } from "@/components/ui/menu-items";
20
21
  import { Tooltip } from "@/components/ui/tooltip";
21
22
  import { toast } from "@/components/ui/use-toast";
22
23
  import { useCellData, useCellRuntime } from "@/core/cells/cells";
@@ -219,19 +220,40 @@ export const CellActionsContextMenu = ({
219
220
  }
220
221
 
221
222
  return (
222
- <ContextMenuItem
223
- key={action.label}
224
- className={action.disabled ? "opacity-50!" : ""}
225
- onSelect={(evt) => {
226
- if (action.disableClick || action.disabled) {
227
- return;
228
- }
229
- action.handle(evt);
230
- }}
231
- variant={action.variant}
232
- >
233
- {body}
234
- </ContextMenuItem>
223
+ <Fragment key={action.label}>
224
+ {
225
+ // Set disableClick items such as cell name input
226
+ // to div to prevent roving focus
227
+ action.disableClick ? (
228
+ <div
229
+ className={menuItemVariants({
230
+ className: action.disabled ? "opacity-50!" : "",
231
+ variant: action.variant,
232
+ })}
233
+ onKeyDown={(evt) => {
234
+ evt.stopPropagation();
235
+ }}
236
+ // Prevent keydown propagation, that focus does not jump to shortcut which start with same letter
237
+ // e.g. input "C", then focus jump to "Copy"
238
+ >
239
+ {body}
240
+ </div>
241
+ ) : (
242
+ <ContextMenuItem
243
+ className={action.disabled ? "opacity-50!" : ""}
244
+ onSelect={(evt) => {
245
+ if (action.disableClick || action.disabled) {
246
+ return;
247
+ }
248
+ action.handle(evt);
249
+ }}
250
+ variant={action.variant}
251
+ >
252
+ {body}
253
+ </ContextMenuItem>
254
+ )
255
+ }
256
+ </Fragment>
235
257
  );
236
258
  })}
237
259
  {i < allActions.length - 1 && <ContextMenuSeparator />}
@@ -234,6 +234,34 @@ describe("AiModelRegistry", () => {
234
234
  });
235
235
  });
236
236
 
237
+ describe("getListModelsByProvider", () => {
238
+ /**
239
+ * Provider sort order depends on `provider.json`. We can hardcode for tests
240
+ * OpenAI, Bedrock, Azure, Anthropic, Google, Ollama, GitHub, Marimo
241
+ */
242
+ const PROVIDER_SORT_ORDER = ["openai", "anthropic", "google"];
243
+
244
+ it("should return list of models by provider", () => {
245
+ const registry = AiModelRegistry.create({});
246
+ const listModelsByProvider = registry.getListModelsByProvider();
247
+ expect(listModelsByProvider).toHaveLength(3);
248
+
249
+ // Should be sorted by provider
250
+ const providers = listModelsByProvider.map(([provider]) => provider);
251
+ expect(providers).toEqual(PROVIDER_SORT_ORDER);
252
+ });
253
+
254
+ it("should include custom providers at the top", () => {
255
+ const customModels = ["openrouter/custom-gpt"];
256
+ const registry = AiModelRegistry.create({ customModels });
257
+ const listModelsByProvider = registry.getListModelsByProvider();
258
+ expect(listModelsByProvider).toHaveLength(4);
259
+
260
+ const providers = listModelsByProvider.map(([provider]) => provider);
261
+ expect(providers).toEqual(["openrouter", ...PROVIDER_SORT_ORDER]);
262
+ });
263
+ });
264
+
237
265
  describe("getCustomModels", () => {
238
266
  it("should return empty set when no custom models", () => {
239
267
  const registry = AiModelRegistry.create({});
@@ -13,6 +13,18 @@ import { once } from "@/utils/once";
13
13
  import type { ProviderId } from "./ids/ids";
14
14
  import { AiModelId, type QualifiedModelId, type ShortModelId } from "./ids/ids";
15
15
 
16
+ export const PROVIDER_SORT_ORDER: ProviderId[] = [
17
+ // Sort by popular ones
18
+ "anthropic",
19
+ "openai",
20
+ "google",
21
+ "github",
22
+ "deepseek",
23
+ "azure",
24
+ "bedrock",
25
+ "ollama",
26
+ ];
27
+
16
28
  export interface AiModel extends AiModelType {
17
29
  roles: Role[];
18
30
  model: ShortModelId;
@@ -41,13 +53,21 @@ const getKnownModelMap = once((): ReadonlyMap<QualifiedModelId, AiModel> => {
41
53
  return modelMap;
42
54
  });
43
55
 
44
- const getProviderMap = once((): ReadonlyMap<ProviderId, AiProvider> => {
45
- const providerMap = new Map<ProviderId, AiProvider>();
46
- for (const provider of providers) {
47
- providerMap.set(provider.id as ProviderId, provider);
48
- }
49
- return providerMap;
50
- });
56
+ const getProviderMap = once(
57
+ (): {
58
+ providerMap: ReadonlyMap<ProviderId, AiProvider>;
59
+ providerToOrderIdx: ReadonlyMap<ProviderId, number>;
60
+ } => {
61
+ const providerMap = new Map<ProviderId, AiProvider>();
62
+ const providerToOrderIdx = new Map<ProviderId, number>();
63
+ providers.forEach((provider, idx) => {
64
+ const providerId = provider.id as ProviderId;
65
+ providerMap.set(providerId, provider);
66
+ providerToOrderIdx.set(providerId, idx);
67
+ });
68
+ return { providerMap, providerToOrderIdx };
69
+ },
70
+ );
51
71
 
52
72
  export class AiModelRegistry {
53
73
  private modelsByProviderMap = new MultiMap<ProviderId, AiModel>();
@@ -66,7 +86,8 @@ export class AiModelRegistry {
66
86
  }
67
87
 
68
88
  static getProviderInfo(providerId: ProviderId) {
69
- return getProviderMap().get(providerId);
89
+ const { providerMap } = getProviderMap();
90
+ return providerMap.get(providerId);
70
91
  }
71
92
 
72
93
  /**
@@ -180,6 +201,21 @@ export class AiModelRegistry {
180
201
  return this.modelsByProviderMap;
181
202
  }
182
203
 
204
+ getListModelsByProvider(): Array<[ProviderId, AiModel[]]> {
205
+ const modelsByProvider = this.getGroupedModelsByProvider();
206
+ const arrayModels = [...modelsByProvider.entries()];
207
+ const providerToOrderIdx = getProviderMap().providerToOrderIdx;
208
+
209
+ arrayModels.sort((a, b) => {
210
+ const aProvider = a[0];
211
+ const bProvider = b[0];
212
+ const aOrderIdx = providerToOrderIdx.get(aProvider) ?? 0;
213
+ const bOrderIdx = providerToOrderIdx.get(bProvider) ?? 0;
214
+ return aOrderIdx - bOrderIdx;
215
+ });
216
+ return arrayModels;
217
+ }
218
+
183
219
  getModelsMap() {
184
220
  return this.modelsMap;
185
221
  }
@@ -12,7 +12,10 @@ import {
12
12
  SQLite,
13
13
  StandardSQL,
14
14
  } from "@codemirror/lang-sql";
15
- import { DuckDBDialect } from "@marimo-team/codemirror-sql/dialects";
15
+ import {
16
+ BigQueryDialect,
17
+ DuckDBDialect,
18
+ } from "@marimo-team/codemirror-sql/dialects";
16
19
  import type { DataSourceConnection } from "@/core/kernel/messages";
17
20
 
18
21
  export function guessDialect(
@@ -38,6 +41,8 @@ export function guessDialect(
38
41
  case "oracledb":
39
42
  case "oracle":
40
43
  return PLSQL;
44
+ case "bigquery":
45
+ return BigQueryDialect;
41
46
  default:
42
47
  return undefined;
43
48
  }