@marimo-team/islands 0.23.15-dev4 → 0.23.15-dev41

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 (92) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CU4fZJzG.js → ConnectedDataExplorerComponent-7p7rt9iw.js} +4 -4
  2. package/dist/{ErrorBoundary-DE6tzZf-.js → ErrorBoundary-B_CAG7_e.js} +1 -1
  3. package/dist/{ImageComparisonComponent-CHrI72em.js → ImageComparisonComponent-1i5IBhon.js} +173 -129
  4. package/dist/assets/__vite-browser-external-CjNAy01L.js +1 -0
  5. package/dist/assets/worker-B3XPCb6y.js +98 -0
  6. package/dist/{chat-ui-DinOvbWu.js → chat-ui-C0z13gJB.js} +3111 -3111
  7. package/dist/{code-visibility-Dk8cVOny.js → code-visibility-COokQS5X.js} +946 -864
  8. package/dist/{constants-T20xxyNf.js → debounce-BOD3DbfP.js} +1 -24
  9. package/dist/{formats-Dzx4J_z1.js → formats-CitsMtUm.js} +1 -1
  10. package/dist/{glide-data-editor-CjTu7ukN.js → glide-data-editor-CwZz71BD.js} +3 -3
  11. package/dist/{html-to-image-_wGfk8V-.js → html-to-image-CEo5pRYw.js} +2291 -2250
  12. package/dist/{input-DtsN7xm-.js → input-BGPrFH3g.js} +2 -2
  13. package/dist/main.js +1297 -1201
  14. package/dist/{mermaid-BYqXy_NE.js → mermaid-UdmxG2PZ.js} +2 -2
  15. package/dist/{process-output-Mh4UrjwM.js → process-output-BOvvilRG.js} +1 -1
  16. package/dist/{reveal-component-B5DXLyO7.js → reveal-component-D3ySPwH5.js} +618 -605
  17. package/dist/{spec-Cz-Bj1JI.js → spec-DSs9v0xx.js} +1 -1
  18. package/dist/style.css +1 -1
  19. package/dist/{toDate-CWNNlFEX.js → toDate-BRJgtOGm.js} +14 -5
  20. package/dist/{useAsyncData-KfHB8wQR.js → useAsyncData-BSOyAbac.js} +1 -1
  21. package/dist/{useDeepCompareMemoize-nJMtxhm4.js → useDeepCompareMemoize-BTLeWzuR.js} +1 -1
  22. package/dist/{useLifecycle-DegSo0lV.js → useLifecycle-C6wHjkhW.js} +1 -1
  23. package/dist/{useTheme-6eZ3GOTS.js → useTheme-Df_vGflw.js} +62 -27
  24. package/dist/{vega-component-DzyyM9fc.js → vega-component-BAvmvTjO.js} +6 -6
  25. package/package.json +1 -1
  26. package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
  27. package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
  28. package/src/components/chat/chat-panel.tsx +196 -67
  29. package/src/components/chat/chat-utils.ts +111 -2
  30. package/src/components/editor/SortableCell.tsx +6 -2
  31. package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
  32. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
  33. package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
  34. package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
  35. package/src/components/editor/ai/add-cell-with-ai.tsx +14 -5
  36. package/src/components/editor/ai/ai-completion-editor.tsx +4 -1
  37. package/src/components/editor/cell/cell-context-menu.tsx +7 -0
  38. package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
  39. package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
  40. package/src/components/editor/columns/cell-column.tsx +34 -4
  41. package/src/components/editor/columns/sortable-column.tsx +24 -4
  42. package/src/components/editor/file-tree/download.ts +46 -0
  43. package/src/components/editor/file-tree/file-explorer.tsx +3 -21
  44. package/src/components/editor/file-tree/file-viewer.tsx +4 -27
  45. package/src/components/editor/navigation/__tests__/navigation.test.ts +33 -0
  46. package/src/components/editor/navigation/navigation.ts +8 -1
  47. package/src/components/editor/notebook-cell.tsx +203 -106
  48. package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
  49. package/src/components/editor/renderers/cell-array.tsx +26 -13
  50. package/src/components/editor/renderers/cells-renderer.tsx +8 -2
  51. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
  52. package/src/components/editor/renderers/slides-layout/types.ts +4 -0
  53. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
  54. package/src/components/slides/reveal-component.tsx +37 -4
  55. package/src/components/slides/slide-form.tsx +72 -0
  56. package/src/components/ui/toast.tsx +11 -3
  57. package/src/components/ui/toaster.tsx +65 -9
  58. package/src/core/cells/__tests__/utils.test.ts +59 -0
  59. package/src/core/cells/utils.ts +51 -0
  60. package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +97 -3
  61. package/src/core/codemirror/completion/signature-hint.ts +78 -11
  62. package/src/core/constants.ts +6 -0
  63. package/src/core/islands/__tests__/bridge.test.ts +341 -46
  64. package/src/core/islands/__tests__/main.test.ts +176 -0
  65. package/src/core/islands/__tests__/parse.test.ts +105 -0
  66. package/src/core/islands/bootstrap.ts +8 -3
  67. package/src/core/islands/bridge.ts +116 -23
  68. package/src/core/islands/components/__tests__/web-components.test.tsx +214 -0
  69. package/src/core/islands/components/web-components.tsx +76 -15
  70. package/src/core/islands/constants.ts +1 -0
  71. package/src/core/islands/main.ts +69 -3
  72. package/src/core/islands/parse.ts +70 -23
  73. package/src/core/islands/worker/__tests__/controller.test.ts +173 -0
  74. package/src/core/islands/worker/worker.tsx +145 -57
  75. package/src/core/runtime/__tests__/runtime.test.ts +64 -0
  76. package/src/core/runtime/runtime.ts +30 -10
  77. package/src/core/wasm/worker/bootstrap.ts +113 -20
  78. package/src/css/app/Cell.css +10 -0
  79. package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
  80. package/src/hooks/useHotkey.ts +29 -4
  81. package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
  82. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
  83. package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
  84. package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
  85. package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +53 -2
  86. package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +71 -0
  87. package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
  88. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
  89. package/src/theme/__tests__/useTheme.test.ts +68 -0
  90. package/src/theme/useTheme.ts +16 -1
  91. package/dist/assets/__vite-browser-external-BQCLNwri.js +0 -1
  92. package/dist/assets/worker-DAWRHcPq.js +0 -73
@@ -33,11 +33,14 @@ import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
33
33
  import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
34
34
  import { useResolvedMarimoConfig } from "@/core/config/config";
35
35
  import { CSSClasses, KnownQueryParams } from "@/core/constants";
36
- import type { MarimoError, OutputMessage } from "@/core/kernel/messages";
36
+ import type { OutputMessage } from "@/core/kernel/messages";
37
37
  import { kernelStateAtom } from "@/core/kernel/state";
38
38
  import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
39
39
  import { showCodeInRunModeAtom } from "@/core/meta/state";
40
- import { isErrorMime } from "@/core/mime";
40
+ import {
41
+ publishedCellClasses,
42
+ shouldHidePublishedCell,
43
+ } from "@/core/cells/utils";
41
44
  import { type AppMode, kioskModeAtom } from "@/core/mode";
42
45
  import { useRequestClient } from "@/core/network/requests";
43
46
  import type { CellConfig } from "@/core/network/types";
@@ -362,12 +365,13 @@ const VerticalCell = memo(
362
365
  const className = cn(
363
366
  "marimo-cell",
364
367
  "hover-actions-parent empty:invisible",
365
- {
366
- published: published,
367
- "has-error": errored,
368
- stopped: stopped,
369
- borderless: isPureMarkdown && !published,
370
- },
368
+ published
369
+ ? publishedCellClasses({ errored, stopped })
370
+ : {
371
+ "has-error": errored,
372
+ stopped: stopped,
373
+ borderless: isPureMarkdown,
374
+ },
371
375
  );
372
376
 
373
377
  // Read mode and show code
@@ -419,19 +423,15 @@ const VerticalCell = memo(
419
423
  );
420
424
  }
421
425
 
422
- const outputIsError = isErrorMime(output?.mimetype);
423
426
  // When show_tracebacks is enabled, show error outputs inline
424
427
  // instead of hiding them
425
- const hasTraceback =
426
- showErrorTracebacks &&
427
- outputIsError &&
428
- Array.isArray(output?.data) &&
429
- output.data.some(
430
- (e: MarimoError) =>
431
- e.type === "exception" && "traceback" in e && e.traceback,
432
- );
433
- const hidden =
434
- (errored || interrupted || stopped || outputIsError) && !hasTraceback;
428
+ const hidden = shouldHidePublishedCell({
429
+ errored,
430
+ interrupted,
431
+ stopped,
432
+ output,
433
+ showErrorTracebacks,
434
+ });
435
435
  if (hidden) {
436
436
  return null;
437
437
  }
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import {
4
+ type CSSProperties,
4
5
  startTransition,
5
6
  useEffect,
6
7
  useMemo,
@@ -26,6 +27,7 @@ import { Logger } from "@/utils/Logger";
26
27
  import "./slides.css";
27
28
  import "./reveal-slides.css";
28
29
  import type {
30
+ DeckVerticalAlign,
29
31
  SlideConfig,
30
32
  SlidesLayout,
31
33
  SlideType,
@@ -39,6 +41,7 @@ import {
39
41
  type ComposedSubslide,
40
42
  } from "./compose-slides";
41
43
  import {
44
+ DEFAULT_DECK_VERTICAL_ALIGN,
42
45
  DEFAULT_DECK_TRANSITION,
43
46
  DEFAULT_SLIDE_TYPE,
44
47
  SlideSidebar,
@@ -292,16 +295,38 @@ export function useParkedPreview(options: {
292
295
  };
293
296
  }
294
297
 
298
+ /**
299
+ * Margin style that positions a slide's content vertically within the
300
+ * full-height slide. The content is a flex item, so the vertical margins decide
301
+ * where the free space lands: `auto` on both sides centers it, while pinning one
302
+ * side to `0` pushes content to the top or bottom. The horizontal `20px` keeps
303
+ * content off the slide edges regardless of alignment.
304
+ */
305
+ function resolveSlideContentStyle(
306
+ verticalAlign: DeckVerticalAlign | undefined,
307
+ ): CSSProperties {
308
+ switch (verticalAlign ?? DEFAULT_DECK_VERTICAL_ALIGN) {
309
+ case "top":
310
+ return { margin: "0 20px auto" };
311
+ case "bottom":
312
+ return { margin: "auto 20px 0" };
313
+ default:
314
+ return { margin: "auto 20px" };
315
+ }
316
+ }
317
+
295
318
  const SubslideView = ({
296
319
  subslide,
297
320
  resolveShowCode,
298
321
  isEditable,
299
322
  slideConfigs,
323
+ contentStyle,
300
324
  }: {
301
325
  subslide: ComposedSubslide<RuntimeCell>;
302
326
  resolveShowCode: (cellId: CellId) => boolean;
303
327
  isEditable: boolean;
304
328
  slideConfigs: ReadonlyMap<CellId, SlideConfig>;
329
+ contentStyle: CSSProperties;
305
330
  }) => {
306
331
  const { slideLevel, cumulativeByBlock } = buildSubslideNotes(
307
332
  subslide,
@@ -321,9 +346,7 @@ const SubslideView = ({
321
346
  ? "mo-slide-content flex flex-col gap-3"
322
347
  : "mo-slide-content"
323
348
  }
324
- style={{
325
- margin: "auto 20px",
326
- }}
349
+ style={contentStyle}
327
350
  >
328
351
  {subslide.blocks.map((block, i) => {
329
352
  const rendered = block.cells.map((cell) => {
@@ -514,6 +537,10 @@ const RevealSlidesComponent = ({
514
537
  );
515
538
 
516
539
  const deckTransition = layout.deck?.transition ?? DEFAULT_DECK_TRANSITION;
540
+ const slideContentStyle = resolveSlideContentStyle(
541
+ layout.deck?.verticalAlign,
542
+ );
543
+
517
544
  // Reveal's Notes plugin iframes the deck for the current/upcoming-slide
518
545
  // previews. We load the same URL but as a read-only kiosk client with the
519
546
  // app chrome hidden, which `<SlidesLayoutRenderer>` interprets the same as
@@ -536,6 +563,10 @@ const RevealSlidesComponent = ({
536
563
  transition: deckTransition,
537
564
  keyboardCondition: (event: KeyboardEvent) => !Events.fromInput(event),
538
565
  url: kioskUrl,
566
+ // reveal.js auto-switches to its scroll view for mobile.
567
+ // We disable this mode because it rewrites the slide DOM that
568
+ // `@revealjs/react` owns, which crashes on `reveal.sync()` re-renders.
569
+ scrollActivationWidth: 0,
539
570
  }),
540
571
  [width, height, deckTransition, kioskUrl],
541
572
  );
@@ -704,6 +735,7 @@ const RevealSlidesComponent = ({
704
735
  resolveShowCode={resolveShowCode}
705
736
  isEditable={isEditable}
706
737
  slideConfigs={layout.cells}
738
+ contentStyle={slideContentStyle}
707
739
  />
708
740
  );
709
741
  }
@@ -717,6 +749,7 @@ const RevealSlidesComponent = ({
717
749
  resolveShowCode={resolveShowCode}
718
750
  isEditable={isEditable}
719
751
  slideConfigs={layout.cells}
752
+ contentStyle={slideContentStyle}
720
753
  />
721
754
  );
722
755
  })}
@@ -743,7 +776,7 @@ const RevealSlidesComponent = ({
743
776
  ? "mo-slide-content flex flex-col gap-3"
744
777
  : "mo-slide-content"
745
778
  }
746
- style={{ margin: "auto 20px" }}
779
+ style={slideContentStyle}
747
780
  >
748
781
  <ParkedPreviewContent
749
782
  cell={parkedPreviewCell}
@@ -23,6 +23,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
23
23
  import type { CellId } from "@/core/cells/ids";
24
24
  import { cn } from "@/utils/cn";
25
25
  import type {
26
+ DeckVerticalAlign,
26
27
  DeckTransition,
27
28
  SlidesLayout,
28
29
  SlideType,
@@ -37,6 +38,7 @@ import { jotaiJsonStorage } from "@/utils/storage/jotai";
37
38
 
38
39
  export const DEFAULT_SLIDE_TYPE: SlideType = "slide";
39
40
  export const DEFAULT_DECK_TRANSITION: DeckTransition = "slide";
41
+ export const DEFAULT_DECK_VERTICAL_ALIGN: DeckVerticalAlign = "center";
40
42
  const COLLAPSED_CONFIG_WIDTH = 36;
41
43
  const slideConfigOpenAtom = atomWithStorage<boolean>(
42
44
  "marimo:slides:config-open",
@@ -118,6 +120,30 @@ const DECK_TRANSITION_OPTIONS: DeckTransitionOption[] = [
118
120
  { value: "zoom", label: "Zoom", description: "Zoom into the next slide." },
119
121
  ];
120
122
 
123
+ interface DeckVerticalAlignOption {
124
+ value: DeckVerticalAlign;
125
+ label: string;
126
+ description: string;
127
+ }
128
+
129
+ const DECK_VERTICAL_ALIGN_OPTIONS: DeckVerticalAlignOption[] = [
130
+ {
131
+ value: "center",
132
+ label: "Center",
133
+ description: "Vertically center each slide's content.",
134
+ },
135
+ {
136
+ value: "top",
137
+ label: "Top",
138
+ description: "Align content to the top, like the cell view.",
139
+ },
140
+ {
141
+ value: "bottom",
142
+ label: "Bottom",
143
+ description: "Align content to the bottom of each slide.",
144
+ },
145
+ ];
146
+
121
147
  const SlidesForm = ({
122
148
  layout,
123
149
  setLayout,
@@ -300,6 +326,12 @@ const DeckConfigForm = ({
300
326
  (opt) => opt.value === currentTransition,
301
327
  )?.description;
302
328
 
329
+ const currentVerticalAlign: DeckVerticalAlign =
330
+ layout.deck?.verticalAlign ?? DEFAULT_DECK_VERTICAL_ALIGN;
331
+ const activeVerticalAlignDescription = DECK_VERTICAL_ALIGN_OPTIONS.find(
332
+ (opt) => opt.value === currentVerticalAlign,
333
+ )?.description;
334
+
303
335
  const handleTransitionChange = (value: DeckTransition) => {
304
336
  setLayout({
305
337
  ...layout,
@@ -307,6 +339,13 @@ const DeckConfigForm = ({
307
339
  });
308
340
  };
309
341
 
342
+ const handleVerticalAlignChange = (value: DeckVerticalAlign) => {
343
+ setLayout({
344
+ ...layout,
345
+ deck: { ...layout.deck, verticalAlign: value },
346
+ });
347
+ };
348
+
310
349
  return (
311
350
  <div className="flex flex-col gap-3">
312
351
  <div className="flex flex-col gap-1.5">
@@ -337,6 +376,39 @@ const DeckConfigForm = ({
337
376
  <p className="text-xs text-foreground/70">{activeDescription}</p>
338
377
  )}
339
378
  </div>
379
+ <div className="flex flex-col gap-1.5">
380
+ <label
381
+ htmlFor="deck-vertical-align"
382
+ className="font-semibold text-sm text-foreground"
383
+ >
384
+ Vertical alignment
385
+ </label>
386
+ <Select
387
+ value={currentVerticalAlign}
388
+ onValueChange={(value) =>
389
+ handleVerticalAlignChange(value as DeckVerticalAlign)
390
+ }
391
+ >
392
+ <SelectTrigger
393
+ id="deck-vertical-align"
394
+ aria-label="Vertical alignment"
395
+ >
396
+ <SelectValue />
397
+ </SelectTrigger>
398
+ <SelectContent>
399
+ {DECK_VERTICAL_ALIGN_OPTIONS.map(({ value, label }) => (
400
+ <SelectItem key={value} value={value}>
401
+ {label}
402
+ </SelectItem>
403
+ ))}
404
+ </SelectContent>
405
+ </Select>
406
+ {activeVerticalAlignDescription && (
407
+ <p className="text-xs text-foreground/70">
408
+ {activeVerticalAlignDescription}
409
+ </p>
410
+ )}
411
+ </div>
340
412
  </div>
341
413
  );
342
414
  };
@@ -34,7 +34,7 @@ const VARIANT_CLASSES = {
34
34
  type ToastVariant = keyof typeof VARIANT_CLASSES;
35
35
 
36
36
  const toastVariants = cva(
37
- "data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full",
37
+ "data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-start justify-between gap-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full",
38
38
  {
39
39
  variants: {
40
40
  variant: VARIANT_CLASSES,
@@ -117,10 +117,18 @@ ToastTitle.displayName = ToastPrimitives.Title.displayName;
117
117
  const ToastDescription = React.forwardRef<
118
118
  React.ElementRef<typeof ToastPrimitives.Description>,
119
119
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
120
- >(({ className, ...props }, ref) => (
120
+ >(({ className, onPointerDown, ...props }, ref) => (
121
121
  <ToastPrimitives.Description
122
122
  ref={ref}
123
- className={cn("text-sm opacity-90", className)}
123
+ className={cn(
124
+ "max-h-48 overflow-auto text-sm opacity-90 whitespace-pre-wrap wrap-break-word select-text cursor-text",
125
+ className,
126
+ )}
127
+ // Keep swipe-to-dismiss from fighting text selection / scroll.
128
+ onPointerDown={(event) => {
129
+ event.stopPropagation();
130
+ onPointerDown?.(event);
131
+ }}
124
132
  {...props}
125
133
  />
126
134
  ));
@@ -1,5 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
3
+ import { useRef } from "react";
4
+ import { CopyClipboardIcon } from "@/components/icons/copy-icon";
2
5
  import { useToast } from "@/components/ui/use-toast";
6
+ import { cn } from "@/utils/cn";
3
7
  import {
4
8
  Toast,
5
9
  ToastClose,
@@ -14,17 +18,69 @@ export const Toaster = () => {
14
18
 
15
19
  return (
16
20
  <ToastProvider>
17
- {toasts.map(({ id, title, description, action, ...props }) => (
18
- <Toast key={id} {...props}>
19
- <div className="grid gap-1">
20
- {title && <ToastTitle>{title}</ToastTitle>}
21
- {description && <ToastDescription>{description}</ToastDescription>}
22
- </div>
23
- {action}
24
- <ToastClose />
25
- </Toast>
21
+ {toasts.map(({ id, title, description, action, variant, ...props }) => (
22
+ <ToastItem
23
+ key={id}
24
+ toastTitle={title}
25
+ toastDescription={description}
26
+ action={action}
27
+ variant={variant}
28
+ {...props}
29
+ />
26
30
  ))}
27
31
  <ToastViewport />
28
32
  </ToastProvider>
29
33
  );
30
34
  };
35
+
36
+ type ToastItemProps = Omit<
37
+ ComponentPropsWithoutRef<typeof Toast>,
38
+ "title" | "children"
39
+ > & {
40
+ toastTitle?: ReactNode;
41
+ toastDescription?: ReactNode;
42
+ action?: ReactElement;
43
+ };
44
+
45
+ const ToastItem = ({
46
+ toastTitle,
47
+ toastDescription,
48
+ action,
49
+ variant,
50
+ ...props
51
+ }: ToastItemProps) => {
52
+ const descriptionRef = useRef<HTMLDivElement>(null);
53
+ // Show copy button for danger toasts without action, typically used for errors.
54
+ const showCopy = Boolean(toastDescription) && !action && variant === "danger";
55
+
56
+ return (
57
+ <Toast variant={variant} {...props}>
58
+ <div className="grid min-w-0 flex-1 gap-1">
59
+ {toastTitle && <ToastTitle>{toastTitle}</ToastTitle>}
60
+ {toastDescription && (
61
+ <ToastDescription ref={descriptionRef}>
62
+ {toastDescription}
63
+ </ToastDescription>
64
+ )}
65
+ </div>
66
+ {action}
67
+ {showCopy && (
68
+ <CopyClipboardIcon
69
+ tooltip="Copy error"
70
+ ariaLabel="Copy error"
71
+ className="h-4 w-4"
72
+ buttonClassName={cn(
73
+ "absolute right-8 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity",
74
+ "hover:text-foreground focus:opacity-100 focus:outline-hidden group-hover:opacity-100",
75
+ "group-[.destructive]:text-red-300 hover:group-[.destructive]:text-red-500 focus:group-[.destructive]:ring-red-400 focus:group-[.destructive]:ring-offset-red-600",
76
+ )}
77
+ value={() =>
78
+ descriptionRef.current?.innerText ??
79
+ (typeof toastDescription === "string" ? toastDescription : "")
80
+ }
81
+ />
82
+ )}
83
+ <ToastClose />
84
+ </Toast>
85
+ );
86
+ };
@@ -8,11 +8,13 @@ import {
8
8
  } from "@/core/cells/cells";
9
9
  import { CellId } from "@/core/cells/ids";
10
10
  import type { CellData, CellRuntimeState } from "@/core/cells/types";
11
+ import type { OutputMessage } from "@/core/kernel/messages";
11
12
  import { MultiColumn } from "@/utils/id-tree";
12
13
  import {
13
14
  disabledCellIds,
14
15
  enabledCellIds,
15
16
  isUninstantiated,
17
+ shouldHidePublishedCell,
16
18
  staleCellIds,
17
19
  } from "../utils";
18
20
 
@@ -426,3 +428,60 @@ describe("enabledCellIds", () => {
426
428
  expect(result).toEqual([cellId2, cellId3]);
427
429
  });
428
430
  });
431
+
432
+ describe("shouldHidePublishedCell", () => {
433
+ const base = {
434
+ errored: false,
435
+ interrupted: false,
436
+ stopped: false,
437
+ output: null,
438
+ showErrorTracebacks: false,
439
+ };
440
+ const errorOutput = (traceback?: string): OutputMessage => ({
441
+ channel: "marimo-error",
442
+ mimetype: "application/vnd.marimo+error",
443
+ data: [
444
+ { type: "exception", exception_type: "ValueError", msg: "!", traceback },
445
+ ],
446
+ timestamp: 0,
447
+ });
448
+
449
+ it("does not hide a healthy cell", () => {
450
+ expect(shouldHidePublishedCell(base)).toBe(false);
451
+ });
452
+
453
+ it.each(["errored", "interrupted", "stopped"] as const)(
454
+ "hides a %s cell",
455
+ (key) => {
456
+ expect(shouldHidePublishedCell({ ...base, [key]: true })).toBe(true);
457
+ },
458
+ );
459
+
460
+ it("hides error outputs", () => {
461
+ expect(shouldHidePublishedCell({ ...base, output: errorOutput() })).toBe(
462
+ true,
463
+ );
464
+ });
465
+
466
+ it("shows error outputs with a traceback when show_tracebacks is enabled", () => {
467
+ expect(
468
+ shouldHidePublishedCell({
469
+ ...base,
470
+ errored: true,
471
+ output: errorOutput("Traceback (most recent call last) ..."),
472
+ showErrorTracebacks: true,
473
+ }),
474
+ ).toBe(false);
475
+ });
476
+
477
+ it("hides error outputs without a traceback even when show_tracebacks is enabled", () => {
478
+ expect(
479
+ shouldHidePublishedCell({
480
+ ...base,
481
+ errored: true,
482
+ output: errorOutput(),
483
+ showErrorTracebacks: true,
484
+ }),
485
+ ).toBe(true);
486
+ });
487
+ });
@@ -2,6 +2,8 @@
2
2
 
3
3
  import type { EditorView } from "@codemirror/view";
4
4
  import { Objects } from "@/utils/objects";
5
+ import type { MarimoError, OutputMessage } from "../kernel/messages";
6
+ import { isErrorMime } from "../mime";
5
7
  import type { RuntimeState } from "../network/types";
6
8
  import type { NotebookState } from "./cells";
7
9
  import type { CellId } from "./ids";
@@ -196,3 +198,52 @@ export function cellStatusClasses({
196
198
  stale: status === "disabled-transitively",
197
199
  };
198
200
  }
201
+
202
+ /**
203
+ * Status classes for a published cell (read or present mode, output only).
204
+ */
205
+ export function publishedCellClasses({
206
+ errored,
207
+ stopped,
208
+ }: {
209
+ errored: boolean;
210
+ stopped: boolean;
211
+ }) {
212
+ return {
213
+ published: true,
214
+ "has-error": errored,
215
+ stopped: stopped,
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Whether a published cell (read or present mode) should be hidden.
221
+ *
222
+ * Errored, interrupted, and stopped cells (and error outputs) are hidden in
223
+ * published views, unless `show_tracebacks` is enabled and the output carries
224
+ * an exception traceback to display inline.
225
+ */
226
+ export function shouldHidePublishedCell({
227
+ errored,
228
+ interrupted,
229
+ stopped,
230
+ output,
231
+ showErrorTracebacks,
232
+ }: {
233
+ errored: boolean;
234
+ interrupted: boolean;
235
+ stopped: boolean;
236
+ output: OutputMessage | null;
237
+ showErrorTracebacks: boolean;
238
+ }): boolean {
239
+ const outputIsError = isErrorMime(output?.mimetype);
240
+ const hasTraceback =
241
+ showErrorTracebacks &&
242
+ outputIsError &&
243
+ Array.isArray(output?.data) &&
244
+ output.data.some(
245
+ (e: MarimoError) =>
246
+ e.type === "exception" && "traceback" in e && e.traceback,
247
+ );
248
+ return (errored || interrupted || stopped || outputIsError) && !hasTraceback;
249
+ }
@@ -5,6 +5,7 @@ import { EditorView, type Tooltip } from "@codemirror/view";
5
5
  import { describe, expect, it } from "vitest";
6
6
  import {
7
7
  asSignatureHint,
8
+ closeSignatureHint,
8
9
  setSignatureHintEffect,
9
10
  signatureHintField,
10
11
  } from "../signature-hint";
@@ -51,12 +52,79 @@ describe("signatureHintField", () => {
51
52
  expect(state.field(signatureHintField)).toBeNull();
52
53
  });
53
54
 
54
- it("keeps and re-anchors the tooltip across edits", () => {
55
+ it("keeps and re-anchors the tooltip across edits inside the call", () => {
55
56
  let state = stateWithHint("plt.plot(", 9);
56
- // Insert before the tooltip position; it should shift to stay anchored.
57
- state = state.update({ changes: { from: 0, insert: "xy" } }).state;
57
+ // Insert before the tooltip position while the cursor stays inside the
58
+ // call; the anchor should shift but the hint should remain.
59
+ state = state.update({
60
+ changes: { from: 0, insert: "xy" },
61
+ selection: { anchor: 11 },
62
+ }).state;
58
63
  expect(state.field(signatureHintField)?.pos).toBe(11);
59
64
  });
65
+
66
+ it("dismisses the tooltip when the closing paren is typed", () => {
67
+ let state = stateWithHint("plt.plot(", 9);
68
+ // Type the closing paren; the cursor is now outside the call.
69
+ state = state.update({
70
+ changes: { from: 9, insert: ")" },
71
+ selection: { anchor: 10 },
72
+ }).state;
73
+ expect(state.field(signatureHintField)).toBeNull();
74
+ });
75
+
76
+ it("dismisses the tooltip when the anchored call closes inside grouping parens", () => {
77
+ // Regression for the `(plt.plot())` case: the outer grouping paren must not
78
+ // keep the (now-closed) plt.plot hint alive.
79
+ let state = stateWithHint("(plt.plot(", 10);
80
+ // Close plt.plot's call; the outer `(` is still open but we've left the
81
+ // anchored call.
82
+ state = state.update({
83
+ changes: { from: 10, insert: ")" },
84
+ selection: { anchor: 11 },
85
+ }).state;
86
+ expect(state.field(signatureHintField)).toBeNull();
87
+ });
88
+
89
+ it("keeps the tooltip while typing a nested call inside the anchored call", () => {
90
+ // Cursor inside the anchored call of `f(g(<cursor>`; opening/typing a nested
91
+ // call stays inside the anchored call, so the hint should remain.
92
+ let state = stateWithHint("f(g(", 4);
93
+ state = state.update({
94
+ changes: { from: 4, insert: "x(" },
95
+ selection: { anchor: 6 },
96
+ }).state;
97
+ expect(state.field(signatureHintField)?.pos).toBe(4);
98
+ });
99
+
100
+ it("keeps the tooltip when a nested call closes inside the anchored call", () => {
101
+ const anchor = "f(".length;
102
+ let state = stateWithHint("f(g(x", anchor);
103
+ state = state.update({
104
+ changes: { from: 5, insert: ")" },
105
+ selection: { anchor: 6 },
106
+ }).state;
107
+ expect(state.field(signatureHintField)?.pos).toBe(anchor);
108
+ });
109
+
110
+ it("dismisses the tooltip when the closing paren is typed in a large multi-line call", () => {
111
+ const anchor = "f(".length;
112
+ const prefix = `f(\n${" x,\n".repeat(25)}`;
113
+ let state = EditorState.create({
114
+ doc: prefix,
115
+ selection: { anchor: prefix.length },
116
+ extensions: [signatureHintField],
117
+ });
118
+ state = state.update({
119
+ effects: setSignatureHintEffect.of(fakeTooltip(anchor)),
120
+ }).state;
121
+ const head = prefix.length;
122
+ state = state.update({
123
+ changes: { from: head, insert: ")" },
124
+ selection: { anchor: head + 1 },
125
+ }).state;
126
+ expect(state.field(signatureHintField)).toBeNull();
127
+ });
60
128
  });
61
129
 
62
130
  describe("asSignatureHint", () => {
@@ -92,3 +160,29 @@ describe("asSignatureHint", () => {
92
160
  expect(wrapped.above).toBe(true);
93
161
  });
94
162
  });
163
+
164
+ describe("closeSignatureHint", () => {
165
+ it("returns false when no hint is showing", () => {
166
+ const view = new EditorView({
167
+ state: EditorState.create({ extensions: [signatureHintField] }),
168
+ });
169
+ expect(closeSignatureHint(view)).toBe(false);
170
+ expect(view.state.field(signatureHintField)).toBeNull();
171
+ view.destroy();
172
+ });
173
+
174
+ it("dismisses the hint and returns true when one is showing", () => {
175
+ const view = new EditorView({
176
+ state: EditorState.create({
177
+ doc: "plt.plot(",
178
+ extensions: [signatureHintField],
179
+ }),
180
+ });
181
+ view.dispatch({ effects: setSignatureHintEffect.of(fakeTooltip(9)) });
182
+ expect(view.state.field(signatureHintField)?.pos).toBe(9);
183
+
184
+ expect(closeSignatureHint(view)).toBe(true);
185
+ expect(view.state.field(signatureHintField)).toBeNull();
186
+ view.destroy();
187
+ });
188
+ });