@assistant-ui/react 0.12.14 → 0.12.16
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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.d.ts +1 -4
- package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.js +2 -527
- package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/legacy-runtime/hooks/AttachmentContext.d.ts +96 -96
- package/dist/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.d.ts +1 -16
- package/dist/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.js +1 -14
- package/dist/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.d.ts +1 -13
- package/dist/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.js +2 -82
- package/dist/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.d.ts +1 -23
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js +1 -305
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/external-store/createMessageConverter.d.ts +1 -16
- package/dist/legacy-runtime/runtime-cores/external-store/createMessageConverter.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/external-store/createMessageConverter.js +1 -48
- package/dist/legacy-runtime/runtime-cores/external-store/createMessageConverter.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.d.ts +1 -33
- package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.js +1 -295
- package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.d.ts +1 -3
- package/dist/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.js +1 -17
- package/dist/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/local/LocalRuntimeOptions.d.ts +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts +1 -96
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.js +1 -110
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts +1 -112
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.js +1 -439
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.d.ts +1 -12
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.js +1 -102
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.d.ts +1 -3
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.js +1 -46
- package/dist/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarInteractionContext.d.ts +6 -0
- package/dist/primitives/actionBar/ActionBarInteractionContext.d.ts.map +1 -0
- package/dist/primitives/actionBar/ActionBarInteractionContext.js +5 -0
- package/dist/primitives/actionBar/ActionBarInteractionContext.js.map +1 -0
- package/dist/primitives/actionBar/ActionBarRoot.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarRoot.js +18 -4
- package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts +2 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts.map +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.js +3 -2
- package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreRoot.d.ts.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +35 -2
- package/dist/primitives/actionBarMore/ActionBarMoreRoot.js.map +1 -1
- package/dist/utils/createActionButton.js +1 -1
- package/dist/utils/createActionButton.js.map +1 -1
- package/dist/utils/json/is-json-equal.d.ts +2 -0
- package/dist/utils/json/is-json-equal.d.ts.map +1 -0
- package/dist/utils/json/is-json-equal.js +31 -0
- package/dist/utils/json/is-json-equal.js.map +1 -0
- package/dist/utils/json/is-json.d.ts +1 -0
- package/dist/utils/json/is-json.d.ts.map +1 -1
- package/dist/utils/json/is-json.js +5 -3
- package/dist/utils/json/is-json.js.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +1 -1
- package/src/internal.ts +1 -1
- package/src/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.ts +2 -784
- package/src/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.tsx +5 -43
- package/src/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.ts +2 -100
- package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.test.ts +225 -2
- package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.ts +4 -439
- package/src/legacy-runtime/runtime-cores/external-store/createMessageConverter.ts +1 -76
- package/src/legacy-runtime/runtime-cores/external-store/external-message-converter.ts +4 -465
- package/src/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.ts +1 -27
- package/src/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.tsx +1 -178
- package/src/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.tsx +1 -529
- package/src/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.tsx +1 -152
- package/src/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.ts +1 -80
- package/src/primitives/actionBar/ActionBarInteractionContext.ts +13 -0
- package/src/primitives/actionBar/ActionBarRoot.tsx +38 -8
- package/src/primitives/actionBar/useActionBarFloatStatus.ts +4 -1
- package/src/primitives/actionBarMore/ActionBarMoreRoot.tsx +52 -2
- package/src/tests/BaseComposerRuntimeCore.test.ts +2 -3
- package/src/tests/external-message-converter.test.ts +80 -0
- package/src/utils/createActionButton.tsx +1 -1
- package/src/utils/json/is-json-equal.ts +48 -0
- package/src/utils/json/is-json.ts +6 -3
|
@@ -1,82 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
BaseAssistantRuntimeCore,
|
|
6
|
-
AssistantRuntimeImpl,
|
|
7
|
-
type RemoteThreadListOptions,
|
|
8
|
-
} from "@assistant-ui/core/internal";
|
|
9
|
-
import type {
|
|
10
|
-
AssistantRuntimeCore,
|
|
11
|
-
AssistantRuntime,
|
|
12
|
-
} from "@assistant-ui/core";
|
|
13
|
-
import { RemoteThreadListThreadListRuntimeCore } from "./RemoteThreadListThreadListRuntimeCore";
|
|
14
|
-
import { useAui } from "@assistant-ui/store";
|
|
15
|
-
|
|
16
|
-
class RemoteThreadListRuntimeCore
|
|
17
|
-
extends BaseAssistantRuntimeCore
|
|
18
|
-
implements AssistantRuntimeCore
|
|
19
|
-
{
|
|
20
|
-
public readonly threads;
|
|
21
|
-
|
|
22
|
-
constructor(options: RemoteThreadListOptions) {
|
|
23
|
-
super();
|
|
24
|
-
this.threads = new RemoteThreadListThreadListRuntimeCore(
|
|
25
|
-
options,
|
|
26
|
-
this._contextProvider,
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public get RenderComponent() {
|
|
31
|
-
return this.threads.__internal_RenderComponent;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const useRemoteThreadListRuntimeImpl = (
|
|
36
|
-
options: RemoteThreadListOptions,
|
|
37
|
-
): AssistantRuntime => {
|
|
38
|
-
const [runtime] = useState(() => new RemoteThreadListRuntimeCore(options));
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
runtime.threads.__internal_setOptions(options);
|
|
41
|
-
runtime.threads.__internal_load();
|
|
42
|
-
}, [runtime, options]);
|
|
43
|
-
return useMemo(() => new AssistantRuntimeImpl(runtime), [runtime]);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export const useRemoteThreadListRuntime = (
|
|
47
|
-
options: RemoteThreadListOptions,
|
|
48
|
-
): AssistantRuntime => {
|
|
49
|
-
const runtimeHookRef = useRef(options.runtimeHook);
|
|
50
|
-
runtimeHookRef.current = options.runtimeHook;
|
|
51
|
-
|
|
52
|
-
const stableRuntimeHook = useCallback(() => {
|
|
53
|
-
return runtimeHookRef.current();
|
|
54
|
-
}, []);
|
|
55
|
-
|
|
56
|
-
const stableOptions = useMemo<RemoteThreadListOptions>(
|
|
57
|
-
() => ({
|
|
58
|
-
adapter: options.adapter,
|
|
59
|
-
allowNesting: options.allowNesting,
|
|
60
|
-
runtimeHook: stableRuntimeHook,
|
|
61
|
-
}),
|
|
62
|
-
[options.adapter, options.allowNesting, stableRuntimeHook],
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const aui = useAui();
|
|
66
|
-
const isNested = aui.threadListItem.source !== null;
|
|
67
|
-
|
|
68
|
-
if (isNested) {
|
|
69
|
-
if (!stableOptions.allowNesting) {
|
|
70
|
-
throw new Error(
|
|
71
|
-
"useRemoteThreadListRuntime cannot be nested inside another RemoteThreadListRuntime. " +
|
|
72
|
-
"Set allowNesting: true to allow nesting (the inner runtime will become a no-op).",
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// If allowNesting is true and already inside a thread list context,
|
|
77
|
-
// just call the runtimeHook directly (no-op behavior)
|
|
78
|
-
return stableRuntimeHook();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return useRemoteThreadListRuntimeImpl(stableOptions);
|
|
82
|
-
};
|
|
3
|
+
export { useRemoteThreadListRuntime } from "@assistant-ui/core/react";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
export type ActionBarInteractionContextValue = {
|
|
6
|
+
acquireInteractionLock: () => () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const ActionBarInteractionContext =
|
|
10
|
+
createContext<ActionBarInteractionContextValue | null>(null);
|
|
11
|
+
|
|
12
|
+
export const useActionBarInteractionContext = () =>
|
|
13
|
+
useContext(ActionBarInteractionContext);
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Primitive } from "@radix-ui/react-primitive";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type ComponentRef,
|
|
6
|
+
forwardRef,
|
|
7
|
+
ComponentPropsWithoutRef,
|
|
8
|
+
useCallback,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
5
12
|
import {
|
|
6
13
|
useActionBarFloatStatus,
|
|
7
14
|
HideAndFloatStatus,
|
|
8
15
|
} from "./useActionBarFloatStatus";
|
|
16
|
+
import { ActionBarInteractionContext } from "./ActionBarInteractionContext";
|
|
9
17
|
|
|
10
18
|
type PrimitiveDivProps = ComponentPropsWithoutRef<typeof Primitive.div>;
|
|
11
19
|
|
|
@@ -60,22 +68,44 @@ export const ActionBarPrimitiveRoot = forwardRef<
|
|
|
60
68
|
ActionBarPrimitiveRoot.Element,
|
|
61
69
|
ActionBarPrimitiveRoot.Props
|
|
62
70
|
>(({ hideWhenRunning, autohide, autohideFloat, ...rest }, ref) => {
|
|
71
|
+
const [interactionCount, setInteractionCount] = useState(0);
|
|
72
|
+
|
|
73
|
+
const acquireInteractionLock = useCallback(() => {
|
|
74
|
+
let released = false;
|
|
75
|
+
|
|
76
|
+
setInteractionCount((count) => count + 1);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
if (released) return;
|
|
80
|
+
released = true;
|
|
81
|
+
setInteractionCount((count) => Math.max(0, count - 1));
|
|
82
|
+
};
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const interactionContext = useMemo(
|
|
86
|
+
() => ({ acquireInteractionLock }),
|
|
87
|
+
[acquireInteractionLock],
|
|
88
|
+
);
|
|
89
|
+
|
|
63
90
|
const hideAndfloatStatus = useActionBarFloatStatus({
|
|
64
91
|
hideWhenRunning,
|
|
65
92
|
autohide,
|
|
66
93
|
autohideFloat,
|
|
94
|
+
forceVisible: interactionCount > 0,
|
|
67
95
|
});
|
|
68
96
|
|
|
69
97
|
if (hideAndfloatStatus === HideAndFloatStatus.Hidden) return null;
|
|
70
98
|
|
|
71
99
|
return (
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
<ActionBarInteractionContext.Provider value={interactionContext}>
|
|
101
|
+
<Primitive.div
|
|
102
|
+
{...(hideAndfloatStatus === HideAndFloatStatus.Floating
|
|
103
|
+
? { "data-floating": "true" }
|
|
104
|
+
: null)}
|
|
105
|
+
{...rest}
|
|
106
|
+
ref={ref}
|
|
107
|
+
/>
|
|
108
|
+
</ActionBarInteractionContext.Provider>
|
|
79
109
|
);
|
|
80
110
|
});
|
|
81
111
|
|
|
@@ -12,24 +12,27 @@ export type UseActionBarFloatStatusProps = {
|
|
|
12
12
|
hideWhenRunning?: boolean | undefined;
|
|
13
13
|
autohide?: "always" | "not-last" | "never" | undefined;
|
|
14
14
|
autohideFloat?: "always" | "single-branch" | "never" | undefined;
|
|
15
|
+
forceVisible?: boolean | undefined;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export const useActionBarFloatStatus = ({
|
|
18
19
|
hideWhenRunning,
|
|
19
20
|
autohide,
|
|
20
21
|
autohideFloat,
|
|
22
|
+
forceVisible,
|
|
21
23
|
}: UseActionBarFloatStatusProps) => {
|
|
22
24
|
return useAuiState((s) => {
|
|
23
25
|
if (hideWhenRunning && s.thread.isRunning) return HideAndFloatStatus.Hidden;
|
|
24
26
|
|
|
25
27
|
const autohideEnabled =
|
|
26
28
|
autohide === "always" || (autohide === "not-last" && !s.message.isLast);
|
|
29
|
+
const isVisibleByInteraction = forceVisible || s.message.isHovering;
|
|
27
30
|
|
|
28
31
|
// normal status
|
|
29
32
|
if (!autohideEnabled) return HideAndFloatStatus.Normal;
|
|
30
33
|
|
|
31
34
|
// hidden status
|
|
32
|
-
if (!
|
|
35
|
+
if (!isVisibleByInteraction) return HideAndFloatStatus.Hidden;
|
|
33
36
|
|
|
34
37
|
// floating status
|
|
35
38
|
if (
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { FC } from "react";
|
|
3
|
+
import { FC, useCallback, useEffect, useRef } from "react";
|
|
4
4
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
|
5
5
|
import { ScopedProps, useDropdownMenuScope } from "./scope";
|
|
6
|
+
import { useActionBarInteractionContext } from "../actionBar/ActionBarInteractionContext";
|
|
6
7
|
|
|
7
8
|
export namespace ActionBarMorePrimitiveRoot {
|
|
8
9
|
export type Props = DropdownMenuPrimitive.DropdownMenuProps;
|
|
@@ -12,11 +13,60 @@ export const ActionBarMorePrimitiveRoot: FC<
|
|
|
12
13
|
ActionBarMorePrimitiveRoot.Props
|
|
13
14
|
> = ({
|
|
14
15
|
__scopeActionBarMore,
|
|
16
|
+
open,
|
|
17
|
+
onOpenChange,
|
|
15
18
|
...rest
|
|
16
19
|
}: ScopedProps<ActionBarMorePrimitiveRoot.Props>) => {
|
|
17
20
|
const scope = useDropdownMenuScope(__scopeActionBarMore);
|
|
21
|
+
const actionBarInteraction = useActionBarInteractionContext();
|
|
22
|
+
const releaseInteractionLockRef = useRef<(() => void) | null>(null);
|
|
23
|
+
const isControlled = open !== undefined;
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
const setInteractionOpen = useCallback(
|
|
26
|
+
(nextOpen: boolean) => {
|
|
27
|
+
if (nextOpen) {
|
|
28
|
+
if (releaseInteractionLockRef.current) return;
|
|
29
|
+
releaseInteractionLockRef.current =
|
|
30
|
+
actionBarInteraction?.acquireInteractionLock() ?? null;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
releaseInteractionLockRef.current?.();
|
|
35
|
+
releaseInteractionLockRef.current = null;
|
|
36
|
+
},
|
|
37
|
+
[actionBarInteraction],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const handleOpenChange = useCallback(
|
|
41
|
+
(nextOpen: boolean) => {
|
|
42
|
+
if (!isControlled) {
|
|
43
|
+
setInteractionOpen(nextOpen);
|
|
44
|
+
}
|
|
45
|
+
onOpenChange?.(nextOpen);
|
|
46
|
+
},
|
|
47
|
+
[isControlled, setInteractionOpen, onOpenChange],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!isControlled) return;
|
|
52
|
+
setInteractionOpen(Boolean(open));
|
|
53
|
+
}, [isControlled, open, setInteractionOpen]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
return () => {
|
|
57
|
+
releaseInteractionLockRef.current?.();
|
|
58
|
+
releaseInteractionLockRef.current = null;
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<DropdownMenuPrimitive.Root
|
|
64
|
+
{...scope}
|
|
65
|
+
{...rest}
|
|
66
|
+
{...(open !== undefined ? { open } : null)}
|
|
67
|
+
onOpenChange={handleOpenChange}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
20
70
|
};
|
|
21
71
|
|
|
22
72
|
ActionBarMorePrimitiveRoot.displayName = "ActionBarMorePrimitive.Root";
|
|
@@ -181,11 +181,10 @@ describe("BaseComposerRuntimeCore", () => {
|
|
|
181
181
|
expect(composer.quote).toBeUndefined();
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it("send with empty text
|
|
184
|
+
it("send with empty text is a no-op", async () => {
|
|
185
185
|
await composer.send();
|
|
186
186
|
|
|
187
|
-
expect(composer.sentMessages).toHaveLength(
|
|
188
|
-
expect(composer.sentMessages[0]!.content).toEqual([]);
|
|
187
|
+
expect(composer.sentMessages).toHaveLength(0);
|
|
189
188
|
});
|
|
190
189
|
|
|
191
190
|
it("addAttachment throws when no adapter", async () => {
|
|
@@ -158,6 +158,86 @@ describe("convertExternalMessages", () => {
|
|
|
158
158
|
expect(toolCallParts).toHaveLength(1);
|
|
159
159
|
expect((toolCallParts[0] as any).result).toEqual({ data: "result" });
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
it("should merge duplicate tool calls by toolCallId across assistant messages", () => {
|
|
163
|
+
const messages = [
|
|
164
|
+
{
|
|
165
|
+
id: "msg1",
|
|
166
|
+
role: "assistant" as const,
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: "tool-call" as const,
|
|
170
|
+
toolCallId: "tc1",
|
|
171
|
+
toolName: "search",
|
|
172
|
+
args: { query: "old" },
|
|
173
|
+
argsText: '{"query":"old"',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "msg2",
|
|
179
|
+
role: "assistant" as const,
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: "tool-call" as const,
|
|
183
|
+
toolCallId: "tc1",
|
|
184
|
+
toolName: "search",
|
|
185
|
+
args: { query: "new" },
|
|
186
|
+
argsText: '{"query":"new"}',
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const callback: useExternalMessageConverter.Callback<
|
|
193
|
+
(typeof messages)[number]
|
|
194
|
+
> = (msg) => msg;
|
|
195
|
+
|
|
196
|
+
const result = convertExternalMessages(messages, callback, false, {});
|
|
197
|
+
|
|
198
|
+
expect(result).toHaveLength(1);
|
|
199
|
+
expect(result[0]!.role).toBe("assistant");
|
|
200
|
+
const toolCallParts = result[0]!.content.filter(
|
|
201
|
+
(p) => p.type === "tool-call",
|
|
202
|
+
);
|
|
203
|
+
expect(toolCallParts).toHaveLength(1);
|
|
204
|
+
expect((toolCallParts[0] as any).args).toEqual({ query: "new" });
|
|
205
|
+
expect((toolCallParts[0] as any).argsText).toBe('{"query":"new"}');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should ignore orphaned tool results without throwing", () => {
|
|
209
|
+
const messages = [
|
|
210
|
+
{
|
|
211
|
+
id: "msg1",
|
|
212
|
+
role: "assistant" as const,
|
|
213
|
+
content: "First response",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
role: "tool" as const,
|
|
217
|
+
toolCallId: "missing-tool-call",
|
|
218
|
+
toolName: "search",
|
|
219
|
+
result: { data: "orphan result" },
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "msg2",
|
|
223
|
+
role: "assistant" as const,
|
|
224
|
+
content: "Second response",
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const callback: useExternalMessageConverter.Callback<
|
|
229
|
+
(typeof messages)[number]
|
|
230
|
+
> = (msg) => msg;
|
|
231
|
+
|
|
232
|
+
const result = convertExternalMessages(messages, callback, false, {});
|
|
233
|
+
expect(result).toHaveLength(1);
|
|
234
|
+
expect(result[0]!.role).toBe("assistant");
|
|
235
|
+
|
|
236
|
+
const textParts = result[0]!.content.filter((p) => p.type === "text");
|
|
237
|
+
expect(textParts).toHaveLength(2);
|
|
238
|
+
expect((textParts[0] as any).text).toBe("First response");
|
|
239
|
+
expect((textParts[1] as any).text).toBe("Second response");
|
|
240
|
+
});
|
|
161
241
|
});
|
|
162
242
|
|
|
163
243
|
describe("synthetic error message", () => {
|
|
@@ -41,8 +41,8 @@ export const createActionButton = <TProps,>(
|
|
|
41
41
|
const callback = useActionButton(forwardedProps as TProps) ?? undefined;
|
|
42
42
|
return (
|
|
43
43
|
<Primitive.button
|
|
44
|
-
type="button"
|
|
45
44
|
{...primitiveProps}
|
|
45
|
+
type="button"
|
|
46
46
|
ref={forwardedRef}
|
|
47
47
|
disabled={primitiveProps.disabled || !callback}
|
|
48
48
|
onClick={composeEventHandlers(primitiveProps.onClick, callback)}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
2
|
+
import { isJSONValue, isRecord } from "./is-json";
|
|
3
|
+
|
|
4
|
+
const MAX_JSON_DEPTH = 100;
|
|
5
|
+
|
|
6
|
+
const isJSONValueEqualAtDepth = (
|
|
7
|
+
a: ReadonlyJSONValue,
|
|
8
|
+
b: ReadonlyJSONValue,
|
|
9
|
+
currentDepth: number,
|
|
10
|
+
): boolean => {
|
|
11
|
+
if (a === b) return true;
|
|
12
|
+
if (currentDepth > MAX_JSON_DEPTH) return false;
|
|
13
|
+
|
|
14
|
+
if (a == null || b == null) return false;
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(a)) {
|
|
17
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
18
|
+
return a.every((item, index) =>
|
|
19
|
+
isJSONValueEqualAtDepth(
|
|
20
|
+
item,
|
|
21
|
+
b[index] as ReadonlyJSONValue,
|
|
22
|
+
currentDepth + 1,
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (Array.isArray(b)) return false;
|
|
28
|
+
if (!isRecord(a) || !isRecord(b)) return false;
|
|
29
|
+
|
|
30
|
+
const aKeys = Object.keys(a);
|
|
31
|
+
const bKeys = Object.keys(b);
|
|
32
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
33
|
+
|
|
34
|
+
return aKeys.every(
|
|
35
|
+
(key) =>
|
|
36
|
+
Object.hasOwn(b, key) &&
|
|
37
|
+
isJSONValueEqualAtDepth(
|
|
38
|
+
a[key] as ReadonlyJSONValue,
|
|
39
|
+
b[key] as ReadonlyJSONValue,
|
|
40
|
+
currentDepth + 1,
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const isJSONValueEqual = (a: unknown, b: unknown): boolean => {
|
|
46
|
+
if (!isJSONValue(a) || !isJSONValue(b)) return false;
|
|
47
|
+
return isJSONValueEqualAtDepth(a, b, 0);
|
|
48
|
+
};
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
ReadonlyJSONValue,
|
|
5
5
|
} from "assistant-stream/utils";
|
|
6
6
|
|
|
7
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
8
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export function isJSONValue(
|
|
8
12
|
value: unknown,
|
|
9
13
|
currentDepth: number = 0,
|
|
@@ -30,7 +34,7 @@ export function isJSONValue(
|
|
|
30
34
|
return value.every((item) => isJSONValue(item, currentDepth + 1));
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
if (
|
|
37
|
+
if (isRecord(value)) {
|
|
34
38
|
return Object.entries(value).every(
|
|
35
39
|
([key, val]) =>
|
|
36
40
|
typeof key === "string" && isJSONValue(val, currentDepth + 1),
|
|
@@ -46,8 +50,7 @@ export function isJSONArray(value: unknown): value is ReadonlyJSONArray {
|
|
|
46
50
|
|
|
47
51
|
export function isJSONObject(value: unknown): value is ReadonlyJSONObject {
|
|
48
52
|
return (
|
|
49
|
-
value
|
|
50
|
-
typeof value === "object" &&
|
|
53
|
+
isRecord(value) &&
|
|
51
54
|
Object.entries(value).every(
|
|
52
55
|
([key, val]) => typeof key === "string" && isJSONValue(val),
|
|
53
56
|
)
|