@marimo-team/islands 0.23.15-dev4 → 0.23.15-dev40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConnectedDataExplorerComponent-CU4fZJzG.js → ConnectedDataExplorerComponent-7p7rt9iw.js} +4 -4
- package/dist/{ErrorBoundary-DE6tzZf-.js → ErrorBoundary-B_CAG7_e.js} +1 -1
- package/dist/{ImageComparisonComponent-CHrI72em.js → ImageComparisonComponent-1i5IBhon.js} +173 -129
- package/dist/assets/__vite-browser-external-CjNAy01L.js +1 -0
- package/dist/assets/worker-B3XPCb6y.js +98 -0
- package/dist/{chat-ui-DinOvbWu.js → chat-ui-C0z13gJB.js} +3111 -3111
- package/dist/{code-visibility-Dk8cVOny.js → code-visibility-CSsHnVZu.js} +946 -864
- package/dist/{constants-T20xxyNf.js → debounce-BOD3DbfP.js} +1 -24
- package/dist/{formats-Dzx4J_z1.js → formats-CitsMtUm.js} +1 -1
- package/dist/{glide-data-editor-CjTu7ukN.js → glide-data-editor-CwZz71BD.js} +3 -3
- package/dist/{html-to-image-_wGfk8V-.js → html-to-image-CEo5pRYw.js} +2291 -2250
- package/dist/{input-DtsN7xm-.js → input-BGPrFH3g.js} +2 -2
- package/dist/main.js +1297 -1201
- package/dist/{mermaid-BYqXy_NE.js → mermaid-UdmxG2PZ.js} +2 -2
- package/dist/{process-output-Mh4UrjwM.js → process-output-BOvvilRG.js} +1 -1
- package/dist/{reveal-component-B5DXLyO7.js → reveal-component-DGjJVNMQ.js} +618 -605
- package/dist/{spec-Cz-Bj1JI.js → spec-DSs9v0xx.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CWNNlFEX.js → toDate-BRJgtOGm.js} +14 -5
- package/dist/{useAsyncData-KfHB8wQR.js → useAsyncData-BSOyAbac.js} +1 -1
- package/dist/{useDeepCompareMemoize-nJMtxhm4.js → useDeepCompareMemoize-BTLeWzuR.js} +1 -1
- package/dist/{useLifecycle-DegSo0lV.js → useLifecycle-C6wHjkhW.js} +1 -1
- package/dist/{useTheme-6eZ3GOTS.js → useTheme-Df_vGflw.js} +62 -27
- package/dist/{vega-component-DzyyM9fc.js → vega-component-BAvmvTjO.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
- package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
- package/src/components/chat/chat-panel.tsx +196 -67
- package/src/components/chat/chat-utils.ts +111 -2
- package/src/components/editor/SortableCell.tsx +6 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
- package/src/components/editor/ai/add-cell-with-ai.tsx +14 -5
- package/src/components/editor/ai/ai-completion-editor.tsx +4 -1
- package/src/components/editor/cell/cell-context-menu.tsx +7 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
- package/src/components/editor/columns/cell-column.tsx +34 -4
- package/src/components/editor/columns/sortable-column.tsx +24 -4
- package/src/components/editor/file-tree/download.ts +46 -0
- package/src/components/editor/file-tree/file-explorer.tsx +3 -21
- package/src/components/editor/file-tree/file-viewer.tsx +4 -27
- package/src/components/editor/navigation/__tests__/navigation.test.ts +33 -0
- package/src/components/editor/navigation/navigation.ts +8 -1
- package/src/components/editor/notebook-cell.tsx +203 -106
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
- package/src/components/editor/renderers/cell-array.tsx +26 -13
- package/src/components/editor/renderers/cells-renderer.tsx +8 -2
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
- package/src/components/editor/renderers/slides-layout/types.ts +4 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +37 -4
- package/src/components/slides/slide-form.tsx +72 -0
- package/src/components/ui/toast.tsx +11 -3
- package/src/components/ui/toaster.tsx +65 -9
- package/src/core/cells/__tests__/utils.test.ts +59 -0
- package/src/core/cells/utils.ts +51 -0
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +97 -3
- package/src/core/codemirror/completion/signature-hint.ts +78 -11
- package/src/core/constants.ts +6 -0
- package/src/core/islands/__tests__/bridge.test.ts +341 -46
- package/src/core/islands/__tests__/main.test.ts +176 -0
- package/src/core/islands/__tests__/parse.test.ts +105 -0
- package/src/core/islands/bootstrap.ts +8 -3
- package/src/core/islands/bridge.ts +116 -23
- package/src/core/islands/components/__tests__/web-components.test.tsx +214 -0
- package/src/core/islands/components/web-components.tsx +76 -15
- package/src/core/islands/constants.ts +1 -0
- package/src/core/islands/main.ts +69 -3
- package/src/core/islands/parse.ts +70 -23
- package/src/core/islands/worker/__tests__/controller.test.ts +173 -0
- package/src/core/islands/worker/worker.tsx +145 -57
- package/src/core/runtime/__tests__/runtime.test.ts +64 -0
- package/src/core/runtime/runtime.ts +30 -10
- package/src/core/wasm/worker/bootstrap.ts +113 -20
- package/src/css/app/Cell.css +10 -0
- package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
- package/src/hooks/useHotkey.ts +29 -4
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
- package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
- package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +53 -2
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +71 -0
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
- package/src/theme/__tests__/useTheme.test.ts +68 -0
- package/src/theme/useTheme.ts +16 -1
- package/dist/assets/__vite-browser-external-BQCLNwri.js +0 -1
- package/dist/assets/worker-DAWRHcPq.js +0 -73
|
@@ -5,8 +5,7 @@ import { t as __commonJSMin } from "./chunk-BNovOVIE.js";
|
|
|
5
5
|
import { _ as Logger } from "./button-BacYv-bE.js";
|
|
6
6
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
7
7
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
8
|
-
import {
|
|
9
|
-
import { S as atom, g as isIslands, h as waitFor, p as store } from "./useTheme-6eZ3GOTS.js";
|
|
8
|
+
import { T as atom, a as KnownQueryParams, g as store, v as waitFor, y as isIslands } from "./useTheme-Df_vGflw.js";
|
|
10
9
|
import { t as invariant } from "./invariant-wRzNXIsJ.js";
|
|
11
10
|
var CircleQuestionMark = createLucideIcon("circle-question-mark", [
|
|
12
11
|
["circle", {
|
|
@@ -516,18 +515,28 @@ var RuntimeManager = class {
|
|
|
516
515
|
return isWasm() || isIslands() || isStaticNotebook();
|
|
517
516
|
}
|
|
518
517
|
formatHttpURL({ path: e = "", searchParams: b, restrictToKnownQueryParams: x = true }) {
|
|
519
|
-
let S = this.httpURL,
|
|
518
|
+
let S = this.httpURL, C = new URLSearchParams(window.location.search);
|
|
520
519
|
if (b) for (let [e2, x2] of b.entries()) S.searchParams.set(e2, x2);
|
|
521
|
-
for (let [e2, b2] of
|
|
520
|
+
for (let [e2, b2] of C.entries()) x && !Object.values(KnownQueryParams).includes(e2) || S.searchParams.set(e2, b2);
|
|
522
521
|
return S.pathname = `${S.pathname.replace(/\/$/, "")}/${e.replace(/^\//, "")}`, S.hash = "", S;
|
|
523
522
|
}
|
|
523
|
+
appendCrossOriginAuth(e) {
|
|
524
|
+
return !this.isSameOrigin && this.config.authToken && e.searchParams.set(KnownQueryParams.accessToken, this.config.authToken), e;
|
|
525
|
+
}
|
|
526
|
+
formatNavigableHttpURL(e, b) {
|
|
527
|
+
let x = this.formatHttpURL({
|
|
528
|
+
path: e,
|
|
529
|
+
searchParams: b
|
|
530
|
+
});
|
|
531
|
+
return x.searchParams.delete(KnownQueryParams.accessToken), this.appendCrossOriginAuth(x);
|
|
532
|
+
}
|
|
524
533
|
formatWsURL(e, b) {
|
|
525
534
|
let x = this.formatHttpURL({
|
|
526
535
|
path: e,
|
|
527
536
|
searchParams: b,
|
|
528
537
|
restrictToKnownQueryParams: false
|
|
529
538
|
});
|
|
530
|
-
return
|
|
539
|
+
return asWsUrl(this.appendCrossOriginAuth(x).toString());
|
|
531
540
|
}
|
|
532
541
|
getSessionSearchParams(e) {
|
|
533
542
|
let b = new URL(this.config.url), x = new URLSearchParams(b.search);
|
|
@@ -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 {
|
|
4
|
+
import { A as useEvent_default } from "./useTheme-Df_vGflw.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,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 {
|
|
3
|
+
import { k as dequal } from "./useTheme-Df_vGflw.js";
|
|
4
4
|
var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
|
|
5
5
|
function useDeepCompareMemoize(e) {
|
|
6
6
|
let i = import_react.useRef(e);
|
|
@@ -4,7 +4,7 @@ import { t as require_react } from "./react-DA-nE2FX.js";
|
|
|
4
4
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
5
5
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
6
6
|
import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
|
|
7
|
-
import {
|
|
7
|
+
import { C as useSetAtom, T as atom } from "./useTheme-Df_vGflw.js";
|
|
8
8
|
var Calendar = createLucideIcon("calendar", [
|
|
9
9
|
["path", {
|
|
10
10
|
d: "M8 2v4",
|
|
@@ -736,7 +736,39 @@ atom((e) => e(appConfigAtom).width), atom((e) => {
|
|
|
736
736
|
var _a;
|
|
737
737
|
return ((_a = e(resolvedMarimoConfigAtom).server) == null ? void 0 : _a.disable_file_downloads) ?? false;
|
|
738
738
|
});
|
|
739
|
-
|
|
739
|
+
const Constants = {
|
|
740
|
+
githubPage: "https://github.com/marimo-team/marimo",
|
|
741
|
+
releasesPage: "https://github.com/marimo-team/marimo/releases",
|
|
742
|
+
issuesPage: "https://github.com/marimo-team/marimo/issues",
|
|
743
|
+
feedbackForm: "https://marimo.io/feedback",
|
|
744
|
+
discordLink: "https://marimo.io/discord?ref=notebook",
|
|
745
|
+
docsPage: "https://docs.marimo.io",
|
|
746
|
+
youtube: "https://www.youtube.com/@marimo-team",
|
|
747
|
+
molab: "https://molab.marimo.io"
|
|
748
|
+
}, KnownQueryParams = {
|
|
749
|
+
showCode: "show-code",
|
|
750
|
+
includeCode: "include-code",
|
|
751
|
+
sessionId: "session_id",
|
|
752
|
+
kiosk: "kiosk",
|
|
753
|
+
vscode: "vscode",
|
|
754
|
+
filePath: "file",
|
|
755
|
+
accessToken: "access_token",
|
|
756
|
+
viewAs: "view-as",
|
|
757
|
+
showChrome: "show-chrome",
|
|
758
|
+
theme: "theme"
|
|
759
|
+
}, CSSClasses = { outputArea: "output-area" };
|
|
760
|
+
var import_compiler_runtime = require_compiler_runtime();
|
|
761
|
+
const THEMES = [
|
|
762
|
+
"light",
|
|
763
|
+
"dark",
|
|
764
|
+
"system"
|
|
765
|
+
];
|
|
766
|
+
function themeFromQueryParam() {
|
|
767
|
+
if (typeof window > "u") return;
|
|
768
|
+
let e = new URLSearchParams(window.location.search).get(KnownQueryParams.theme);
|
|
769
|
+
return THEMES.includes(e) ? e : void 0;
|
|
770
|
+
}
|
|
771
|
+
var themeAtom = atom((e) => {
|
|
740
772
|
if (isIslands()) {
|
|
741
773
|
if (document.body.classList.contains("dark-mode") || document.body.classList.contains("dark") || document.body.dataset.theme === "dark" || document.body.dataset.mode === "dark" || getVsCodeTheme() === "dark") return "dark";
|
|
742
774
|
let e2 = window.getComputedStyle(document.body), A = e2.getPropertyValue("color-scheme").trim();
|
|
@@ -748,7 +780,7 @@ var import_compiler_runtime = require_compiler_runtime(), themeAtom = atom((e) =
|
|
|
748
780
|
}
|
|
749
781
|
return "light";
|
|
750
782
|
}
|
|
751
|
-
return e(resolvedMarimoConfigAtom).display.theme;
|
|
783
|
+
return themeFromQueryParam() ?? e(resolvedMarimoConfigAtom).display.theme;
|
|
752
784
|
}), prefersDarkModeAtom = atom(false);
|
|
753
785
|
function setupThemeListener() {
|
|
754
786
|
if (typeof window > "u" || !window.matchMedia) return;
|
|
@@ -793,31 +825,34 @@ function useTheme() {
|
|
|
793
825
|
return e[1] === j ? M = e[2] : (M = { theme: j }, e[1] = j, e[2] = M), M;
|
|
794
826
|
}
|
|
795
827
|
export {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
getBuildingBlocks as
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
828
|
+
useEvent_default as A,
|
|
829
|
+
useSetAtom as C,
|
|
830
|
+
buildStore as D,
|
|
831
|
+
createStore as E,
|
|
832
|
+
getBuildingBlocks as O,
|
|
833
|
+
useAtomValue as S,
|
|
834
|
+
atom as T,
|
|
835
|
+
useJotaiEffect as _,
|
|
836
|
+
KnownQueryParams as a,
|
|
837
|
+
Provider as b,
|
|
838
|
+
autoInstantiateAtom as c,
|
|
839
|
+
resolvedMarimoConfigAtom as d,
|
|
840
|
+
useResolvedMarimoConfig as f,
|
|
841
|
+
store as g,
|
|
842
|
+
createDeepEqualAtom as h,
|
|
843
|
+
Constants as i,
|
|
844
|
+
dequal as k,
|
|
845
|
+
getResolvedMarimoConfig as l,
|
|
846
|
+
AppConfigSchema as m,
|
|
812
847
|
useTheme as n,
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
848
|
+
aiFeaturesEnabledAtom as o,
|
|
849
|
+
userConfigAtom as p,
|
|
850
|
+
CSSClasses as r,
|
|
851
|
+
aiModelConfiguredAtom as s,
|
|
817
852
|
resolvedThemeAtom as t,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
853
|
+
localeAtom as u,
|
|
854
|
+
waitFor as v,
|
|
855
|
+
useStore as w,
|
|
856
|
+
useAtom as x,
|
|
857
|
+
isIslands as y
|
|
823
858
|
};
|
|
@@ -2,23 +2,23 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
|
2
2
|
import { _ as Logger, c as Objects, g as cn, h as Events } from "./button-BacYv-bE.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, v as CircleQuestionMark } from "./toDate-
|
|
5
|
+
import { c as asRemoteURL, v as CircleQuestionMark } from "./toDate-BRJgtOGm.js";
|
|
6
6
|
import "./react-dom-BTJzcVJ9.js";
|
|
7
7
|
import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
|
|
8
8
|
import "./zod-CijjQh4u.js";
|
|
9
9
|
import { n as ErrorBanner } from "./error-banner-DFPfz_Qf.js";
|
|
10
10
|
import { t as Tooltip } from "./tooltip-Czds6Qr8.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { t as debounce_default } from "./debounce-BOD3DbfP.js";
|
|
12
|
+
import { A as useEvent_default, n as useTheme } from "./useTheme-Df_vGflw.js";
|
|
13
13
|
import { s as uniq } from "./arrays-sEtDRoG4.js";
|
|
14
|
-
import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-
|
|
14
|
+
import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-CitsMtUm.js";
|
|
15
15
|
import { n as formats } from "./vega-loader.browser-CZ-J8Py3.js";
|
|
16
16
|
import { a as getContainerWidth, n as vegaLoadData, s as tooltipHandler } from "./loader-Boph2xIS.js";
|
|
17
17
|
import { t as j } from "./react-vega-B0sAlDTL.js";
|
|
18
18
|
import "./defaultLocale-u-3osm0P.js";
|
|
19
19
|
import "./defaultLocale-BoHTsDG6.js";
|
|
20
|
-
import { t as useAsyncData } from "./useAsyncData-
|
|
21
|
-
import { t as useDeepCompareMemoize } from "./useDeepCompareMemoize-
|
|
20
|
+
import { t as useAsyncData } from "./useAsyncData-BSOyAbac.js";
|
|
21
|
+
import { t as useDeepCompareMemoize } from "./useDeepCompareMemoize-BTLeWzuR.js";
|
|
22
22
|
import { t as Semaphore } from "./semaphore-CNDGTzkX.js";
|
|
23
23
|
var import_compiler_runtime = require_compiler_runtime(), import_react = /* @__PURE__ */ __toESM(require_react(), 1);
|
|
24
24
|
function fixRelativeUrl(e) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import type { UIMessage } from "ai";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
hasPendingToolCalls,
|
|
7
|
+
hasUnresolvedToolCalls,
|
|
8
|
+
shouldFlushQueue,
|
|
9
|
+
} from "../chat-utils";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* `hasPendingToolCalls` powers `sendAutomaticallyWhen` in `mo.ui.chat`:
|
|
@@ -81,6 +85,23 @@ describe("hasPendingToolCalls", () => {
|
|
|
81
85
|
).toBe(true);
|
|
82
86
|
});
|
|
83
87
|
|
|
88
|
+
it("returns true when the user denied a tool approval", () => {
|
|
89
|
+
expect(
|
|
90
|
+
hasPendingToolCalls([
|
|
91
|
+
userMessage("delete it"),
|
|
92
|
+
assistantToolMessage([
|
|
93
|
+
{
|
|
94
|
+
type: "tool-delete_file",
|
|
95
|
+
toolCallId: "call-1",
|
|
96
|
+
state: "output-denied",
|
|
97
|
+
input: { path: "secrets.env" },
|
|
98
|
+
approval: { id: "approval-1", approved: false },
|
|
99
|
+
} as unknown as UIMessage["parts"][number],
|
|
100
|
+
]),
|
|
101
|
+
]),
|
|
102
|
+
).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
84
105
|
it("returns true when a tool reached a terminal output state", () => {
|
|
85
106
|
expect(
|
|
86
107
|
hasPendingToolCalls([
|
|
@@ -267,3 +288,225 @@ describe("hasPendingToolCalls", () => {
|
|
|
267
288
|
).toBe(true);
|
|
268
289
|
});
|
|
269
290
|
});
|
|
291
|
+
|
|
292
|
+
describe("hasUnresolvedToolCalls", () => {
|
|
293
|
+
it("returns false when there are no messages", () => {
|
|
294
|
+
expect(hasUnresolvedToolCalls([])).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("returns true while approval is still requested", () => {
|
|
298
|
+
expect(
|
|
299
|
+
hasUnresolvedToolCalls([
|
|
300
|
+
userMessage("delete it"),
|
|
301
|
+
assistantToolMessage([
|
|
302
|
+
{
|
|
303
|
+
type: "tool-delete_file",
|
|
304
|
+
toolCallId: "call-1",
|
|
305
|
+
state: "approval-requested",
|
|
306
|
+
input: { path: "secrets.env" },
|
|
307
|
+
approval: { id: "approval-1" },
|
|
308
|
+
} as unknown as UIMessage["parts"][number],
|
|
309
|
+
]),
|
|
310
|
+
]),
|
|
311
|
+
).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("returns true while a tool is still running", () => {
|
|
315
|
+
expect(
|
|
316
|
+
hasUnresolvedToolCalls([
|
|
317
|
+
userMessage("run it"),
|
|
318
|
+
assistantToolMessage([
|
|
319
|
+
{
|
|
320
|
+
type: "tool-run_query",
|
|
321
|
+
toolCallId: "call-1",
|
|
322
|
+
state: "input-available",
|
|
323
|
+
input: { sql: "select 1" },
|
|
324
|
+
} as unknown as UIMessage["parts"][number],
|
|
325
|
+
]),
|
|
326
|
+
]),
|
|
327
|
+
).toBe(true);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("returns false once tools are ready to round-trip", () => {
|
|
331
|
+
expect(
|
|
332
|
+
hasUnresolvedToolCalls([
|
|
333
|
+
userMessage("run it"),
|
|
334
|
+
assistantToolMessage([
|
|
335
|
+
{
|
|
336
|
+
type: "tool-run_query",
|
|
337
|
+
toolCallId: "call-1",
|
|
338
|
+
state: "output-available",
|
|
339
|
+
input: { sql: "select 1" },
|
|
340
|
+
output: 1,
|
|
341
|
+
} as unknown as UIMessage["parts"][number],
|
|
342
|
+
]),
|
|
343
|
+
]),
|
|
344
|
+
).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("returns false once the user denied a tool approval", () => {
|
|
348
|
+
expect(
|
|
349
|
+
hasUnresolvedToolCalls([
|
|
350
|
+
userMessage("delete it"),
|
|
351
|
+
assistantToolMessage([
|
|
352
|
+
{
|
|
353
|
+
type: "tool-delete_file",
|
|
354
|
+
toolCallId: "call-1",
|
|
355
|
+
state: "output-denied",
|
|
356
|
+
input: { path: "secrets.env" },
|
|
357
|
+
approval: { id: "approval-1", approved: false },
|
|
358
|
+
} as unknown as UIMessage["parts"][number],
|
|
359
|
+
]),
|
|
360
|
+
]),
|
|
361
|
+
).toBe(false);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("shouldFlushQueue", () => {
|
|
366
|
+
it("flushes when the turn completed without error or pending tools", () => {
|
|
367
|
+
expect(
|
|
368
|
+
shouldFlushQueue({
|
|
369
|
+
isError: false,
|
|
370
|
+
isAbort: false,
|
|
371
|
+
hasPendingToolCalls: false,
|
|
372
|
+
hasUnresolvedToolCalls: false,
|
|
373
|
+
}),
|
|
374
|
+
).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("does not flush on error", () => {
|
|
378
|
+
expect(
|
|
379
|
+
shouldFlushQueue({
|
|
380
|
+
isError: true,
|
|
381
|
+
isAbort: false,
|
|
382
|
+
hasPendingToolCalls: false,
|
|
383
|
+
hasUnresolvedToolCalls: false,
|
|
384
|
+
}),
|
|
385
|
+
).toBe(false);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("does not flush while a tool round-trip is still pending", () => {
|
|
389
|
+
expect(
|
|
390
|
+
shouldFlushQueue({
|
|
391
|
+
isError: false,
|
|
392
|
+
isAbort: false,
|
|
393
|
+
hasPendingToolCalls: true,
|
|
394
|
+
hasUnresolvedToolCalls: false,
|
|
395
|
+
}),
|
|
396
|
+
).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("does not flush while approval is still requested", () => {
|
|
400
|
+
expect(
|
|
401
|
+
shouldFlushQueue({
|
|
402
|
+
isError: false,
|
|
403
|
+
isAbort: false,
|
|
404
|
+
hasPendingToolCalls: false,
|
|
405
|
+
hasUnresolvedToolCalls: true,
|
|
406
|
+
}),
|
|
407
|
+
).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("flushes on abort even when a tool is still streaming", () => {
|
|
411
|
+
expect(
|
|
412
|
+
shouldFlushQueue({
|
|
413
|
+
isError: false,
|
|
414
|
+
isAbort: true,
|
|
415
|
+
hasPendingToolCalls: false,
|
|
416
|
+
hasUnresolvedToolCalls: true,
|
|
417
|
+
}),
|
|
418
|
+
).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("does not flush on abort when the run ended in error", () => {
|
|
422
|
+
expect(
|
|
423
|
+
shouldFlushQueue({
|
|
424
|
+
isError: true,
|
|
425
|
+
isAbort: true,
|
|
426
|
+
hasPendingToolCalls: false,
|
|
427
|
+
hasUnresolvedToolCalls: true,
|
|
428
|
+
}),
|
|
429
|
+
).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("flushes once trailing text is present and tools are resolved", () => {
|
|
433
|
+
const messages = [
|
|
434
|
+
userMessage("run it"),
|
|
435
|
+
assistantToolMessage([
|
|
436
|
+
{
|
|
437
|
+
type: "tool-run_query",
|
|
438
|
+
toolCallId: "call-1",
|
|
439
|
+
state: "output-available",
|
|
440
|
+
input: {},
|
|
441
|
+
output: 1,
|
|
442
|
+
} as unknown as UIMessage["parts"][number],
|
|
443
|
+
{ type: "text", text: "Done." },
|
|
444
|
+
]),
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
expect(
|
|
448
|
+
shouldFlushQueue({
|
|
449
|
+
isError: false,
|
|
450
|
+
isAbort: false,
|
|
451
|
+
hasPendingToolCalls: hasPendingToolCalls(messages),
|
|
452
|
+
hasUnresolvedToolCalls: hasUnresolvedToolCalls(messages),
|
|
453
|
+
}),
|
|
454
|
+
).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("blocks flush while text trails a still-running tool, then allows it after resolution", () => {
|
|
458
|
+
const running = [
|
|
459
|
+
userMessage("run it"),
|
|
460
|
+
assistantToolMessage([
|
|
461
|
+
{
|
|
462
|
+
type: "tool-run_query",
|
|
463
|
+
toolCallId: "call-1",
|
|
464
|
+
state: "input-available",
|
|
465
|
+
input: {},
|
|
466
|
+
} as unknown as UIMessage["parts"][number],
|
|
467
|
+
{ type: "text", text: "Running your query..." },
|
|
468
|
+
]),
|
|
469
|
+
];
|
|
470
|
+
const resolved = [
|
|
471
|
+
userMessage("run it"),
|
|
472
|
+
assistantToolMessage([
|
|
473
|
+
{
|
|
474
|
+
type: "tool-run_query",
|
|
475
|
+
toolCallId: "call-1",
|
|
476
|
+
state: "output-available",
|
|
477
|
+
input: {},
|
|
478
|
+
output: 1,
|
|
479
|
+
} as unknown as UIMessage["parts"][number],
|
|
480
|
+
{ type: "text", text: "Running your query..." },
|
|
481
|
+
]),
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
expect(
|
|
485
|
+
shouldFlushQueue({
|
|
486
|
+
isError: false,
|
|
487
|
+
isAbort: false,
|
|
488
|
+
hasPendingToolCalls: hasPendingToolCalls(running),
|
|
489
|
+
hasUnresolvedToolCalls: hasUnresolvedToolCalls(running),
|
|
490
|
+
}),
|
|
491
|
+
).toBe(false);
|
|
492
|
+
expect(
|
|
493
|
+
shouldFlushQueue({
|
|
494
|
+
isError: false,
|
|
495
|
+
isAbort: false,
|
|
496
|
+
hasPendingToolCalls: hasPendingToolCalls(resolved),
|
|
497
|
+
hasUnresolvedToolCalls: hasUnresolvedToolCalls(resolved),
|
|
498
|
+
}),
|
|
499
|
+
).toBe(true);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("does not flush on error even when tools are also pending", () => {
|
|
503
|
+
expect(
|
|
504
|
+
shouldFlushQueue({
|
|
505
|
+
isError: true,
|
|
506
|
+
isAbort: false,
|
|
507
|
+
hasPendingToolCalls: true,
|
|
508
|
+
hasUnresolvedToolCalls: true,
|
|
509
|
+
}),
|
|
510
|
+
).toBe(false);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { type ChatMessagePart, useMessageQueue } from "../chat-utils";
|
|
6
|
+
|
|
7
|
+
function textPart(text: string): ChatMessagePart {
|
|
8
|
+
return { type: "text", text };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("useMessageQueue", () => {
|
|
12
|
+
it("initializes empty", () => {
|
|
13
|
+
const { result } = renderHook(() => useMessageQueue());
|
|
14
|
+
expect(result.current.messages).toEqual([]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("enqueues messages in order with unique ids", () => {
|
|
18
|
+
const { result } = renderHook(() => useMessageQueue());
|
|
19
|
+
|
|
20
|
+
act(() => result.current.enqueue([textPart("first")]));
|
|
21
|
+
act(() => result.current.enqueue([textPart("second")]));
|
|
22
|
+
|
|
23
|
+
const [a, b] = result.current.messages;
|
|
24
|
+
expect(a.parts).toEqual([textPart("first")]);
|
|
25
|
+
expect(b.parts).toEqual([textPart("second")]);
|
|
26
|
+
expect(a.id).toBeTypeOf("string");
|
|
27
|
+
expect(a.id).not.toBe(b.id);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("flushNext sends the oldest message's parts and dequeues it", () => {
|
|
31
|
+
const { result } = renderHook(() => useMessageQueue());
|
|
32
|
+
const send = vi.fn();
|
|
33
|
+
|
|
34
|
+
act(() => result.current.enqueue([textPart("first")]));
|
|
35
|
+
act(() => result.current.enqueue([textPart("second")]));
|
|
36
|
+
act(() => result.current.flushNext(send));
|
|
37
|
+
|
|
38
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
39
|
+
expect(send).toHaveBeenCalledWith([textPart("first")]);
|
|
40
|
+
expect(result.current.messages).toEqual([
|
|
41
|
+
expect.objectContaining({ parts: [textPart("second")] }),
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("flushNext sees a message enqueued in the same act (before re-render)", () => {
|
|
46
|
+
const { result } = renderHook(() => useMessageQueue());
|
|
47
|
+
const send = vi.fn();
|
|
48
|
+
|
|
49
|
+
// Mirrors enqueue + onFinish in the same tick: the ref must be updated
|
|
50
|
+
// synchronously inside enqueue, not only on the next render.
|
|
51
|
+
act(() => {
|
|
52
|
+
result.current.enqueue([textPart("queued while finishing")]);
|
|
53
|
+
result.current.flushNext(send);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(send).toHaveBeenCalledWith([textPart("queued while finishing")]);
|
|
58
|
+
expect(result.current.messages).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("drains sequentially, even across synchronous flushNext calls", () => {
|
|
62
|
+
const { result } = renderHook(() => useMessageQueue());
|
|
63
|
+
const send = vi.fn();
|
|
64
|
+
|
|
65
|
+
act(() => result.current.enqueue([textPart("first")]));
|
|
66
|
+
act(() => result.current.enqueue([textPart("second")]));
|
|
67
|
+
|
|
68
|
+
// Two flushes in the same act: the ref must stay in sync so the second
|
|
69
|
+
// flush releases the next message rather than re-sending the first.
|
|
70
|
+
act(() => {
|
|
71
|
+
result.current.flushNext(send);
|
|
72
|
+
result.current.flushNext(send);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(send).toHaveBeenNthCalledWith(1, [textPart("first")]);
|
|
76
|
+
expect(send).toHaveBeenNthCalledWith(2, [textPart("second")]);
|
|
77
|
+
expect(result.current.messages).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("flushNext on an empty queue is a no-op", () => {
|
|
81
|
+
const { result } = renderHook(() => useMessageQueue());
|
|
82
|
+
const send = vi.fn();
|
|
83
|
+
|
|
84
|
+
act(() => result.current.flushNext(send));
|
|
85
|
+
|
|
86
|
+
expect(send).not.toHaveBeenCalled();
|
|
87
|
+
expect(result.current.messages).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("clear empties the queue", () => {
|
|
91
|
+
const { result } = renderHook(() => useMessageQueue());
|
|
92
|
+
|
|
93
|
+
act(() => result.current.enqueue([textPart("first")]));
|
|
94
|
+
act(() => result.current.enqueue([textPart("second")]));
|
|
95
|
+
act(() => result.current.clear());
|
|
96
|
+
|
|
97
|
+
expect(result.current.messages).toEqual([]);
|
|
98
|
+
expect(result.current.hasQueuedRef.current).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("hasQueuedRef stays in sync when enqueue and flush run in the same act", () => {
|
|
102
|
+
const { result } = renderHook(() => useMessageQueue());
|
|
103
|
+
const send = vi.fn();
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
result.current.enqueue([textPart("first")]);
|
|
107
|
+
result.current.enqueue([textPart("second")]);
|
|
108
|
+
});
|
|
109
|
+
expect(result.current.hasQueuedRef.current).toBe(true);
|
|
110
|
+
|
|
111
|
+
act(() => {
|
|
112
|
+
result.current.flushNext(send);
|
|
113
|
+
});
|
|
114
|
+
expect(result.current.hasQueuedRef.current).toBe(true);
|
|
115
|
+
|
|
116
|
+
act(() => {
|
|
117
|
+
result.current.flushNext(send);
|
|
118
|
+
});
|
|
119
|
+
expect(result.current.hasQueuedRef.current).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|