@marimo-team/islands 0.23.12-dev8 → 0.23.12

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 (41) hide show
  1. package/dist/{chat-ui-BEOvjkmJ.js → chat-ui-CsPewo4h.js} +2 -2
  2. package/dist/{code-visibility-B9yvB9rV.js → code-visibility-BFhOAQbo.js} +714 -707
  3. package/dist/{html-to-image-Di0mtt6O.js → html-to-image-DXwLcQ6l.js} +22 -15
  4. package/dist/main.js +1160 -1027
  5. package/dist/{process-output-BLd4KuwX.js → process-output-C6_e1pT_.js} +1 -1
  6. package/dist/{reveal-component-D6wEWbxH.js → reveal-component-ghVwQgXR.js} +13 -13
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/components/data-table/TableBottomBar.tsx +4 -1
  10. package/src/components/data-table/data-table.tsx +26 -17
  11. package/src/components/data-table/utils.ts +1 -4
  12. package/src/components/editor/actions/useNotebookActions.tsx +4 -4
  13. package/src/components/editor/ai/__tests__/completion-utils.test.ts +48 -2
  14. package/src/components/editor/ai/completion-utils.ts +54 -36
  15. package/src/components/editor/app-container.tsx +3 -1
  16. package/src/components/editor/output/ImageOutput.tsx +12 -3
  17. package/src/components/editor/renderers/vertical-layout/vertical-layout-wrapper.tsx +2 -2
  18. package/src/components/home/components.tsx +4 -4
  19. package/src/components/icons/github.tsx +21 -0
  20. package/src/components/icons/youtube.tsx +21 -0
  21. package/src/components/storage/components.tsx +3 -7
  22. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +67 -0
  23. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +47 -0
  24. package/src/core/codemirror/go-to-definition/commands.ts +47 -30
  25. package/src/core/codemirror/go-to-definition/utils.ts +0 -1
  26. package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +54 -0
  27. package/src/core/codemirror/reactive-references/analyzer.ts +44 -35
  28. package/src/core/islands/__tests__/bridge.test.ts +25 -0
  29. package/src/core/islands/__tests__/parse.test.ts +585 -1
  30. package/src/core/islands/__tests__/test-utils.tsx +10 -1
  31. package/src/core/islands/bridge.ts +6 -1
  32. package/src/core/islands/constants.ts +2 -0
  33. package/src/core/islands/parse.ts +290 -13
  34. package/src/plugins/impl/DataTablePlugin.tsx +20 -1
  35. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +141 -1
  36. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +54 -4
  37. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +104 -1
  38. package/src/plugins/impl/anywidget/__tests__/model.test.ts +19 -0
  39. package/src/plugins/impl/anywidget/model.ts +15 -0
  40. package/src/utils/__tests__/records.test.ts +27 -0
  41. package/src/utils/records.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.12-dev8",
3
+ "version": "0.23.12",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -3,6 +3,7 @@
3
3
 
4
4
  import type { RowSelectionState, Table } from "@tanstack/react-table";
5
5
  import { useLocale } from "react-aria";
6
+ import { isStaticNotebook } from "@/core/static/static-state";
6
7
  import type { GetRowIds } from "@/plugins/impl/DataTablePlugin";
7
8
  import { cn } from "@/utils/cn";
8
9
  import { Events } from "@/utils/events";
@@ -40,6 +41,8 @@ export const TableBottomBar = <TData,>({
40
41
  className,
41
42
  }: TableBottomBarProps<TData>) => {
42
43
  const { locale } = useLocale();
44
+ // Pagination fetches each page via a kernel RPC, absent in static exports.
45
+ const isStatic = isStaticNotebook();
43
46
  const handleSelectAllRows = (value: boolean) => {
44
47
  if (!onRowSelectionChange) {
45
48
  return;
@@ -171,7 +174,7 @@ export const TableBottomBar = <TData,>({
171
174
  <CellSelectionStats table={table} className="lg:hidden" />
172
175
  </div>
173
176
  <div className="ml-auto lg:ml-0 lg:justify-self-center flex items-center shrink-0">
174
- {pagination && (
177
+ {pagination && !isStatic && (
175
178
  <DataTablePagination
176
179
  table={table}
177
180
  tableLoading={tableLoading}
@@ -25,6 +25,7 @@ import { useLocale } from "react-aria";
25
25
 
26
26
  import { Button } from "@/components/ui/button";
27
27
  import { Table } from "@/components/ui/table";
28
+ import { isStaticNotebook } from "@/core/static/static-state";
28
29
  import { Banner } from "@/plugins/impl/common/error-banner";
29
30
  import type {
30
31
  CalculateTopKRows,
@@ -182,6 +183,11 @@ const DataTableInternal = <TData,>({
182
183
  onViewedRowChange,
183
184
  renderTableExplorerPanel,
184
185
  }: DataTableProps<TData>) => {
186
+ // The top bar's controls (search, filters, column explorer, chart builder)
187
+ // all require a live kernel, which static exports don't have.
188
+ const isStatic = isStaticNotebook();
189
+ const showTableTopBar = !isStatic;
190
+
185
191
  const [showLoadingBar, setShowLoadingBar] = React.useState<boolean>(false);
186
192
  const { locale } = useLocale();
187
193
 
@@ -267,11 +273,12 @@ const DataTableInternal = <TData,>({
267
273
  }
268
274
  : {}),
269
275
  manualSorting: manualSorting,
276
+ enableSorting: !isStatic,
270
277
  enableMultiSort: true,
271
278
  getSortedRowModel: getSortedRowModel(),
272
279
  // filtering
273
280
  manualFiltering: true,
274
- enableColumnFilters: showFilters,
281
+ enableColumnFilters: showFilters && !isStatic,
275
282
  getFilteredRowModel: getFilteredRowModel(),
276
283
  onColumnFiltersChange: onFiltersChange,
277
284
  // selection
@@ -355,22 +362,24 @@ const DataTableInternal = <TData,>({
355
362
  part="table-wrapper"
356
363
  className={cn(className || "rounded-md border overflow-hidden")}
357
364
  >
358
- <TableTopBar
359
- table={table}
360
- showSearch={showSearch}
361
- searchQuery={searchQuery}
362
- onSearchQueryChange={onSearchQueryChange}
363
- reloading={reloading}
364
- showChartBuilder={showChartBuilder}
365
- isChartBuilderOpen={isChartBuilderOpen}
366
- toggleDisplayHeader={toggleDisplayHeader}
367
- showTableExplorer={showTableExplorer}
368
- togglePanel={togglePanel}
369
- isAnyPanelOpen={isAnyPanelOpen}
370
- downloadAs={downloadAs}
371
- sizeBytes={sizeBytes}
372
- sizeBytesIsLoading={sizeBytesIsLoading}
373
- />
365
+ {showTableTopBar && (
366
+ <TableTopBar
367
+ table={table}
368
+ showSearch={showSearch}
369
+ searchQuery={searchQuery}
370
+ onSearchQueryChange={onSearchQueryChange}
371
+ reloading={reloading}
372
+ showChartBuilder={showChartBuilder}
373
+ isChartBuilderOpen={isChartBuilderOpen}
374
+ toggleDisplayHeader={toggleDisplayHeader}
375
+ showTableExplorer={showTableExplorer}
376
+ togglePanel={togglePanel}
377
+ isAnyPanelOpen={isAnyPanelOpen}
378
+ downloadAs={downloadAs}
379
+ sizeBytes={sizeBytes}
380
+ sizeBytesIsLoading={sizeBytesIsLoading}
381
+ />
382
+ )}
374
383
  {allUserColumnsHidden && (
375
384
  <Banner className="mb-1 mx-2 rounded flex items-center justify-between">
376
385
  <span>All columns are hidden.</span>
@@ -4,6 +4,7 @@ import type { Table } from "@tanstack/react-table";
4
4
  import type { TableData } from "@/plugins/impl/DataTablePlugin";
5
5
  import { vegaLoadData } from "@/plugins/impl/vega/loader";
6
6
  import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
7
+ import { isRecord } from "@/utils/records";
7
8
  import { getMimeValues } from "./mime-cell";
8
9
  import type { DataType } from "@/core/kernel/messages";
9
10
  import {
@@ -232,10 +233,6 @@ function stripHtml(html: string): string {
232
233
 
233
234
  const HTML_MIMETYPES = new Set(["text/html", "text/markdown"]);
234
235
 
235
- function isRecord(value: unknown): value is Record<string, unknown> {
236
- return value !== null && typeof value === "object" && !Array.isArray(value);
237
- }
238
-
239
236
  /**
240
237
  * Get clipboard-ready text and optional HTML for a cell.
241
238
  *
@@ -21,7 +21,6 @@ import {
21
21
  Files,
22
22
  FileTextIcon,
23
23
  FolderDownIcon,
24
- GithubIcon,
25
24
  GlobeIcon,
26
25
  HardDrive,
27
26
  Home,
@@ -39,7 +38,6 @@ import {
39
38
  SparklesIcon,
40
39
  Undo2Icon,
41
40
  XCircleIcon,
42
- YoutubeIcon,
43
41
  ZapIcon,
44
42
  } from "lucide-react";
45
43
  import {
@@ -47,7 +45,9 @@ import {
47
45
  useOpenSettingsToTab,
48
46
  } from "@/components/app-config/state";
49
47
  import { MarkdownIcon } from "@/components/editor/cell/code/icons";
48
+ import { GitHubIcon } from "@/components/icons/github";
50
49
  import { MarimoPlusIcon } from "@/components/icons/marimo-icons";
50
+ import { YouTubeIcon } from "@/components/icons/youtube";
51
51
  import { useImperativeModal } from "@/components/modal/ImperativeModal";
52
52
  import { renderShortcut } from "@/components/shortcuts/renderShortcut";
53
53
  import { PairWithAgentModal } from "@/components/editor/actions/pair-with-agent-modal";
@@ -655,7 +655,7 @@ export function useNotebookActions() {
655
655
  },
656
656
  },
657
657
  {
658
- icon: <GithubIcon size={14} strokeWidth={1.5} />,
658
+ icon: <GitHubIcon className="h-3.5 w-3.5" />,
659
659
  label: "GitHub",
660
660
  handle: () => {
661
661
  window.open(Constants.githubPage, "_blank");
@@ -669,7 +669,7 @@ export function useNotebookActions() {
669
669
  },
670
670
  },
671
671
  {
672
- icon: <YoutubeIcon size={14} strokeWidth={1.5} />,
672
+ icon: <YouTubeIcon className="h-3.5 w-3.5" />,
673
673
  label: "YouTube",
674
674
  handle: () => {
675
675
  window.open(Constants.youtube, "_blank");
@@ -1,6 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { beforeEach, describe, expect, it, type Mock, vi } from "vitest";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ type Mock,
9
+ vi,
10
+ } from "vitest";
3
11
  import { variableName } from "@/__tests__/branded";
12
+ import * as aiContext from "@/core/ai/context/context";
4
13
  import { getCodes } from "@/core/codemirror/copilot/getCodes";
5
14
  import { dataSourceConnectionsAtom } from "@/core/datasets/data-source-connections";
6
15
  import { DUCKDB_ENGINE } from "@/core/datasets/engines";
@@ -8,10 +17,11 @@ import { datasetsAtom } from "@/core/datasets/state";
8
17
  import type { DatasetsState } from "@/core/datasets/types";
9
18
  import { store } from "@/core/state/jotai";
10
19
  import { variablesAtom } from "@/core/variables/state";
11
- import type { UIMessage } from "ai";
20
+ import type { FileUIPart, UIMessage } from "ai";
12
21
  import {
13
22
  codeToCells,
14
23
  getAICompletionBody,
24
+ getAICompletionBodyWithAttachments,
15
25
  isContextAttachment,
16
26
  MARIMO_CONTEXT_PART_TYPE,
17
27
  resolveChatContext,
@@ -440,6 +450,42 @@ describe("isContextAttachment", () => {
440
450
  });
441
451
  });
442
452
 
453
+ describe("context attachment stamping", () => {
454
+ const rawAttachment: FileUIPart = {
455
+ type: "file",
456
+ mediaType: "image/png",
457
+ url: "data:image/png;base64,abc",
458
+ };
459
+
460
+ beforeEach(() => {
461
+ vi.spyOn(aiContext, "getAIContextRegistry").mockReturnValue({
462
+ parseAllContextIds: () => ["data://t1"],
463
+ formatContextForAI: () => '<data name="t1" />',
464
+ getAttachmentsForContext: async () => [rawAttachment],
465
+ } as unknown as ReturnType<typeof aiContext.getAIContextRegistry>);
466
+ });
467
+
468
+ afterEach(() => {
469
+ vi.restoreAllMocks();
470
+ });
471
+
472
+ it("stamps chat attachments as context-derived", async () => {
473
+ const { attachments } = await resolveChatContext("see @data://t1");
474
+ expect(attachments).toHaveLength(1);
475
+ expect(isContextAttachment(attachments[0])).toBe(true);
476
+ // The original attachment is left untouched (we return a stamped copy).
477
+ expect(rawAttachment.providerMetadata).toBeUndefined();
478
+ });
479
+
480
+ it("stamps completion attachments the same way as chat", async () => {
481
+ const { attachments } = await getAICompletionBodyWithAttachments({
482
+ input: "see @data://t1",
483
+ });
484
+ expect(attachments).toHaveLength(1);
485
+ expect(isContextAttachment(attachments[0])).toBe(true);
486
+ });
487
+ });
488
+
443
489
  describe("codeToCells", () => {
444
490
  it("should return empty array for empty string", () => {
445
491
  const code = "";
@@ -9,6 +9,7 @@ import {
9
9
  import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
10
10
  import type { DataUIPart, FileUIPart, UIMessage } from "ai";
11
11
  import { getAIContextRegistry } from "@/core/ai/context/context";
12
+ import type { ContextLocatorId } from "@/core/ai/context/registry";
12
13
  import { getCodes } from "@/core/codemirror/copilot/getCodes";
13
14
  import type { LanguageAdapterType } from "@/core/codemirror/language/types";
14
15
  import type { AiCompletionRequest } from "@/core/network/types";
@@ -89,20 +90,51 @@ export function isContextAttachment(part: UIMessage["parts"][number]): boolean {
89
90
  }
90
91
 
91
92
  /**
92
- * Resolve @-context for messages. They represent referenced
93
- * datasets, variables, or other context from the user's prompt.
93
+ * Stamp a context-derived attachment with a provenance marker.
94
+ *
95
+ * Some @-mentions resolve to file attachments (e.g. a cell's image output),
96
+ * which get appended to the user message right alongside files the user
97
+ * uploaded by hand. Once they're in the message the two are indistinguishable,
98
+ * so we mark the context-derived ones. This matters on message edit: we
99
+ * re-resolve context from the edited text, and `isContextAttachment` lets us
100
+ * drop only the stale context attachments while preserving the user's own
101
+ * uploads
94
102
  */
95
- export async function resolveChatContext(
103
+ function stampContextAttachment(attachment: FileUIPart): FileUIPart {
104
+ return {
105
+ ...attachment,
106
+ providerMetadata: {
107
+ ...attachment.providerMetadata,
108
+ // Merge within the `marimo` namespace so we don't clobber any other
109
+ // marimo metadata a provider may have already set.
110
+ marimo: {
111
+ ...attachment.providerMetadata?.marimo,
112
+ ...CONTEXT_ATTACHMENT_METADATA.marimo,
113
+ },
114
+ },
115
+ };
116
+ }
117
+
118
+ interface ResolvedContext {
119
+ plainText: string;
120
+ contextIds: ContextLocatorId[];
121
+ attachments: FileUIPart[];
122
+ }
123
+
124
+ /**
125
+ * Parse @-context for messages
126
+ */
127
+ async function resolveContextAttachments(
96
128
  input: string,
97
- ): Promise<ResolvedChatContext> {
129
+ ): Promise<ResolvedContext> {
98
130
  if (!input.includes(CONTEXT_TRIGGER)) {
99
- return { contextPart: null, attachments: [] };
131
+ return { plainText: "", contextIds: [], attachments: [] };
100
132
  }
101
133
 
102
134
  const registry = getAIContextRegistry(store);
103
135
  const contextIds = registry.parseAllContextIds(input);
104
136
  if (contextIds.length === 0) {
105
- return { contextPart: null, attachments: [] };
137
+ return { plainText: "", contextIds: [], attachments: [] };
106
138
  }
107
139
 
108
140
  const plainText = registry.formatContextForAI(contextIds);
@@ -110,20 +142,24 @@ export async function resolveChatContext(
110
142
  let attachments: FileUIPart[] = [];
111
143
  try {
112
144
  const resolved = await registry.getAttachmentsForContext(contextIds);
113
- attachments = resolved.map((attachment) => ({
114
- ...attachment,
115
- providerMetadata: {
116
- ...attachment.providerMetadata,
117
- marimo: {
118
- ...attachment.providerMetadata?.marimo,
119
- ...CONTEXT_ATTACHMENT_METADATA.marimo,
120
- },
121
- },
122
- }));
145
+ attachments = resolved.map(stampContextAttachment);
123
146
  } catch (error) {
124
147
  Logger.error("Error getting attachments:", error);
125
148
  }
126
149
 
150
+ return { plainText, contextIds, attachments };
151
+ }
152
+
153
+ /**
154
+ * Resolve @-context for messages. They represent referenced
155
+ * datasets, variables, or other context from the user's prompt.
156
+ */
157
+ export async function resolveChatContext(
158
+ input: string,
159
+ ): Promise<ResolvedChatContext> {
160
+ const { plainText, contextIds, attachments } =
161
+ await resolveContextAttachments(input);
162
+
127
163
  let contextPart: MarimoContextUIPart | null = null;
128
164
  if (plainText.trim()) {
129
165
  contextPart = {
@@ -141,31 +177,13 @@ export async function resolveChatContext(
141
177
  export async function getAICompletionBodyWithAttachments({
142
178
  input,
143
179
  }: Opts): Promise<AICompletionBodyWithAttachments> {
144
- let contextString = "";
145
- let attachments: FileUIPart[] = [];
146
-
147
- // Skip if no '@' in the input
148
- if (input.includes("@")) {
149
- const registry = getAIContextRegistry(store);
150
- const contextIds = registry.parseAllContextIds(input);
151
-
152
- // Get context string
153
- contextString = registry.formatContextForAI(contextIds);
154
-
155
- // Get attachments
156
- try {
157
- attachments = await registry.getAttachmentsForContext(contextIds);
158
- Logger.debug("Included attachments", attachments.length);
159
- } catch (error) {
160
- Logger.error("Error getting attachments:", error);
161
- }
162
- }
180
+ const { plainText, attachments } = await resolveContextAttachments(input);
163
181
 
164
182
  return {
165
183
  body: {
166
184
  includeOtherCode: getCodes(""),
167
185
  context: {
168
- plainText: contextString,
186
+ plainText,
169
187
  schema: [],
170
188
  variables: [],
171
189
  },
@@ -48,7 +48,9 @@ export const AppContainer: React.FC<PropsWithChildren<Props>> = ({
48
48
  "bg-background w-full h-full text-textColor",
49
49
  "flex flex-col overflow-y-auto",
50
50
  width === "full" && "config-width-full",
51
- width === "columns" ? "overflow-x-auto" : "overflow-x-hidden",
51
+ width === "columns"
52
+ ? "overflow-x-auto"
53
+ : "overflow-x-auto sm:overflow-x-hidden",
52
54
  "print:height-fit",
53
55
  )}
54
56
  >
@@ -5,8 +5,8 @@ import type { JSX } from "react";
5
5
  interface Props {
6
6
  src: string;
7
7
  alt?: string;
8
- width?: number;
9
- height?: number;
8
+ width?: number | string;
9
+ height?: number | string;
10
10
  className?: string;
11
11
  }
12
12
 
@@ -17,9 +17,18 @@ export const ImageOutput = ({
17
17
  height,
18
18
  className,
19
19
  }: Props): JSX.Element => {
20
+ // Convert numeric values to pixel strings, pass string values (like "100%") as-is
21
+ const style: React.CSSProperties = {};
22
+ if (width !== undefined) {
23
+ style.width = typeof width === "number" ? `${width}px` : width;
24
+ }
25
+ if (height !== undefined) {
26
+ style.height = typeof height === "number" ? `${height}px` : height;
27
+ }
28
+
20
29
  return (
21
30
  <span className={className}>
22
- <img src={src} alt={alt} width={width} height={height} />
31
+ <img src={src} alt={alt} style={style} />
23
32
  </span>
24
33
  );
25
34
  };
@@ -32,9 +32,9 @@ export const VerticalLayoutWrapper: React.FC<PropsWithChildren<Props>> = ({
32
32
  // This padding needs to be the same from above to be correctly applied
33
33
  "pb-24 sm:pb-12",
34
34
  appConfig.width === "compact" &&
35
- "max-w-(--content-width) min-w-[400px]",
35
+ "max-w-(--content-width) sm:min-w-[400px]",
36
36
  appConfig.width === "medium" &&
37
- "max-w-(--content-width-medium) min-w-[400px]",
37
+ "max-w-(--content-width-medium) sm:min-w-[400px]",
38
38
  appConfig.width === "columns" && "w-fit",
39
39
  appConfig.width === "full" && "max-w-full",
40
40
  // Hide the cells for a fake loading effect, to avoid flickering
@@ -9,7 +9,6 @@ import {
9
9
  DatabaseIcon,
10
10
  FileIcon,
11
11
  FileTextIcon,
12
- GithubIcon,
13
12
  GraduationCapIcon,
14
13
  GridIcon,
15
14
  LayoutIcon,
@@ -17,10 +16,11 @@ import {
17
16
  MessagesSquareIcon,
18
17
  OrbitIcon,
19
18
  PackageIcon,
20
- YoutubeIcon,
21
19
  } from "lucide-react";
22
20
  import type React from "react";
23
21
  import { MarkdownIcon } from "@/components/editor/cell/code/icons";
22
+ import { GitHubIcon } from "@/components/icons/github";
23
+ import { YouTubeIcon } from "@/components/icons/youtube";
24
24
  import { Button } from "@/components/ui/button";
25
25
  import {
26
26
  DropdownMenu,
@@ -130,7 +130,7 @@ const RESOURCES = [
130
130
  {
131
131
  title: "GitHub",
132
132
  description: "View source code, report issues, or contribute",
133
- icon: GithubIcon,
133
+ icon: GitHubIcon,
134
134
  url: Constants.githubPage,
135
135
  },
136
136
  {
@@ -148,7 +148,7 @@ const RESOURCES = [
148
148
  {
149
149
  title: "YouTube",
150
150
  description: "Watch tutorials and demos",
151
- icon: YoutubeIcon,
151
+ icon: YouTubeIcon,
152
152
  url: Constants.youtube,
153
153
  },
154
154
  {
@@ -0,0 +1,21 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { SVGProps } from "react";
3
+
4
+ // Artwork from Simple Icons (https://simpleicons.org/?q=github), licensed CC0 1.0.
5
+ // The GitHub name and logo are trademarks of GitHub, Inc.
6
+ export const GitHubIcon = (props: SVGProps<SVGSVGElement>) => {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="1em"
11
+ height="1em"
12
+ viewBox="0 0 24 24"
13
+ fill="currentColor"
14
+ aria-hidden="true"
15
+ focusable="false"
16
+ {...props}
17
+ >
18
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
19
+ </svg>
20
+ );
21
+ };
@@ -0,0 +1,21 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { SVGProps } from "react";
3
+
4
+ // Artwork from Simple Icons (https://simpleicons.org/?q=youtube), licensed CC0 1.0.
5
+ // The YouTube name and logo are trademarks of Google LLC.
6
+ export const YouTubeIcon = (props: SVGProps<SVGSVGElement>) => {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="1em"
11
+ height="1em"
12
+ viewBox="0 0 24 24"
13
+ fill="currentColor"
14
+ aria-hidden="true"
15
+ focusable="false"
16
+ {...props}
17
+ >
18
+ <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
19
+ </svg>
20
+ );
21
+ };
@@ -6,14 +6,10 @@ import AzureIcon from "@marimo-team/llm-info/icons/azure.svg?inline";
6
6
  import CloudflareIcon from "@marimo-team/llm-info/icons/cloudflare.svg?inline";
7
7
  import CoreweaveIcon from "@marimo-team/llm-info/icons/coreweave.svg?inline";
8
8
  import CoreweaveDarkIcon from "@marimo-team/llm-info/icons/coreweave-dark.svg?inline";
9
- import {
10
- DatabaseZapIcon,
11
- GithubIcon,
12
- GlobeIcon,
13
- HardDriveIcon,
14
- } from "lucide-react";
9
+ import { DatabaseZapIcon, GlobeIcon, HardDriveIcon } from "lucide-react";
15
10
  import GoogleCloudIcon from "@/components/databases/icons/google-cloud-storage.svg?inline";
16
11
  import GoogleDriveIcon from "@/components/databases/icons/google-drive.svg?inline";
12
+ import { GitHubIcon } from "@/components/icons/github";
17
13
  import type { KnownStorageProtocol } from "@/core/storage/types";
18
14
  import { useTheme } from "@/theme/useTheme";
19
15
  import { cn } from "@/utils/cn";
@@ -32,7 +28,7 @@ const PROTOCOL_ICONS: Record<KnownStorageProtocol, IconEntry> = {
32
28
  file: HardDriveIcon,
33
29
  "in-memory": DatabaseZapIcon,
34
30
  gdrive: { src: GoogleDriveIcon },
35
- github: GithubIcon,
31
+ github: GitHubIcon,
36
32
  };
37
33
 
38
34
  export const ProtocolIcon: React.FC<{
@@ -253,6 +253,73 @@ a = 10`;
253
253
  `);
254
254
  });
255
255
 
256
+ test("from-import alias is the binding, not the imported name", async () => {
257
+ const code = `\
258
+ from math import sin as my_sin
259
+ print(my_sin)`;
260
+ view = createEditor(code);
261
+ const usagePosition = code.lastIndexOf("my_sin");
262
+ const result = goToVariableDefinition(view, "my_sin", usagePosition);
263
+
264
+ expect(result).toBe(true);
265
+ await tick();
266
+ // The alias `my_sin` (after `as`) is the real binding.
267
+ expect(renderEditorView(view)).toMatchInlineSnapshot(`
268
+ "
269
+ from math import sin as my_sin
270
+ ^
271
+ print(my_sin)
272
+ "
273
+ `);
274
+ });
275
+
276
+ test("module path in from-import is not a local definition", async () => {
277
+ const code = `\
278
+ from math import sin
279
+ print(math)`;
280
+ view = createEditor(code);
281
+ const usagePosition = code.lastIndexOf("math");
282
+ // `math` is a module reference in the from-clause, not a binding in this
283
+ // cell, so the scoped resolver should return false and let the caller fall
284
+ // through to cross-cell resolution.
285
+ const result = goToVariableDefinition(view, "math", usagePosition);
286
+
287
+ expect(result).toBe(false);
288
+ expect(view.state.selection.main.head).toBe(0);
289
+ });
290
+
291
+ test("imported name without `as` is a local definition", async () => {
292
+ const code = `\
293
+ from math import sin
294
+ print(sin)`;
295
+ view = createEditor(code);
296
+ const usagePosition = code.lastIndexOf("sin");
297
+ const result = goToVariableDefinition(view, "sin", usagePosition);
298
+
299
+ expect(result).toBe(true);
300
+ await tick();
301
+ expect(renderEditorView(view)).toMatchInlineSnapshot(`
302
+ "
303
+ from math import sin
304
+ ^
305
+ print(sin)
306
+ "
307
+ `);
308
+ });
309
+
310
+ test("imported name shadowed by `as` is not a binding", async () => {
311
+ const code = `\
312
+ from math import sin as my_sin
313
+ print(sin)`;
314
+ view = createEditor(code);
315
+ const usagePosition = code.lastIndexOf("sin");
316
+ // `sin` here refers to nothing in this cell (it was renamed to `my_sin`),
317
+ // so the scoped resolver should return false.
318
+ const result = goToVariableDefinition(view, "sin", usagePosition);
319
+
320
+ expect(result).toBe(false);
321
+ });
322
+
256
323
  test("selects outer-scope function declaration", async () => {
257
324
  view = createEditor(`\
258
325
  def x():
@@ -133,4 +133,51 @@ output = _x + 10`;
133
133
  await tick();
134
134
  expect(view.state.selection.main.head).toBe(code.indexOf("_x = 10"));
135
135
  });
136
+
137
+ test("falls through to cross-cell when in-cell occurrence is only a module path in a from-import", async () => {
138
+ // Regression: ImportStatement used to register every VariableName child
139
+ // (the module path and pre-`as` names) as in-cell declarations, so the
140
+ // local-first short-circuit would steal F12 from cross-cell resolution.
141
+ const moduleCell = cellId("module-cell");
142
+ const usageCell = cellId("usage-cell");
143
+ const moduleCode = `mymodule = 100`;
144
+ const usageCode = `\
145
+ from mymodule import something
146
+ print(mymodule)`;
147
+
148
+ const moduleView = createEditor(moduleCode, moduleCode.length);
149
+ const usageView = createEditor(
150
+ usageCode,
151
+ usageCode.lastIndexOf("mymodule"),
152
+ );
153
+ views.push(moduleView, usageView);
154
+
155
+ const notebook = initialNotebookState();
156
+ notebook.cellHandles[moduleCell] = {
157
+ current: { editorView: moduleView, editorViewOrNull: moduleView },
158
+ } as never;
159
+ notebook.cellHandles[usageCell] = {
160
+ current: { editorView: usageView, editorViewOrNull: usageView },
161
+ } as never;
162
+
163
+ store.set(notebookAtom, notebook);
164
+ store.set(variablesAtom, {
165
+ [variableName("mymodule")]: {
166
+ dataType: "int",
167
+ declaredBy: [moduleCell],
168
+ name: variableName("mymodule"),
169
+ usedBy: [usageCell],
170
+ value: "100",
171
+ },
172
+ });
173
+
174
+ const result = goToDefinitionAtCursorPosition(usageView);
175
+
176
+ expect(result).toBe(true);
177
+ await tick();
178
+ // Cross-cell jump: moduleView's cursor should land on `mymodule = 100`.
179
+ expect(moduleView.state.selection.main.head).toBe(
180
+ moduleCode.indexOf("mymodule"),
181
+ );
182
+ });
136
183
  });