@marimo-team/islands 0.25.1-dev25 → 0.25.1-dev27

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.1-dev25",
3
+ "version": "0.25.1-dev27",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -59,6 +59,7 @@ import {
59
59
  listModelsForAiSettings,
60
60
  } from "../ai/ai-utils";
61
61
  import { getTagColour } from "../ai/display-helpers";
62
+ import { PairWithAgentBanner } from "../editor/actions/pair-with-agent";
62
63
  import {
63
64
  Accordion,
64
65
  AccordionContent,
@@ -2005,6 +2006,7 @@ export const AiConfig: React.FC<AiConfigProps> = ({
2005
2006
  {aiEnabled !== false && (
2006
2007
  <>
2007
2008
  <TabsContent value="ai-providers">
2009
+ <PairWithAgentBanner className="mb-4" />
2008
2010
  <AiProvidersConfig
2009
2011
  form={form}
2010
2012
  config={config}
@@ -64,6 +64,10 @@ import { cn } from "@/utils/cn";
64
64
  import { Logger } from "@/utils/Logger";
65
65
  import { AIModelDropdown } from "../ai/ai-model-dropdown";
66
66
  import { useOpenSettingsToTab } from "../app-config/state";
67
+ import {
68
+ PairWithAgentBanner,
69
+ PairWithAgentButton,
70
+ } from "../editor/actions/pair-with-agent";
67
71
  import { PairWithAgentModal } from "../editor/actions/pair-with-agent-modal";
68
72
  import { PromptInput } from "../editor/ai/add-cell-with-ai";
69
73
  import {
@@ -163,6 +167,9 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
163
167
  </Button>
164
168
  </Tooltip>
165
169
  <div className="flex items-center gap-2">
170
+ <Tooltip content="Connect your agent">
171
+ <PairWithAgentButton className="mr-1" label="Connect" />
172
+ </Tooltip>
166
173
  <MCPStatusIndicator />
167
174
  <Tooltip content="AI Settings">
168
175
  <Button
@@ -493,7 +500,7 @@ const PairWithAgentCallout: React.FC<{
493
500
  onClick={onPairWithAgent}
494
501
  >
495
502
  <SparklesIcon className="h-3.5 w-3.5 shrink-0" />
496
- <span>Work on this notebook with your own agent</span>
503
+ <span>Connect your own agent to this notebook</span>
497
504
  <ArrowRightIcon className="h-3 w-3 shrink-0" />
498
505
  </Button>
499
506
  );
@@ -509,13 +516,17 @@ const ChatPanel = () => {
509
516
  title="Chat with AI"
510
517
  description="No AI provider configured or Chat model not selected"
511
518
  action={
512
- <Button
513
- variant="outline"
514
- size="sm"
515
- onClick={() => handleClick("ai", "ai-providers")}
516
- >
517
- Edit AI settings
518
- </Button>
519
+ <div className="flex flex-col gap-3">
520
+ <Button
521
+ variant="outline"
522
+ size="sm"
523
+ className="self-start"
524
+ onClick={() => handleClick("ai", "ai-providers")}
525
+ >
526
+ Edit AI settings
527
+ </Button>
528
+ <PairWithAgentBanner />
529
+ </div>
519
530
  }
520
531
  icon={<BotMessageSquareIcon />}
521
532
  />
@@ -0,0 +1,140 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import { expect, it, vi } from "vitest";
6
+ import { MockRequestClient } from "@/__mocks__/requests";
7
+ import { TooltipProvider } from "@/components/ui/tooltip";
8
+ import { alertAtom } from "@/core/alerts/state";
9
+ import {
10
+ defaultUserConfig,
11
+ type PackageManagerName,
12
+ } from "@/core/config/config-schema";
13
+ import { userConfigAtom } from "@/core/config/config";
14
+ import { requestClientAtom } from "@/core/network/requests";
15
+ import { sandboxAtom } from "@/core/packages/sandbox-state";
16
+ import { PackageAlert } from "../package-alert";
17
+
18
+ vi.mock("@/hooks/usePackageMetadata", () => ({
19
+ usePackageMetadata: () => ({
20
+ data: { versions: ["1.2.3"], extras: [] },
21
+ error: null,
22
+ isPending: false,
23
+ }),
24
+ }));
25
+
26
+ function mountPackageAlert({
27
+ configuredManager,
28
+ sandboxBackend,
29
+ source = "kernel",
30
+ }: {
31
+ configuredManager: PackageManagerName;
32
+ sandboxBackend: "uv" | "pixi" | null;
33
+ source?: "kernel" | "server";
34
+ }) {
35
+ const store = createStore();
36
+ const client = MockRequestClient.create();
37
+ const config = defaultUserConfig();
38
+ store.set(userConfigAtom, {
39
+ ...config,
40
+ package_management: { manager: configuredManager },
41
+ });
42
+ store.set(requestClientAtom, client);
43
+ store.set(
44
+ sandboxAtom,
45
+ sandboxBackend
46
+ ? {
47
+ backend: sandboxBackend,
48
+ manifest: "",
49
+ filename: "notebook.py",
50
+ }
51
+ : null,
52
+ );
53
+ store.set(alertAtom, {
54
+ ...store.get(alertAtom),
55
+ packageAlert: {
56
+ id: "missing-package",
57
+ kind: "missing",
58
+ packages: ["polars"],
59
+ isolated: true,
60
+ source,
61
+ },
62
+ });
63
+ render(
64
+ <Provider store={store}>
65
+ <TooltipProvider>
66
+ <PackageAlert />
67
+ </TooltipProvider>
68
+ </Provider>,
69
+ );
70
+ return client;
71
+ }
72
+
73
+ it.each(["uv", "pixi"] as const)(
74
+ "shows the fixed %s manager for a sandbox",
75
+ (sandboxBackend) => {
76
+ const client = mountPackageAlert({
77
+ configuredManager: sandboxBackend === "uv" ? "pixi" : "uv",
78
+ sandboxBackend,
79
+ });
80
+
81
+ const managerSelect = screen.getByTestId("install-package-manager-select");
82
+ expect(managerSelect).toHaveValue(sandboxBackend);
83
+ expect(managerSelect).toBeDisabled();
84
+
85
+ fireEvent.click(screen.getByTestId("install-packages-button"));
86
+ expect(client.sendInstallMissingPackages).toHaveBeenCalledWith(
87
+ expect.objectContaining({ manager: sandboxBackend }),
88
+ );
89
+ },
90
+ );
91
+
92
+ it("keeps the configured package manager selectable outside a sandbox", () => {
93
+ mountPackageAlert({ configuredManager: "uv", sandboxBackend: null });
94
+
95
+ const managerSelect = screen.getByTestId("install-package-manager-select");
96
+ expect(managerSelect).toHaveValue("uv");
97
+ expect(managerSelect).not.toBeDisabled();
98
+ });
99
+
100
+ it("installs a selected version in a Pixi sandbox", () => {
101
+ const client = mountPackageAlert({
102
+ configuredManager: "uv",
103
+ sandboxBackend: "pixi",
104
+ });
105
+
106
+ fireEvent.change(screen.getByDisplayValue("latest"), {
107
+ target: { value: "1.2.3" },
108
+ });
109
+ fireEvent.click(screen.getByTestId("install-packages-button"));
110
+ expect(client.sendInstallMissingPackages).toHaveBeenCalledExactlyOnceWith({
111
+ manager: "pixi",
112
+ versions: { polars: "1.2.3" },
113
+ source: "kernel",
114
+ });
115
+ });
116
+
117
+ it("does not offer version selection for Pixi outside a sandbox", () => {
118
+ mountPackageAlert({ configuredManager: "pixi", sandboxBackend: null });
119
+
120
+ expect(screen.queryByDisplayValue("latest")).not.toBeInTheDocument();
121
+ });
122
+
123
+ it("uses the configured package manager for a server installation", () => {
124
+ const client = mountPackageAlert({
125
+ configuredManager: "uv",
126
+ sandboxBackend: "pixi",
127
+ source: "server",
128
+ });
129
+
130
+ expect(
131
+ screen.queryByTestId("install-package-manager-select"),
132
+ ).not.toBeInTheDocument();
133
+
134
+ fireEvent.click(screen.getByTestId("install-packages-button"));
135
+ expect(client.sendInstallMissingPackages).toHaveBeenCalledExactlyOnceWith({
136
+ manager: "uv",
137
+ versions: { polars: "" },
138
+ source: "server",
139
+ });
140
+ });
@@ -0,0 +1,107 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { Provider } from "jotai";
5
+ import type React from "react";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { ModalProvider } from "@/components/modal/ImperativeModal";
8
+ import { TooltipProvider } from "@/components/ui/tooltip";
9
+ import { store } from "@/core/state/jotai";
10
+ import { PairWithAgentBanner, PairWithAgentButton } from "../pair-with-agent";
11
+
12
+ const mocks = vi.hoisted(() => ({
13
+ isWasm: vi.fn(() => false),
14
+ }));
15
+
16
+ vi.mock("@/core/wasm/utils", () => ({
17
+ isWasm: mocks.isWasm,
18
+ }));
19
+
20
+ function wrapper({ children }: { children: React.ReactNode }) {
21
+ return (
22
+ <Provider store={store}>
23
+ <TooltipProvider>
24
+ <ModalProvider>{children}</ModalProvider>
25
+ </TooltipProvider>
26
+ </Provider>
27
+ );
28
+ }
29
+
30
+ function stubTokenFetch() {
31
+ vi.stubGlobal(
32
+ "fetch",
33
+ vi.fn().mockResolvedValue(new Response(JSON.stringify({ token: null }))),
34
+ );
35
+ }
36
+
37
+ describe("PairWithAgent", () => {
38
+ afterEach(() => {
39
+ vi.unstubAllGlobals();
40
+ mocks.isWasm.mockReset();
41
+ mocks.isWasm.mockReturnValue(false);
42
+ });
43
+
44
+ it("shows local agent options", () => {
45
+ render(<PairWithAgentBanner />, { wrapper });
46
+
47
+ expect(
48
+ screen.getByRole("group", { name: "Supported agents" }),
49
+ ).toBeVisible();
50
+ expect(screen.getByRole("figure", { name: "Claude Code" })).toBeVisible();
51
+ expect(screen.getByRole("figure", { name: "Codex" })).toBeVisible();
52
+ expect(screen.getByRole("figure", { name: "Cursor" })).toBeVisible();
53
+ expect(screen.getByRole("figure", { name: "Gemini CLI" })).toBeVisible();
54
+ expect(screen.getByRole("figure", { name: "OpenCode" })).toBeVisible();
55
+ expect(
56
+ screen.getByRole("figure", { name: "GitHub Copilot" }),
57
+ ).toBeVisible();
58
+ expect(
59
+ screen.getByRole("figure", { name: "Any local agent" }),
60
+ ).toBeVisible();
61
+ expect(
62
+ screen.getByRole("button", {
63
+ name: "Learn more.",
64
+ }),
65
+ ).toBeVisible();
66
+ });
67
+
68
+ it("opens the pairing dialog from the banner description", () => {
69
+ stubTokenFetch();
70
+ render(<PairWithAgentBanner />, { wrapper });
71
+
72
+ fireEvent.click(
73
+ screen.getByRole("button", {
74
+ name: "Learn more.",
75
+ }),
76
+ );
77
+
78
+ expect(
79
+ screen.getByRole("dialog", { name: "Pair with an agent" }),
80
+ ).toBeVisible();
81
+ });
82
+
83
+ it("opens the pairing dialog and preserves a passed click handler", () => {
84
+ const onClick = vi.fn();
85
+ stubTokenFetch();
86
+ render(<PairWithAgentButton onClick={onClick} />, { wrapper });
87
+
88
+ fireEvent.click(screen.getByRole("button", { name: "Connect your agent" }));
89
+
90
+ expect(onClick).toHaveBeenCalledOnce();
91
+ expect(
92
+ screen.getByRole("dialog", { name: "Pair with an agent" }),
93
+ ).toBeVisible();
94
+ });
95
+
96
+ it("hides pairing controls in WASM", () => {
97
+ mocks.isWasm.mockReturnValue(true);
98
+
99
+ const { container, rerender } = render(<PairWithAgentButton />, {
100
+ wrapper,
101
+ });
102
+ expect(container).toBeEmptyDOMElement();
103
+
104
+ rerender(<PairWithAgentBanner />);
105
+ expect(container).toBeEmptyDOMElement();
106
+ });
107
+ });
@@ -0,0 +1,119 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { BotIcon } from "lucide-react";
4
+ import React from "react";
5
+ import { AiProviderIcon } from "@/components/ai/ai-provider-icon";
6
+ import { useImperativeModal } from "@/components/modal/ImperativeModal";
7
+ import { Button, type ButtonProps } from "@/components/ui/button";
8
+ import { isWasm } from "@/core/wasm/utils";
9
+ import { cn } from "@/utils/cn";
10
+ import { PairWithAgentModal } from "./pair-with-agent-modal";
11
+
12
+ const FEATURED_AGENTS = [
13
+ { id: "claude", label: "Claude Code" },
14
+ { id: "codex", label: "Codex" },
15
+ { id: "cursor", label: "Cursor" },
16
+ { id: "gemini", label: "Gemini CLI" },
17
+ { id: "opencode", label: "OpenCode" },
18
+ { id: "github", label: "GitHub Copilot" },
19
+ { id: "openai-compatible", label: "Any local agent" },
20
+ ] as const satisfies ReadonlyArray<{
21
+ id: React.ComponentProps<typeof AiProviderIcon>["provider"];
22
+ label: string;
23
+ }>;
24
+
25
+ export const usePairWithAgentModal = () => {
26
+ const { openModal, closeModal } = useImperativeModal();
27
+
28
+ return React.useCallback(
29
+ () => openModal(<PairWithAgentModal onClose={closeModal} />),
30
+ [openModal, closeModal],
31
+ );
32
+ };
33
+
34
+ export const PairWithAgentButton: React.FC<
35
+ ButtonProps & { label?: string }
36
+ > = ({ className, label = "Connect your agent", onClick, ...props }) => {
37
+ const openPairWithAgentModal = usePairWithAgentModal();
38
+
39
+ if (isWasm()) {
40
+ return null;
41
+ }
42
+
43
+ return (
44
+ <Button
45
+ type="button"
46
+ variant="link"
47
+ size="xs"
48
+ className={cn("h-auto gap-1 px-0", className)}
49
+ onClick={(event) => {
50
+ onClick?.(event);
51
+ if (!event.defaultPrevented) {
52
+ openPairWithAgentModal();
53
+ }
54
+ }}
55
+ {...props}
56
+ >
57
+ {label}
58
+ </Button>
59
+ );
60
+ };
61
+
62
+ export const PairWithAgentBanner: React.FC<{
63
+ label?: string;
64
+ className?: string;
65
+ }> = ({ label = "Connect your agent", className }) => {
66
+ const openPairWithAgentModal = usePairWithAgentModal();
67
+
68
+ if (isWasm()) {
69
+ return null;
70
+ }
71
+
72
+ return (
73
+ <div
74
+ className={cn(
75
+ "rounded-lg border border-(--blue-6) bg-(--blue-2)/50 p-3",
76
+ className,
77
+ )}
78
+ >
79
+ <div className="flex items-center gap-2 text-sm font-semibold">
80
+ {label}
81
+ <fieldset
82
+ className="m-0 flex min-w-0 -space-x-1.5 border-0 p-0 mb-0.5"
83
+ aria-label="Supported agents"
84
+ >
85
+ {FEATURED_AGENTS.map(({ id, label }) => (
86
+ <figure
87
+ key={id}
88
+ title={label}
89
+ aria-label={label}
90
+ className="flex size-6 items-center justify-center rounded-full border bg-background shadow-xs"
91
+ >
92
+ {id === "openai-compatible" ? (
93
+ <BotIcon className="size-3.5" aria-hidden={true} />
94
+ ) : (
95
+ <AiProviderIcon
96
+ provider={id}
97
+ className="size-3.5"
98
+ aria-hidden={true}
99
+ />
100
+ )}
101
+ </figure>
102
+ ))}
103
+ </fieldset>
104
+ </div>
105
+ <div className="mt-1 text-xs leading-relaxed">
106
+ <p className="inline">
107
+ Use your AI coding agent to pair-program on this notebook.
108
+ </p>
109
+ <button
110
+ type="button"
111
+ onClick={openPairWithAgentModal}
112
+ className="text-link hover:underline ml-1"
113
+ >
114
+ Learn more.
115
+ </button>
116
+ </div>
117
+ </div>
118
+ );
119
+ };
@@ -297,11 +297,12 @@ export function useNotebookActions({
297
297
 
298
298
  {
299
299
  icon: <SparklesIcon size={14} strokeWidth={1.5} />,
300
- label: "Pair with an agent",
300
+ label: "Connect your agent",
301
301
  hidden: isWasm(),
302
302
  handle: async () => {
303
303
  openModal(<PairWithAgentModal onClose={closeModal} />);
304
304
  },
305
+ additionalKeywords: ["connect"],
305
306
  },
306
307
 
307
308
  {
@@ -29,6 +29,7 @@ import useEvent from "react-use-event-hook";
29
29
  import { DefaultChatTransport } from "ai";
30
30
  import { z } from "zod";
31
31
  import { AIModelDropdown } from "@/components/ai/ai-model-dropdown";
32
+ import { PairWithAgentButton } from "@/components/editor/actions/pair-with-agent";
32
33
  import {
33
34
  AddContextButton,
34
35
  AttachFileButton,
@@ -301,6 +302,7 @@ export const AddCellWithAI: React.FC<{
301
302
  showAddCustomModelDocs={true}
302
303
  />
303
304
  {languageDropdown}
305
+ <PairWithAgentButton />
304
306
  </div>
305
307
  <div className="flex flex-row items-center">
306
308
  {files.length > 0 && (
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { useAtomValue } from "jotai";
4
5
  import {
5
6
  BoxIcon,
6
7
  CheckIcon,
@@ -34,6 +35,7 @@ import {
34
35
  import { useResolvedMarimoConfig } from "@/core/config/config";
35
36
  import type { PackageOperationStatus } from "@/core/kernel/messages";
36
37
  import { useRequestClient } from "@/core/network/requests";
38
+ import { sandboxAtom } from "@/core/packages/sandbox-state";
37
39
  import { RESTART_REQUIRED_DESCRIPTION } from "@/core/packages/toast-components";
38
40
  import { isWasm } from "@/core/wasm/utils";
39
41
  import { usePackageMetadata } from "@/hooks/usePackageMetadata";
@@ -104,6 +106,7 @@ export const PackageAlert: React.FC = () => {
104
106
  const { packageAlert } = useAlerts();
105
107
  const { clearPackageAlert } = useAlertActions();
106
108
  const [userConfig] = useResolvedMarimoConfig();
109
+ const sandbox = useAtomValue(sandboxAtom);
107
110
  const [desiredPackageVersions, setDesiredPackageVersions] = useState<
108
111
  Record<string, string>
109
112
  >({});
@@ -132,8 +135,14 @@ export const PackageAlert: React.FC = () => {
132
135
  return null;
133
136
  }
134
137
 
138
+ const sandboxPackageManager =
139
+ packageAlert.source === "server" ? null : sandbox?.backend;
140
+ const packageManager =
141
+ sandboxPackageManager ?? userConfig.package_management.manager;
135
142
  const doesSupportVersioning =
136
- userConfig.package_management.manager !== "pixi";
143
+ packageAlert.source === "server" ||
144
+ sandbox !== null ||
145
+ packageManager !== "pixi";
137
146
 
138
147
  if (isMissingPackageAlert(packageAlert)) {
139
148
  return (
@@ -207,7 +216,7 @@ export const PackageAlert: React.FC = () => {
207
216
  {packageAlert.isolated ? (
208
217
  <>
209
218
  <InstallPackagesButton
210
- manager={userConfig.package_management.manager}
219
+ manager={packageManager}
211
220
  packages={packageAlert.packages.map((pkg) => {
212
221
  const parsed = parsePackageSpecifier(pkg);
213
222
  const currentExtras =
@@ -219,10 +228,12 @@ export const PackageAlert: React.FC = () => {
219
228
  source={packageAlert.source}
220
229
  />
221
230
 
222
- {!isWasm() && (
231
+ {!isWasm() && packageAlert.source !== "server" && (
223
232
  <>
224
233
  <span className="px-2 text-sm">with</span>{" "}
225
- <PackageManagerForm />
234
+ <PackageManagerForm
235
+ fixedManager={sandboxPackageManager ?? undefined}
236
+ />
226
237
  </>
227
238
  )}
228
239
  </>
@@ -449,7 +460,9 @@ const InstallPackagesButton = ({
449
460
  );
450
461
  };
451
462
 
452
- const PackageManagerForm: React.FC = () => {
463
+ const PackageManagerForm: React.FC<{
464
+ fixedManager?: PackageManagerName;
465
+ }> = ({ fixedManager }) => {
453
466
  const [config, setConfig] = useResolvedMarimoConfig();
454
467
  const { saveUserConfig } = useRequestClient();
455
468
 
@@ -490,8 +503,8 @@ const PackageManagerForm: React.FC = () => {
490
503
  <NativeSelect
491
504
  data-testid="install-package-manager-select"
492
505
  onChange={(e) => field.onChange(e.target.value)}
493
- value={field.value}
494
- disabled={field.disabled}
506
+ value={fixedManager ?? field.value}
507
+ disabled={fixedManager !== undefined || field.disabled}
495
508
  className="inline-flex mr-2"
496
509
  >
497
510
  {PackageManagerNames.map((option) => (