@marimo-team/islands 0.24.3-dev21 → 0.24.3-dev23

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.
@@ -3,12 +3,12 @@ import { c as n, g as r, h as i, t as a } from "./button-DdXAm8iz.js";
3
3
  import { t as o } from "./react-ofJbxM4x.js";
4
4
  import { t as s } from "./compiler-runtime-Cj8ZWiNS.js";
5
5
  import { r as c } from "./copy-C0N4m1lm.js";
6
- import { Cn as l, F as u, Ft as d, Gt as f, Hn as p, In as m, Pn as h, Qn as g, Un as _, Vt as v, Wt as y, Xt as b, Yn as x, Zn as S, _ as C, a as w, c as T, d as E, er as D, f as O, g as ee, gn as k, gt as te, h as ne, hn as A, ht as re, in as ie, l as ae, m as j, mn as oe, n as se, nn as ce, on as le, p as ue, pn as de, pt as fe, q as pe, r as me, rn as he, rr as ge, rt as _e, t as ve, v as ye, vn as be, x as xe, y as Se, yt as Ce, zt as we } from "./html-to-image-CvfXy1tG.js";
6
+ import { Cn as l, F as u, Ft as d, Gt as f, Hn as p, In as m, Pn as h, Qn as g, Un as _, Vt as v, Wt as y, Xt as b, Yn as x, Zn as S, _ as C, a as w, c as T, d as E, er as D, f as O, g as ee, gn as k, gt as te, h as ne, hn as A, ht as re, in as ie, l as ae, m as j, mn as oe, n as se, nn as ce, on as le, p as ue, pn as de, pt as fe, q as pe, r as me, rn as he, rr as ge, rt as _e, t as ve, v as ye, vn as be, x as xe, y as Se, yt as Ce, zt as we } from "./html-to-image-CSU-_5BI.js";
7
7
  import { u as M } from "./dist-6ChWbo8z.js";
8
8
  import { C as Te, I as Ee, R as De, b as Oe } from "./select-UMOSCa3W.js";
9
9
  import { _ as ke, f as Ae, h as je, i as Me, n as Ne, t as Pe } from "./dropdown-menu-C73aTomr.js";
10
10
  import { p as Fe, v as Ie } from "./toDate-C3JOXlJT.js";
11
- import { d as Le, f as Re, i as ze, n as Be, t as Ve, u as He } from "./process-output-BTPsjdfd.js";
11
+ import { d as Le, f as Re, i as ze, n as Be, t as Ve, u as He } from "./process-output-B6HE2ojm.js";
12
12
  import { t as Ue } from "./jsx-runtime-DgDPJV9S.js";
13
13
  import { A as We, B as Ge, C as Ke, D as qe, E as Je, I as Ye, L as Xe, N as Ze, P as Qe, R as $e, S as et, T as tt, _ as nt, b as rt, j as it, k as at, v as ot, w as st, x as ct, y as lt } from "./schemas-BDZr-nP0.js";
14
14
  import { n as ut } from "./error-banner-hCaH06e3.js";
@@ -3,7 +3,7 @@ import { _ as n, c as r, f as i, g as a, h as o, m as s, n as c, p as l, r as u,
3
3
  import { t as f } from "./react-ofJbxM4x.js";
4
4
  import { t as p } from "./compiler-runtime-Cj8ZWiNS.js";
5
5
  import { n as m, r as h, t as g } from "./copy-C0N4m1lm.js";
6
- import { $ as _, $n as v, $t as y, A as b, An as x, At as S, Cn as C, Ct as w, D as T, E, Et as D, F as O, Gn as k, H as A, Hn as j, Ht as M, Jn as N, Kt as P, Ln as F, M as I, Mn as L, N as R, Nt as ee, Pn as z, Q as te, Qn as ne, Qt as re, Rt as ie, S as ae, T as oe, Tt as se, Un as ce, Ut as le, V as ue, Wt as de, Xt as fe, Yn as pe, Z as me, Zn as he, _ as ge, _t as _e, ar as ve, cn as ye, en as be, er as xe, g as Se, gt as Ce, ht as we, it as Te, jn as Ee, jt as De, kt as Oe, lt as ke, mt as Ae, n as je, on as Me, or as Ne, ot as Pe, pt as Fe, q as Ie, qn as Le, r as Re, rr as ze, s as Be, sr as Ve, t as He, tt as Ue, u as We, v as Ge, vt as Ke, w as qe, wn as Je, y as Ye, yn as Xe, yt as Ze } from "./html-to-image-CvfXy1tG.js";
6
+ import { $ as _, $n as v, $t as y, A as b, An as x, At as S, Cn as C, Ct as w, D as T, E, Et as D, F as O, Gn as k, H as A, Hn as j, Ht as M, Jn as N, Kt as P, Ln as F, M as I, Mn as L, N as R, Nt as ee, Pn as z, Q as te, Qn as ne, Qt as re, Rt as ie, S as ae, T as oe, Tt as se, Un as ce, Ut as le, V as ue, Wt as de, Xt as fe, Yn as pe, Z as me, Zn as he, _ as ge, _t as _e, ar as ve, cn as ye, en as be, er as xe, g as Se, gt as Ce, ht as we, it as Te, jn as Ee, jt as De, kt as Oe, lt as ke, mt as Ae, n as je, on as Me, or as Ne, ot as Pe, pt as Fe, q as Ie, qn as Le, r as Re, rr as ze, s as Be, sr as Ve, t as He, tt as Ue, u as We, v as Ge, vt as Ke, w as qe, wn as Je, y as Ye, yn as Xe, yt as Ze } from "./html-to-image-CSU-_5BI.js";
7
7
  import { o as Qe, u as B } from "./dist-6ChWbo8z.js";
8
8
  import { c as $e, i as et, r as tt } from "./useLifecycle-BGRBkTFT.js";
9
9
  import { a as nt, i as rt, n as it, o as at, r as ot, t as st } from "./useDateFormatter-BKpQHt-W.js";
@@ -32442,7 +32442,7 @@ function Iq() {
32442
32442
  return a.warn("Failed to get version from mount config"), null;
32443
32443
  }
32444
32444
  }
32445
- var Lq = Iq() || "0.24.3-dev21", Rq = V(Lq), zq = V(!0);
32445
+ var Lq = Iq() || "0.24.3-dev23", Rq = V(Lq), zq = V(!0);
32446
32446
  V(null);
32447
32447
  //#endregion
32448
32448
  //#region src/core/wasm/state.ts
@@ -30169,9 +30169,12 @@ var NT = [], PT = (e) => {
30169
30169
  }, WT = (e, t) => {
30170
30170
  if (t instanceof er.Element && t.attribs?.["data-tooltip"]) {
30171
30171
  if ((t.name?.toLowerCase() ?? "").startsWith("marimo-")) return;
30172
- let n = t.attribs["data-tooltip"], r = n.split("\n"), i = r.length === 1 ? n : r.flatMap((e, t) => t === 0 ? [e] : [/* @__PURE__ */ (0, z.jsx)("br", {}, t), e]);
30172
+ let n = t.attribs["data-tooltip"].replace(/^[\r\n]+|[\r\n]+$/g, "");
30173
30173
  return /* @__PURE__ */ (0, z.jsx)(it, {
30174
- content: i,
30174
+ content: n.includes("\n") ? /* @__PURE__ */ (0, z.jsx)("span", {
30175
+ className: "whitespace-pre-wrap",
30176
+ children: n.split(/\r?\n/).map((e, t) => /* @__PURE__ */ (0, z.jsxs)(L.Fragment, { children: [t > 0 && /* @__PURE__ */ (0, z.jsx)("br", {}), e] }, t))
30177
+ }) : n,
30175
30178
  children: e
30176
30179
  });
30177
30180
  }
package/dist/main.js CHANGED
@@ -3,16 +3,16 @@ import { _ as a, c as o, f as s, g as c, h as l, i as u, l as d, m as f, n as p,
3
3
  import { t as y } from "./react-ofJbxM4x.js";
4
4
  import { t as b } from "./compiler-runtime-Cj8ZWiNS.js";
5
5
  import { n as x, r as S, t as C } from "./copy-C0N4m1lm.js";
6
- import { $n as w, An as T, B as E, Bn as D, Bt as O, C as k, Ct as A, Dn as j, Dt as M, En as N, F as P, Fn as F, G as I, I as L, It as R, J as ee, Jt as z, K as te, Kn as B, L as ne, Lt as re, Mt as ie, Nn as ae, Nt as oe, On as se, Ot as ce, P as le, Pn as ue, Pt as de, Q as fe, Qn as pe, Qt as me, R as he, Rn as ge, Sn as _e, St as ve, U as ye, Un as be, Vn as xe, Vt as Se, W as Ce, Wn as we, Wt as Te, X as Ee, Xn as De, Y as Oe, Yn as ke, Yt as Ae, Z as je, Zn as Me, Zt as Ne, _ as Pe, _n as Fe, a as Ie, an as Le, ar as Re, at as ze, b as Be, bn as Ve, bt as He, cn as Ue, ct as We, dn as Ge, dt as Ke, et as qe, fn as Je, ft as Ye, g as Xe, i as Ze, ir as Qe, j as $e, jt as et, k as tt, kn as nt, kt as rt, ln as it, n as at, nr as ot, o as st, on as ct, ot as lt, q as ut, qt as dt, sn as ft, st as pt, tn as mt, tr as ht, un as gt, ut as _t, v as vt, wn as yt, wt as bt, xn as xt, xt as St, y as Ct, z as wt, zn as Tt } from "./html-to-image-CvfXy1tG.js";
6
+ import { $n as w, An as T, B as E, Bn as D, Bt as O, C as k, Ct as A, Dn as j, Dt as M, En as N, F as P, Fn as F, G as I, I as L, It as R, J as ee, Jt as z, K as te, Kn as B, L as ne, Lt as re, Mt as ie, Nn as ae, Nt as oe, On as se, Ot as ce, P as le, Pn as ue, Pt as de, Q as fe, Qn as pe, Qt as me, R as he, Rn as ge, Sn as _e, St as ve, U as ye, Un as be, Vn as xe, Vt as Se, W as Ce, Wn as we, Wt as Te, X as Ee, Xn as De, Y as Oe, Yn as ke, Yt as Ae, Z as je, Zn as Me, Zt as Ne, _ as Pe, _n as Fe, a as Ie, an as Le, ar as Re, at as ze, b as Be, bn as Ve, bt as He, cn as Ue, ct as We, dn as Ge, dt as Ke, et as qe, fn as Je, ft as Ye, g as Xe, i as Ze, ir as Qe, j as $e, jt as et, k as tt, kn as nt, kt as rt, ln as it, n as at, nr as ot, o as st, on as ct, ot as lt, q as ut, qt as dt, sn as ft, st as pt, tn as mt, tr as ht, un as gt, ut as _t, v as vt, wn as yt, wt as bt, xn as xt, xt as St, y as Ct, z as wt, zn as Tt } from "./html-to-image-CSU-_5BI.js";
7
7
  import { o as Et, s as Dt, u as Ot } from "./dist-6ChWbo8z.js";
8
8
  import { D as kt, E as At, O as jt, S as Mt, T as Nt, _ as Pt, a as Ft, c as It, d as Lt, f as Rt, g as zt, h as Bt, i as Vt, l as Ht, m as Ut, n as Wt, o as Gt, p as Kt, r as qt, s as Jt, t as Yt, u as Xt, v as Zt, w as Qt, x as $t, y as en } from "./spec-g740fOWu.js";
9
- import { $ as tn, $t as nn, A as rn, At as an, B as on, Bt as sn, C as cn, Cn as ln, Ct as un, D as dn, Dn as fn, Dt as pn, E as mn, En as hn, Et as gn, F as _n, Ft as vn, G as yn, Gt as bn, H as xn, Ht as Sn, I as Cn, It as wn, J as Tn, Jt as En, K as Dn, Kt as On, L as kn, Lt as An, M as jn, N as Mn, Nt as Nn, O as Pn, Ot as Fn, P as In, Pt as Ln, Q as Rn, Qt as zn, R as Bn, Rt as Vn, S as Hn, Sn as Un, St as Wn, T as Gn, Tn as Kn, Tt as qn, U as Jn, Ut as Yn, V as Xn, Vt as Zn, W as Qn, Wt as $n, X as er, Xt as tr, Y as nr, Yt as rr, Z as ir, Zt as ar, _ as or, _n as sr, _t as cr, a as lr, an as ur, at as dr, b as fr, bn as pr, bt as mr, cn as hr, dn as gr, dt as _r, en as vr, et as yr, fn as br, ft as xr, g as Sr, gn as Cr, gt as wr, hn as Tr, ht as Er, i as Dr, in as Or, it as kr, j as Ar, k as jr, kt as Mr, l as Nr, ln as Pr, m as Fr, mn as Ir, mt as Lr, n as Rr, nn as zr, nt as Br, on as Vr, ot as Hr, pn as Ur, pt as Wr, q as Gr, qt as Kr, rn as qr, rt as Jr, s as Yr, sn as Xr, st as Zr, t as Qr, tn as $r, tt as ei, un as ti, v as ni, vt as ri, w as ii, wn as ai, wt as oi, x as si, xn as ci, xt as li, y as ui, yn as di, yt as fi, z as pi, zt as mi } from "./common-BxjexIJy.js";
9
+ import { $ as tn, $t as nn, A as rn, At as an, B as on, Bt as sn, C as cn, Cn as ln, Ct as un, D as dn, Dn as fn, Dt as pn, E as mn, En as hn, Et as gn, F as _n, Ft as vn, G as yn, Gt as bn, H as xn, Ht as Sn, I as Cn, It as wn, J as Tn, Jt as En, K as Dn, Kt as On, L as kn, Lt as An, M as jn, N as Mn, Nt as Nn, O as Pn, Ot as Fn, P as In, Pt as Ln, Q as Rn, Qt as zn, R as Bn, Rt as Vn, S as Hn, Sn as Un, St as Wn, T as Gn, Tn as Kn, Tt as qn, U as Jn, Ut as Yn, V as Xn, Vt as Zn, W as Qn, Wt as $n, X as er, Xt as tr, Y as nr, Yt as rr, Z as ir, Zt as ar, _ as or, _n as sr, _t as cr, a as lr, an as ur, at as dr, b as fr, bn as pr, bt as mr, cn as hr, dn as gr, dt as _r, en as vr, et as yr, fn as br, ft as xr, g as Sr, gn as Cr, gt as wr, hn as Tr, ht as Er, i as Dr, in as Or, it as kr, j as Ar, k as jr, kt as Mr, l as Nr, ln as Pr, m as Fr, mn as Ir, mt as Lr, n as Rr, nn as zr, nt as Br, on as Vr, ot as Hr, pn as Ur, pt as Wr, q as Gr, qt as Kr, rn as qr, rt as Jr, s as Yr, sn as Xr, st as Zr, t as Qr, tn as $r, tt as ei, un as ti, v as ni, vt as ri, w as ii, wn as ai, wt as oi, x as si, xn as ci, xt as li, y as ui, yn as di, yt as fi, z as pi, zt as mi } from "./common-BWiXcWeK.js";
10
10
  import { c as hi, i as gi, n as _i, o as vi, t as yi } from "./useLifecycle-BGRBkTFT.js";
11
11
  import { t as bi } from "./check-mcQyZmFG.js";
12
12
  import { A as xi, C as Si, D as Ci, E as wi, F as Ti, I as Ei, M as Di, N as Oi, P as ki, R as Ai, S as ji, a as Mi, b as Ni, c as Pi, i as Fi, j as Ii, l as Li, n as Ri, o as zi, r as Bi, s as Vi, t as Hi, w as Ui, x as Wi } from "./select-UMOSCa3W.js";
13
13
  import { B as Gi, C as Ki, Ht as qi, J as Ji, Nt as Yi, Pt as Xi, S as Zi, W as Qi, Y as $i, _ as ea, a as ta, at as na, c as ra, f as ia, ft as aa, g as oa, h as sa, i as ca, m as la, mn as ua, n as da, ot as fa, p as pa, pn as ma, r as ha, s as ga, t as _a, v as va, x as ya, yt as ba, z as xa } from "./dropdown-menu-C73aTomr.js";
14
14
  import { _ as Sa, c as Ca, d as wa, f as Ta, g as Ea, h as Da, m as Oa, u as ka, v as Aa } from "./toDate-C3JOXlJT.js";
15
- import { a as ja, c as Ma, d as Na, f as Pa, i as Fa, l as Ia, n as La, o as Ra, r as za, s as Ba, t as Va } from "./process-output-BTPsjdfd.js";
15
+ import { a as ja, c as Ma, d as Na, f as Pa, i as Fa, l as Ia, n as La, o as Ra, r as za, s as Ba, t as Va } from "./process-output-B6HE2ojm.js";
16
16
  import { a as Ha, c as Ua, d as Wa, i as Ga, l as Ka, r as qa, u as Ja } from "./glide-utils-BMnFm0vZ.js";
17
17
  import { n as Ya, t as Xa } from "./ErrorBoundary-B7uSdDUm.js";
18
18
  import { t as Za } from "./react-dom-BUgJ-ab_.js";
@@ -10009,7 +10009,7 @@ var FS = (e) => {
10009
10009
  output: t
10010
10010
  };
10011
10011
  } };
10012
- } }, VS = q.lazy(() => import("./chat-ui-DliLTEJt.js").then((e) => ({ default: e.Chatbot }))), HS = G(W({
10012
+ } }, VS = q.lazy(() => import("./chat-ui-Cn1TFk66.js").then((e) => ({ default: e.Chatbot }))), HS = G(W({
10013
10013
  id: H(),
10014
10014
  role: _o([
10015
10015
  "system",
@@ -29942,7 +29942,7 @@ function xR(e) {
29942
29942
  }
29943
29943
  //#endregion
29944
29944
  //#region src/components/editor/renderers/slides-layout/slides-layout.tsx
29945
- var SR = q.lazy(() => import("./reveal-component-COGORv1a.js")), CR = (e) => {
29945
+ var SR = q.lazy(() => import("./reveal-component-B9C4Lv2K.js")), CR = (e) => {
29946
29946
  let t = (0, Y.c)(33), { layout: n, setLayout: r, cells: i, mode: a } = e, o = fs(rt), s = a === "read" || o, c = fs(Oe) > 1, [l, u] = (0, q.useState)(null), d;
29947
29947
  t[0] !== i || t[1] !== n ? (d = yP(i, n), t[0] = i, t[1] = n, t[2] = d) : d = t[2];
29948
29948
  let { slideCells: f, skippedIds: p, noOutputIds: m, slideTypes: h, startCellIndex: g } = d, _;
@@ -1,4 +1,4 @@
1
- import { O as e, nt as t, rt as n } from "./html-to-image-CvfXy1tG.js";
1
+ import { O as e, nt as t, rt as n } from "./html-to-image-CSU-_5BI.js";
2
2
  import { u as r } from "./dist-6ChWbo8z.js";
3
3
  import { b as i } from "./select-UMOSCa3W.js";
4
4
  import { n as a } from "./semaphore-89jsoHtS.js";
@@ -2,9 +2,9 @@ import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { c as t, g as n, h as r, m as i, t as a } from "./button-DdXAm8iz.js";
3
3
  import { t as o } from "./react-ofJbxM4x.js";
4
4
  import { t as s } from "./compiler-runtime-Cj8ZWiNS.js";
5
- import { kt as c, st as l } from "./html-to-image-CvfXy1tG.js";
5
+ import { kt as c, st as l } from "./html-to-image-CSU-_5BI.js";
6
6
  import { u } from "./dist-6ChWbo8z.js";
7
- import { _n as d, c as f, ct as p, g as m, h, l as g, lt as _, n as v, o as y, r as b, t as x, u as S, ut as C, vn as w, xn as T } from "./common-BxjexIJy.js";
7
+ import { _n as d, c as f, ct as p, g as m, h, l as g, lt as _, n as v, o as y, r as b, t as x, u as S, ut as C, vn as w, xn as T } from "./common-BWiXcWeK.js";
8
8
  import { R as E } from "./select-UMOSCa3W.js";
9
9
  import { Y as D } from "./dropdown-menu-C73aTomr.js";
10
10
  import { t as O } from "./jsx-runtime-DgDPJV9S.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.24.3-dev21",
3
+ "version": "0.24.3-dev23",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,270 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ act,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ waitFor,
9
+ } from "@testing-library/react";
10
+ import { Provider } from "jotai";
11
+ import type { ComponentProps } from "react";
12
+ import { JsonRpcError } from "use-acp";
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
+ import { MockRequestClient } from "@/__mocks__/requests";
15
+ import type { PromptInput } from "@/components/editor/ai/add-cell-with-ai";
16
+ import { TooltipProvider } from "@/components/ui/tooltip";
17
+ import { toast } from "@/components/ui/use-toast";
18
+ import { pendingAiPromptAtom } from "@/core/ai/state";
19
+ import { requestClientAtom } from "@/core/network/requests";
20
+ import { cwdAtom, filenameAtom } from "@/core/saving/file-state";
21
+ import { store } from "@/core/state/jotai";
22
+ import AgentPanel from "../agent-panel";
23
+ import { parseContextFromPrompt } from "../context-utils";
24
+ import {
25
+ addSession,
26
+ agentSessionStateAtom,
27
+ selectedTabAtom,
28
+ updateSessionExternalAgentSessionId,
29
+ } from "../state";
30
+ import type { ExternalAgentSessionId } from "../types";
31
+
32
+ const { agent, client } = vi.hoisted(() => {
33
+ const agent = {
34
+ initialize: vi.fn().mockResolvedValue({}),
35
+ prompt: vi.fn().mockResolvedValue({ stopReason: "end_turn" }),
36
+ cancel: vi.fn().mockResolvedValue({}),
37
+ };
38
+ return {
39
+ agent,
40
+ client: {
41
+ agent,
42
+ activeSessionId: "session-1",
43
+ connectionState: { status: "connected" },
44
+ notifications: [],
45
+ connect: vi.fn().mockResolvedValue(undefined),
46
+ disconnect: vi.fn(),
47
+ setActiveSessionId: vi.fn(),
48
+ },
49
+ };
50
+ });
51
+
52
+ vi.mock("use-acp", async (importOriginal) => ({
53
+ ...(await importOriginal<typeof import("use-acp")>()),
54
+ useAcpClient: () => client,
55
+ }));
56
+
57
+ vi.mock("@/components/editor/ai/add-cell-with-ai", () => ({
58
+ PromptInput: ({
59
+ value,
60
+ onChange,
61
+ onAddFiles,
62
+ onSubmit,
63
+ }: ComponentProps<typeof PromptInput>) => (
64
+ <>
65
+ <textarea
66
+ aria-label="Prompt"
67
+ value={value}
68
+ onChange={(event) => onChange(event.target.value)}
69
+ onKeyDown={(event) => {
70
+ if (event.key === "Enter") {
71
+ onSubmit(undefined, value);
72
+ }
73
+ }}
74
+ />
75
+ <input
76
+ type="file"
77
+ aria-label="Attachment"
78
+ onChange={(event) => onAddFiles?.([...(event.target.files ?? [])])}
79
+ />
80
+ </>
81
+ ),
82
+ }));
83
+ vi.mock("../session-tabs", () => ({ SessionTabs: () => null }));
84
+ vi.mock("../blocks", () => ({ ReadyToChatBlock: () => null }));
85
+ vi.mock("../context-utils", async (importOriginal) => ({
86
+ ...(await importOriginal<typeof import("../context-utils")>()),
87
+ parseContextFromPrompt: vi
88
+ .fn()
89
+ .mockResolvedValue({ contextBlocks: [], attachmentBlocks: [] }),
90
+ }));
91
+ vi.mock("@/components/ui/use-toast", () => ({ toast: vi.fn() }));
92
+
93
+ function renderPanel() {
94
+ return render(
95
+ <Provider store={store}>
96
+ <TooltipProvider>
97
+ <AgentPanel />
98
+ </TooltipProvider>
99
+ </Provider>,
100
+ );
101
+ }
102
+
103
+ function submitPrompt(prompt = "Explain this notebook") {
104
+ fireEvent.change(screen.getByRole("textbox", { name: "Prompt" }), {
105
+ target: { value: prompt },
106
+ });
107
+ fireEvent.keyDown(screen.getByRole("textbox", { name: "Prompt" }), {
108
+ key: "Enter",
109
+ });
110
+ }
111
+
112
+ describe("AgentPanel prompt submission", () => {
113
+ beforeEach(() => {
114
+ agent.prompt.mockReset().mockResolvedValue({ stopReason: "end_turn" });
115
+ vi.mocked(parseContextFromPrompt).mockReset().mockResolvedValue({
116
+ contextBlocks: [],
117
+ attachmentBlocks: [],
118
+ });
119
+ store.set(requestClientAtom, MockRequestClient.create());
120
+ store.set(filenameAtom, "notebook.py");
121
+ store.set(cwdAtom, "/notebooks");
122
+ store.set(pendingAiPromptAtom, null);
123
+ store.set(
124
+ agentSessionStateAtom,
125
+ updateSessionExternalAgentSessionId(
126
+ addSession({ sessions: [], activeTabId: null }, { agentId: "claude" }),
127
+ "session-1" as ExternalAgentSessionId,
128
+ ),
129
+ );
130
+ });
131
+
132
+ afterEach(() => {
133
+ store.set(pendingAiPromptAtom, null);
134
+ store.set(agentSessionStateAtom, { sessions: [], activeTabId: null });
135
+ store.set(filenameAtom, null);
136
+ store.set(cwdAtom, null);
137
+ });
138
+
139
+ it("preserves the draft, attachment and title when the notebook is unnamed", async () => {
140
+ store.set(filenameAtom, null);
141
+ renderPanel();
142
+ const title = store.get(selectedTabAtom)?.title;
143
+ fireEvent.change(screen.getByLabelText("Attachment"), {
144
+ target: {
145
+ files: [new File(["notes"], "notes.txt", { type: "text/plain" })],
146
+ },
147
+ });
148
+ submitPrompt();
149
+
150
+ expect(toast).toHaveBeenCalledWith({
151
+ title: "Notebook must be named",
152
+ description: "Please name the notebook to use the agent",
153
+ variant: "danger",
154
+ });
155
+ expect(agent.prompt).not.toHaveBeenCalled();
156
+ expect(screen.getByRole("textbox", { name: "Prompt" })).toHaveValue(
157
+ "Explain this notebook",
158
+ );
159
+ expect(screen.getByText("notes.txt")).toBeInTheDocument();
160
+ expect(store.get(selectedTabAtom)?.title).toBe(title);
161
+ expect(screen.queryByText("Agent is working...")).not.toBeInTheDocument();
162
+
163
+ act(() => store.set(filenameAtom, "notebook.py"));
164
+ fireEvent.keyDown(screen.getByRole("textbox", { name: "Prompt" }), {
165
+ key: "Enter",
166
+ });
167
+ await waitFor(() => expect(agent.prompt).toHaveBeenCalledOnce());
168
+ expect(agent.prompt).toHaveBeenCalledWith({
169
+ sessionId: "session-1",
170
+ prompt: expect.arrayContaining([
171
+ { type: "text", text: "Explain this notebook" },
172
+ expect.objectContaining({ type: "resource_link", name: "notes.txt" }),
173
+ ]),
174
+ });
175
+ await waitFor(() =>
176
+ expect(screen.queryByText("Agent is working...")).not.toBeInTheDocument(),
177
+ );
178
+ });
179
+
180
+ it.each([new Error("Agent unavailable"), "Agent unavailable"])(
181
+ "shows a rejected prompt and allows another submission after dismissal (%s)",
182
+ async (error) => {
183
+ agent.prompt.mockRejectedValueOnce(error);
184
+ renderPanel();
185
+ submitPrompt();
186
+
187
+ expect(await screen.findByText(/Agent unavailable/)).toBeInTheDocument();
188
+ fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
189
+ expect(screen.queryByText(/Agent unavailable/)).not.toBeInTheDocument();
190
+ submitPrompt("Try again");
191
+ await waitFor(() => expect(agent.prompt).toHaveBeenCalledTimes(2));
192
+ await waitFor(() =>
193
+ expect(
194
+ screen.queryByText("Agent is working..."),
195
+ ).not.toBeInTheDocument(),
196
+ );
197
+ },
198
+ );
199
+
200
+ it("preserves the existing authentication error action and detail", async () => {
201
+ agent.prompt.mockRejectedValueOnce(
202
+ new JsonRpcError({
203
+ code: -32_000,
204
+ message: "Authentication required",
205
+ data: { message: "Sign in to the agent" },
206
+ }),
207
+ );
208
+ renderPanel();
209
+ submitPrompt();
210
+
211
+ expect(await screen.findByText("Sign in to the agent")).toBeInTheDocument();
212
+ expect(
213
+ screen.getByRole("button", { name: "Restart session" }),
214
+ ).toBeInTheDocument();
215
+ expect(
216
+ screen.queryByRole("button", { name: "Dismiss" }),
217
+ ).not.toBeInTheDocument();
218
+ });
219
+
220
+ it("clears busy state and reports failures while preparing prompt context", async () => {
221
+ vi.mocked(parseContextFromPrompt).mockRejectedValueOnce(
222
+ new Error("Context unavailable"),
223
+ );
224
+ renderPanel();
225
+ submitPrompt();
226
+
227
+ expect(await screen.findByText("Context unavailable")).toBeInTheDocument();
228
+ expect(agent.prompt).not.toHaveBeenCalled();
229
+ fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
230
+ expect(screen.queryByText("Agent is working...")).not.toBeInTheDocument();
231
+ submitPrompt();
232
+ await waitFor(() => expect(agent.prompt).toHaveBeenCalledOnce());
233
+ });
234
+
235
+ it("reports failures from a queued prompt through the same handler", async () => {
236
+ agent.prompt.mockRejectedValueOnce(new Error("Queued prompt failed"));
237
+ renderPanel();
238
+ act(() =>
239
+ store.set(pendingAiPromptAtom, {
240
+ prompt: "Help with this cell",
241
+ submit: true,
242
+ }),
243
+ );
244
+
245
+ expect(await screen.findByText("Queued prompt failed")).toBeInTheDocument();
246
+ expect(store.get(pendingAiPromptAtom)).toBeNull();
247
+ expect(agent.prompt).toHaveBeenCalledWith({
248
+ sessionId: "session-1",
249
+ prompt: expect.arrayContaining([
250
+ { type: "text", text: "Help with this cell" },
251
+ ]),
252
+ });
253
+ });
254
+
255
+ it("clears busy state when a cancelled prompt settles", async () => {
256
+ const response = Promise.withResolvers<{ stopReason: string }>();
257
+ agent.prompt.mockReturnValueOnce(response.promise);
258
+ renderPanel();
259
+ submitPrompt();
260
+ await waitFor(() => expect(agent.prompt).toHaveBeenCalledOnce());
261
+ expect(screen.getByText("Agent is working...")).toBeInTheDocument();
262
+ fireEvent.click(screen.getByRole("button", { name: "Stop" }));
263
+ await act(async () => response.resolve({ stopReason: "cancelled" }));
264
+ expect(agent.cancel).toHaveBeenCalledWith({ sessionId: "session-1" });
265
+ expect(screen.queryByText("Agent is working...")).not.toBeInTheDocument();
266
+ expect(
267
+ screen.queryByRole("button", { name: "Dismiss" }),
268
+ ).not.toBeInTheDocument();
269
+ });
270
+ });
@@ -902,6 +902,18 @@ const AgentPanel: React.FC = () => {
902
902
  return;
903
903
  }
904
904
 
905
+ let absoluteFilename: string;
906
+ try {
907
+ absoluteFilename = getAbsoluteFilename();
908
+ } catch {
909
+ toast({
910
+ title: "Notebook must be named",
911
+ description: "Please name the notebook to use the agent",
912
+ variant: "danger",
913
+ });
914
+ return;
915
+ }
916
+
905
917
  logger.debug("Submitting prompt to agent", {
906
918
  sessionId: activeSessionId,
907
919
  });
@@ -914,62 +926,51 @@ const AgentPanel: React.FC = () => {
914
926
  setSessionState((prev) => updateSessionTitle(prev, prompt));
915
927
  }
916
928
 
917
- let absoluteFilename: string;
918
929
  try {
919
- absoluteFilename = getAbsoluteFilename();
920
- } catch {
921
- toast({
922
- title: "Notebook must be named",
923
- description: "Please name the notebook to use the agent",
924
- variant: "danger",
925
- });
926
- return;
927
- }
928
-
929
- const promptBlocks: ContentBlock[] = [{ type: "text", text: prompt }];
930
+ const promptBlocks: ContentBlock[] = [{ type: "text", text: prompt }];
930
931
 
931
- // Parse context from the prompt
932
- const { contextBlocks, attachmentBlocks } =
933
- await parseContextFromPrompt(prompt);
934
- promptBlocks.push(...contextBlocks, ...attachmentBlocks);
932
+ // Parse context from the prompt
933
+ const { contextBlocks, attachmentBlocks } =
934
+ await parseContextFromPrompt(prompt);
935
+ promptBlocks.push(...contextBlocks, ...attachmentBlocks);
935
936
 
936
- // Add manually uploaded files as resource links
937
- if (files && files.length > 0) {
938
- const fileResourceLinks = await convertFilesToResourceLinks(files);
939
- promptBlocks.push(...fileResourceLinks);
940
- }
937
+ // Add manually uploaded files as resource links
938
+ if (files && files.length > 0) {
939
+ const fileResourceLinks = await convertFilesToResourceLinks(files);
940
+ promptBlocks.push(...fileResourceLinks);
941
+ }
941
942
 
942
- const hasGivenRules = notifications.some(
943
- (notification) =>
944
- notification.type === "session_notification" &&
945
- notification.data.update.sessionUpdate === "user_message_chunk",
946
- );
947
- if (!hasGivenRules) {
948
- promptBlocks.push(
949
- {
950
- type: "resource_link",
951
- uri: absoluteFilename,
952
- mimeType: "text/x-python",
953
- name: absoluteFilename,
954
- },
955
- {
956
- type: "resource",
957
- resource: {
958
- uri: "marimo_rules.md",
959
- mimeType: "text/plain",
960
- text: getAgentPrompt(absoluteFilename),
961
- },
962
- },
943
+ const hasGivenRules = notifications.some(
944
+ (notification) =>
945
+ notification.type === "session_notification" &&
946
+ notification.data.update.sessionUpdate === "user_message_chunk",
963
947
  );
964
- }
948
+ if (!hasGivenRules) {
949
+ promptBlocks.push(
950
+ {
951
+ type: "resource_link",
952
+ uri: absoluteFilename,
953
+ mimeType: "text/x-python",
954
+ name: absoluteFilename,
955
+ },
956
+ {
957
+ type: "resource",
958
+ resource: {
959
+ uri: "marimo_rules.md",
960
+ mimeType: "text/plain",
961
+ text: getAgentPrompt(absoluteFilename),
962
+ },
963
+ },
964
+ );
965
+ }
965
966
 
966
- try {
967
967
  await agent.prompt({
968
968
  sessionId: activeSessionId,
969
969
  prompt: promptBlocks,
970
970
  });
971
971
  } catch (error) {
972
972
  logger.error("Failed to send prompt", { error });
973
+ setError(error instanceof Error ? error : String(error));
973
974
  } finally {
974
975
  setIsLoading(false);
975
976
  }
@@ -262,18 +262,30 @@ const wrapTooltipTargets: TransformFn = (
262
262
  if (tagName.startsWith("marimo-")) {
263
263
  return undefined;
264
264
  }
265
- const tooltipContent = domNode.attribs["data-tooltip"];
266
- // Convert literal newlines to <br/> elements so the tooltip renders
267
- // multiline content. &#10; is decoded to \n by html-react-parser
268
- // before this transform runs.
269
- const lines = tooltipContent.split("\n");
270
- const content: ReactNode =
271
- lines.length === 1
272
- ? tooltipContent
273
- : lines.flatMap((line, i) =>
274
- i === 0 ? [line] : [<br key={i} />, line],
275
- );
276
- return <Tooltip content={content}>{reactNode as JSX.Element}</Tooltip>;
265
+ const tooltipContent = domNode.attribs["data-tooltip"].replace(
266
+ /^[\r\n]+|[\r\n]+$/g,
267
+ "",
268
+ );
269
+ return (
270
+ <Tooltip
271
+ content={
272
+ tooltipContent.includes("\n") ? (
273
+ <span className="whitespace-pre-wrap">
274
+ {tooltipContent.split(/\r?\n/).map((line, i) => (
275
+ <React.Fragment key={i}>
276
+ {i > 0 && <br />}
277
+ {line}
278
+ </React.Fragment>
279
+ ))}
280
+ </span>
281
+ ) : (
282
+ tooltipContent
283
+ )
284
+ }
285
+ >
286
+ {reactNode as JSX.Element}
287
+ </Tooltip>
288
+ );
277
289
  }
278
290
  };
279
291
 
@@ -1,5 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type { ExtractAtomValue } from "jotai";
3
+ import { isValidElement, type ReactNode } from "react";
4
+ import { renderToStaticMarkup } from "react-dom/server";
3
5
  import { afterEach, beforeEach, describe, expect, test } from "vitest";
4
6
  import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
5
7
  import { userConfigAtom } from "@/core/config/config";
@@ -458,13 +460,21 @@ describe("wrapTooltipTargets", () => {
458
460
  expect(result).toMatchInlineSnapshot(`
459
461
  <Tooltip
460
462
  content={
461
- [
462
- "Line A",
463
- <br />,
464
- "Line B",
465
- <br />,
466
- "Line C",
467
- ]
463
+ <span
464
+ className="whitespace-pre-wrap"
465
+ >
466
+ <React.Fragment>
467
+ Line A
468
+ </React.Fragment>
469
+ <React.Fragment>
470
+ <br />
471
+ Line B
472
+ </React.Fragment>
473
+ <React.Fragment>
474
+ <br />
475
+ Line C
476
+ </React.Fragment>
477
+ </span>
468
478
  }
469
479
  >
470
480
  <span
@@ -483,11 +493,17 @@ describe("wrapTooltipTargets", () => {
483
493
  expect(parseHtml({ html })).toMatchInlineSnapshot(`
484
494
  <Tooltip
485
495
  content={
486
- [
487
- "Line A",
488
- <br />,
489
- "Line B",
490
- ]
496
+ <span
497
+ className="whitespace-pre-wrap"
498
+ >
499
+ <React.Fragment>
500
+ Line A
501
+ </React.Fragment>
502
+ <React.Fragment>
503
+ <br />
504
+ Line B
505
+ </React.Fragment>
506
+ </span>
491
507
  }
492
508
  >
493
509
  <span
@@ -514,6 +530,113 @@ describe("wrapTooltipTargets", () => {
514
530
  </Tooltip>
515
531
  `);
516
532
  });
533
+
534
+ test.each([
535
+ ["CRLF", "A\r\nB", '<span class="whitespace-pre-wrap">A<br/>B</span>'],
536
+ [
537
+ "encoded CRLF",
538
+ "A&#13;&#10;B",
539
+ '<span class="whitespace-pre-wrap">A<br/>B</span>',
540
+ ],
541
+ ["leading newline", "\nA", "A"],
542
+ ["trailing newline", "A\n", "A"],
543
+ [
544
+ "boundary newlines",
545
+ "\n\nA\nB\n\n",
546
+ '<span class="whitespace-pre-wrap">A<br/>B</span>',
547
+ ],
548
+ [
549
+ "boundary CRLF",
550
+ "&#13;&#10;A&#13;&#10;B&#13;&#10;",
551
+ '<span class="whitespace-pre-wrap">A<br/>B</span>',
552
+ ],
553
+ ["only newlines", "\n\n", ""],
554
+ ["only CRLF", "&#13;&#10;&#13;&#10;", ""],
555
+ [
556
+ "internal blank line",
557
+ "A\n\nB",
558
+ '<span class="whitespace-pre-wrap">A<br/><br/>B</span>',
559
+ ],
560
+ [
561
+ "spaces and tabs",
562
+ "\n A\t\n\tB \n",
563
+ '<span class="whitespace-pre-wrap"> A\t<br/>\tB </span>',
564
+ ],
565
+ [
566
+ "HTML text",
567
+ "&lt;b&gt;A&lt;/b&gt;\nB",
568
+ '<span class="whitespace-pre-wrap">&lt;b&gt;A&lt;/b&gt;<br/>B</span>',
569
+ ],
570
+ ])("data-tooltip handles %s", (_, content, expected) => {
571
+ const result = parseHtml({
572
+ html: `<span data-tooltip="${content}">Hover me</span>`,
573
+ });
574
+ if (!isValidElement<{ content: ReactNode }>(result)) {
575
+ throw new Error("Expected a Tooltip element");
576
+ }
577
+ expect(renderToStaticMarkup(result.props.content)).toBe(expected);
578
+ });
579
+
580
+ test("data-tooltip with newline renders as line breaks", () => {
581
+ const html = '<span data-tooltip="Line one\nLine two">Hover me</span>';
582
+ expect(parseHtml({ html })).toMatchInlineSnapshot(`
583
+ <Tooltip
584
+ content={
585
+ <span
586
+ className="whitespace-pre-wrap"
587
+ >
588
+ <React.Fragment>
589
+ Line one
590
+ </React.Fragment>
591
+ <React.Fragment>
592
+ <br />
593
+ Line two
594
+ </React.Fragment>
595
+ </span>
596
+ }
597
+ >
598
+ <span
599
+ data-tooltip="Line one
600
+ Line two"
601
+ >
602
+ Hover me
603
+ </span>
604
+ </Tooltip>
605
+ `);
606
+ });
607
+
608
+ test("data-tooltip with multiple newlines renders all as line breaks", () => {
609
+ const html = '<span data-tooltip="A\nB\nC">Hover me</span>';
610
+ expect(parseHtml({ html })).toMatchInlineSnapshot(`
611
+ <Tooltip
612
+ content={
613
+ <span
614
+ className="whitespace-pre-wrap"
615
+ >
616
+ <React.Fragment>
617
+ A
618
+ </React.Fragment>
619
+ <React.Fragment>
620
+ <br />
621
+ B
622
+ </React.Fragment>
623
+ <React.Fragment>
624
+ <br />
625
+ C
626
+ </React.Fragment>
627
+ </span>
628
+ }
629
+ >
630
+ <span
631
+ data-tooltip="A
632
+ B
633
+ C"
634
+ >
635
+ Hover me
636
+ </span>
637
+ </Tooltip>
638
+ `);
639
+ });
517
640
  });
518
641
 
519
642
  describe("parseHtml with < nad >", () => {