@marimo-team/islands 0.16.4 → 0.16.5

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 (65) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CCjhPKMy.js → ConnectedDataExplorerComponent-D96i9G-X.js} +3 -3
  2. package/dist/assets/__vite-browser-external-Dv_SHu1h.js +1 -0
  3. package/dist/assets/{worker-DnuXpGWN.js → worker-DVOR9oZG.js} +2 -2
  4. package/dist/{formats-D5C6JAJf.js → formats-ChrNdVdJ.js} +1 -1
  5. package/dist/{glide-data-editor-CYfKmSNp.js → glide-data-editor-D_kEsT07.js} +68 -68
  6. package/dist/main.js +84 -213
  7. package/dist/{mermaid-BlJDcO4M.js → mermaid-MWiyXDcI.js} +2 -2
  8. package/dist/style.css +1 -1
  9. package/dist/{types-Dcb1hf55.js → types-1X1uZB4y.js} +323 -179
  10. package/dist/{useAsyncData-DAtPzJzP.js → useAsyncData-C4IqQK0g.js} +1 -1
  11. package/dist/{useDateFormatter-CiUlIu7v.js → useDateFormatter-BCsBqetx.js} +1 -1
  12. package/dist/{useTheme-CmsvrO5o.js → useTheme-C2pgJzDH.js} +1 -0
  13. package/dist/{vega-component-B3LA6qbm.js → vega-component-Cv4J8CHz.js} +3 -3
  14. package/package.json +1 -1
  15. package/src/__tests__/chat-history.test.ts +123 -0
  16. package/src/components/app-config/ai-config.tsx +23 -0
  17. package/src/components/app-config/mcp-config.tsx +42 -2
  18. package/src/components/app-config/user-config-form.tsx +0 -24
  19. package/src/components/chat/acp/__tests__/context-utils.test.ts +1 -1
  20. package/src/components/chat/acp/agent-panel.tsx +1 -1
  21. package/src/components/chat/acp/blocks.tsx +46 -53
  22. package/src/components/chat/acp/common.tsx +1 -1
  23. package/src/components/chat/acp/context-utils.ts +1 -1
  24. package/src/components/chat/acp/session-tabs.tsx +1 -1
  25. package/src/components/chat/chat-history-popover.tsx +125 -0
  26. package/src/components/chat/chat-history-utils.ts +69 -0
  27. package/src/components/chat/chat-panel.tsx +9 -57
  28. package/src/components/editor/__tests__/data-attributes.test.tsx +1 -1
  29. package/src/components/editor/actions/useNotebookActions.tsx +2 -4
  30. package/src/components/editor/ai/__tests__/completion-utils.test.ts +23 -31
  31. package/src/components/editor/cell/CreateCellButton.tsx +14 -2
  32. package/src/components/editor/cell/code/cell-editor.tsx +1 -0
  33. package/src/components/editor/database/schemas.ts +2 -10
  34. package/src/components/editor/{Cell.tsx → notebook-cell.tsx} +5 -1
  35. package/src/components/editor/output/MarimoErrorOutput.tsx +4 -34
  36. package/src/components/editor/renderers/{CellArray.tsx → cell-array.tsx} +1 -1
  37. package/src/components/forms/__tests__/form-utils.test.ts +2 -2
  38. package/src/components/mcp/hooks.ts +48 -0
  39. package/src/components/mcp/mcp-status-indicator.tsx +144 -0
  40. package/src/components/ui/number-field.tsx +4 -1
  41. package/src/core/ai/context/providers/__tests__/__snapshots__/tables.test.ts.snap +13 -19
  42. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +0 -1
  43. package/src/core/ai/context/providers/__tests__/datasource.test.ts +5 -6
  44. package/src/core/ai/context/providers/__tests__/error.test.ts +24 -15
  45. package/src/core/ai/context/providers/cell-output.ts +5 -5
  46. package/src/core/ai/context/providers/common.ts +13 -4
  47. package/src/core/ai/context/providers/datasource.ts +31 -20
  48. package/src/core/ai/context/providers/error.ts +3 -4
  49. package/src/core/ai/context/providers/file.ts +2 -2
  50. package/src/core/ai/context/providers/tables.ts +36 -8
  51. package/src/core/ai/context/providers/variable.ts +2 -3
  52. package/src/core/cells/__tests__/cells.test.ts +6 -6
  53. package/src/core/cells/cells.ts +12 -13
  54. package/src/core/cells/scrollCellIntoView.ts +1 -1
  55. package/src/core/codemirror/__tests__/setup.test.ts +1 -0
  56. package/src/core/codemirror/cm.ts +3 -2
  57. package/src/core/config/__tests__/config-schema.test.ts +2 -0
  58. package/src/core/config/config-schema.ts +2 -0
  59. package/src/core/config/feature-flag.tsx +0 -2
  60. package/src/core/edit-app.tsx +1 -1
  61. package/src/core/network/CachingRequestRegistry.ts +2 -2
  62. package/src/stories/cell.stories.tsx +1 -1
  63. package/src/stories/layout/vertical/one-column.stories.tsx +1 -1
  64. package/src/utils/numbers.ts +24 -1
  65. package/dist/assets/__vite-browser-external-BeNtI_tJ.js +0 -1
@@ -0,0 +1,69 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import type { Chat } from "@/core/ai/state";
4
+
5
+ const DATE_GROUP_CONFIG = [
6
+ { label: "Today", days: 0 },
7
+ { label: "Yesterday", days: 1 },
8
+ { label: "2d ago", days: 2 },
9
+ { label: "3d ago", days: 3 },
10
+ { label: "This week", days: 7 },
11
+ { label: "This month", days: 30 },
12
+ ] as const;
13
+
14
+ interface DateGroup {
15
+ label: string;
16
+ days: number;
17
+ chats: Chat[];
18
+ }
19
+
20
+ // Utility function to group chats by date periods
21
+ export const groupChatsByDate = (chats: Chat[]): DateGroup[] => {
22
+ const now = Date.now();
23
+ const oneDayMs = 24 * 60 * 60 * 1000;
24
+
25
+ // Initialize groups with empty chat arrays
26
+ const groups: DateGroup[] = DATE_GROUP_CONFIG.map((config) => ({
27
+ ...config,
28
+ chats: [],
29
+ }));
30
+
31
+ const olderGroup: DateGroup = {
32
+ label: "Older",
33
+ days: Infinity,
34
+ chats: [],
35
+ };
36
+
37
+ // Helper function to determine which group a chat belongs to
38
+ const getGroupForChat = (daysDiff: number): DateGroup => {
39
+ // Use switch for exact day matches, then handle ranges
40
+ switch (daysDiff) {
41
+ case 0:
42
+ return groups[0]; // Today
43
+ case 1:
44
+ return groups[1]; // Yesterday
45
+ case 2:
46
+ return groups[2]; // 2d ago
47
+ case 3:
48
+ return groups[3]; // 3d ago
49
+ default:
50
+ // Handle range-based grouping for older chats
51
+ if (daysDiff >= 4 && daysDiff <= 7) {
52
+ return groups[4]; // This week
53
+ } else if (daysDiff >= 8 && daysDiff <= 30) {
54
+ return groups[5]; // This month
55
+ }
56
+ // Everything else goes to Older
57
+ return olderGroup;
58
+ }
59
+ };
60
+
61
+ for (const chat of chats) {
62
+ const daysDiff = Math.floor((now - chat.updatedAt) / oneDayMs);
63
+ const targetGroup = getGroupForChat(daysDiff);
64
+ targetGroup.chats.push(chat);
65
+ }
66
+
67
+ // Return only non-empty groups
68
+ return [...groups, olderGroup].filter((group) => group.chats.length > 0);
69
+ };
@@ -9,7 +9,6 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai";
9
9
  import {
10
10
  AtSignIcon,
11
11
  BotMessageSquareIcon,
12
- ClockIcon,
13
12
  Loader2,
14
13
  PaperclipIcon,
15
14
  PlusIcon,
@@ -17,16 +16,9 @@ import {
17
16
  SettingsIcon,
18
17
  SquareIcon,
19
18
  } from "lucide-react";
20
- import { memo, useEffect, useMemo, useRef, useState } from "react";
21
- import { useLocale } from "react-aria";
19
+ import { memo, useEffect, useRef, useState } from "react";
22
20
  import useEvent from "react-use-event-hook";
23
21
  import { Button } from "@/components/ui/button";
24
- import {
25
- Popover,
26
- PopoverContent,
27
- PopoverTrigger,
28
- } from "@/components/ui/popover";
29
- import { ScrollArea } from "@/components/ui/scroll-area";
30
22
  import {
31
23
  Select,
32
24
  SelectContent,
@@ -52,8 +44,8 @@ import { useRequestClient } from "@/core/network/requests";
52
44
  import { useRuntimeManager } from "@/core/runtime/config";
53
45
  import { ErrorBanner } from "@/plugins/impl/common/error-banner";
54
46
  import { cn } from "@/utils/cn";
55
- import { timeAgo } from "@/utils/dates";
56
47
  import { Logger } from "@/utils/Logger";
48
+
57
49
  import { AIModelDropdown } from "../ai/ai-model-dropdown";
58
50
  import { useOpenSettingsToTab } from "../app-config/state";
59
51
  import { PromptInput } from "../editor/ai/add-cell-with-ai";
@@ -63,10 +55,12 @@ import {
63
55
  } from "../editor/ai/completion-utils";
64
56
  import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
65
57
  import { CopyClipboardIcon } from "../icons/copy-icon";
58
+ import { MCPStatusIndicator } from "../mcp/mcp-status-indicator";
66
59
  import { Input } from "../ui/input";
67
60
  import { Tooltip, TooltipProvider } from "../ui/tooltip";
68
61
  import { toast } from "../ui/use-toast";
69
62
  import { AttachmentRenderer, FileAttachmentPill } from "./chat-components";
63
+ import { ChatHistoryPopover } from "./chat-history-popover";
70
64
  import {
71
65
  buildCompletionRequestBody,
72
66
  convertToFileUIPart,
@@ -104,13 +98,6 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
104
98
  setActiveChat,
105
99
  }) => {
106
100
  const { handleClick } = useOpenSettingsToTab();
107
- const chatState = useAtomValue(chatStateAtom);
108
- const { locale } = useLocale();
109
- const chats = useMemo(() => {
110
- return [...chatState.chats.values()].sort(
111
- (a, b) => b.updatedAt - a.updatedAt,
112
- );
113
- }, [chatState.chats]);
114
101
 
115
102
  return (
116
103
  <div className="flex border-b px-2 py-1 justify-between shrink-0 items-center">
@@ -120,6 +107,7 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
120
107
  </Button>
121
108
  </Tooltip>
122
109
  <div className="flex items-center gap-2">
110
+ <MCPStatusIndicator />
123
111
  <Tooltip content="AI Settings">
124
112
  <Button
125
113
  variant="text"
@@ -130,46 +118,10 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
130
118
  <SettingsIcon className="h-4 w-4" />
131
119
  </Button>
132
120
  </Tooltip>
133
- <Popover>
134
- <Tooltip content="Previous chats">
135
- <PopoverTrigger asChild={true}>
136
- <Button variant="text" size="icon">
137
- <ClockIcon className="h-4 w-4" />
138
- </Button>
139
- </PopoverTrigger>
140
- </Tooltip>
141
- <PopoverContent className="w-[520px] p-0" align="start" side="right">
142
- <ScrollArea className="h-[500px] p-4">
143
- <div className="space-y-4">
144
- {chats.length === 0 && (
145
- <PanelEmptyState
146
- title="No chats yet"
147
- description="Start a new chat to get started"
148
- icon={<BotMessageSquareIcon />}
149
- />
150
- )}
151
- {chats.map((chat) => (
152
- <button
153
- key={chat.id}
154
- className={cn(
155
- "w-full p-3 rounded-md cursor-pointer hover:bg-accent text-left",
156
- chat.id === activeChatId && "bg-accent",
157
- )}
158
- onClick={() => {
159
- setActiveChat(chat.id);
160
- }}
161
- type="button"
162
- >
163
- <div className="font-medium">{chat.title}</div>
164
- <div className="text-sm text-muted-foreground">
165
- {timeAgo(chat.updatedAt, locale)}
166
- </div>
167
- </button>
168
- ))}
169
- </div>
170
- </ScrollArea>
171
- </PopoverContent>
172
- </Popover>
121
+ <ChatHistoryPopover
122
+ activeChatId={activeChatId}
123
+ setActiveChat={setActiveChat}
124
+ />
173
125
  </div>
174
126
  </div>
175
127
  );
@@ -12,7 +12,7 @@ import type { UserConfig } from "@/core/config/config-schema";
12
12
  import type { OutputMessage } from "@/core/kernel/messages";
13
13
  import type { AppMode } from "@/core/mode";
14
14
  import { requestClientAtom } from "@/core/network/requests";
15
- import { Cell } from "../Cell";
15
+ import { Cell } from "../notebook-cell";
16
16
  import { OutputArea } from "../Output";
17
17
 
18
18
  function createTestWrapper() {
@@ -102,7 +102,7 @@ export function useNotebookActions() {
102
102
  updateCellConfig,
103
103
  undoDeleteCell,
104
104
  clearAllCellOutputs,
105
- upsertSetupCell,
105
+ addSetupCellIfDoesntExist,
106
106
  collapseAllCells,
107
107
  expandAllCells,
108
108
  } = useCellActions();
@@ -401,9 +401,7 @@ export function useNotebookActions() {
401
401
  icon: <DiamondPlusIcon size={14} strokeWidth={1.5} />,
402
402
  label: "Add setup cell",
403
403
  handle: () => {
404
- upsertSetupCell({
405
- code: "# Initialization code that runs before all other cells",
406
- });
404
+ addSetupCellIfDoesntExist({});
407
405
  },
408
406
  },
409
407
  {
@@ -55,15 +55,13 @@ describe("getAICompletionBody", () => {
55
55
  expect(result).toMatchInlineSnapshot(`
56
56
  {
57
57
  "context": {
58
- "plainText": "<data name="dataset1" source="unknown">
59
- Columns:
60
- - col1: number
61
- - col2: string</data>
62
-
63
- <data name="dataset2" source="unknown">
64
- Columns:
65
- - col3: boolean
66
- - col4: date</data>",
58
+ "plainText": "<data name="dataset1" source="unknown">Columns:
59
+ col1 (number)
60
+ col2 (string)</data>
61
+
62
+ <data name="dataset2" source="unknown">Columns:
63
+ col3 (boolean)
64
+ col4 (date)</data>",
67
65
  "schema": [],
68
66
  "variables": [],
69
67
  },
@@ -108,10 +106,9 @@ describe("getAICompletionBody", () => {
108
106
  expect(result).toMatchInlineSnapshot(`
109
107
  {
110
108
  "context": {
111
- "plainText": "<data name="existingDataset" source="unknown">
112
- Columns:
113
- - col1: number
114
- - col2: string</data>",
109
+ "plainText": "<data name="existingDataset" source="unknown">Columns:
110
+ col1 (number)
111
+ col2 (string)</data>",
115
112
  "schema": [],
116
113
  "variables": [],
117
114
  },
@@ -144,14 +141,12 @@ describe("getAICompletionBody", () => {
144
141
  expect(result).toMatchInlineSnapshot(`
145
142
  {
146
143
  "context": {
147
- "plainText": "<data name="dataset.with.dots" source="unknown">
148
- Columns:
149
- - col1: number
150
- - col2: string</data>
151
-
152
- <data name="regular_dataset" source="unknown">
153
- Columns:
154
- - col3: boolean</data>",
144
+ "plainText": "<data name="dataset.with.dots" source="unknown">Columns:
145
+ col1 (number)
146
+ col2 (string)</data>
147
+
148
+ <data name="regular_dataset" source="unknown">Columns:
149
+ col3 (boolean)</data>",
155
150
  "schema": [],
156
151
  "variables": [],
157
152
  },
@@ -198,9 +193,8 @@ describe("getAICompletionBody", () => {
198
193
  expect(result).toMatchInlineSnapshot(`
199
194
  {
200
195
  "context": {
201
- "plainText": "<data name="table1" source="unknown">
202
- Columns:
203
- - col1: number</data>",
196
+ "plainText": "<data name="table1" source="unknown">Columns:
197
+ col1 (number)</data>",
204
198
  "schema": [],
205
199
  "variables": [],
206
200
  },
@@ -276,10 +270,9 @@ describe("getAICompletionBody", () => {
276
270
  expect(result).toMatchInlineSnapshot(`
277
271
  {
278
272
  "context": {
279
- "plainText": "<data name="dataset1" source="unknown">
280
- Columns:
281
- - col1: number
282
- - col2: string</data>
273
+ "plainText": "<data name="dataset1" source="unknown">Columns:
274
+ col1 (number)
275
+ col2 (string)</data>
283
276
 
284
277
  <variable name="var1" dataType="string">"string value"</variable>",
285
278
  "schema": [],
@@ -346,9 +339,8 @@ describe("getAICompletionBody", () => {
346
339
  expect(result).toMatchInlineSnapshot(`
347
340
  {
348
341
  "context": {
349
- "plainText": "<data name="conflict" source="unknown">
350
- Columns:
351
- - col1: number</data>",
342
+ "plainText": "<data name="conflict" source="unknown">Columns:
343
+ col1 (number)</data>",
352
344
  "schema": [],
353
345
  "variables": [],
354
346
  },
@@ -1,5 +1,5 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
- import { DatabaseIcon, PlusIcon } from "lucide-react";
2
+ import { DatabaseIcon, DiamondPlusIcon, PlusIcon } from "lucide-react";
3
3
  import { Button } from "@/components/editor/inputs/Inputs";
4
4
  import {
5
5
  ContextMenu,
@@ -69,7 +69,7 @@ const CreateCellButtonContextMenu = (props: {
69
69
  children: React.ReactNode;
70
70
  }) => {
71
71
  const { children, onClick } = props;
72
- const { createNewCell } = useCellActions();
72
+ const { createNewCell, addSetupCellIfDoesntExist } = useCellActions();
73
73
 
74
74
  if (!onClick) {
75
75
  return children;
@@ -125,6 +125,18 @@ const CreateCellButtonContextMenu = (props: {
125
125
  </div>
126
126
  SQL cell
127
127
  </ContextMenuItem>
128
+ <ContextMenuItem
129
+ key="setup"
130
+ onSelect={(evt) => {
131
+ evt.stopPropagation();
132
+ addSetupCellIfDoesntExist({});
133
+ }}
134
+ >
135
+ <div className="mr-3 text-muted-foreground">
136
+ <DiamondPlusIcon size={13} strokeWidth={1.5} />
137
+ </div>
138
+ Setup cell
139
+ </ContextMenuItem>
128
140
  </ContextMenuContent>
129
141
  </ContextMenu>
130
142
  );
@@ -187,6 +187,7 @@ const CellEditorInternal = ({
187
187
  hotkeys: new OverridingHotkeyProvider(userConfig.keymap.overrides ?? {}),
188
188
  diagnosticsConfig: userConfig.diagnostics,
189
189
  displayConfig: userConfig.display,
190
+ inlineAiTooltip: userConfig.ai?.inline_tooltip ?? false,
190
191
  });
191
192
 
192
193
  extensions.push(
@@ -18,11 +18,7 @@ function passwordField() {
18
18
 
19
19
  function tokenField(label?: string, required?: boolean) {
20
20
  let field: z.ZodString | z.ZodOptional<z.ZodString> = z.string();
21
- if (required) {
22
- field = field.nonempty();
23
- } else {
24
- field = field.optional();
25
- }
21
+ field = required ? field.nonempty() : field.optional();
26
22
 
27
23
  field = field.describe(
28
24
  FieldOptions.of({
@@ -50,11 +46,7 @@ function warehouseNameField() {
50
46
 
51
47
  function uriField(label?: string, required?: boolean) {
52
48
  let field: z.ZodString | z.ZodOptional<z.ZodString> = z.string();
53
- if (required) {
54
- field = field.nonempty();
55
- } else {
56
- field = field.optional();
57
- }
49
+ field = required ? field.nonempty() : field.optional();
58
50
 
59
51
  return field.describe(
60
52
  FieldOptions.of({ label: label || "URI", optionRegex: ".*uri.*" }),
@@ -1051,12 +1051,16 @@ const SetupCellComponent = ({
1051
1051
  data-status={cellRuntime.status}
1052
1052
  ref={cellRef}
1053
1053
  {...mergeProps(navigationProps, {
1054
- className,
1054
+ className: cn(
1055
+ className,
1056
+ "focus:ring-1 focus:ring-(--blue-7) focus:ring-offset-0",
1057
+ ),
1055
1058
  onBlur: closeCompletionHandler,
1056
1059
  onKeyDown: resumeCompletionHandler,
1057
1060
  })}
1058
1061
  {...cellDomProps(cellId, cellData.name)}
1059
1062
  title={renderCellTitle()}
1063
+ tabIndex={-1}
1060
1064
  data-setup-cell={true}
1061
1065
  >
1062
1066
  <div className={cn("tray")} data-hidden={!isCellCodeShown}>
@@ -1,10 +1,6 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
- import {
4
- InfoIcon,
5
- NotebookPenIcon,
6
- SquareArrowOutUpRightIcon,
7
- } from "lucide-react";
3
+ import { NotebookPenIcon, SquareArrowOutUpRightIcon } from "lucide-react";
8
4
  import { Fragment, type JSX } from "react";
9
5
  import {
10
6
  Accordion,
@@ -499,37 +495,11 @@ export const MarimoErrorOutput = ({
499
495
  messages.push(
500
496
  <div key="sql-errors">
501
497
  {sqlErrors.map((error, idx) => {
502
- const line =
503
- error.sql_line == null ? null : Math.trunc(error?.sql_line) + 1;
504
- const col =
505
- error.sql_col == null ? null : Math.trunc(error?.sql_col) + 1;
506
498
  return (
507
499
  <div key={`sql-error-${idx}`} className="space-y-2 mt-2">
508
- <p className="text-muted-foreground font-medium">{error.msg}</p>
509
- {error.hint && (
510
- <div className="flex items-start gap-2">
511
- <InfoIcon
512
- size={11}
513
- className="text-muted-foreground mt-1 flex-shrink-0"
514
- />
515
- <p className="whitespace-pre-wrap text-sm text-muted-foreground">
516
- {error.hint}
517
- </p>
518
- </div>
519
- )}
520
- {error.sql_statement && (
521
- <pre
522
- lang="sql"
523
- className="text-xs bg-muted/80 rounded whitespace-pre-wrap p-3.5"
524
- >
525
- {error.sql_statement.trim()}
526
- </pre>
527
- )}
528
- {line !== null && col !== null && (
529
- <p className="text-xs text-muted-foreground">
530
- Error at line {line}, column {col}
531
- </p>
532
- )}
500
+ <p className="text-muted-foreground whitespace-pre-wrap">
501
+ {error.msg}
502
+ </p>
533
503
  </div>
534
504
  );
535
505
  })}
@@ -14,7 +14,7 @@ import {
14
14
  } from "lucide-react";
15
15
  import { useEffect } from "react";
16
16
  import { StartupLogsAlert } from "@/components/editor/alerts/startup-logs-alert";
17
- import { Cell } from "@/components/editor/Cell";
17
+ import { Cell } from "@/components/editor/notebook-cell";
18
18
  import { PackageAlert } from "@/components/editor/package-alert";
19
19
  import { SortableCellsProvider } from "@/components/sort/SortableCellsProvider";
20
20
  import { Button } from "@/components/ui/button";
@@ -191,7 +191,7 @@ describe("getDefaults", () => {
191
191
  });
192
192
  const result = getDefaults(schema) as { map: Map<string, number> };
193
193
  expect(result.map instanceof Map).toBe(true);
194
- expect(Array.from(result.map.entries())).toEqual([["a", 1]]);
194
+ expect([...result.map.entries()]).toEqual([["a", 1]]);
195
195
  });
196
196
 
197
197
  it("should handle ZodSet with default", () => {
@@ -200,7 +200,7 @@ describe("getDefaults", () => {
200
200
  });
201
201
  const result = getDefaults(schema) as { set: Set<string> };
202
202
  expect(result.set instanceof Set).toBe(true);
203
- expect(Array.from(result.set)).toEqual(["a", "b"]);
203
+ expect([...result.set]).toEqual(["a", "b"]);
204
204
  });
205
205
 
206
206
  it("should handle deeply nested defaults", () => {
@@ -0,0 +1,48 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import type { components } from "@marimo-team/marimo-api";
4
+ import { useState } from "react";
5
+ import { API } from "@/core/network/api";
6
+ import { useAsyncData } from "@/hooks/useAsyncData";
7
+ import { toast } from "../ui/use-toast";
8
+
9
+ export type MCPStatus = components["schemas"]["MCPStatusResponse"];
10
+ export type MCPRefreshResponse = components["schemas"]["MCPRefreshResponse"];
11
+
12
+ /**
13
+ * Hook to fetch MCP server status
14
+ */
15
+ export function useMCPStatus() {
16
+ return useAsyncData<MCPStatus>(async () => {
17
+ return API.get<MCPStatus>("/ai/mcp/status");
18
+ }, []);
19
+ }
20
+
21
+ /**
22
+ * Hook to refresh MCP server configuration
23
+ */
24
+ export function useMCPRefresh() {
25
+ const [isRefreshing, setIsRefreshing] = useState(false);
26
+
27
+ const refresh = async () => {
28
+ setIsRefreshing(true);
29
+ try {
30
+ await API.post<object, MCPRefreshResponse>("/ai/mcp/refresh", {});
31
+ toast({
32
+ title: "MCP refreshed",
33
+ description: "MCP server configuration has been refreshed successfully",
34
+ });
35
+ } catch (error) {
36
+ toast({
37
+ title: "Refresh failed",
38
+ description:
39
+ error instanceof Error ? error.message : "Failed to refresh MCP",
40
+ variant: "danger",
41
+ });
42
+ } finally {
43
+ setIsRefreshing(false);
44
+ }
45
+ };
46
+
47
+ return { refresh, isRefreshing };
48
+ }