@marimo-team/islands 0.17.8 → 0.18.0

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 (53) hide show
  1. package/dist/{Combination-BH_L276x.js → Combination-D68fi0fY.js} +22 -21
  2. package/dist/{ConnectedDataExplorerComponent-WbiFXhKG.js → ConnectedDataExplorerComponent-BUgUSo2B.js} +7 -7
  3. package/dist/{any-language-editor-YPQMljy9.js → any-language-editor-BS-Z5AY5.js} +3 -3
  4. package/dist/assets/__vite-browser-external-CSegkGa0.js +1 -0
  5. package/dist/assets/{worker-BrDpRi2I.js → worker-CiT2i-Vo.js} +2 -2
  6. package/dist/{error-banner-BqE1uF21.js → error-banner-CPLhCPHA.js} +24 -24
  7. package/dist/{esm-hR1r0nyt.js → esm-DxgKy8Wv.js} +1 -1
  8. package/dist/{formats-dvT8nDgH.js → formats-oddMfm9_.js} +27 -7
  9. package/dist/{glide-data-editor-B26PhZvE.js → glide-data-editor-BFv4VQnc.js} +4 -4
  10. package/dist/{label-D3LNCORf.js → label-Dsm6T1fr.js} +72 -72
  11. package/dist/main.js +359 -250
  12. package/dist/{mermaid-Dl3ywmV2.js → mermaid-BeGlg1JH.js} +2 -2
  13. package/dist/{react-vega-ypEMYp9o.js → react-vega-DDXWt_PN.js} +852 -1544
  14. package/dist/{react-vega-BIDT9Ttp.js → react-vega-DV2IwPx_.js} +1 -1
  15. package/dist/{spec-qDDGe5hl.js → spec-BotzCMo3.js} +2 -2
  16. package/dist/style.css +1 -1
  17. package/dist/{types-2eTEqSwS.js → types-IRrkdH-H.js} +14 -14
  18. package/dist/{useAsyncData-6gisQ4pR.js → useAsyncData-CsSW6_Zh.js} +1 -1
  19. package/dist/{useTheme-B-2frT0L.js → useTheme-D56Xlrez.js} +1 -0
  20. package/dist/{vega-component-C-bCSv1b.js → vega-component-CLjz4see.js} +6 -6
  21. package/package.json +2 -2
  22. package/src/components/chat/chat-panel.tsx +6 -2
  23. package/src/components/data-table/TableActions.tsx +18 -14
  24. package/src/components/data-table/data-table.tsx +3 -0
  25. package/src/components/editor/chrome/panels/packages-panel.tsx +3 -1
  26. package/src/components/editor/file-tree/__tests__/file-expolorer.test.ts +178 -0
  27. package/src/components/editor/file-tree/file-explorer.tsx +70 -1
  28. package/src/components/pages/home-page.tsx +8 -3
  29. package/src/core/ai/tools/__tests__/registry.test.ts +6 -2
  30. package/src/core/ai/tools/registry.ts +5 -2
  31. package/src/core/cells/__tests__/session.test.ts +0 -9
  32. package/src/core/cells/session.ts +0 -1
  33. package/src/core/codemirror/copilot/client.ts +21 -1
  34. package/src/core/codemirror/copilot/copilot-config.tsx +29 -1
  35. package/src/core/config/__tests__/config-schema.test.ts +2 -0
  36. package/src/core/config/config-schema.ts +1 -0
  37. package/src/core/packages/__tests__/package-input-utils.test.ts +93 -0
  38. package/src/core/packages/package-input-utils.ts +36 -0
  39. package/src/css/md.css +5 -0
  40. package/src/plugins/core/__test__/sanitize.test.ts +1 -1
  41. package/src/plugins/core/sanitize.ts +3 -1
  42. package/src/plugins/impl/DataTablePlugin.tsx +10 -1
  43. package/src/plugins/impl/chat/ChatPlugin.tsx +1 -0
  44. package/src/plugins/impl/chat/chat-ui.tsx +140 -10
  45. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -0
  46. package/src/plugins/layout/NavigationMenuPlugin.tsx +14 -3
  47. package/src/plugins/layout/ProgressPlugin.tsx +8 -5
  48. package/src/plugins/layout/StatPlugin.tsx +11 -4
  49. package/src/plugins/layout/__test__/ProgressPlugin.test.ts +37 -21
  50. package/src/utils/__tests__/urls.test.ts +165 -1
  51. package/src/utils/urls.ts +120 -0
  52. package/src/utils/vitals.ts +1 -1
  53. package/dist/assets/__vite-browser-external-BTNiCQ6O.js +0 -1
@@ -3,14 +3,14 @@ import { t as require_react } from "./react-DXQlph6m.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-DTozApu4.js";
4
4
  import { t as createLucideIcon } from "./createLucideIcon-ixdBmsqu.js";
5
5
  import { t as Check } from "./check-DvmFwGOM.js";
6
- import { A as upperFirst_default, C as createCollection, I as $b5e257d569688ac6$export$619500959fc48b26, L as X, M as $488c6ddbf4ef74c2$export$cc77c4ff7e8673c5, N as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, R as ChevronUp, S as useDirection, _ as menuLabelVariants, g as menuItemVariants, h as menuControlVariants, j as $a916eb452884faea$export$b7a616150fdb9f44, m as menuControlCheckVariants, p as menuContentCommon, v as menuSeparatorVariants, y as menuSubTriggerVariants, z as ChevronDown } from "./label-D3LNCORf.js";
6
+ import { A as upperFirst_default, C as createCollection, I as $b5e257d569688ac6$export$619500959fc48b26, L as X, M as $488c6ddbf4ef74c2$export$cc77c4ff7e8673c5, N as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, R as ChevronUp, S as useDirection, _ as menuLabelVariants, g as menuItemVariants, h as menuControlVariants, j as $a916eb452884faea$export$b7a616150fdb9f44, m as menuControlCheckVariants, p as menuContentCommon, v as menuSeparatorVariants, y as menuSubTriggerVariants, z as ChevronDown } from "./label-Dsm6T1fr.js";
7
7
  import { n as clsx_default } from "./clsx-JH8KOlt9.js";
8
8
  import { c as composeRefs, d as cn, l as useComposedRefs, o as createSlot, t as Button, u as Events } from "./button-Hye6-2X8.js";
9
9
  import { s as Logger } from "./hotkeys-C3KM59Ph.js";
10
- import { C as DismissableLayer, E as dispatchDiscreteCustomEvent, O as createContextScope, T as Primitive, _ as Content, c as withSmartCollisionBoundary, f as useControllableState, g as Arrow, h as Anchor, i as useFocusGuards, k as composeEventHandlers, l as StyleNamespace, m as Portal, n as hideOthers, o as MAX_HEIGHT_OFFSET, p as Presence, r as FocusScope, s as withFullScreenAsRoot, t as Combination_default, v as Root2$1, w as useCallbackRef, x as useId, y as createPopperScope } from "./Combination-BH_L276x.js";
10
+ import { A as composeEventHandlers, D as dispatchDiscreteCustomEvent, E as Primitive, S as useId, T as useCallbackRef, _ as Arrow, b as createPopperScope, c as withSmartCollisionBoundary, g as Anchor, h as Portal, i as useFocusGuards, k as createContextScope, m as Presence, n as hideOthers, o as MAX_HEIGHT_OFFSET, p as useControllableState, r as FocusScope, s as withFullScreenAsRoot, t as Combination_default, u as StyleNamespace, v as Content, w as DismissableLayer, y as Root2$1 } from "./Combination-D68fi0fY.js";
11
11
  import { t as require_jsx_runtime } from "./jsx-runtime-Duz3IlLt.js";
12
12
  import { t as require_react_dom } from "./react-dom-DBPBYhx0.js";
13
- import { m as useEvent_default } from "./useTheme-B-2frT0L.js";
13
+ import { m as useEvent_default } from "./useTheme-D56Xlrez.js";
14
14
  import { t as toString_default } from "./toString-MxUAFb1C.js";
15
15
  import { r as debounce_default, t as Constants } from "./constants-CN19buVX.js";
16
16
  import { t as memoizeLastValue } from "./once-BdyR-H8C.js";
@@ -3029,10 +3029,10 @@ var CONTENT_NAME$1 = "MenuContent", [MenuContentProvider, useMenuContentContext]
3029
3029
  onDismiss: () => n.onOpenChange(false)
3030
3030
  });
3031
3031
  }), Slot = createSlot("MenuContent.ScrollLock"), MenuContentImpl = import_react.forwardRef((e2, t) => {
3032
- let { __scopeMenu: n, loop: r = false, trapFocus: i, onOpenAutoFocus: a, onCloseAutoFocus: o, disableOutsidePointerEvents: s, onEntryFocus: c, onEscapeKeyDown: l, onPointerDownOutside: u, onFocusOutside: d, onInteractOutside: f, onDismiss: p, disableOutsideScroll: m, ...h } = e2, g = useMenuContext(CONTENT_NAME$1, n), _ = useMenuRootContext(CONTENT_NAME$1, n), v = usePopperScope(n), y = useRovingFocusGroupScope(n), b = useCollection(n), [x, S] = import_react.useState(null), C = import_react.useRef(null), w = useComposedRefs(t, C, g.onContentChange), E = import_react.useRef(0), D = import_react.useRef(""), O = import_react.useRef(0), k = import_react.useRef(null), j = import_react.useRef("right"), M = import_react.useRef(0), N = m ? Combination_default : import_react.Fragment, F = m ? {
3032
+ let { __scopeMenu: n, loop: r = false, trapFocus: i, onOpenAutoFocus: a, onCloseAutoFocus: o, disableOutsidePointerEvents: s, onEntryFocus: c, onEscapeKeyDown: l, onPointerDownOutside: u, onFocusOutside: d, onInteractOutside: f, onDismiss: p, disableOutsideScroll: m, ...h } = e2, g = useMenuContext(CONTENT_NAME$1, n), _ = useMenuRootContext(CONTENT_NAME$1, n), v = usePopperScope(n), y = useRovingFocusGroupScope(n), b = useCollection(n), [x, S] = import_react.useState(null), C = import_react.useRef(null), w = useComposedRefs(t, C, g.onContentChange), E = import_react.useRef(0), D = import_react.useRef(""), O = import_react.useRef(0), k = import_react.useRef(null), j = import_react.useRef("right"), M = import_react.useRef(0), N = m ? Combination_default : import_react.Fragment, P = m ? {
3033
3033
  as: Slot,
3034
3034
  allowPinchZoom: true
3035
- } : void 0, I = (e3) => {
3035
+ } : void 0, F = (e3) => {
3036
3036
  var _a, _b;
3037
3037
  let t2 = D.current + e3, n2 = b().filter((e4) => !e4.disabled), r2 = document.activeElement, i2 = (_a = n2.find((e4) => e4.ref.current === r2)) == null ? void 0 : _a.textValue, a2 = getNextMatch(n2.map((e4) => e4.textValue), t2, i2), o2 = (_b = n2.find((e4) => e4.textValue === a2)) == null ? void 0 : _b.ref.current;
3038
3038
  (function e4(t3) {
@@ -3040,7 +3040,7 @@ var CONTENT_NAME$1 = "MenuContent", [MenuContentProvider, useMenuContentContext]
3040
3040
  })(t2), o2 && setTimeout(() => o2.focus());
3041
3041
  };
3042
3042
  import_react.useEffect(() => () => window.clearTimeout(E.current), []), useFocusGuards();
3043
- let L = import_react.useCallback((e3) => {
3043
+ let I = import_react.useCallback((e3) => {
3044
3044
  var _a, _b;
3045
3045
  return j.current === ((_a = k.current) == null ? void 0 : _a.side) && isPointerInGraceArea(e3, (_b = k.current) == null ? void 0 : _b.area);
3046
3046
  }, []);
@@ -3048,21 +3048,21 @@ var CONTENT_NAME$1 = "MenuContent", [MenuContentProvider, useMenuContentContext]
3048
3048
  scope: n,
3049
3049
  searchRef: D,
3050
3050
  onItemEnter: import_react.useCallback((e3) => {
3051
- L(e3) && e3.preventDefault();
3052
- }, [L]),
3051
+ I(e3) && e3.preventDefault();
3052
+ }, [I]),
3053
3053
  onItemLeave: import_react.useCallback((e3) => {
3054
3054
  var _a;
3055
- L(e3) || ((_a = C.current) == null ? void 0 : _a.focus(), S(null));
3056
- }, [L]),
3055
+ I(e3) || ((_a = C.current) == null ? void 0 : _a.focus(), S(null));
3056
+ }, [I]),
3057
3057
  onTriggerLeave: import_react.useCallback((e3) => {
3058
- L(e3) && e3.preventDefault();
3059
- }, [L]),
3058
+ I(e3) && e3.preventDefault();
3059
+ }, [I]),
3060
3060
  pointerGraceTimerRef: O,
3061
3061
  onPointerGraceIntentChange: import_react.useCallback((e3) => {
3062
3062
  k.current = e3;
3063
3063
  }, []),
3064
3064
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(N, {
3065
- ...F,
3065
+ ...P,
3066
3066
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FocusScope, {
3067
3067
  asChild: true,
3068
3068
  trapped: i,
@@ -3106,7 +3106,7 @@ var CONTENT_NAME$1 = "MenuContent", [MenuContentProvider, useMenuContentContext]
3106
3106
  },
3107
3107
  onKeyDown: composeEventHandlers(h.onKeyDown, (e3) => {
3108
3108
  let t2 = e3.target.closest("[data-radix-menu-content]") === e3.currentTarget, n2 = e3.ctrlKey || e3.altKey || e3.metaKey, r2 = e3.key.length === 1;
3109
- t2 && (e3.key === "Tab" && e3.preventDefault(), !n2 && r2 && I(e3.key));
3109
+ t2 && (e3.key === "Tab" && e3.preventDefault(), !n2 && r2 && F(e3.key));
3110
3110
  let i2 = C.current;
3111
3111
  if (e3.target !== i2 || !FIRST_LAST_KEYS.includes(e3.key)) return;
3112
3112
  e3.preventDefault();
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-DgPTj83v.js";
2
2
  import { t as require_react } from "./react-DXQlph6m.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-DTozApu4.js";
4
- import { m as useEvent_default } from "./useTheme-B-2frT0L.js";
4
+ import { m as useEvent_default } from "./useTheme-D56Xlrez.js";
5
5
  import { t as invariant } from "./invariant-BEpJUgj9.js";
6
6
  var import_compiler_runtime = /* @__PURE__ */ __toESM(require_compiler_runtime(), 1), import_react = /* @__PURE__ */ __toESM(require_react(), 1), Result = {
7
7
  error(e, s) {
@@ -487,6 +487,7 @@ const UserConfigSchema = looseObject({
487
487
  "lazy",
488
488
  "autorun"
489
489
  ]).prefault("off"),
490
+ reactive_tests: boolean().prefault(true),
490
491
  watcher_on_save: _enum(["lazy", "autorun"]).prefault("lazy"),
491
492
  default_sql_output: _enum(VALID_SQL_OUTPUT_FORMATS).prefault("auto"),
492
493
  default_auto_download: array(_enum(AUTO_DOWNLOAD_FORMATS)).prefault([])
@@ -2,15 +2,15 @@ import { s as __toESM } from "./chunk-DgPTj83v.js";
2
2
  import { t as require_react } from "./react-DXQlph6m.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-DTozApu4.js";
4
4
  import "./createLucideIcon-ixdBmsqu.js";
5
- import { S as CircleQuestionMark, h as asRemoteURL, i as Alert, n as useDeepCompareMemoize, o as AlertTitle, s as isValid, t as arrow } from "./formats-dvT8nDgH.js";
5
+ import { C as CircleQuestionMark, h as asRemoteURL, i as Alert, n as useDeepCompareMemoize, o as AlertTitle, s as isValid, t as arrow } from "./formats-oddMfm9_.js";
6
6
  import "./clsx-JH8KOlt9.js";
7
7
  import { u as Events } from "./button-Hye6-2X8.js";
8
8
  import { o as Objects, s as Logger } from "./hotkeys-C3KM59Ph.js";
9
- import "./Combination-BH_L276x.js";
9
+ import "./Combination-D68fi0fY.js";
10
10
  import { t as require_jsx_runtime } from "./jsx-runtime-Duz3IlLt.js";
11
11
  import "./react-dom-DBPBYhx0.js";
12
- import { c as Tooltip, n as ErrorBanner } from "./error-banner-BqE1uF21.js";
13
- import { m as useEvent_default, t as useTheme } from "./useTheme-B-2frT0L.js";
12
+ import { c as Tooltip, n as ErrorBanner } from "./error-banner-CPLhCPHA.js";
13
+ import { m as useEvent_default, t as useTheme } from "./useTheme-D56Xlrez.js";
14
14
  import "./isArrayLikeObject-LulrZoeO.js";
15
15
  import "./isSymbol-bQx0U8Lk.js";
16
16
  import "./toNumber-C6hfsNmF.js";
@@ -24,11 +24,11 @@ import { a as tooltipHandler, n as vegaLoadData } from "./loader-BCInj1Mf.js";
24
24
  import { t as uniq_default } from "./uniq-CMMw6UdM.js";
25
25
  import "./zod-AnT9BweX.js";
26
26
  import "./invariant-BEpJUgj9.js";
27
- import { t as useAsyncData } from "./useAsyncData-6gisQ4pR.js";
27
+ import { t as useAsyncData } from "./useAsyncData-CsSW6_Zh.js";
28
28
  import { n as formats } from "./vega-loader.browser-DQF9pFhs.js";
29
29
  import "./precisionRound-vxMVRvgy.js";
30
30
  import "./linear-B8XR-NVr.js";
31
- import { t as j } from "./react-vega-ypEMYp9o.js";
31
+ import { t as j } from "./react-vega-DDXWt_PN.js";
32
32
  import "./ordinal-DncHRC0c.js";
33
33
  import "./time-DRnNkqSS.js";
34
34
  import "./range-CqZlY8CG.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.17.8",
3
+ "version": "0.18.0",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -168,7 +168,7 @@
168
168
  "typescript-memoize": "^1.1.1",
169
169
  "use-acp": "0.2.5",
170
170
  "use-resize-observer": "^9.1.0",
171
- "vega-lite": "6.2.0",
171
+ "vega-lite": "6.4.0",
172
172
  "vega-loader": "^5.1.0",
173
173
  "vega-parser": "^7.1.0",
174
174
  "vega-tooltip": "^1.1.0",
@@ -182,7 +182,7 @@ const ChatMessageDisplay: React.FC<ChatMessageProps> = memo(
182
182
  const content = textParts.map((p) => p.text).join("\n");
183
183
 
184
184
  return (
185
- <div className="w-[95%] break-words">
185
+ <div className="w-[95%] wrap-break-word">
186
186
  <div className="absolute right-1 top-1 opacity-0 group-hover:opacity-100 transition-opacity">
187
187
  <CopyClipboardIcon className="h-3 w-3" value={content || ""} />
188
188
  </div>
@@ -567,9 +567,13 @@ const ChatPanelBody = () => {
567
567
  options.messages,
568
568
  );
569
569
 
570
+ // Call this here to ensure the value is not stale
571
+ const chatMode = store.get(aiAtom)?.mode || DEFAULT_MODE;
572
+ const tools = FRONTEND_TOOL_REGISTRY.getToolSchemas(chatMode);
573
+
570
574
  return {
571
575
  body: {
572
- tools: FRONTEND_TOOL_REGISTRY.getToolSchemas(),
576
+ tools,
573
577
  ...options,
574
578
  ...completionBody,
575
579
  },
@@ -34,6 +34,7 @@ interface TableActionsProps<TData> {
34
34
  toggleDisplayHeader?: () => void;
35
35
  showChartBuilder?: boolean;
36
36
  showColumnExplorer?: boolean;
37
+ showRowExplorer?: boolean;
37
38
  showPageSizeSelector?: boolean;
38
39
  togglePanel?: (panelType: PanelType) => void;
39
40
  isPanelOpen?: (panelType: PanelType) => boolean;
@@ -55,6 +56,7 @@ export const TableActions = <TData,>({
55
56
  toggleDisplayHeader,
56
57
  showChartBuilder,
57
58
  showColumnExplorer,
59
+ showRowExplorer,
58
60
  showPageSizeSelector,
59
61
  togglePanel,
60
62
  isPanelOpen,
@@ -132,20 +134,22 @@ export const TableActions = <TData,>({
132
134
  )}
133
135
  {togglePanel && isPanelOpen !== undefined && (
134
136
  <>
135
- <Tooltip content="Toggle row viewer">
136
- <Button
137
- variant="text"
138
- size="xs"
139
- onClick={() => togglePanel("row-viewer")}
140
- >
141
- <PanelRightIcon
142
- className={cn(
143
- "w-4 h-4 text-muted-foreground",
144
- isPanelOpen("row-viewer") && "text-primary",
145
- )}
146
- />
147
- </Button>
148
- </Tooltip>
137
+ {showRowExplorer && (
138
+ <Tooltip content="Toggle row viewer">
139
+ <Button
140
+ variant="text"
141
+ size="xs"
142
+ onClick={() => togglePanel("row-viewer")}
143
+ >
144
+ <PanelRightIcon
145
+ className={cn(
146
+ "w-4 h-4 text-muted-foreground",
147
+ isPanelOpen("row-viewer") && "text-primary",
148
+ )}
149
+ />
150
+ </Button>
151
+ </Tooltip>
152
+ )}
149
153
  {showColumnExplorer && (
150
154
  <Tooltip content="Toggle column explorer">
151
155
  <Button
@@ -91,6 +91,7 @@ interface DataTableProps<TData> extends Partial<DownloadActionProps> {
91
91
  showChartBuilder?: boolean;
92
92
  showPageSizeSelector?: boolean;
93
93
  showColumnExplorer?: boolean;
94
+ showRowExplorer?: boolean;
94
95
  togglePanel?: (panelType: PanelType) => void;
95
96
  isPanelOpen?: (panelType: PanelType) => boolean;
96
97
  }
@@ -133,6 +134,7 @@ const DataTableInternal = <TData,>({
133
134
  showChartBuilder,
134
135
  showPageSizeSelector,
135
136
  showColumnExplorer,
137
+ showRowExplorer,
136
138
  togglePanel,
137
139
  isPanelOpen,
138
140
  viewedRowIdx,
@@ -337,6 +339,7 @@ const DataTableInternal = <TData,>({
337
339
  showChartBuilder={showChartBuilder}
338
340
  showPageSizeSelector={showPageSizeSelector}
339
341
  showColumnExplorer={showColumnExplorer}
342
+ showRowExplorer={showRowExplorer}
340
343
  togglePanel={togglePanel}
341
344
  isPanelOpen={isPanelOpen}
342
345
  tableLoading={reloading}
@@ -23,6 +23,7 @@ import { toast } from "@/components/ui/use-toast";
23
23
  import { useResolvedMarimoConfig } from "@/core/config/config";
24
24
  import { useRequestClient } from "@/core/network/requests";
25
25
  import type { DependencyTreeNode } from "@/core/network/types";
26
+ import { stripPackageManagerPrefix } from "@/core/packages/package-input-utils";
26
27
  import {
27
28
  showRemovePackageToast,
28
29
  showUpgradePackageToast,
@@ -191,8 +192,9 @@ const InstallPackageForm: React.FC<{
191
192
  };
192
193
 
193
194
  const installPackages = () => {
195
+ const cleanedInput = stripPackageManagerPrefix(input);
194
196
  handleInstallPackages(
195
- input.split(",").map((p) => p.trim()),
197
+ cleanedInput.split(",").map((p) => p.trim()),
196
198
  onSuccessInstallPackages,
197
199
  );
198
200
  };
@@ -0,0 +1,178 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { FileInfo } from "@/core/network/types";
3
+ import { filterHiddenTree, isDirectoryOrFileHidden } from "../file-explorer";
4
+
5
+ // Helpers to build FileInfo objects for tests
6
+ const file = (name: string, path: string): FileInfo => ({
7
+ id: path,
8
+ name,
9
+ path,
10
+ isDirectory: false,
11
+ isMarimoFile: false,
12
+ children: [],
13
+ });
14
+
15
+ const dir = (
16
+ name: string,
17
+ path: string,
18
+ children: FileInfo[] = [],
19
+ ): FileInfo => ({
20
+ id: path,
21
+ name,
22
+ path,
23
+ isDirectory: true,
24
+ isMarimoFile: false,
25
+ children,
26
+ });
27
+
28
+ describe("isDirectoryOrFileHidden", () => {
29
+ it("should return true for files starting with dot", () => {
30
+ expect(isDirectoryOrFileHidden(".git")).toBe(true);
31
+ expect(isDirectoryOrFileHidden(".env")).toBe(true);
32
+ expect(isDirectoryOrFileHidden(".gitignore")).toBe(true);
33
+ });
34
+
35
+ it("should return false for normal files", () => {
36
+ expect(isDirectoryOrFileHidden("README.md")).toBe(false);
37
+ expect(isDirectoryOrFileHidden("package.json")).toBe(false);
38
+ expect(isDirectoryOrFileHidden("index.ts")).toBe(false);
39
+ });
40
+
41
+ it("should return false for files with dots in the middle", () => {
42
+ expect(isDirectoryOrFileHidden("file.test.ts")).toBe(false);
43
+ expect(isDirectoryOrFileHidden("my.config.js")).toBe(false);
44
+ });
45
+ });
46
+
47
+ describe("filterHiddenTree", () => {
48
+ it("should return all items when showHidden is true", () => {
49
+ const list: FileInfo[] = [
50
+ dir(".git", "/.git", []),
51
+ file("README.md", "/README.md"),
52
+ file(".env", "/.env"),
53
+ ];
54
+
55
+ const result = filterHiddenTree(list, true);
56
+
57
+ expect(result).toBe(list); // should be the exact same reference
58
+ expect(result).toHaveLength(3);
59
+ });
60
+
61
+ it("should filter out hidden files when showHidden is false", () => {
62
+ const list: FileInfo[] = [
63
+ dir(".git", "/.git"),
64
+ file("README.md", "/README.md"),
65
+ file(".env", "/.env"),
66
+ file("package.json", "/package.json"),
67
+ ];
68
+
69
+ const result = filterHiddenTree(list, false);
70
+
71
+ expect(result).toHaveLength(2);
72
+ expect(result[0].name).toBe("README.md");
73
+ expect(result[1].name).toBe("package.json");
74
+ });
75
+
76
+ it("should filter hidden directories recursively", () => {
77
+ const list: FileInfo[] = [
78
+ dir("src", "/src", [
79
+ file("index.ts", "/src/index.ts"),
80
+ file(".DS_Store", "/src/.DS_Store"),
81
+ file("utils.ts", "/src/utils.ts"),
82
+ ]),
83
+ dir(".git", "/.git", [file("config", "/.git/config")]),
84
+ ];
85
+
86
+ const result = filterHiddenTree(list, false);
87
+
88
+ expect(result).toHaveLength(1);
89
+ expect(result[0].name).toBe("src");
90
+ expect(result[0].children).toHaveLength(2);
91
+ expect(result[0].children?.[0].name).toBe("index.ts");
92
+ expect(result[0].children?.[1].name).toBe("utils.ts");
93
+ });
94
+
95
+ it("should handle nested hidden files", () => {
96
+ const list: FileInfo[] = [
97
+ dir("project", "/project", [
98
+ dir("src", "/project/src", [
99
+ file("index.ts", "/project/src/index.ts"),
100
+ file(".backup", "/project/src/.backup"),
101
+ ]),
102
+ file(".env", "/project/.env"),
103
+ ]),
104
+ ];
105
+
106
+ const result = filterHiddenTree(list, false);
107
+
108
+ expect(result).toHaveLength(1);
109
+ expect(result[0].children).toHaveLength(1);
110
+ expect(result[0].children?.[0].name).toBe("src");
111
+ expect(result[0].children?.[0].children).toHaveLength(1);
112
+ expect(result[0].children?.[0].children?.[0].name).toBe("index.ts");
113
+ });
114
+
115
+ it("should preserve directory structure when no children are filtered", () => {
116
+ const list: FileInfo[] = [
117
+ dir("src", "/src", [
118
+ file("index.ts", "/src/index.ts"),
119
+ file("utils.ts", "/src/utils.ts"),
120
+ ]),
121
+ ];
122
+
123
+ const result = filterHiddenTree(list, true);
124
+
125
+ // Should return the same reference since nothing changed
126
+ expect(result[0]).toBe(list[0]);
127
+ });
128
+
129
+ it("should create new object only when children are filtered", () => {
130
+ const list: FileInfo[] = [
131
+ dir("src", "/src", [
132
+ file("index.ts", "/src/index.ts"),
133
+ file(".hidden", "/src/.hidden"),
134
+ ]),
135
+ ];
136
+
137
+ const result = filterHiddenTree(list, false);
138
+
139
+ // Should be a new object since children changed
140
+ expect(result[0]).not.toBe(list[0]);
141
+ expect(result[0].children).not.toBe(list[0].children);
142
+ });
143
+
144
+ it("should handle empty list", () => {
145
+ const result = filterHiddenTree([], false);
146
+ expect(result).toEqual([]);
147
+ });
148
+
149
+ it("should handle empty children arrays", () => {
150
+ const list: FileInfo[] = [dir("empty-dir", "/empty-dir", [])];
151
+
152
+ const result = filterHiddenTree(list, false);
153
+
154
+ expect(result).toHaveLength(1);
155
+ expect(result[0].children).toEqual([]);
156
+ });
157
+
158
+ it("should handle deeply nested structures", () => {
159
+ const list: FileInfo[] = [
160
+ dir("level1", "/level1", [
161
+ dir("level2", "/level1/level2", [
162
+ dir("level3", "/level1/level2/level3", [
163
+ file("file.ts", "/level1/level2/level3/file.ts"),
164
+ file(".hidden", "/level1/level2/level3/.hidden"),
165
+ ]),
166
+ ]),
167
+ file(".ignore", "/level1/.ignore"),
168
+ ]),
169
+ ];
170
+
171
+ const result = filterHiddenTree(list, false);
172
+
173
+ expect(result[0].children?.[0].children?.[0].children).toHaveLength(1);
174
+ expect(result[0].children?.[0].children?.[0].children?.[0].name).toBe(
175
+ "file.ts",
176
+ );
177
+ });
178
+ });
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
3
  import { useAtom } from "jotai";
4
+ import { atomWithStorage } from "jotai/utils";
4
5
  import {
5
6
  ArrowLeftIcon,
6
7
  BetweenHorizontalStartIcon,
@@ -12,6 +13,7 @@ import {
12
13
  DownloadIcon,
13
14
  Edit3Icon,
14
15
  ExternalLinkIcon,
16
+ EyeOffIcon,
15
17
  FilePlus2Icon,
16
18
  FolderPlusIcon,
17
19
  ListTreeIcon,
@@ -56,6 +58,7 @@ import { downloadBlob } from "@/utils/download";
56
58
  import { openNotebook } from "@/utils/links";
57
59
  import type { FilePath } from "@/utils/paths";
58
60
  import { fileSplit } from "@/utils/pathUtils";
61
+ import { jotaiJsonStorage } from "@/utils/storage/jotai";
59
62
  import marimoIcon from "../../../assets/icon-32x32.png";
60
63
  import { FileViewer } from "./file-viewer";
61
64
  import type { RequestingTree } from "./requesting-tree";
@@ -68,6 +71,15 @@ import {
68
71
  } from "./types";
69
72
  import { useFileExplorerUpload } from "./upload";
70
73
 
74
+ const hiddenFilesState = atomWithStorage(
75
+ "marimo:showHiddenFiles",
76
+ true,
77
+ jotaiJsonStorage,
78
+ {
79
+ getOnInit: true,
80
+ },
81
+ );
82
+
71
83
  const RequestingTreeContext = React.createContext<RequestingTree | null>(null);
72
84
 
73
85
  export const FileExplorer: React.FC<{
@@ -77,6 +89,9 @@ export const FileExplorer: React.FC<{
77
89
  const [tree] = useAtom(treeAtom);
78
90
  const [data, setData] = useState<FileInfo[]>([]);
79
91
  const [openFile, setOpenFile] = useState<FileInfo | null>(null);
92
+ const [showHiddenFiles, setShowHiddenFiles] =
93
+ useAtom<boolean>(hiddenFilesState);
94
+
80
95
  const { openPrompt } = useImperativeModal();
81
96
  // Keep external state to remember which folders are open
82
97
  // when this component is unmounted
@@ -87,6 +102,11 @@ export const FileExplorer: React.FC<{
87
102
  tree.refreshAll(Object.keys(openState).filter((id) => openState[id]));
88
103
  });
89
104
 
105
+ const handleHiddenFilesToggle = useEvent(() => {
106
+ const newValue = !showHiddenFiles;
107
+ setShowHiddenFiles(newValue);
108
+ });
109
+
90
110
  const handleCreateFolder = useEvent(async () => {
91
111
  openPrompt({
92
112
  title: "Folder name",
@@ -148,10 +168,15 @@ export const FileExplorer: React.FC<{
148
168
  );
149
169
  }
150
170
 
171
+ const visibleData = React.useMemo(
172
+ () => filterHiddenTree(data, showHiddenFiles),
173
+ [data, showHiddenFiles],
174
+ );
151
175
  return (
152
176
  <>
153
177
  <Toolbar
154
178
  onRefresh={handleRefresh}
179
+ onHidden={handleHiddenFilesToggle}
155
180
  onCreateFile={handleCreateFile}
156
181
  onCreateFolder={handleCreateFolder}
157
182
  onCollapseAll={handleCollapseAll}
@@ -163,7 +188,7 @@ export const FileExplorer: React.FC<{
163
188
  ref={treeRef}
164
189
  height={height - 33}
165
190
  className="h-full"
166
- data={data}
191
+ data={visibleData}
167
192
  initialOpenState={openState}
168
193
  openByDefault={false}
169
194
  // Hide the drop cursor
@@ -215,6 +240,7 @@ const INDENT_STEP = 15;
215
240
 
216
241
  interface ToolbarProps {
217
242
  onRefresh: () => void;
243
+ onHidden: () => void;
218
244
  onCreateFile: () => void;
219
245
  onCreateFolder: () => void;
220
246
  onCollapseAll: () => void;
@@ -223,6 +249,7 @@ interface ToolbarProps {
223
249
 
224
250
  const Toolbar = ({
225
251
  onRefresh,
252
+ onHidden,
226
253
  onCreateFile,
227
254
  onCreateFolder,
228
255
  onCollapseAll,
@@ -277,6 +304,16 @@ const Toolbar = ({
277
304
  <RefreshCcwIcon size={16} />
278
305
  </Button>
279
306
  </Tooltip>
307
+ <Tooltip content="Toggle hidden files">
308
+ <Button
309
+ data-testid="file-explorer-hidden-files-button"
310
+ onClick={onHidden}
311
+ variant="text"
312
+ size="xs"
313
+ >
314
+ <EyeOffIcon size={16} />
315
+ </Button>
316
+ </Tooltip>
280
317
  <Tooltip content="Collapse all folders">
281
318
  <Button
282
319
  data-testid="file-explorer-collapse-button"
@@ -677,3 +714,35 @@ function openMarimoNotebook(
677
714
  event.preventDefault();
678
715
  openNotebook(path);
679
716
  }
717
+
718
+ export function filterHiddenTree(
719
+ list: FileInfo[],
720
+ showHidden: boolean,
721
+ ): FileInfo[] {
722
+ if (showHidden) {
723
+ return list;
724
+ }
725
+
726
+ const out: FileInfo[] = [];
727
+ for (const item of list) {
728
+ if (isDirectoryOrFileHidden(item.name)) {
729
+ continue;
730
+ }
731
+ let next = item;
732
+ if (item.children && item.children.length) {
733
+ const kids = filterHiddenTree(item.children, showHidden);
734
+ if (kids !== item.children) {
735
+ next = { ...item, children: kids };
736
+ }
737
+ }
738
+ out.push(next);
739
+ }
740
+ return out;
741
+ }
742
+
743
+ export function isDirectoryOrFileHidden(filename: string): boolean {
744
+ if (filename.startsWith(".")) {
745
+ return true;
746
+ }
747
+ return false;
748
+ }
@@ -195,10 +195,15 @@ const WorkspaceNotebooks: React.FC = () => {
195
195
  }
196
196
  >
197
197
  Workspace
198
- <RefreshCcwIcon
199
- className="w-4 h-4 ml-1 cursor-pointer opacity-70 hover:opacity-100"
198
+ <Button
199
+ variant="text"
200
+ size="icon"
201
+ className="w-4 h-4 ml-1 p-0 opacity-70 hover:opacity-100"
200
202
  onClick={() => refetch()}
201
- />
203
+ aria-label="Refresh workspace"
204
+ >
205
+ <RefreshCcwIcon className="w-4 h-4" />
206
+ </Button>
202
207
  {isFetching && <Spinner size="small" />}
203
208
  </Header>
204
209
  <div className="flex flex-col divide-y divide-(--slate-3) border rounded overflow-hidden max-h-192 overflow-y-auto shadow-sm bg-background">
@@ -63,7 +63,7 @@ describe("FrontendToolRegistry", () => {
63
63
  it("returns tool schemas with expected shape and memoizes the result", () => {
64
64
  const registry = new FrontendToolRegistry([new TestFrontendTool()]);
65
65
 
66
- const schemas1 = registry.getToolSchemas();
66
+ const schemas1 = registry.getToolSchemas("ask");
67
67
  expect(Array.isArray(schemas1)).toBe(true);
68
68
  expect(schemas1.length).toBe(1);
69
69
 
@@ -80,7 +80,11 @@ describe("FrontendToolRegistry", () => {
80
80
  expect(properties && typeof properties === "object").toBe(true);
81
81
  expect("name" in (properties ?? {})).toBe(true);
82
82
 
83
- const schemas2 = registry.getToolSchemas();
83
+ const schemas2 = registry.getToolSchemas("ask");
84
84
  expect(schemas2).toBe(schemas1);
85
+
86
+ // Should not include tools for other modes
87
+ const schemas3 = registry.getToolSchemas("agent");
88
+ expect(schemas3.length).toBe(0);
85
89
  });
86
90
  });
@@ -123,8 +123,11 @@ export class FrontendToolRegistry {
123
123
  }
124
124
 
125
125
  @Memoize()
126
- getToolSchemas(): FrontendToolDefinition[] {
127
- return [...this.tools.values()].map((tool) => ({
126
+ getToolSchemas(mode: CopilotMode): FrontendToolDefinition[] {
127
+ const tools = [...this.tools.values()].filter((tool) =>
128
+ tool.mode.includes(mode),
129
+ );
130
+ return tools.map((tool) => ({
128
131
  name: tool.name,
129
132
  description: formatToolDescription(tool.description),
130
133
  parameters: z.toJSONSchema(tool.schema),