@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.
Files changed (102) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal.d.ts +1 -1
  6. package/dist/internal.d.ts.map +1 -1
  7. package/dist/internal.js +1 -1
  8. package/dist/internal.js.map +1 -1
  9. package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.d.ts +1 -4
  10. package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  11. package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.js +2 -527
  12. package/dist/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  13. package/dist/legacy-runtime/hooks/AttachmentContext.d.ts +96 -96
  14. package/dist/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.d.ts +1 -16
  15. package/dist/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.d.ts.map +1 -1
  16. package/dist/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.js +1 -14
  17. package/dist/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.js.map +1 -1
  18. package/dist/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.d.ts +1 -13
  19. package/dist/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.d.ts.map +1 -1
  20. package/dist/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.js +2 -82
  21. package/dist/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.js.map +1 -1
  22. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.d.ts +1 -23
  23. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.d.ts.map +1 -1
  24. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js +1 -305
  25. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js.map +1 -1
  26. package/dist/legacy-runtime/runtime-cores/external-store/createMessageConverter.d.ts +1 -16
  27. package/dist/legacy-runtime/runtime-cores/external-store/createMessageConverter.d.ts.map +1 -1
  28. package/dist/legacy-runtime/runtime-cores/external-store/createMessageConverter.js +1 -48
  29. package/dist/legacy-runtime/runtime-cores/external-store/createMessageConverter.js.map +1 -1
  30. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.d.ts +1 -33
  31. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.d.ts.map +1 -1
  32. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.js +1 -295
  33. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.js.map +1 -1
  34. package/dist/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.d.ts +1 -3
  35. package/dist/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.d.ts.map +1 -1
  36. package/dist/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.js +1 -17
  37. package/dist/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.js.map +1 -1
  38. package/dist/legacy-runtime/runtime-cores/local/LocalRuntimeOptions.d.ts +1 -1
  39. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts +1 -96
  40. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  41. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.js +1 -110
  42. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.js.map +1 -1
  43. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts +1 -112
  44. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  45. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.js +1 -439
  46. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  47. package/dist/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.d.ts +1 -12
  48. package/dist/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.d.ts.map +1 -1
  49. package/dist/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.js +1 -102
  50. package/dist/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.js.map +1 -1
  51. package/dist/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.d.ts +1 -3
  52. package/dist/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.d.ts.map +1 -1
  53. package/dist/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.js +1 -46
  54. package/dist/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.js.map +1 -1
  55. package/dist/primitives/actionBar/ActionBarInteractionContext.d.ts +6 -0
  56. package/dist/primitives/actionBar/ActionBarInteractionContext.d.ts.map +1 -0
  57. package/dist/primitives/actionBar/ActionBarInteractionContext.js +5 -0
  58. package/dist/primitives/actionBar/ActionBarInteractionContext.js.map +1 -0
  59. package/dist/primitives/actionBar/ActionBarRoot.d.ts.map +1 -1
  60. package/dist/primitives/actionBar/ActionBarRoot.js +18 -4
  61. package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
  62. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts +2 -1
  63. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts.map +1 -1
  64. package/dist/primitives/actionBar/useActionBarFloatStatus.js +3 -2
  65. package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
  66. package/dist/primitives/actionBarMore/ActionBarMoreRoot.d.ts.map +1 -1
  67. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +35 -2
  68. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js.map +1 -1
  69. package/dist/utils/createActionButton.js +1 -1
  70. package/dist/utils/createActionButton.js.map +1 -1
  71. package/dist/utils/json/is-json-equal.d.ts +2 -0
  72. package/dist/utils/json/is-json-equal.d.ts.map +1 -0
  73. package/dist/utils/json/is-json-equal.js +31 -0
  74. package/dist/utils/json/is-json-equal.js.map +1 -0
  75. package/dist/utils/json/is-json.d.ts +1 -0
  76. package/dist/utils/json/is-json.d.ts.map +1 -1
  77. package/dist/utils/json/is-json.js +5 -3
  78. package/dist/utils/json/is-json.js.map +1 -1
  79. package/package.json +8 -8
  80. package/src/index.ts +1 -1
  81. package/src/internal.ts +1 -1
  82. package/src/legacy-runtime/cloud/AssistantCloudThreadHistoryAdapter.ts +2 -784
  83. package/src/legacy-runtime/runtime-cores/adapters/RuntimeAdapterProvider.tsx +5 -43
  84. package/src/legacy-runtime/runtime-cores/adapters/attachment/CloudFileAttachmentAdapter.ts +2 -100
  85. package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.test.ts +225 -2
  86. package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.ts +4 -439
  87. package/src/legacy-runtime/runtime-cores/external-store/createMessageConverter.ts +1 -76
  88. package/src/legacy-runtime/runtime-cores/external-store/external-message-converter.ts +4 -465
  89. package/src/legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime.ts +1 -27
  90. package/src/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.tsx +1 -178
  91. package/src/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.tsx +1 -529
  92. package/src/legacy-runtime/runtime-cores/remote-thread-list/adapter/cloud.tsx +1 -152
  93. package/src/legacy-runtime/runtime-cores/remote-thread-list/useRemoteThreadListRuntime.ts +1 -80
  94. package/src/primitives/actionBar/ActionBarInteractionContext.ts +13 -0
  95. package/src/primitives/actionBar/ActionBarRoot.tsx +38 -8
  96. package/src/primitives/actionBar/useActionBarFloatStatus.ts +4 -1
  97. package/src/primitives/actionBarMore/ActionBarMoreRoot.tsx +52 -2
  98. package/src/tests/BaseComposerRuntimeCore.test.ts +2 -3
  99. package/src/tests/external-message-converter.test.ts +80 -0
  100. package/src/utils/createActionButton.tsx +1 -1
  101. package/src/utils/json/is-json-equal.ts +48 -0
  102. package/src/utils/json/is-json.ts +6 -3
@@ -1,82 +1,3 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useMemo, useRef, useCallback } from "react";
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 { type ComponentRef, forwardRef, ComponentPropsWithoutRef } from "react";
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
- <Primitive.div
73
- {...(hideAndfloatStatus === HideAndFloatStatus.Floating
74
- ? { "data-floating": "true" }
75
- : null)}
76
- {...rest}
77
- ref={ref}
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 (!s.message.isHovering) return HideAndFloatStatus.Hidden;
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
- return <DropdownMenuPrimitive.Root {...scope} {...rest} />;
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 produces empty content array", async () => {
184
+ it("send with empty text is a no-op", async () => {
185
185
  await composer.send();
186
186
 
187
- expect(composer.sentMessages).toHaveLength(1);
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 (typeof value === "object") {
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 != null &&
50
- typeof value === "object" &&
53
+ isRecord(value) &&
51
54
  Object.entries(value).every(
52
55
  ([key, val]) => typeof key === "string" && isJSONValue(val),
53
56
  )