@marimo-team/islands 0.23.17-dev33 → 0.23.17-dev34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BEb8RZam.js → ConnectedDataExplorerComponent-emE_tsrt.js} +11 -11
  2. package/dist/{ErrorBoundary-8ZHte0h9.js → ErrorBoundary-o6wEnSCc.js} +2 -2
  3. package/dist/{any-language-editor-CioNaP-T.js → any-language-editor-Cls8lx9d.js} +4 -4
  4. package/dist/chat-ui-1-XgwfDv.js +8620 -0
  5. package/dist/{check-CCvJTB2E.js → check-D-BaEjEh.js} +1 -1
  6. package/dist/{common-C2Coc2Bh.js → common-Czr-rNF3.js} +18 -18
  7. package/dist/{copy-D45ZUY3j.js → copy-BkA3t7rJ.js} +1 -1
  8. package/dist/{dist-DgzzIPS4.js → dist-DarwE0ZO.js} +10 -10
  9. package/dist/{error-banner-DHfPl1w3.js → error-banner-dmkbm21G.js} +1 -1
  10. package/dist/{esm-DJsE6wi-.js → esm-CfyRntAe.js} +1 -1
  11. package/dist/{extends-xQ_2Cylt.js → extends-DzpiytFg.js} +3 -3
  12. package/dist/{formats-D10vOEsT.js → formats-DcO6TofP.js} +1 -1
  13. package/dist/{glide-data-editor-FiwJJzrV.js → glide-data-editor-C99Bp4Hk.js} +8 -8
  14. package/dist/{html-to-image-Bgb6aaXi.js → html-to-image-CgznkzKG.js} +13 -13
  15. package/dist/{input-DtTqU0xh.js → input-CwPuFkJt.js} +5 -5
  16. package/dist/{label-5IC840tS.js → label-CzHncDvS.js} +1 -1
  17. package/dist/main.js +29 -29
  18. package/dist/{mermaid-BNhC-PrB.js → mermaid-Sy0p3RJY.js} +4 -4
  19. package/dist/{process-output-D2Oxf11p.js → process-output-DUNqOGZS.js} +3 -3
  20. package/dist/{reveal-component--Ln8gdbm.js → reveal-component-CZ2AHfVt.js} +9 -9
  21. package/dist/{spec-C7rMyZ_8.js → spec-Be9MhFWh.js} +4 -4
  22. package/dist/{strings-CITcoFNY.js → strings-BUDhKc0s.js} +298 -298
  23. package/dist/{toDate-kxcuDOWd.js → toDate-eW__SzBR.js} +2 -2
  24. package/dist/{tooltip-BkSv4x8S.js → tooltip-CkCWlUOf.js} +2 -2
  25. package/dist/{types-kNUyqkDD.js → types-uENjXsiC.js} +1 -1
  26. package/dist/{useAsyncData-Uo4VFafD.js → useAsyncData-B8UxSH4O.js} +1 -1
  27. package/dist/{useDateFormatter-ZCfHadYP.js → useDateFormatter-BeN3fHMH.js} +2 -2
  28. package/dist/{useDeepCompareMemoize-s3ZvmdK3.js → useDeepCompareMemoize-BnhKck7C.js} +1 -1
  29. package/dist/{useLifecycle-CLPBATCv.js → useLifecycle-B7J7yvFt.js} +2 -2
  30. package/dist/{useTheme-CEMsu1Js.js → useTheme-BUY7pfI9.js} +1 -1
  31. package/dist/{vega-component-DGkcoXu3.js → vega-component-BOeMnvAb.js} +8 -8
  32. package/dist/{zod-DdNi9290.js → zod-yTHHzJiv.js} +36 -37
  33. package/package.json +3 -3
  34. package/src/components/chat/__tests__/chat-abort.test.ts +51 -0
  35. package/src/components/chat/chat-abort.ts +44 -0
  36. package/src/components/chat/chat-display.tsx +17 -0
  37. package/src/components/chat/chat-panel.tsx +85 -37
  38. package/src/components/chat/chat-utils.ts +1 -1
  39. package/src/components/editor/ai/add-cell-with-ai.tsx +2 -2
  40. package/src/components/editor/ai/ai-completion-editor.tsx +2 -2
  41. package/src/core/ai/constants.ts +7 -0
  42. package/src/core/ai/staged-cells.ts +3 -0
  43. package/src/plugins/impl/chat/chat-ui.tsx +51 -3
  44. package/dist/chat-ui-Cjd9kIcO.js +0 -8369
@@ -4,8 +4,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4
4
  import { t as __commonJSMin } from "./chunk-BNovOVIE.js";
5
5
  import { g as Logger } from "./button-8aS1QcHr.js";
6
6
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
7
- import { u as createLucideIcon } from "./dist-DgzzIPS4.js";
8
- import { S as useAtomValue, T as atom, a as KnownQueryParams, g as store, v as waitFor, y as isIslands } from "./useTheme-CEMsu1Js.js";
7
+ import { u as createLucideIcon } from "./dist-DarwE0ZO.js";
8
+ import { S as useAtomValue, T as atom, a as KnownQueryParams, g as store, v as waitFor, y as isIslands } from "./useTheme-BUY7pfI9.js";
9
9
  import { t as invariant } from "./invariant-wRzNXIsJ.js";
10
10
  var CircleQuestionMark = createLucideIcon("circle-question-mark", [
11
11
  ["circle", {
@@ -2,9 +2,9 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { d as createSlottable, h as cn, p as useComposedRefs } from "./button-8aS1QcHr.js";
3
3
  import { t as require_react } from "./react-DA-nE2FX.js";
4
4
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
5
- import { a as createPopperScope, i as Root2, n as Arrow, r as Content, s as Root, t as Anchor } from "./dist-DgzzIPS4.js";
5
+ import { a as createPopperScope, i as Root2, n as Arrow, r as Content, s as Root, t as Anchor } from "./dist-DarwE0ZO.js";
6
6
  import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
7
- import { Q as withSmartCollisionBoundary, Z as withFullScreenAsRoot, _t as createContextScope, at as Portal, ct as DismissableLayer, dt as useId, et as StyleNamespace, ft as Presence, ht as composeEventHandlers, pt as useControllableState, vt as Primitive } from "./zod-DdNi9290.js";
7
+ import { $ as StyleNamespace, X as withFullScreenAsRoot, Z as withSmartCollisionBoundary, _t as Primitive, dt as Presence, ft as useControllableState, gt as createContextScope, it as Portal, mt as composeEventHandlers, st as DismissableLayer, ut as useId } from "./zod-yTHHzJiv.js";
8
8
  var import_react = /* @__PURE__ */ __toESM(require_react(), 1), import_jsx_runtime = /* @__PURE__ */ __toESM(require_jsx_runtime(), 1), [createTooltipContext, createTooltipScope] = createContextScope("Tooltip", [createPopperScope]), usePopperScope = createPopperScope(), PROVIDER_NAME = "TooltipProvider", DEFAULT_DELAY_DURATION = 700, TOOLTIP_OPEN = "tooltip.open", [TooltipProviderContextProvider, useTooltipProviderContext] = createTooltipContext(PROVIDER_NAME), TooltipProvider$1 = (n) => {
9
9
  let { __scopeTooltip: j, delayDuration: M = DEFAULT_DELAY_DURATION, skipDelayDuration: N = 300, disableHoverableContent: P = false, children: F } = n, I = import_react.useRef(true), L = import_react.useRef(false), R = import_react.useRef(0);
10
10
  return import_react.useEffect(() => {
@@ -1,4 +1,4 @@
1
- import { u as createLucideIcon } from "./dist-DgzzIPS4.js";
1
+ import { u as createLucideIcon } from "./dist-DarwE0ZO.js";
2
2
  var Pencil = createLucideIcon("pencil", [["path", {
3
3
  d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",
4
4
  key: "1a8usu"
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-DA-nE2FX.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
4
- import { A as useEvent_default } from "./useTheme-CEMsu1Js.js";
4
+ import { A as useEvent_default } from "./useTheme-BUY7pfI9.js";
5
5
  import { t as invariant } from "./invariant-wRzNXIsJ.js";
6
6
  var import_compiler_runtime = require_compiler_runtime(), import_react = /* @__PURE__ */ __toESM(require_react(), 1), Result = {
7
7
  error(e, s) {
@@ -1,7 +1,7 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-DA-nE2FX.js";
3
- import { u as createLucideIcon } from "./dist-DgzzIPS4.js";
4
- import { E as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7 } from "./strings-CITcoFNY.js";
3
+ import { u as createLucideIcon } from "./dist-DarwE0ZO.js";
4
+ import { E as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7 } from "./strings-BUDhKc0s.js";
5
5
  var ChartPie = createLucideIcon("chart-pie", [["path", {
6
6
  d: "M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z",
7
7
  key: "pzmjnu"
@@ -1,6 +1,6 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-DA-nE2FX.js";
3
- import { k as dequal } from "./useTheme-CEMsu1Js.js";
3
+ import { k as dequal } from "./useTheme-BUY7pfI9.js";
4
4
  var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
5
5
  function useDeepCompareMemoize(e) {
6
6
  let i = import_react.useRef(e);
@@ -2,9 +2,9 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { g as Logger, h as cn, r as cva } from "./button-8aS1QcHr.js";
3
3
  import { t as require_react } from "./react-DA-nE2FX.js";
4
4
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
5
- import { u as createLucideIcon } from "./dist-DgzzIPS4.js";
5
+ import { u as createLucideIcon } from "./dist-DarwE0ZO.js";
6
6
  import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
7
- import { C as useSetAtom, T as atom } from "./useTheme-CEMsu1Js.js";
7
+ import { C as useSetAtom, T as atom } from "./useTheme-BUY7pfI9.js";
8
8
  var Calendar = createLucideIcon("calendar", [
9
9
  ["path", {
10
10
  d: "M8 2v4",
@@ -2,7 +2,7 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { a as OverridingHotkeyProvider, g as Logger, s as resolvePlatform } from "./button-8aS1QcHr.js";
3
3
  import { t as require_react } from "./react-DA-nE2FX.js";
4
4
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
5
- import { A as looseObject, B as union, I as record, N as number, P as object, R as string, T as boolean, b as _enum, w as array } from "./zod-DdNi9290.js";
5
+ import { A as looseObject, I as record, L as string, N as number, P as object, T as boolean, b as _enum, w as array, z as union } from "./zod-yTHHzJiv.js";
6
6
  import { t as merge_default } from "./merge-Be1CqGnU.js";
7
7
  var import_react = /* @__PURE__ */ __toESM(require_react()), useInsertionEffect = typeof window < "u" ? import_react.useInsertionEffect || import_react.useLayoutEffect : () => {
8
8
  };
@@ -2,24 +2,24 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { g as Logger, h as cn, m as Events } from "./button-8aS1QcHr.js";
3
3
  import { t as require_react } from "./react-DA-nE2FX.js";
4
4
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
5
- import { c as asRemoteURL, w as CircleQuestionMark } from "./toDate-kxcuDOWd.js";
5
+ import { c as asRemoteURL, w as CircleQuestionMark } from "./toDate-eW__SzBR.js";
6
6
  import "./react-dom-BTJzcVJ9.js";
7
7
  import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
8
- import "./zod-DdNi9290.js";
9
- import { n as ErrorBanner } from "./error-banner-DHfPl1w3.js";
10
- import { t as Tooltip } from "./tooltip-BkSv4x8S.js";
8
+ import "./zod-yTHHzJiv.js";
9
+ import { n as ErrorBanner } from "./error-banner-dmkbm21G.js";
10
+ import { t as Tooltip } from "./tooltip-CkCWlUOf.js";
11
11
  import { t as debounce_default } from "./debounce-BOD3DbfP.js";
12
- import { A as useEvent_default, n as useTheme } from "./useTheme-CEMsu1Js.js";
12
+ import { A as useEvent_default, n as useTheme } from "./useTheme-BUY7pfI9.js";
13
13
  import { s as uniq } from "./arrays-sEtDRoG4.js";
14
14
  import { t as Objects } from "./objects-De59twF-.js";
15
- import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-D10vOEsT.js";
15
+ import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-DcO6TofP.js";
16
16
  import { V as getContainerWidth, n as formats } from "./vega-loader.browser-DmpxCJqx.js";
17
17
  import { a as tooltipHandler, n as vegaLoadData } from "./loader-DP2JkPth.js";
18
18
  import { t as j } from "./react-vega-BazDALhI.js";
19
19
  import "./defaultLocale-BpsHxBd7.js";
20
20
  import "./defaultLocale-DoeErsX2.js";
21
- import { t as useAsyncData } from "./useAsyncData-Uo4VFafD.js";
22
- import { t as useDeepCompareMemoize } from "./useDeepCompareMemoize-s3ZvmdK3.js";
21
+ import { t as useAsyncData } from "./useAsyncData-B8UxSH4O.js";
22
+ import { t as useDeepCompareMemoize } from "./useDeepCompareMemoize-BnhKck7C.js";
23
23
  import { t as Semaphore } from "./semaphore-C8rmHdhj.js";
24
24
  import { t as useVegaContainerRemeasure } from "./use-vega-container-remeasure-DGNrIQSk.js";
25
25
  var import_compiler_runtime = require_compiler_runtime(), import_react = /* @__PURE__ */ __toESM(require_react(), 1);
@@ -11758,77 +11758,76 @@ var external_exports = /* @__PURE__ */ __export({
11758
11758
  config(en_default());
11759
11759
  var zod_default = external_exports;
11760
11760
  export {
11761
- isInVscodeExtension as $,
11761
+ StyleNamespace as $,
11762
11762
  looseObject as A,
11763
- union as B,
11763
+ unknown as B,
11764
11764
  any as C,
11765
11765
  discriminatedUnion as D,
11766
11766
  custom as E,
11767
11767
  optional as F,
11768
- parse$1 as G,
11769
- safeParseAsync as H,
11768
+ parseAsync$1 as G,
11769
+ ZodError as H,
11770
11770
  record as I,
11771
- prettifyError as J,
11772
- parseAsync$1 as K,
11773
- strictObject as L,
11771
+ MAX_HEIGHT_OFFSET as J,
11772
+ $ZodError as K,
11773
+ string$1 as L,
11774
11774
  never as M,
11775
11775
  number$1 as N,
11776
11776
  lazy as O,
11777
11777
  object as P,
11778
- withSmartCollisionBoundary as Q,
11779
- string$1 as R,
11778
+ isInVscodeExtension as Q,
11779
+ tuple as R,
11780
11780
  _null as S,
11781
11781
  boolean$1 as T,
11782
- ZodError as U,
11783
- unknown as V,
11784
- toJSONSchema as W,
11785
- useFullScreenElement as X,
11786
- MAX_HEIGHT_OFFSET as Y,
11787
- withFullScreenAsRoot as Z,
11782
+ toJSONSchema as U,
11783
+ safeParseAsync as V,
11784
+ parse$1 as W,
11785
+ withFullScreenAsRoot as X,
11786
+ useFullScreenElement as Y,
11787
+ withSmartCollisionBoundary as Z,
11788
11788
  ZodString as _,
11789
- createContextScope as _t,
11789
+ Primitive as _t,
11790
11790
  ZodIssueCode as a,
11791
- Portal as at,
11791
+ FocusScope as at,
11792
11792
  _enum as b,
11793
11793
  ZodBoolean as c,
11794
- DismissableLayer as ct,
11794
+ Root as ct,
11795
11795
  ZodDiscriminatedUnion as d,
11796
- useId as dt,
11797
- StyleNamespace as et,
11796
+ Presence as dt,
11797
+ hideOthers as et,
11798
11798
  ZodEnum as f,
11799
- Presence as ft,
11799
+ useControllableState as ft,
11800
11800
  ZodOptional as g,
11801
- createContext2 as gt,
11801
+ createContextScope as gt,
11802
11802
  ZodObject as h,
11803
- composeEventHandlers as ht,
11803
+ createContext2 as ht,
11804
11804
  string as i,
11805
- useFocusGuards as it,
11805
+ Portal as it,
11806
11806
  nan as j,
11807
11807
  literal as k,
11808
11808
  ZodDate as l,
11809
- Root as lt,
11809
+ useCallbackRef$1 as lt,
11810
11810
  ZodNumber as m,
11811
- useLayoutEffect2 as mt,
11811
+ composeEventHandlers as mt,
11812
11812
  date as n,
11813
- Combination_default as nt,
11813
+ __awaiter as nt,
11814
11814
  ZodAny as o,
11815
- FocusScope as ot,
11815
+ Branch as ot,
11816
11816
  ZodLiteral as p,
11817
- useControllableState as pt,
11818
- $ZodError as q,
11817
+ useLayoutEffect2 as pt,
11818
+ prettifyError as q,
11819
11819
  number as r,
11820
- __awaiter as rt,
11820
+ useFocusGuards as rt,
11821
11821
  ZodArray as s,
11822
- Branch as st,
11822
+ DismissableLayer as st,
11823
11823
  zod_default as t,
11824
- hideOthers as tt,
11824
+ Combination_default as tt,
11825
11825
  ZodDefault as u,
11826
- useCallbackRef$1 as ut,
11826
+ useId as ut,
11827
11827
  ZodType as v,
11828
- Primitive as vt,
11828
+ dispatchDiscreteCustomEvent as vt,
11829
11829
  array as w,
11830
11830
  _instanceof as x,
11831
11831
  ZodUnion as y,
11832
- dispatchDiscreteCustomEvent as yt,
11833
- tuple as z
11832
+ union as z
11834
11833
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.17-dev33",
3
+ "version": "0.23.17-dev34",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  "./unstable_internal/*": "./src/*"
22
22
  },
23
23
  "dependencies": {
24
- "@ai-sdk/react": "^3.0.134",
24
+ "@ai-sdk/react": "^4.0.40",
25
25
  "@anywidget/types": "^0.4.0",
26
26
  "@codemirror/autocomplete": "^6.20.1",
27
27
  "@codemirror/commands": "^6.10.2",
@@ -92,7 +92,7 @@
92
92
  "@xterm/addon-web-links": "^0.12.0",
93
93
  "@xterm/xterm": "^5.5.0",
94
94
  "@zed-industries/agent-client-protocol": "^0.4.5",
95
- "ai": "^6.0.129",
95
+ "ai": "^7.0.37",
96
96
  "ansi_up": "^6.0.6",
97
97
  "class-variance-authority": "^0.7.1",
98
98
  "clsx": "^2.1.1",
@@ -0,0 +1,51 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { UIMessageChunk } from "ai";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ describeChatAbortReason,
7
+ getAbortReasonFromChunk,
8
+ resolveChatAbortReason,
9
+ USER_CANCELLED_ABORT_REASON,
10
+ } from "../chat-abort";
11
+
12
+ describe("chat-abort", () => {
13
+ it("reads reason from abort chunks", () => {
14
+ const chunk: UIMessageChunk = {
15
+ type: "abort",
16
+ reason: USER_CANCELLED_ABORT_REASON,
17
+ };
18
+ expect(getAbortReasonFromChunk(chunk)).toBe(USER_CANCELLED_ABORT_REASON);
19
+ });
20
+
21
+ it("returns undefined for non-abort chunks", () => {
22
+ expect(
23
+ getAbortReasonFromChunk({ type: "text-delta", id: "1", delta: "hi" }),
24
+ ).toBeUndefined();
25
+ });
26
+
27
+ it("defaults client-side aborts to user_cancelled", () => {
28
+ expect(resolveChatAbortReason({ isAbort: true, streamReason: null })).toBe(
29
+ USER_CANCELLED_ABORT_REASON,
30
+ );
31
+ expect(resolveChatAbortReason({ isAbort: false })).toBeNull();
32
+ });
33
+
34
+ it("preserves stream abort reasons when present", () => {
35
+ expect(
36
+ resolveChatAbortReason({ isAbort: true, streamReason: "timeout" }),
37
+ ).toBe("timeout");
38
+ });
39
+
40
+ it("describes known and unknown abort reasons", () => {
41
+ expect(describeChatAbortReason(USER_CANCELLED_ABORT_REASON)).toBe(
42
+ "Generation stopped",
43
+ );
44
+ expect(describeChatAbortReason("timeout")).toBe(
45
+ "Generation stopped: timed out",
46
+ );
47
+ expect(describeChatAbortReason("provider_reset")).toBe(
48
+ "Generation stopped: provider_reset",
49
+ );
50
+ });
51
+ });
@@ -0,0 +1,44 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { UIMessageChunk } from "ai";
4
+
5
+ /** Matches pydantic-ai / marimo AbortChunk.reason for user Stop. */
6
+ export const USER_CANCELLED_ABORT_REASON = "user_cancelled";
7
+
8
+ /**
9
+ * Read an abort reason from a UI message stream chunk, if present.
10
+ */
11
+ export function getAbortReasonFromChunk(
12
+ chunk: UIMessageChunk,
13
+ ): string | undefined {
14
+ if (chunk.type !== "abort") {
15
+ return undefined;
16
+ }
17
+ return chunk.reason;
18
+ }
19
+
20
+ /**
21
+ * Resolve the abort reason for logging/UI. Client-side Stop often never
22
+ * receives a server AbortChunk (the fetch is aborted first), so default to
23
+ * user_cancelled when the turn ended as an abort.
24
+ */
25
+ export function resolveChatAbortReason(opts: {
26
+ isAbort: boolean;
27
+ streamReason?: string | null;
28
+ }): string | null {
29
+ if (!opts.isAbort) {
30
+ return null;
31
+ }
32
+ return opts.streamReason || USER_CANCELLED_ABORT_REASON;
33
+ }
34
+
35
+ export function describeChatAbortReason(reason: string): string {
36
+ switch (reason) {
37
+ case USER_CANCELLED_ABORT_REASON:
38
+ return "Generation stopped";
39
+ case "timeout":
40
+ return "Generation stopped: timed out";
41
+ default:
42
+ return `Generation stopped: ${reason}`;
43
+ }
44
+ }
@@ -92,6 +92,17 @@ export const renderUIMessage = ({
92
92
  );
93
93
  case "file":
94
94
  return <AttachmentRenderer attachment={part} key={index} />;
95
+ case "reasoning-file":
96
+ return (
97
+ <AttachmentRenderer
98
+ key={index}
99
+ attachment={{
100
+ type: "file",
101
+ mediaType: part.mediaType,
102
+ url: part.url,
103
+ }}
104
+ />
105
+ );
95
106
  case "dynamic-tool":
96
107
  return (
97
108
  <ToolCallView
@@ -128,6 +139,12 @@ export const renderUIMessage = ({
128
139
  href={part.url}
129
140
  />
130
141
  );
142
+ case "custom":
143
+ return (
144
+ <div key={index} className="text-sm text-muted-foreground">
145
+ {part.kind}
146
+ </div>
147
+ );
131
148
  case "step-start":
132
149
  return null;
133
150
  default:
@@ -6,7 +6,6 @@ import { storePrompt } from "@marimo-team/codemirror-ai";
6
6
  import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
7
7
  import {
8
8
  type ChatAddToolApproveResponseFunction,
9
- DefaultChatTransport,
10
9
  type FileUIPart,
11
10
  safeValidateUIMessages,
12
11
  type TextUIPart,
@@ -38,6 +37,7 @@ import {
38
37
  } from "@/components/ui/select";
39
38
  import { replaceMessagesInChat } from "@/core/ai/chat-utils";
40
39
  import { useModelChange } from "@/core/ai/config";
40
+ import { AI_SDK_UI_THROTTLE_MS } from "@/core/ai/constants";
41
41
  import { AiModelId } from "@/core/ai/ids/ids";
42
42
  import { useStagedAICellsActions } from "@/core/ai/staged-cells";
43
43
  import {
@@ -71,6 +71,7 @@ import {
71
71
  isContextAttachment,
72
72
  resolveChatContext,
73
73
  } from "../editor/ai/completion-utils";
74
+ import { StreamingChunkTransport } from "../editor/ai/transport/chat-transport";
74
75
  import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
75
76
  import { CopyClipboardIcon } from "../icons/copy-icon";
76
77
  import { useImperativeModal } from "../modal/ImperativeModal";
@@ -83,6 +84,11 @@ import {
83
84
  FileAttachmentPill,
84
85
  SendButton,
85
86
  } from "./chat-components";
87
+ import {
88
+ describeChatAbortReason,
89
+ getAbortReasonFromChunk,
90
+ resolveChatAbortReason,
91
+ } from "./chat-abort";
86
92
  import { renderUIMessage } from "./chat-display";
87
93
  import { ChatHistoryPopover } from "./chat-history-popover";
88
94
  import {
@@ -533,6 +539,12 @@ const ChatPanelBody = () => {
533
539
  const newMessageInputRef = useRef<ReactCodeMirrorRef>(null);
534
540
  const scrollContainerRef = useRef<HTMLDivElement>(null);
535
541
  const fileInputRef = useRef<HTMLInputElement>(null);
542
+ const lastAbortReasonRef = useRef<string | null>(null);
543
+ const [abortNotice, setAbortNotice] = useState<string | null>(null);
544
+ const clearAbortState = useEvent(() => {
545
+ lastAbortReasonRef.current = null;
546
+ setAbortNotice(null);
547
+ });
536
548
  const runtimeManager = useRuntimeManager();
537
549
  const { invokeAiTool, sendRun } = useRequestClient();
538
550
  const { openModal, closeModal } = useImperativeModal();
@@ -562,45 +574,54 @@ const ChatPanelBody = () => {
562
574
  id: chatId,
563
575
  } = useChat({
564
576
  id: activeChatId,
577
+ throttle: AI_SDK_UI_THROTTLE_MS,
565
578
  sendAutomaticallyWhen: ({ messages }) => hasPendingToolCalls(messages),
566
579
  messages: activeChat?.messages || [], // initial messages
567
- transport: new DefaultChatTransport({
568
- api: runtimeManager.getAiURL("chat").toString(),
569
- headers: () => runtimeManager.headers(),
570
- prepareSendMessagesRequest: async (options) => {
571
- // Canary: flag outgoing messages that don't match the AI SDK's own
572
- // schema. The server-side sanitizer in `_pydantic_ai_utils.py` corrects these before validation;
573
- // this log surfaces drift early without affecting the request.
574
- const validation = await safeValidateUIMessages({
575
- messages: options.messages,
576
- });
577
- if (!validation.success) {
578
- Logger.debug(
579
- "Outgoing chat messages failed AI SDK schema validation",
580
- validation.error,
581
- );
582
- }
580
+ transport: new StreamingChunkTransport(
581
+ {
582
+ api: runtimeManager.getAiURL("chat").toString(),
583
+ headers: () => runtimeManager.headers(),
584
+ prepareSendMessagesRequest: async (options) => {
585
+ // Canary: flag outgoing messages that don't match the AI SDK's own
586
+ // schema. The server-side sanitizer in `_pydantic_ai_utils.py` corrects these before validation;
587
+ // this log surfaces drift early without affecting the request.
588
+ const validation = await safeValidateUIMessages({
589
+ messages: options.messages,
590
+ });
591
+ if (!validation.success) {
592
+ Logger.debug(
593
+ "Outgoing chat messages failed AI SDK schema validation",
594
+ validation.error,
595
+ );
596
+ }
583
597
 
584
- const completionBody = {
585
- uiMessages: options.messages,
586
- includeOtherCode: getCodes(""),
587
- };
588
-
589
- // Call this here to ensure the value is not stale
590
- const chatMode = store.get(aiAtom)?.mode || DEFAULT_MODE;
591
- const tools = FRONTEND_TOOL_REGISTRY.getToolSchemas(chatMode);
592
-
593
- return {
594
- api: runtimeManager.getAiURL("chat").toString(),
595
- body: {
596
- tools,
597
- ...options,
598
- ...completionBody,
599
- },
600
- };
598
+ const completionBody = {
599
+ uiMessages: options.messages,
600
+ includeOtherCode: getCodes(""),
601
+ };
602
+
603
+ // Call this here to ensure the value is not stale
604
+ const chatMode = store.get(aiAtom)?.mode || DEFAULT_MODE;
605
+ const tools = FRONTEND_TOOL_REGISTRY.getToolSchemas(chatMode);
606
+
607
+ return {
608
+ api: runtimeManager.getAiURL("chat").toString(),
609
+ body: {
610
+ tools,
611
+ ...options,
612
+ ...completionBody,
613
+ },
614
+ };
615
+ },
616
+ },
617
+ (chunk) => {
618
+ const abortReason = getAbortReasonFromChunk(chunk);
619
+ if (abortReason !== undefined) {
620
+ lastAbortReasonRef.current = abortReason;
621
+ }
601
622
  },
602
- }),
603
- onFinish: ({ messages, isError, isAbort }) => {
623
+ ),
624
+ onFinish: ({ messages, isError, isAbort, finishReason }) => {
604
625
  setChatState((prev) => {
605
626
  return replaceMessagesInChat({
606
627
  chatState: prev,
@@ -608,6 +629,22 @@ const ChatPanelBody = () => {
608
629
  messages: messages,
609
630
  });
610
631
  });
632
+
633
+ if (isAbort) {
634
+ const reason = resolveChatAbortReason({
635
+ isAbort: true,
636
+ streamReason: lastAbortReasonRef.current,
637
+ });
638
+ clearAbortState();
639
+ if (reason != null) {
640
+ const description = describeChatAbortReason(reason);
641
+ Logger.debug("Chat stream aborted", { reason, finishReason });
642
+ setAbortNotice(description);
643
+ }
644
+ } else {
645
+ clearAbortState();
646
+ }
647
+
611
648
  tryFlushQueuedMessages(messages, { isError, isAbort });
612
649
  },
613
650
  onToolCall: async ({ toolCall }) => {
@@ -628,6 +665,7 @@ const ChatPanelBody = () => {
628
665
  });
629
666
 
630
667
  const sendUserMessage = useEvent((parts: ChatMessagePart[]) => {
668
+ clearAbortState();
631
669
  sendMessage({ role: "user", parts });
632
670
  });
633
671
 
@@ -701,7 +739,8 @@ const ChatPanelBody = () => {
701
739
  useEffect(() => {
702
740
  setIsScrolledToBottom(true);
703
741
  clearQueuedMessages();
704
- }, [activeChatId, clearQueuedMessages]);
742
+ clearAbortState();
743
+ }, [activeChatId, clearQueuedMessages, clearAbortState]);
705
744
 
706
745
  useEffect(() => {
707
746
  if (!isScrolledToBottom) {
@@ -833,6 +872,7 @@ const ChatPanelBody = () => {
833
872
  );
834
873
 
835
874
  const handleReload = () => {
875
+ clearAbortState();
836
876
  regenerate();
837
877
  };
838
878
 
@@ -987,6 +1027,14 @@ const ChatPanelBody = () => {
987
1027
  </Button>
988
1028
  </div>
989
1029
  )}
1030
+
1031
+ {abortNotice && !isLoading && !error && (
1032
+ <div className="flex justify-center py-2">
1033
+ <span className="text-xs text-muted-foreground">
1034
+ {abortNotice}
1035
+ </span>
1036
+ </div>
1037
+ )}
990
1038
  </div>
991
1039
 
992
1040
  <ScrollToBottomButton
@@ -291,7 +291,7 @@ export function shouldFlushQueue(opts: {
291
291
  export function useMessageQueue() {
292
292
  const [messages, setMessages] = useState<QueuedUserMessage[]>([]);
293
293
  // Mirror the queue in a ref so `flushNext` reads the latest value even when
294
- // invoked from a callback (e.g. `onFinish`) captured on an earlier render.
294
+ // invoked from a callback (e.g. `onFinish`) in the same tick as `enqueue`.
295
295
  const messagesRef = useRef<QueuedUserMessage[]>([]);
296
296
  const hasQueuedRef = useRef(false);
297
297
  messagesRef.current = messages;
@@ -51,6 +51,7 @@ import {
51
51
  } from "@/components/ui/dropdown-menu";
52
52
  import { toast } from "@/components/ui/use-toast";
53
53
  import { AiModelId } from "@/core/ai/ids/ids";
54
+ import { AI_SDK_UI_THROTTLE_MS } from "@/core/ai/constants";
54
55
  import { stagedAICellsAtom, useStagedCells } from "@/core/ai/staged-cells";
55
56
  import type { ToolNotebookContext } from "@/core/ai/tools/base";
56
57
  import { useCellActions } from "@/core/cells/cells";
@@ -117,8 +118,7 @@ export const AddCellWithAI: React.FC<{
117
118
  };
118
119
 
119
120
  const { sendMessage, stop, status, addToolOutput } = useChat({
120
- // Throttle the messages and data updates to 100ms
121
- experimental_throttle: 100,
121
+ throttle: AI_SDK_UI_THROTTLE_MS,
122
122
  transport: new StreamingChunkTransport(
123
123
  {
124
124
  api: runtimeManager.getAiURL("completion").toString(),
@@ -27,6 +27,7 @@ import { Label } from "@/components/ui/label";
27
27
  import { Switch } from "@/components/ui/switch";
28
28
  import { Tooltip } from "@/components/ui/tooltip";
29
29
  import { toast } from "@/components/ui/use-toast";
30
+ import { AI_SDK_UI_THROTTLE_MS } from "@/core/ai/constants";
30
31
  import { stripWrappingBackticks } from "@/core/ai/strip-wrapping-backticks";
31
32
  import { type AiCompletionCell, includeOtherCellsAtom } from "@/core/ai/state";
32
33
  import type { CellId } from "@/core/cells/ids";
@@ -124,8 +125,7 @@ export const AiCompletionEditor: React.FC<Props> = ({
124
125
  api: runtimeManager.getAiURL("completion").toString(),
125
126
  headers: runtimeManager.headers(),
126
127
  initialInput: initialPrompt,
127
- // Throttle the messages and data updates to 100ms
128
- experimental_throttle: 100,
128
+ throttle: AI_SDK_UI_THROTTLE_MS,
129
129
  body: {
130
130
  ...(Object.keys(completionBody).length > 0
131
131
  ? completionBody
@@ -0,0 +1,7 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /**
4
+ * Throttle wait (ms) for AI SDK React hooks (`useChat` / `useCompletion`).
5
+ * Limits UI re-renders while streaming tokens.
6
+ */
7
+ export const AI_SDK_UI_THROTTLE_MS = 100;