@marimo-team/islands 0.24.3-dev50 → 0.24.3-dev53

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.
@@ -135,7 +135,7 @@ function F(e) {
135
135
  }
136
136
  //#endregion
137
137
  //#region src/components/editor/boundary/ErrorBoundary.tsx
138
- var I = (0, T.lazy)(() => import("./feedback-button-BAHeJT2Q.js").then((e) => ({ default: e.FeedbackModal }))), L = (e) => {
138
+ var I = (0, T.lazy)(() => import("./feedback-button-CJkOCBmT.js").then((e) => ({ default: e.FeedbackModal }))), L = (e) => {
139
139
  let t = (0, w.c)(2), n;
140
140
  return t[0] === e.children ? n = t[1] : (n = /* @__PURE__ */ (0, j.jsx)(O, {
141
141
  FallbackComponent: R,
@@ -18,7 +18,7 @@ function x() {
18
18
  return e.warn("Failed to get version from mount config"), null;
19
19
  }
20
20
  }
21
- var S = x() || "0.24.3-dev50", C = u(S), w = u(!0);
21
+ var S = x() || "0.24.3-dev53", C = u(S), w = u(!0);
22
22
  u(null);
23
23
  //#endregion
24
24
  //#region src/core/wasm/state.ts
@@ -12,7 +12,7 @@ import { t as dt } from "./check-B7cIK6gb.js";
12
12
  import { A as ft, C as pt, D as mt, M as ht, O as gt, S as _t, _ as vt, b as yt, c as bt, d as xt, g as St, h as Ct, i as wt, m as Tt, n as Et, p as Dt, s as Ot, t as kt, u as At, v as jt, y as Mt } from "./select-C6plhmZr.js";
13
13
  import { $ as Nt, $t as Pt, A as Ft, At as It, B as Lt, Bt as Rt, Ct as zt, D as Bt, Dt as Vt, E as Ht, Et as Ut, F as Wt, Ft as Gt, G as Kt, Gt as qt, H as Jt, Ht as Yt, I as Xt, It as Zt, Jt as Qt, K as $t, Kt as en, L as tn, Lt as nn, M as rn, Mt as an, N as on, Nt as sn, O as cn, Ot as ln, P as un, Pt as dn, Q as fn, Qt as pn, R as mn, Rt as hn, S as gn, St as _n, T as vn, Tt as yn, U as bn, Ut as xn, V as Sn, Vt as Cn, W as wn, Wt as Tn, X as En, Xt as Dn, Y as On, Yt as kn, Z as An, Zt as jn, _ as Mn, _t as Nn, a as Pn, an as Fn, b as In, bt as Ln, cn as Rn, ct as zn, d as Bn, dn as Vn, dt as Hn, en as Un, et as Wn, f as Gn, fn as Kn, ft as qn, gt as Jn, h as Yn, ht as Xn, i as Zn, it as Qn, j as $n, jt as er, k as tr, kt as nr, l as rr, ln as ir, lt as ar, mt as or, n as sr, nn as cr, nt as lr, o as ur, on as dr, ot as fr, pn as pr, pt as mr, qt as hr, rn as gr, rt as _r, s as vr, sn as yr, st as br, t as xr, tn as Sr, tt as Cr, u as wr, un as Tr, ut as Er, vt as Dr, w as Or, wt as kr, x as Ar, xt as jr, y as Mr, yt as Nr, z as Pr, zt as Fr } from "./dropdown-menu-CZgjYSAh.js";
14
14
  import { _ as Ir, a as Lr, c as Rr, d as zr, i as Br, n as Vr, o as Hr, p as Ur, r as Wr, s as Gr, t as Kr } from "./toDate-WXg1yLcI.js";
15
- import { n as qr, t as Jr } from "./ErrorBoundary-BW0xr_AR.js";
15
+ import { n as qr, t as Jr } from "./ErrorBoundary-DYE0so6d.js";
16
16
  import { t as Yr } from "./clsx-CoeGtWvn.js";
17
17
  import { t as Xr } from "./react-dom-BUgJ-ab_.js";
18
18
  import { t as Zr } from "./jsx-runtime-DgDPJV9S.js";
@@ -2,7 +2,7 @@ import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { t } from "./react-ofJbxM4x.js";
3
3
  import { t as n } from "./react-dom-BUgJ-ab_.js";
4
4
  import { c as r, l as i, u as a } from "./data-grid-types-BucVmDDp.js";
5
- import { a as o, i as s, n as c, r as l } from "./glide-data-editor-Bp9hhneH.js";
5
+ import { a as o, i as s, n as c, r as l } from "./glide-data-editor-CLOS1M6x.js";
6
6
  //#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.2.8_b9e6aec46d952f631ce71b0585d2a8e5/node_modules/@glideapps/glide-data-grid/dist/esm/internal/data-grid-overlay-editor/data-grid-overlay-editor-style.js
7
7
  var u = /* @__PURE__ */ e(t(), 1), d = /* @__PURE__ */ e(n(), 1), f = /*#__PURE__*/ o("div")({
8
8
  name: "DataGridOverlayEditorStyle",
@@ -5,12 +5,12 @@ import { t as i } from "./compiler-runtime-Cj8ZWiNS.js";
5
5
  import { t as a } from "./copy-icon-CLdENY__.js";
6
6
  import { Bt as o, H as s, Ht as c, L as l, Ot as u, V as d, d as f, hn as p } from "./cells-BSgNCBZ8.js";
7
7
  import { b as m } from "./select-C6plhmZr.js";
8
- import { n as h, t as ee } from "./globals-BUgvsAck.js";
8
+ import { n as h, t as ee } from "./globals-D3nfkGH4.js";
9
9
  import { t as g } from "./jsx-runtime-DgDPJV9S.js";
10
10
  import { C as _, _ as te, i as v } from "./useTheme-DzXmYZ5V.js";
11
11
  import { a as ne, i as re, n as ie, r as ae } from "./dialog-CgNq6y9-.js";
12
12
  import { t as oe } from "./useAsyncData-BD6AStMt.js";
13
- import { t as se } from "./code-visibility-BdKqYmPi.js";
13
+ import { t as se } from "./code-visibility-DD-JHdjk.js";
14
14
  import { i as ce, n as le } from "./error-entries-iJn-Nryx.js";
15
15
  //#region src/components/ui/skeleton.tsx
16
16
  var y = /* @__PURE__ */ e(r(), 1), b = i(), x = g(), ue = (e) => {
@@ -6,7 +6,7 @@ import { i as s, n as c, r as l } from "./copy-icon-CLdENY__.js";
6
6
  import { C as u, c as d, i as f, n as p, s as m, t as h, x as g } from "./select-C6plhmZr.js";
7
7
  import { d as _, h as v, i as y, l as b, n as x, o as S, s as C, t as w, u as T } from "./dropdown-menu-CZgjYSAh.js";
8
8
  import { c as E, d as D, l as O, n as k, o as A, s as ee, t as j, u as te } from "./glide-utils-B3rUT5PI.js";
9
- import { n as ne, t as re } from "./ErrorBoundary-BW0xr_AR.js";
9
+ import { n as ne, t as re } from "./ErrorBoundary-DYE0so6d.js";
10
10
  import { t as M } from "./react-dom-BUgJ-ab_.js";
11
11
  import { t as N } from "./jsx-runtime-DgDPJV9S.js";
12
12
  import { t as ie } from "./label-BJ49YTvk.js";
@@ -7062,7 +7062,7 @@ function rs(e, t, n, r, i) {
7062
7062
  }
7063
7063
  //#endregion
7064
7064
  //#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.2.8_b9e6aec46d952f631ce71b0585d2a8e5/node_modules/@glideapps/glide-data-grid/dist/esm/data-editor/data-editor.js
7065
- var is = /* @__PURE__ */ e(_r(), 1), as = /* @__PURE__ */ e(xr(), 1), os = K.lazy(async () => await import("./data-grid-overlay-editor-BZiw7m3d.js")), ss = 0;
7065
+ var is = /* @__PURE__ */ e(_r(), 1), as = /* @__PURE__ */ e(xr(), 1), os = K.lazy(async () => await import("./data-grid-overlay-editor-pi0SFrLT.js")), ss = 0;
7066
7066
  function cs(e) {
7067
7067
  return (0, is.default)((0, as.default)((0, as.default)(e).filter((e) => e.span !== void 0).map((e) => (0, Za.default)((e.span?.[0] ?? 0) + 1, (e.span?.[1] ?? 0) + 1))));
7068
7068
  }
@@ -9293,7 +9293,7 @@ function Vs(e, t, n, r, i) {
9293
9293
  }
9294
9294
  //#endregion
9295
9295
  //#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.2.8_b9e6aec46d952f631ce71b0585d2a8e5/node_modules/@glideapps/glide-data-grid/dist/esm/cells/number-cell.js
9296
- var Hs = K.lazy(async () => await import("./number-overlay-editor-C743iqsh.js")), Us = {
9296
+ var Hs = K.lazy(async () => await import("./number-overlay-editor-tX2EIR_o.js")), Us = {
9297
9297
  getAccessibilityString: (e) => e.data?.toString() ?? "",
9298
9298
  kind: W.Number,
9299
9299
  needsHover: (e) => e.hoverEffect === !0,
@@ -1,6 +1,6 @@
1
1
  import { g as e } from "./tooltip-qB8NlSqV.js";
2
2
  import { _ as t } from "./useTheme-DzXmYZ5V.js";
3
- import { r as n } from "./code-visibility-BdKqYmPi.js";
3
+ import { r as n } from "./code-visibility-DD-JHdjk.js";
4
4
  var r = e("triangle-alert", [
5
5
  ["path", {
6
6
  d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import { C as T, F as E, G as D, I as O, J as k, K as A, L as j, P as M, R as N,
7
7
  import { a as se, f as ce, g as V, i as le, n as ue, o as de, p as fe, r as pe, t as me } from "./tooltip-qB8NlSqV.js";
8
8
  import { $ as he, $t as ge, At as _e, B as ve, C as ye, D as be, Dt as xe, F as Se, G as Ce, Gt as we, I as Te, J as Ee, L as De, Mt as Oe, Nt as ke, O as Ae, Ot as je, P as Me, Q as Ne, Qt as Pe, S as Fe, T as Ie, Tt as Le, U as Re, Ut as ze, W as Be, Wt as Ve, X as He, Xt as Ue, Y as We, Yt as Ge, Zt as Ke, _n as qe, _t as Je, an as Ye, bn as Xe, c as Ze, cn as Qe, ct as $e, d as et, dn as tt, dt as nt, f as rt, fn as it, ft as at, g as ot, gn as st, gt as ct, h as lt, in as ut, it as dt, jt as ft, k as pt, kt as mt, l as ht, m as gt, mn as _t, n as vt, o as yt, on as bt, p as xt, pn as St, pt as Ct, q as wt, qt as Tt, r as Et, rn as Dt, rt as Ot, u as kt, v as At, w as jt, xt as Mt, yn as Nt, zt as Pt } from "./cells-BSgNCBZ8.js";
9
9
  import { D as Ft, E as It, O as Lt, S as Rt, T as zt, _ as Bt, a as Vt, c as Ht, d as Ut, f as Wt, g as Gt, h as Kt, i as qt, l as Jt, m as Yt, n as Xt, o as Zt, p as Qt, r as $t, s as en, t as tn, u as nn, v as rn, w as an, x as on, y as sn } from "./spec-CAYwFeV6.js";
10
- import { $t as cn, A as ln, At as un, B as dn, Bt as fn, C as pn, Ct as mn, D as hn, Dt as gn, E as _n, Et as vn, F as yn, Ft as bn, G as xn, Gt as Sn, H as Cn, Ht as wn, I as Tn, It as En, J as Dn, Jt as On, K as kn, Kt as An, L as jn, Lt as Mn, M as Nn, Mt as Pn, N as Fn, Nt as In, O as Ln, Ot as Rn, P as zn, Pt as Bn, Qt as Vn, R as Hn, Rt as Un, S as Wn, St as Gn, T 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, Zt as ir, _ as ar, _t as or, an as sr, at as cr, b as lr, c as ur, cn as dr, ct as fr, d as pr, dn as mr, dt as hr, en as gr, et as _r, f as vr, fn as yr, ft as br, g as xr, gt as Sr, h as Cr, hn as wr, ht as Tr, in as Er, it as Dr, j as Or, jt as kr, k as Ar, kt as jr, ln as Mr, lt as Nr, m as Pr, mn as Fr, mt as Ir, nn as Lr, nt as Rr, ot as zr, p as Br, pn as Vr, pt as Hr, q as Ur, qt as Wr, r as Gr, rn as Kr, rt as qr, sn as Jr, st as Yr, t as Xr, tn as Zr, tt as Qr, u as $r, un as ei, ut as ti, v as ni, vt as ri, w as ii, wt as ai, x as oi, y as si, yt as ci, z as li, zt as ui } from "./common-D9PY0k7A.js";
10
+ import { $t as cn, A as ln, At as un, B as dn, Bt as fn, C as pn, Ct as mn, D as hn, Dt as gn, E as _n, Et as vn, F as yn, Ft as bn, G as xn, Gt as Sn, H as Cn, Ht as wn, I as Tn, It as En, J as Dn, Jt as On, K as kn, Kt as An, L as jn, Lt as Mn, M as Nn, Mt as Pn, N as Fn, Nt as In, O as Ln, Ot as Rn, P as zn, Pt as Bn, Qt as Vn, R as Hn, Rt as Un, S as Wn, St as Gn, T 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, Zt as ir, _ as ar, _t as or, an as sr, at as cr, b as lr, c as ur, cn as dr, ct as fr, d as pr, dn as mr, dt as hr, en as gr, et as _r, f as vr, fn as yr, ft as br, g as xr, gt as Sr, h as Cr, hn as wr, ht as Tr, in as Er, it as Dr, j as Or, jt as kr, k as Ar, kt as jr, ln as Mr, lt as Nr, m as Pr, mn as Fr, mt as Ir, nn as Lr, nt as Rr, ot as zr, p as Br, pn as Vr, pt as Hr, q as Ur, qt as Wr, r as Gr, rn as Kr, rt as qr, sn as Jr, st as Yr, t as Xr, tn as Zr, tt as Qr, u as $r, un as ei, ut as ti, v as ni, vt as ri, w as ii, wt as ai, x as oi, y as si, yt as ci, z as li, zt as ui } from "./common-ki0DZSer.js";
11
11
  import { c as di, i as fi, n as pi, o as mi, t as hi } from "./useLifecycle-DPgiiwwU.js";
12
12
  import { t as gi } from "./check-B7cIK6gb.js";
13
13
  import { A as _i, C as vi, D as yi, E as bi, M as xi, O as Si, S as Ci, T as wi, a as Ti, b as Ei, c as Di, i as Oi, k as ki, l as Ai, n as ji, o as Mi, r as Ni, s as Pi, t as Fi, w as Ii, x as Li } from "./select-C6plhmZr.js";
@@ -15,8 +15,8 @@ import { B as Ri, C as zi, J as Bi, Mt as Vi, Nt as Hi, S as Ui, Vt as Wi, W as
15
15
  import { _ as ha, c as ga, d as _a, f as va, g as ya, h as ba, m as xa, u as Sa, v as Ca } from "./toDate-WXg1yLcI.js";
16
16
  import { a as wa, c as Ta, d as Ea, f as Da, i as Oa, l as ka, n as Aa, o as ja, r as Ma, s as Na, t as Pa } from "./process-output-DFe2tVIY.js";
17
17
  import { a as Fa, c as Ia, d as La, i as Ra, l as za, r as Ba, u as Va } from "./glide-utils-B3rUT5PI.js";
18
- import { n as Ha, t as Ua } from "./ErrorBoundary-BW0xr_AR.js";
19
- import { n as Wa, t as Ga } from "./globals-BUgvsAck.js";
18
+ import { n as Ha, t as Ua } from "./ErrorBoundary-DYE0so6d.js";
19
+ import { n as Wa, t as Ga } from "./globals-D3nfkGH4.js";
20
20
  import { t as Ka } from "./react-dom-BUgJ-ab_.js";
21
21
  import { t as qa } from "./jsx-runtime-DgDPJV9S.js";
22
22
  import { B as Ja, C as H, Ct as Ya, D as Xa, E as Za, Et as Qa, F as U, G as $a, H as eo, I as to, L as no, M as W, N as ro, O as io, P as ao, Q as oo, R as so, S as G, St as co, Tt as lo, U as uo, W as fo, Y as po, Z as mo, _ as ho, a as go, b as _o, bt as vo, c as yo, d as bo, dt as xo, f as So, g as Co, gt as wo, h as To, i as Eo, j as K, k as Do, l as Oo, m as ko, n as Ao, o as jo, p as Mo, r as No, s as Po, u as Fo, v as Io, vt as Lo, w as Ro, wt as zo, x as Bo, xt as Vo, y as Ho, yt as Uo } from "./use-restore-focus-D5D9xEjZ.js";
@@ -40,7 +40,7 @@ import { t as Ns } from "./useAsyncData-BD6AStMt.js";
40
40
  import { i as Ps, n as Fs, r as Is } from "./formats-BR5iGb4s.js";
41
41
  import { M as Ls, N as Rs } from "./vega-loader.browser-D95WfUcj.js";
42
42
  import { a as zs, n as Bs } from "./loader-D2W4oM-3.js";
43
- import { c as Vs, d as Hs, i as Us, l as Ws, o as Gs, s as Ks, t as qs, u as Js } from "./code-visibility-BdKqYmPi.js";
43
+ import { c as Vs, d as Hs, i as Us, l as Ws, o as Gs, s as Ks, t as qs, u as Js } from "./code-visibility-DD-JHdjk.js";
44
44
  import { t as Ys } from "./useDeepCompareMemoize-D_HNdyvI.js";
45
45
  import { t as Xs } from "./esm-BElI3qKH.js";
46
46
  //#region ../node_modules/.pnpm/iconify-icon@2.3.0/node_modules/iconify-icon/dist/iconify-icon.mjs
@@ -10458,7 +10458,7 @@ function RC(e, t) {
10458
10458
  }
10459
10459
  //#endregion
10460
10460
  //#region src/plugins/impl/DataEditorPlugin.tsx
10461
- var zC = J.lazy(() => import("./glide-data-editor-Bp9hhneH.js").then((e) => e.t)), BC = xS("marimo-data-editor", { cssStyles: [WS] }).withData(W({
10461
+ var zC = J.lazy(() => import("./glide-data-editor-CLOS1M6x.js").then((e) => e.t)), BC = xS("marimo-data-editor", { cssStyles: [WS] }).withData(W({
10462
10462
  initialValue: W({ edits: G(W({
10463
10463
  rowIdx: K(),
10464
10464
  columnId: U(),
@@ -29922,7 +29922,7 @@ function bR(e) {
29922
29922
  }
29923
29923
  //#endregion
29924
29924
  //#region src/components/editor/renderers/slides-layout/slides-layout.tsx
29925
- var xR = J.lazy(() => import("./reveal-component-CgMzL2w5.js")), SR = (e) => {
29925
+ var xR = J.lazy(() => import("./reveal-component-Bw2DWz-a.js")), SR = (e) => {
29926
29926
  let t = (0, X.c)(33), { layout: n, setLayout: r, cells: i, mode: a } = e, o = rs(Ce), s = a === "read" || o, c = rs(xt) > 1, [l, u] = (0, J.useState)(null), d;
29927
29927
  t[0] !== i || t[1] !== n ? (d = vP(i, n), t[0] = i, t[1] = n, t[2] = d) : d = t[2];
29928
29928
  let { slideCells: f, skippedIds: p, noOutputIds: m, slideTypes: h, startCellIndex: g } = d, _;
@@ -1,6 +1,6 @@
1
1
  import { a as e } from "./rolldown-runtime-Cb76FRpK.js";
2
2
  import { t } from "./react-ofJbxM4x.js";
3
- import { a as n } from "./glide-data-editor-Bp9hhneH.js";
3
+ import { a as n } from "./glide-data-editor-CLOS1M6x.js";
4
4
  //#region ../node_modules/.pnpm/@glideapps+glide-data-grid@6.0.4-alpha24_lodash@4.17.21_marked@15.0.12_react-dom@19.2.8_b9e6aec46d952f631ce71b0585d2a8e5/node_modules/@glideapps/glide-data-grid/dist/esm/internal/data-grid-overlay-editor/private/number-overlay-editor-style.js
5
5
  var r = /* @__PURE__ */ e(t()), i = /*#__PURE__*/ n("div")({
6
6
  name: "NumberOverlayEditorStyle",
@@ -4,13 +4,13 @@ import { t as o } from "./react-ofJbxM4x.js";
4
4
  import { t as s } from "./compiler-runtime-Cj8ZWiNS.js";
5
5
  import { g as c, t as l } from "./tooltip-qB8NlSqV.js";
6
6
  import { G as u, w as d } from "./cells-BSgNCBZ8.js";
7
- import { $ as f, Q as p, Z as m, an as h, i as g, l as _, ln as v, n as y, on as b, r as x, t as S, u as C } from "./common-D9PY0k7A.js";
7
+ import { $ as f, Q as p, Z as m, an as h, i as g, l as _, ln as v, n as y, on as b, r as x, t as S, u as C } from "./common-ki0DZSer.js";
8
8
  import { M as w } from "./select-C6plhmZr.js";
9
9
  import { J as T } from "./dropdown-menu-CZgjYSAh.js";
10
10
  import { t as E } from "./jsx-runtime-DgDPJV9S.js";
11
11
  import { q as ee } from "./use-restore-focus-D5D9xEjZ.js";
12
12
  import { A as D, C as O, b as k } from "./useTheme-DzXmYZ5V.js";
13
- import { a as A, n as te, t as ne } from "./code-visibility-BdKqYmPi.js";
13
+ import { a as A, n as te, t as ne } from "./code-visibility-DD-JHdjk.js";
14
14
  /* empty css */
15
15
  var j = c("sticky-note", [["path", {
16
16
  d: "M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.24.3-dev50",
3
+ "version": "0.24.3-dev53",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,19 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { pairPreviewSchema } from "@/core/config/pair";
6
+
7
+ // Read the Python-owned resource only in tests to check the shared contract.
8
+ export const PAIR_PREVIEW = pairPreviewSchema.parse({
9
+ command: "uv run marimo",
10
+ templates: JSON.parse(
11
+ readFileSync(
12
+ path.resolve(
13
+ import.meta.dirname,
14
+ "../../../../marimo/_cli/pair/prompt.json",
15
+ ),
16
+ "utf8",
17
+ ),
18
+ ),
19
+ });
@@ -8,6 +8,8 @@ import { connectionAtom } from "@/core/network/connection";
8
8
  import { store } from "@/core/state/jotai";
9
9
  import { isStaticNotebook } from "@/core/static/static-state";
10
10
  import { WebSocketState } from "@/core/websocket/types";
11
+ import { pairPreviewAtom } from "@/core/config/pair";
12
+ import { PAIR_PREVIEW } from "./fixtures/pair-preview";
11
13
  import { mount, visibleForTesting } from "../mount";
12
14
 
13
15
  // Mock React DOM
@@ -82,6 +84,22 @@ describe("mount", () => {
82
84
  mockElement,
83
85
  );
84
86
 
87
+ it("hydrates the server's Pair preview configuration", () => {
88
+ const error = mountRead({ pairPreview: PAIR_PREVIEW });
89
+
90
+ expect(error).toBeUndefined();
91
+ expect(store.get(pairPreviewAtom)).toEqual(PAIR_PREVIEW);
92
+ });
93
+
94
+ it("uses legacy Pair behavior when the host omits preview configuration", () => {
95
+ store.set(pairPreviewAtom, PAIR_PREVIEW);
96
+
97
+ const error = mountRead();
98
+
99
+ expect(error).toBeUndefined();
100
+ expect(store.get(pairPreviewAtom)).toBeUndefined();
101
+ });
102
+
85
103
  describe("connection state initialization", () => {
86
104
  it("should set connection to CONNECTING when runtimeConfig has lazy=false", () => {
87
105
  mount(
@@ -10,10 +10,12 @@ import {
10
10
  maskToken,
11
11
  } from "../pair-with-agent-commands";
12
12
  import { shellQuote } from "@/utils/shell";
13
+ import { PAIR_PREVIEW } from "@/__tests__/fixtures/pair-preview";
13
14
 
14
15
  const CONNECTION: ConnectionInfo = {
15
16
  url: "http://localhost:8000",
16
17
  file: "notebooks/example.py",
18
+ session: "s_ab12cd",
17
19
  };
18
20
 
19
21
  const CONNECTION_WITHOUT_FILE: ConnectionInfo = {
@@ -104,6 +106,61 @@ describe("getMarimoCommand", () => {
104
106
  });
105
107
 
106
108
  describe("getTerminalCommand", () => {
109
+ it("uses the preview launcher in production and quotes session-only targets", () => {
110
+ vi.stubEnv("DEV", false);
111
+ try {
112
+ expect(
113
+ getTerminalCommand(
114
+ "claude",
115
+ { url: "http://host:8000?a=1&b=2", session: "s_' {file}" },
116
+ false,
117
+ { ...PAIR_PREVIEW, command: "uvx marimo@latest" },
118
+ ),
119
+ ).toBe(
120
+ String.raw`claude "$(MARIMO_PAIR_NEXT=1 uvx marimo@latest pair prompt \
121
+ --url 'http://host:8000?a=1&b=2' \
122
+ --session 's_'"'"' {file}')"`,
123
+ );
124
+ expect(
125
+ getTerminalCommand(
126
+ "codex",
127
+ CONNECTION_WITHOUT_FILE,
128
+ false,
129
+ PAIR_PREVIEW,
130
+ ),
131
+ ).toBe(
132
+ String.raw`codex "$(MARIMO_PAIR_NEXT=1 uv run marimo pair prompt \
133
+ --url http://localhost:8000)"`,
134
+ );
135
+ } finally {
136
+ vi.unstubAllEnvs();
137
+ }
138
+ });
139
+
140
+ it.each([
141
+ ["claude", "claude"],
142
+ ["codex", "codex"],
143
+ ["opencode", "opencode --prompt"],
144
+ ] as const)(
145
+ "enables the preview inside the %s prompt command",
146
+ (agent, cli) => {
147
+ expect(
148
+ getTerminalCommand(
149
+ agent,
150
+ { ...CONNECTION, session: "s_ab12cd" },
151
+ true,
152
+ PAIR_PREVIEW,
153
+ ),
154
+ ).toBe(
155
+ String.raw`${cli} "$(MARIMO_PAIR_NEXT=1 uv run marimo pair prompt \
156
+ --url http://localhost:8000 \
157
+ --file notebooks/example.py \
158
+ --session s_ab12cd \
159
+ --with-token)"`,
160
+ );
161
+ },
162
+ );
163
+
107
164
  it("includes the url and file for each agent", () => {
108
165
  expect(getTerminalCommand("claude", CONNECTION, false)).toBe(
109
166
  `claude "$(uv run marimo pair prompt --url http://localhost:8000 --file notebooks/example.py --claude)"`,
@@ -169,6 +226,64 @@ describe("getTerminalCommand", () => {
169
226
  });
170
227
 
171
228
  describe("getRawPrompt", () => {
229
+ it("uses supplied templates without reinterpreting inserted values", () => {
230
+ expect(
231
+ getRawPrompt(
232
+ {
233
+ url: "http://host/{file}",
234
+ file: "{session}/it's.py",
235
+ session: "{command}",
236
+ },
237
+ "tok'en {file} $&",
238
+ {
239
+ ...PAIR_PREVIEW,
240
+ command: "custom marimo",
241
+ templates: {
242
+ ...PAIR_PREVIEW.templates,
243
+ prompt: "{command}\n{url}\n{file}{session}{authentication}",
244
+ },
245
+ },
246
+ ),
247
+ ).toBe(
248
+ "custom marimo\nhttp://host/{file}\nFile: {session}/it's.py\nSession: {command}\n" +
249
+ `\n\nFor authenticated Pair commands, set \`export MARIMO_TOKEN='tok'"'"'en {file} $&'\` in the shell that runs marimo.`,
250
+ );
251
+ });
252
+
253
+ it.each([
254
+ [CONNECTION_WITHOUT_FILE, ""],
255
+ [
256
+ { ...CONNECTION_WITHOUT_FILE, session: "s_ab12cd" },
257
+ "Session: s_ab12cd\n",
258
+ ],
259
+ [
260
+ { ...CONNECTION_WITHOUT_FILE, file: "notebook.py" },
261
+ "File: notebook.py\n",
262
+ ],
263
+ ])("omits absent preview fields for %j", (connection, fields) => {
264
+ expect(getRawPrompt(connection, null, PAIR_PREVIEW)).toBe(
265
+ "Pair with me on this running marimo notebook.\n\n" +
266
+ `URL: http://localhost:8000\n${fields}\n` +
267
+ "Run `uv run marimo pair --help` first.\n" +
268
+ "Use `uv run marimo` for all marimo commands.\n\n" +
269
+ "Once connected, send a fun toast using `mo.status.toast(...)` (`import marimo as mo`).",
270
+ );
271
+ });
272
+
273
+ it("renders the shared preview template with optional session context", () => {
274
+ expect(
275
+ getRawPrompt({ ...CONNECTION, session: "s_ab12cd" }, null, PAIR_PREVIEW),
276
+ ).toBe(
277
+ "Pair with me on this running marimo notebook.\n\n" +
278
+ "URL: http://localhost:8000\n" +
279
+ "File: notebooks/example.py\n" +
280
+ "Session: s_ab12cd\n\n" +
281
+ "Run `uv run marimo pair --help` first.\n" +
282
+ "Use `uv run marimo` for all marimo commands.\n\n" +
283
+ "Once connected, send a fun toast using `mo.status.toast(...)` (`import marimo as mo`).",
284
+ );
285
+ });
286
+
172
287
  it("references the file-scoped execute-code command", () => {
173
288
  const prompt = getRawPrompt(CONNECTION, null);
174
289
  expect(prompt).toContain(
@@ -0,0 +1,218 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ act,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ waitFor,
9
+ within,
10
+ } from "@testing-library/react";
11
+ import { Provider } from "jotai";
12
+ import type React from "react";
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
+ import { PAIR_PREVIEW } from "@/__tests__/fixtures/pair-preview";
15
+ import { Dialog } from "@/components/ui/dialog";
16
+ import { TooltipProvider } from "@/components/ui/tooltip";
17
+ import { pairPreviewAtom } from "@/core/config/pair";
18
+ import { getSessionId } from "@/core/kernel/session";
19
+ import { filenameAtom } from "@/core/saving/file-state";
20
+ import {
21
+ DEFAULT_RUNTIME_CONFIG,
22
+ runtimeConfigAtom,
23
+ } from "@/core/runtime/config";
24
+ import { store } from "@/core/state/jotai";
25
+ import { PairWithAgentModal } from "../pair-with-agent-modal";
26
+ import { copyToClipboard } from "@/utils/copy";
27
+
28
+ vi.mock("@/utils/copy", () => ({
29
+ copyToClipboard: vi.fn().mockResolvedValue(undefined),
30
+ }));
31
+
32
+ function wrapper({ children }: { children: React.ReactNode }) {
33
+ return (
34
+ <Provider store={store}>
35
+ <TooltipProvider>
36
+ <Dialog open={true}>{children}</Dialog>
37
+ </TooltipProvider>
38
+ </Provider>
39
+ );
40
+ }
41
+
42
+ describe("PairWithAgentModal", () => {
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ store.set(pairPreviewAtom, PAIR_PREVIEW);
46
+ store.set(filenameAtom, "/project/my notebook.py");
47
+ store.set(runtimeConfigAtom, {
48
+ ...DEFAULT_RUNTIME_CONFIG,
49
+ url: "http://localhost:8000",
50
+ });
51
+ window.history.replaceState({}, "", "/?file=notebook.py");
52
+ vi.stubGlobal(
53
+ "fetch",
54
+ vi.fn().mockResolvedValue(new Response(JSON.stringify({ token: null }))),
55
+ );
56
+ });
57
+
58
+ afterEach(() => {
59
+ store.set(pairPreviewAtom, undefined);
60
+ store.set(filenameAtom, null);
61
+ vi.unstubAllGlobals();
62
+ });
63
+
64
+ it.each(["Claude", "Codex", "OpenCode", "Prompt"])(
65
+ "omits skill installation and includes the current session in %s preview",
66
+ async (tab) => {
67
+ await act(async () => {
68
+ render(<PairWithAgentModal onClose={vi.fn()} />, { wrapper });
69
+ });
70
+ fireEvent.mouseDown(screen.getByRole("tab", { name: tab }), {
71
+ button: 0,
72
+ ctrlKey: false,
73
+ });
74
+
75
+ const panel = screen.getByRole("tabpanel");
76
+ expect(within(panel).queryAllByText(/skill/i)).toEqual([]);
77
+ expect(panel).toHaveTextContent(getSessionId());
78
+ expect(panel).toHaveTextContent(
79
+ tab === "Prompt" ? "File: notebook.py" : "--file notebook.py",
80
+ );
81
+ expect(panel).not.toHaveTextContent("/project/my notebook.py");
82
+ expect(panel).toHaveTextContent(
83
+ tab === "Prompt" ? "1. Copy this prompt" : "1. Run in your terminal",
84
+ );
85
+ },
86
+ );
87
+
88
+ it.each(["Claude", "Codex", "OpenCode", "Prompt"])(
89
+ "includes the known notebook file in %s preview when the URL has no file",
90
+ async (tab) => {
91
+ window.history.replaceState({}, "", "/");
92
+ await act(async () => {
93
+ render(<PairWithAgentModal onClose={vi.fn()} />, { wrapper });
94
+ });
95
+ fireEvent.mouseDown(screen.getByRole("tab", { name: tab }), {
96
+ button: 0,
97
+ ctrlKey: false,
98
+ });
99
+
100
+ const panel = screen.getByRole("tabpanel");
101
+ const fileHint =
102
+ tab === "Prompt"
103
+ ? "File: /project/my notebook.py"
104
+ : "--file '/project/my notebook.py'";
105
+ expect(panel).toHaveTextContent(fileHint);
106
+ fireEvent.click(
107
+ within(panel).getByRole("button", {
108
+ name: tab === "Prompt" ? "Copy prompt" : "Copy command",
109
+ }),
110
+ );
111
+ await waitFor(() => expect(copyToClipboard).toHaveBeenCalledOnce());
112
+ expect(vi.mocked(copyToClipboard).mock.calls[0][0]).toContain(fileHint);
113
+ },
114
+ );
115
+
116
+ it.each([null, ""])(
117
+ "omits the file when the URL and notebook filename are absent (%s)",
118
+ async (filename) => {
119
+ window.history.replaceState({}, "", "/");
120
+ store.set(filenameAtom, filename);
121
+ await act(async () => {
122
+ render(<PairWithAgentModal onClose={vi.fn()} />, { wrapper });
123
+ });
124
+ expect(screen.getByRole("tabpanel")).not.toHaveTextContent("--file");
125
+ fireEvent.mouseDown(screen.getByRole("tab", { name: "Prompt" }), {
126
+ button: 0,
127
+ ctrlKey: false,
128
+ });
129
+ expect(screen.getByRole("tabpanel")).not.toHaveTextContent("File:");
130
+ },
131
+ );
132
+
133
+ it.each(["Claude", "Codex", "OpenCode", "Prompt"])(
134
+ "preserves the %s skill flow without preview configuration",
135
+ async (tab) => {
136
+ store.set(pairPreviewAtom, undefined);
137
+ window.history.replaceState({}, "", "/");
138
+ await act(async () => {
139
+ render(<PairWithAgentModal onClose={vi.fn()} />, { wrapper });
140
+ });
141
+ fireEvent.mouseDown(screen.getByRole("tab", { name: tab }), {
142
+ button: 0,
143
+ ctrlKey: false,
144
+ });
145
+
146
+ const panel = screen.getByRole("tabpanel");
147
+ expect(
148
+ within(panel).getByText("npx skills add marimo-team/marimo-pair"),
149
+ ).toBeVisible();
150
+ expect(panel).not.toHaveTextContent(getSessionId());
151
+ expect(panel).not.toHaveTextContent("MARIMO_PAIR_NEXT");
152
+ expect(panel).not.toHaveTextContent("/project/my notebook.py");
153
+ expect(panel).toHaveTextContent(
154
+ tab === "Prompt" ? "2. Copy this prompt" : "2. Run in your terminal",
155
+ );
156
+ },
157
+ );
158
+
159
+ it.each([true, false])(
160
+ "masks the raw token but copies it intact (preview=%s)",
161
+ async (preview) => {
162
+ store.set(pairPreviewAtom, preview ? PAIR_PREVIEW : undefined);
163
+ vi.mocked(fetch).mockResolvedValue(
164
+ new Response(JSON.stringify({ token: "secret-token" })),
165
+ );
166
+ await act(async () => {
167
+ render(<PairWithAgentModal onClose={vi.fn()} />, { wrapper });
168
+ });
169
+ fireEvent.mouseDown(screen.getByRole("tab", { name: "Prompt" }), {
170
+ button: 0,
171
+ ctrlKey: false,
172
+ });
173
+
174
+ const panel = screen.getByRole("tabpanel");
175
+ expect(panel).toHaveTextContent("********oken");
176
+ expect(panel).not.toHaveTextContent("secret-token");
177
+ fireEvent.click(
178
+ within(panel).getByRole("button", { name: "Copy prompt" }),
179
+ );
180
+ await waitFor(() => expect(copyToClipboard).toHaveBeenCalledOnce());
181
+ expect(vi.mocked(copyToClipboard).mock.calls[0][0]).toContain(
182
+ preview ? "export MARIMO_TOKEN=secret-token" : "--token secret-token",
183
+ );
184
+ expect(vi.mocked(copyToClipboard).mock.calls[0][0]).not.toContain(
185
+ "********oken",
186
+ );
187
+ },
188
+ );
189
+
190
+ it("copies a preview terminal command without the token and lets the user copy the token separately", async () => {
191
+ vi.mocked(fetch).mockResolvedValue(
192
+ new Response(JSON.stringify({ token: "secret-token" })),
193
+ );
194
+ await act(async () => {
195
+ render(<PairWithAgentModal onClose={vi.fn()} />, { wrapper });
196
+ });
197
+
198
+ const panel = screen.getByRole("tabpanel");
199
+ expect(panel).toHaveTextContent("2. Paste when prompted for a token");
200
+ expect(panel).not.toHaveTextContent("secret-token");
201
+ fireEvent.click(
202
+ within(panel).getByRole("button", { name: "Copy command" }),
203
+ );
204
+ await waitFor(() =>
205
+ expect(copyToClipboard).toHaveBeenCalledWith(
206
+ String.raw`claude "$(MARIMO_PAIR_NEXT=1 uv run marimo pair prompt \
207
+ --url http://localhost:8000/ \
208
+ --file notebook.py \
209
+ --session ${getSessionId()} \
210
+ --with-token)"`,
211
+ ),
212
+ );
213
+ fireEvent.click(within(panel).getByRole("button", { name: "Copy token" }));
214
+ await waitFor(() =>
215
+ expect(copyToClipboard).toHaveBeenLastCalledWith("secret-token"),
216
+ );
217
+ });
218
+ });
@@ -3,6 +3,7 @@
3
3
  import { assertNever } from "@/utils/assertNever";
4
4
  import { KnownQueryParams } from "@/core/constants";
5
5
  import { shellQuote } from "@/utils/shell";
6
+ import type { PairPreviewConfig } from "@/core/config/pair";
6
7
 
7
8
  export type AgentTab = "claude" | "codex" | "opencode" | "prompt";
8
9
 
@@ -37,8 +38,9 @@ function getFileFlag(file: string | undefined): string {
37
38
  /** Identifies the specific running notebook to pair on. */
38
39
  export interface ConnectionInfo {
39
40
  url: string;
40
- /** The server's file key, when the page URL identifies a notebook. */
41
+ /** The server's file key or notebook filename, when known. */
41
42
  file?: string;
43
+ session?: string;
42
44
  }
43
45
 
44
46
  /**
@@ -47,19 +49,29 @@ export interface ConnectionInfo {
47
49
  */
48
50
  export function getTerminalCommand(
49
51
  agent: Exclude<AgentTab, "prompt">,
50
- { url, file }: ConnectionInfo,
52
+ { url, file, session }: ConnectionInfo,
51
53
  withToken: boolean,
54
+ preview?: PairPreviewConfig,
52
55
  ): string {
53
- const fileFlag = getFileFlag(file);
54
- const tokenFlag = withToken ? " --with-token" : "";
55
- const base = `${getMarimoCommand()} pair prompt --url ${shellQuote(url)}${fileFlag}${tokenFlag}`;
56
+ const command = preview?.command ?? getMarimoCommand();
57
+ const prefix = preview ? "MARIMO_PAIR_NEXT=1 " : "";
58
+ const base = [
59
+ `${prefix}${command} pair prompt`,
60
+ `--url ${shellQuote(url)}`,
61
+ file ? `--file ${shellQuote(file)}` : "",
62
+ preview && session ? `--session ${shellQuote(session)}` : "",
63
+ withToken ? "--with-token" : "",
64
+ preview ? "" : `--${agent}`,
65
+ ]
66
+ .filter(Boolean)
67
+ .join(preview ? " \\\n " : " ");
56
68
  switch (agent) {
57
69
  case "claude":
58
- return `claude "$(${base} --claude)"`;
70
+ return `claude "$(${base})"`;
59
71
  case "codex":
60
- return `codex "$(${base} --codex)"`;
72
+ return `codex "$(${base})"`;
61
73
  case "opencode":
62
- return `opencode --prompt "$(${base} --opencode)"`;
74
+ return `opencode --prompt "$(${base})"`;
63
75
  default:
64
76
  assertNever(agent);
65
77
  }
@@ -71,9 +83,23 @@ export function getTerminalCommand(
71
83
  * an agent behaves the same as the terminal commands.
72
84
  */
73
85
  export function getRawPrompt(
74
- { url, file }: ConnectionInfo,
86
+ { url, file, session }: ConnectionInfo,
75
87
  token: string | null,
88
+ preview?: PairPreviewConfig,
76
89
  ): string {
90
+ if (preview) {
91
+ const { command, templates } = preview;
92
+ return formatPrompt(templates.prompt, {
93
+ command,
94
+ url,
95
+ file: file ? formatPrompt(templates.file, { file }) : "",
96
+ session: session ? formatPrompt(templates.session, { session }) : "",
97
+ authentication: token
98
+ ? formatPrompt(templates.token, { token: shellQuote(token) })
99
+ : "",
100
+ });
101
+ }
102
+
77
103
  const fileFlag = getFileFlag(file);
78
104
  const fileHint = file ? ` (file ${file})` : "";
79
105
  const executeCmd = `execute-code.sh --url ${shellQuote(url)}${fileFlag}`;
@@ -91,6 +117,17 @@ export function getRawPrompt(
91
117
  ].join("\n");
92
118
  }
93
119
 
120
+ function formatPrompt(
121
+ template: string,
122
+ values: Record<string, string>,
123
+ ): string {
124
+ // Replace once so inserted file names or tokens cannot become placeholders.
125
+ return template.replaceAll(
126
+ /\{(\w+)\}/g,
127
+ (placeholder, key: string) => values[key] ?? placeholder,
128
+ );
129
+ }
130
+
94
131
  /** Mask all but the last 4 chars of a token for display. */
95
132
  export function maskToken(token: string): string {
96
133
  if (token.length <= 4) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { CheckIcon, CopyIcon } from "lucide-react";
4
4
  import React, { useEffect, useState } from "react";
5
+ import { useAtomValue } from "jotai";
5
6
  import { Button } from "@/components/ui/button";
6
7
  import {
7
8
  DialogContent,
@@ -16,6 +17,9 @@ import { Events } from "@/utils/events";
16
17
  import { Tooltip } from "@/components/ui/tooltip";
17
18
  import { asRemoteURL, useRuntimeManager } from "@/core/runtime/config";
18
19
  import { API } from "@/core/network/api";
20
+ import { pairPreviewAtom } from "@/core/config/pair";
21
+ import { getSessionId } from "@/core/kernel/session";
22
+ import { useFilename } from "@/core/saving/filename";
19
23
  import {
20
24
  AGENT_LABELS,
21
25
  AGENT_TABS,
@@ -49,11 +53,17 @@ export const PairWithAgentModal: React.FC<{
49
53
  }> = ({ onClose }) => {
50
54
  const [activeTab, setActiveTab] = useState<AgentTab>("claude");
51
55
  const runtimeManager = useRuntimeManager();
56
+ const preview = useAtomValue(pairPreviewAtom);
57
+ const filename = useFilename();
58
+ const commandStep = preview ? 1 : 2;
52
59
  const authToken = useAuthToken();
53
60
  const hasToken = Boolean(authToken);
54
61
  const connection: ConnectionInfo = {
55
62
  url: runtimeManager.httpURL.toString(),
56
- file: getFileFromURL(window.location.href),
63
+ file:
64
+ getFileFromURL(window.location.href) ??
65
+ (preview ? filename || undefined : undefined),
66
+ session: preview ? getSessionId() : undefined,
57
67
  };
58
68
 
59
69
  return (
@@ -94,23 +104,34 @@ export const PairWithAgentModal: React.FC<{
94
104
  value={tab}
95
105
  className="mt-4 flex flex-col gap-4"
96
106
  >
97
- <Step
98
- index={1}
99
- title="Install the skill"
100
- hint="Run once per machine."
101
- >
102
- <CommandBlock command={SKILL_INSTALL} />
103
- </Step>
104
- <Step index={2} title="Run in your terminal">
107
+ {!preview && (
108
+ <Step
109
+ index={1}
110
+ title="Install the skill"
111
+ hint="Run once per machine."
112
+ >
113
+ <CommandBlock command={SKILL_INSTALL} />
114
+ </Step>
115
+ )}
116
+ <Step index={commandStep} title="Run in your terminal">
105
117
  <CommandBlock
106
- command={getTerminalCommand(tab, connection, hasToken)}
118
+ command={getTerminalCommand(
119
+ tab,
120
+ connection,
121
+ hasToken,
122
+ preview,
123
+ )}
107
124
  />
108
125
  </Step>
109
126
  {hasToken && authToken && (
110
- <Step index={3} title="Paste when prompted for a token">
127
+ <Step
128
+ index={commandStep + 1}
129
+ title="Paste when prompted for a token"
130
+ >
111
131
  <CommandBlock
112
132
  command={authToken}
113
133
  display={maskToken(authToken)}
134
+ copyLabel="Copy token"
114
135
  />
115
136
  </Step>
116
137
  )}
@@ -118,15 +139,17 @@ export const PairWithAgentModal: React.FC<{
118
139
  ))}
119
140
 
120
141
  <TabsContent value="prompt" className="mt-4 flex flex-col gap-4">
142
+ {!preview && (
143
+ <Step
144
+ index={1}
145
+ title="Make sure the marimo-pair skill is available to your agent"
146
+ hint="Skip if your agent already has it."
147
+ >
148
+ <CommandBlock command={SKILL_INSTALL} />
149
+ </Step>
150
+ )}
121
151
  <Step
122
- index={1}
123
- title="Make sure the marimo-pair skill is available to your agent"
124
- hint="Skip if your agent already has it."
125
- >
126
- <CommandBlock command={SKILL_INSTALL} />
127
- </Step>
128
- <Step
129
- index={2}
152
+ index={commandStep}
130
153
  title="Copy this prompt into your agent"
131
154
  hint={
132
155
  hasToken
@@ -135,10 +158,11 @@ export const PairWithAgentModal: React.FC<{
135
158
  }
136
159
  >
137
160
  <CommandBlock
138
- command={getRawPrompt(connection, authToken)}
161
+ command={getRawPrompt(connection, authToken, preview)}
139
162
  display={getRawPrompt(
140
163
  connection,
141
164
  authToken ? maskToken(authToken) : null,
165
+ preview,
142
166
  )}
143
167
  multiline={true}
144
168
  />
@@ -177,7 +201,8 @@ const CommandBlock: React.FC<{
177
201
  command: string;
178
202
  display?: string;
179
203
  multiline?: boolean;
180
- }> = ({ command, display, multiline = false }) => {
204
+ copyLabel?: string;
205
+ }> = ({ command, display, multiline = false, copyLabel = "Copy command" }) => {
181
206
  const [copied, setCopied] = useState(false);
182
207
 
183
208
  const copy = Events.stopPropagation(async (e) => {
@@ -195,6 +220,7 @@ const CommandBlock: React.FC<{
195
220
  </pre>
196
221
  <Tooltip content="Copied!" open={copied}>
197
222
  <Button
223
+ aria-label="Copy prompt"
198
224
  onClick={copy}
199
225
  size="xs"
200
226
  variant="ghost"
@@ -213,11 +239,11 @@ const CommandBlock: React.FC<{
213
239
 
214
240
  return (
215
241
  <div className="flex items-center gap-2 rounded-md bg-muted px-3 py-2 font-mono text-xs">
216
- <code className="flex-1 select-all wrap-break-word">
242
+ <code className="flex-1 whitespace-pre-wrap select-all wrap-break-word">
217
243
  {display ?? command}
218
244
  </code>
219
245
  <Tooltip content="Copied!" open={copied}>
220
- <Button onClick={copy} size="xs" variant="ghost">
246
+ <Button aria-label={copyLabel} onClick={copy} size="xs" variant="ghost">
221
247
  {copied ? (
222
248
  <CheckIcon size={14} strokeWidth={1.5} />
223
249
  ) : (
@@ -0,0 +1,19 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { atom } from "jotai";
4
+ import { z } from "zod";
5
+
6
+ export const pairPreviewSchema = z.object({
7
+ command: z.string(),
8
+ templates: z.object({
9
+ prompt: z.string(),
10
+ file: z.string(),
11
+ session: z.string(),
12
+ token_file: z.string(),
13
+ token: z.string(),
14
+ }),
15
+ });
16
+
17
+ export type PairPreviewConfig = z.infer<typeof pairPreviewSchema>;
18
+
19
+ export const pairPreviewAtom = atom<PairPreviewConfig | undefined>(undefined);
package/src/mount.tsx CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  userConfigAtom,
11
11
  } from "@/core/config/config";
12
12
  import { KnownQueryParams } from "@/core/constants";
13
+ import { pairPreviewAtom, pairPreviewSchema } from "@/core/config/pair";
13
14
  import { getFilenameFromDOM } from "@/core/dom/htmlUtils";
14
15
  import { getMarimoCode } from "@/core/meta/globals";
15
16
  import {
@@ -208,6 +209,7 @@ const mountOptionsSchema = z.object({
208
209
  * marimo config overrides
209
210
  */
210
211
  configOverrides: passthroughObject,
212
+ pairPreview: pairPreviewSchema.optional(),
211
213
  /**
212
214
  * marimo app config
213
215
  */
@@ -345,6 +347,7 @@ function initStore(options: unknown) {
345
347
  );
346
348
  store.set(userConfigAtom, parseUserConfig(parsedOptions.data.config));
347
349
  store.set(appConfigAtom, parseAppConfig(parsedOptions.data.appConfig));
350
+ store.set(pairPreviewAtom, parsedOptions.data.pairPreview);
348
351
 
349
352
  // Runtime config
350
353
  if (parsedOptions.data.runtimeConfig.length > 0) {