@assistant-ui/core 0.3.16 → 0.3.17
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 +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +2 -1
- package/dist/model-context/frame/provider.d.ts +3 -0
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +71 -21
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/tool-call-text.d.ts +26 -0
- package/dist/model-context/tool-call-text.d.ts.map +1 -0
- package/dist/model-context/tool-call-text.js +18 -0
- package/dist/model-context/tool-call-text.js.map +1 -0
- package/dist/react/RuntimeAdapter.js +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/AdaptedRemoteThread.js +1 -1
- package/dist/react/client/InMemoryThreadList.js +1 -1
- package/dist/react/client/Interactables.js +1 -1
- package/dist/react/client/RemoteThreadList.d.ts +15 -0
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +240 -138
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +6 -2
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +2 -1
- package/dist/react/interactables-legacy/Interactables.js +4 -4
- package/dist/react/interactables-legacy/Interactables.js.map +1 -1
- package/dist/react/model-context/toolbox.d.ts +2 -14
- package/dist/react/model-context/toolbox.d.ts.map +1 -1
- package/dist/react/model-context/toolbox.js +1 -13
- package/dist/react/model-context/toolbox.js.map +1 -1
- package/dist/react/model-context/useAssistantContext.js +37 -7
- package/dist/react/model-context/useAssistantContext.js.map +1 -1
- package/dist/react/model-context/useAuiToolOverrides.js +31 -5
- package/dist/react/model-context/useAuiToolOverrides.js.map +1 -1
- package/dist/react/model-context/useInlineRender.d.ts.map +1 -1
- package/dist/react/model-context/useInlineRender.js +10 -36
- package/dist/react/model-context/useInlineRender.js.map +1 -1
- package/dist/react/primitive-hooks/useMessageError.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useMessageError.js +2 -8
- package/dist/react/primitive-hooks/useMessageError.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListLoadMore.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -4
- package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
- package/dist/react/primitives/composer/ComposerAttachments.d.ts.map +1 -1
- package/dist/react/primitives/composer/ComposerAttachments.js +4 -3
- package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
- package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
- package/dist/react/primitives/composer/ComposerQueue.js +4 -3
- package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
- package/dist/react/primitives/message/MessageAttachments.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageAttachments.js +7 -6
- package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +3 -3
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/primitives/threadList/ThreadListItems.d.ts.map +1 -1
- package/dist/react/primitives/threadList/ThreadListItems.js +5 -4
- package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +6 -4
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +44 -22
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +6 -5
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +79 -62
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -6
- package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.js +3 -3
- package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
- package/dist/react/runtimes/assistant-transport/useLatestRef.js +2 -2
- package/dist/react/runtimes/assistant-transport/useLatestRef.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +23 -18
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +5 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +29 -0
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +159 -0
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -0
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts +1 -9
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +16 -153
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -1
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +14 -6
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/types/MessagePartComponentTypes.d.ts +7 -3
- package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
- package/dist/react/types/scopes/tools.d.ts +2 -0
- package/dist/react/types/scopes/tools.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.d.ts +2 -2
- package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.js +1 -1
- package/dist/runtime/api/message-part-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +9 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +5 -0
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -2
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +2 -2
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +14 -2
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +15 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/utils/auto-status.d.ts +1 -1
- package/dist/runtime/utils/auto-status.d.ts.map +1 -1
- package/dist/runtime/utils/auto-status.js +6 -2
- package/dist/runtime/utils/auto-status.js.map +1 -1
- package/dist/runtime/utils/external-message-conversion.d.ts +3 -2
- package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
- package/dist/runtime/utils/external-message-conversion.js +4 -3
- package/dist/runtime/utils/external-message-conversion.js.map +1 -1
- package/dist/runtime/utils/resolveToolApprovalResponse.d.ts +6 -3
- package/dist/runtime/utils/resolveToolApprovalResponse.d.ts.map +1 -1
- package/dist/runtime/utils/resolveToolApprovalResponse.js +12 -3
- package/dist/runtime/utils/resolveToolApprovalResponse.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.d.ts +2 -10
- package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtime/utils/tool-call-tree.d.ts +39 -0
- package/dist/runtime/utils/tool-call-tree.d.ts.map +1 -0
- package/dist/runtime/utils/tool-call-tree.js +85 -0
- package/dist/runtime/utils/tool-call-tree.js.map +1 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts +29 -2
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +34 -28
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +1 -1
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +3 -1
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.js +30 -13
- package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/title.d.ts +9 -0
- package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -0
- package/dist/runtimes/remote-thread-list/title.js +6 -0
- package/dist/runtimes/remote-thread-list/title.js.map +1 -0
- package/dist/runtimes/remote-thread-list/types.d.ts +6 -5
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +31 -8
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +97 -60
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/store/clients/external-thread.d.ts +1 -1
- package/dist/store/clients/external-thread.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +11 -4
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/clients/single-thread-list.js +1 -1
- package/dist/store/clients/suggestions.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +18 -6
- package/dist/store/clients/suggestions.js.map +1 -1
- package/dist/store/clients/thread-message-client.js +1 -1
- package/dist/store/clients/thread-selection-events.js +1 -1
- package/dist/store/index.d.ts +2 -1
- package/dist/store/index.js +2 -1
- package/dist/store/internal.d.ts +2 -2
- package/dist/store/internal.js +2 -2
- package/dist/store/primitive-predicates.d.ts +4 -1
- package/dist/store/primitive-predicates.d.ts.map +1 -1
- package/dist/store/primitive-predicates.js +9 -1
- package/dist/store/primitive-predicates.js.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
- package/dist/store/runtime-clients/message-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +124 -105
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
- package/dist/store/runtime-clients/useSubscribable.d.ts +3 -1
- package/dist/store/runtime-clients/useSubscribable.d.ts.map +1 -1
- package/dist/store/runtime-clients/useSubscribable.js +1 -1
- package/dist/store/runtime-clients/useSubscribable.js.map +1 -1
- package/dist/store/scopes/part.d.ts +6 -3
- package/dist/store/scopes/part.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +8 -1
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +24 -16
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/types/message.d.ts +40 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js +7 -1
- package/dist/types/message.js.map +1 -1
- package/package.json +12 -17
- package/src/index.ts +6 -1
- package/src/internal.ts +8 -0
- package/src/model-context/frame/provider.test.ts +217 -1
- package/src/model-context/frame/provider.ts +111 -23
- package/src/model-context/tool-call-text.test.ts +67 -0
- package/src/model-context/tool-call-text.ts +67 -0
- package/src/react/AssistantRuntimeProvider.test.tsx +66 -2
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +1 -3
- package/src/react/client/RemoteThreadList.background.test.ts +384 -0
- package/src/react/client/RemoteThreadList.concurrent.test.tsx +274 -0
- package/src/react/client/RemoteThreadList.test.ts +273 -1
- package/src/react/client/RemoteThreadList.ts +293 -83
- package/src/react/client/Tools.ts +7 -1
- package/src/react/index.ts +4 -0
- package/src/react/model-context/modelContextHooks.test.tsx +131 -0
- package/src/react/model-context/toolbox.test.ts +9 -9
- package/src/react/model-context/toolbox.ts +9 -36
- package/src/react/model-context/useAssistantContext.ts +4 -2
- package/src/react/model-context/useAuiToolOverrides.ts +4 -2
- package/src/react/model-context/useInlineRender.test.tsx +48 -0
- package/src/react/model-context/useInlineRender.ts +14 -10
- package/src/react/primitive-hooks/useMessageError.ts +2 -19
- package/src/react/primitive-hooks/useThreadListLoadMore.ts +2 -3
- package/src/react/primitives/composer/ComposerAttachments.test.tsx +76 -0
- package/src/react/primitives/composer/ComposerAttachments.tsx +9 -4
- package/src/react/primitives/composer/ComposerQueue.test.tsx +72 -0
- package/src/react/primitives/composer/ComposerQueue.tsx +7 -4
- package/src/react/primitives/message/MessageAttachments.test.tsx +107 -37
- package/src/react/primitives/message/MessageAttachments.tsx +10 -7
- package/src/react/primitives/message/MessageGroupedParts.tsx +2 -2
- package/src/react/primitives/message/MessageParts.tsx +3 -3
- package/src/react/primitives/threadList/ThreadListItems.test.tsx +99 -0
- package/src/react/primitives/threadList/ThreadListItems.tsx +8 -5
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +94 -1
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +72 -31
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +169 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +114 -68
- package/src/react/runtimes/RemoteThreadResource.ts +1 -4
- package/src/react/runtimes/assistant-transport/transport-scheduling.test.ts +51 -2
- package/src/react/runtimes/assistant-transport/useLatestRef.ts +2 -2
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +45 -0
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +65 -1
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +42 -35
- package/src/react/runtimes/cloud/auiV0.ts +5 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +88 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +236 -0
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +90 -1
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +33 -238
- package/src/react/runtimes/external-message-converter.ts +1 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.concurrent.test.tsx +216 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +24 -6
- package/src/react/types/MessagePartComponentTypes.ts +7 -3
- package/src/react/types/scopes/tools.ts +2 -0
- package/src/runtime/api/message-part-runtime.ts +3 -3
- package/src/runtime/api/thread-list-runtime.ts +25 -1
- package/src/runtime/base/base-thread-runtime-core.test.ts +94 -3
- package/src/runtime/base/base-thread-runtime-core.ts +10 -4
- package/src/runtime/interfaces/thread-list-runtime-core.ts +20 -1
- package/src/runtime/interfaces/thread-runtime-core.ts +17 -1
- package/src/runtime/utils/auto-status.test.ts +57 -0
- package/src/runtime/utils/auto-status.ts +13 -1
- package/src/runtime/utils/external-message-conversion.test.ts +97 -0
- package/src/runtime/utils/external-message-conversion.ts +17 -1
- package/src/runtime/utils/resolveToolApprovalResponse.test.ts +73 -0
- package/src/runtime/utils/resolveToolApprovalResponse.ts +27 -6
- package/src/runtime/utils/thread-message-like.ts +2 -10
- package/src/runtime/utils/tool-call-tree.test.ts +191 -0
- package/src/runtime/utils/tool-call-tree.ts +97 -0
- package/src/runtimes/external-store/external-store-adapter.ts +38 -2
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +56 -33
- package/src/runtimes/inert/InertThreadRuntimeCore.ts +1 -1
- package/src/runtimes/local/local-thread-runtime-core.test.ts +29 -3
- package/src/runtimes/local/local-thread-runtime-core.ts +5 -1
- package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +112 -0
- package/src/runtimes/remote-thread-list/remote-thread-state.ts +55 -16
- package/src/runtimes/remote-thread-list/title.ts +3 -0
- package/src/runtimes/remote-thread-list/types.ts +6 -5
- package/src/runtimes/tool-invocations/EDGE_CASES.md +110 -5
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +750 -136
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +144 -91
- package/src/store/clients/external-thread.ts +14 -8
- package/src/store/clients/suggestions.test.ts +155 -1
- package/src/store/clients/suggestions.ts +42 -34
- package/src/store/index.ts +4 -0
- package/src/store/internal.ts +2 -0
- package/src/store/primitive-predicates.ts +35 -0
- package/src/store/runtime-clients/thread-list-runtime-client.ts +16 -2
- package/src/store/runtime-clients/useSubscribable.ts +8 -2
- package/src/store/scopes/part.ts +6 -3
- package/src/subscribable/subscribable.test.ts +50 -0
- package/src/subscribable/subscribable.ts +33 -21
- package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +111 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +78 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-reload.test.ts +52 -0
- package/src/tests/external-store-optimistic-id.test.ts +79 -0
- package/src/tests/external-store-repository-instance.test.ts +166 -0
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +22 -16
- package/src/tests/external-thread-attachments.test.tsx +85 -1
- package/src/tests/remote-thread-list-adapter-switch.test.ts +66 -1
- package/src/tests/thread-switch-events.test.tsx +397 -2
- package/src/types/message.ts +50 -2
|
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
import { resource, withKey } from "@assistant-ui/tap";
|
|
3
3
|
import type { ClientOutput } from "@assistant-ui/store";
|
|
4
4
|
import { useClientLookup } from "@assistant-ui/store/client";
|
|
5
|
+
import { shallowEqual } from "@assistant-ui/store/internal";
|
|
5
6
|
import type { SuggestionsState } from "../scopes/suggestions";
|
|
6
7
|
import type { SuggestionState } from "../scopes/suggestion";
|
|
7
8
|
import type { ThreadSuggestion } from "../../runtime/interfaces/thread-runtime-core";
|
|
@@ -10,6 +11,31 @@ export type SuggestionConfig =
|
|
|
10
11
|
| string
|
|
11
12
|
| { title: string; label: string; prompt: string };
|
|
12
13
|
|
|
14
|
+
const useStableSuggestionsState = (
|
|
15
|
+
next: SuggestionsState,
|
|
16
|
+
): SuggestionsState => {
|
|
17
|
+
const cell = useMemo(() => ({}) as { state?: SuggestionsState }, []);
|
|
18
|
+
const previous = cell.state;
|
|
19
|
+
|
|
20
|
+
const suggestions = next.suggestions.map((suggestion, index) => {
|
|
21
|
+
const previousSuggestion = previous?.suggestions[index];
|
|
22
|
+
return previousSuggestion && shallowEqual(previousSuggestion, suggestion)
|
|
23
|
+
? previousSuggestion
|
|
24
|
+
: suggestion;
|
|
25
|
+
});
|
|
26
|
+
const state =
|
|
27
|
+
previous &&
|
|
28
|
+
previous.suggestions.length === suggestions.length &&
|
|
29
|
+
suggestions.every(
|
|
30
|
+
(suggestion, index) => suggestion === previous.suggestions[index],
|
|
31
|
+
)
|
|
32
|
+
? previous
|
|
33
|
+
: { suggestions };
|
|
34
|
+
|
|
35
|
+
cell.state = state;
|
|
36
|
+
return state;
|
|
37
|
+
};
|
|
38
|
+
|
|
13
39
|
const useSuggestionClient = (
|
|
14
40
|
state: SuggestionState,
|
|
15
41
|
): ClientOutput<"suggestion"> => {
|
|
@@ -21,8 +47,9 @@ const useSuggestionClient = (
|
|
|
21
47
|
const SuggestionClient = resource(useSuggestionClient);
|
|
22
48
|
|
|
23
49
|
const useSuggestionsClient = (
|
|
24
|
-
|
|
50
|
+
nextState: SuggestionsState,
|
|
25
51
|
): ClientOutput<"suggestions"> => {
|
|
52
|
+
const state = useStableSuggestionsState(nextState);
|
|
26
53
|
const suggestionClients = useClientLookup(
|
|
27
54
|
state.suggestions.map((suggestion, index) =>
|
|
28
55
|
withKey(index, SuggestionClient(suggestion), [suggestion]),
|
|
@@ -40,27 +67,13 @@ const useSuggestionsClient = (
|
|
|
40
67
|
const useStaticSuggestions = (
|
|
41
68
|
suggestions?: SuggestionConfig[],
|
|
42
69
|
): ClientOutput<"suggestions"> => {
|
|
43
|
-
|
|
44
|
-
() =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
prompt: s,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
title: s.title,
|
|
55
|
-
label: s.label,
|
|
56
|
-
prompt: s.prompt,
|
|
57
|
-
};
|
|
58
|
-
}),
|
|
59
|
-
}),
|
|
60
|
-
[suggestions],
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
return useSuggestionsClient(state);
|
|
70
|
+
return useSuggestionsClient({
|
|
71
|
+
suggestions: (suggestions ?? []).map((s) =>
|
|
72
|
+
typeof s === "string"
|
|
73
|
+
? { title: s, label: "", prompt: s }
|
|
74
|
+
: { title: s.title, label: s.label, prompt: s.prompt },
|
|
75
|
+
),
|
|
76
|
+
});
|
|
64
77
|
};
|
|
65
78
|
|
|
66
79
|
export const Suggestions = resource(useStaticSuggestions);
|
|
@@ -68,18 +81,13 @@ export const Suggestions = resource(useStaticSuggestions);
|
|
|
68
81
|
const useThreadSuggestions = (
|
|
69
82
|
suggestions: readonly ThreadSuggestion[],
|
|
70
83
|
): ClientOutput<"suggestions"> => {
|
|
71
|
-
|
|
72
|
-
() => ({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}),
|
|
79
|
-
[suggestions],
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
return useSuggestionsClient(state);
|
|
84
|
+
return useSuggestionsClient({
|
|
85
|
+
suggestions: suggestions.map((s) => ({
|
|
86
|
+
title: s.title ?? s.prompt,
|
|
87
|
+
label: s.label ?? "",
|
|
88
|
+
prompt: s.prompt,
|
|
89
|
+
})),
|
|
90
|
+
});
|
|
83
91
|
};
|
|
84
92
|
|
|
85
93
|
export const ThreadSuggestions = resource(useThreadSuggestions);
|
package/src/store/index.ts
CHANGED
|
@@ -108,6 +108,10 @@ export {
|
|
|
108
108
|
createRuntimeExtrasBrand,
|
|
109
109
|
type RuntimeExtrasBrand,
|
|
110
110
|
} from "../runtime/utils/runtime-extras-brand";
|
|
111
|
+
export {
|
|
112
|
+
resolveToolCallText,
|
|
113
|
+
type ToolCallText,
|
|
114
|
+
} from "../model-context/tool-call-text";
|
|
111
115
|
export { defineToolkit } from "../react/model-context/define-toolkit";
|
|
112
116
|
export {
|
|
113
117
|
defineMcpToolkit,
|
package/src/store/internal.ts
CHANGED
|
@@ -15,7 +15,9 @@ export {
|
|
|
15
15
|
composerCancelDisabled,
|
|
16
16
|
composerInputDisabled,
|
|
17
17
|
composerSendDisabled,
|
|
18
|
+
messageErrorText,
|
|
18
19
|
suggestionTriggerDisabled,
|
|
20
|
+
threadListLoadMoreDisabled,
|
|
19
21
|
} from "./primitive-predicates";
|
|
20
22
|
export { isDevelopment } from "./env";
|
|
21
23
|
export { useThreadSelectionEvents } from "./clients/thread-selection-events";
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ReadonlyJSONArray,
|
|
3
|
+
ReadonlyJSONObject,
|
|
4
|
+
} from "assistant-stream/utils";
|
|
1
5
|
import type { AssistantState } from "@assistant-ui/store";
|
|
2
6
|
|
|
3
7
|
/**
|
|
@@ -41,3 +45,34 @@ export const suggestionTriggerDisabled = (
|
|
|
41
45
|
): boolean =>
|
|
42
46
|
s.thread.isDisabled ||
|
|
43
47
|
(send && s.thread.isRunning && !s.thread.capabilities.queue);
|
|
48
|
+
|
|
49
|
+
export const messageErrorText = (
|
|
50
|
+
s: AssistantState,
|
|
51
|
+
):
|
|
52
|
+
| string
|
|
53
|
+
| number
|
|
54
|
+
| boolean
|
|
55
|
+
| ReadonlyJSONObject
|
|
56
|
+
| ReadonlyJSONArray
|
|
57
|
+
| undefined => {
|
|
58
|
+
if (
|
|
59
|
+
s.message.status?.type !== "incomplete" ||
|
|
60
|
+
s.message.status.reason !== "error"
|
|
61
|
+
) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const error = s.message.status.error;
|
|
65
|
+
if (typeof error === "string") return error;
|
|
66
|
+
if (
|
|
67
|
+
typeof error === "object" &&
|
|
68
|
+
error !== null &&
|
|
69
|
+
"message" in error &&
|
|
70
|
+
typeof error.message === "string"
|
|
71
|
+
) {
|
|
72
|
+
return error.message;
|
|
73
|
+
}
|
|
74
|
+
return error ?? "An error occurred";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const threadListLoadMoreDisabled = (s: AssistantState): boolean =>
|
|
78
|
+
!s.threads.hasMore || s.threads.isLoading || s.threads.isLoadingMore;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
2
|
import { useResource, withKey, resource } from "@assistant-ui/tap";
|
|
3
3
|
import type { ClientOutput } from "@assistant-ui/store";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useAssistantEmit,
|
|
6
|
+
useClientLookup,
|
|
7
|
+
useClientResource,
|
|
8
|
+
} from "@assistant-ui/store/client";
|
|
5
9
|
import { useThreadSelectionEvents } from "../clients/thread-selection-events";
|
|
6
10
|
import type { ThreadListRuntime } from "../../runtime/api/thread-list-runtime";
|
|
7
11
|
import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
|
|
@@ -44,6 +48,16 @@ const useThreadListClient = ({
|
|
|
44
48
|
const runtimeState = useSubscribable(runtime);
|
|
45
49
|
useThreadSelectionEvents(runtimeState.mainThreadId);
|
|
46
50
|
|
|
51
|
+
const emit = useAssistantEmit();
|
|
52
|
+
useEffect(
|
|
53
|
+
() =>
|
|
54
|
+
runtime.unstable_subscribeThreadEvents(({ threadId, type }) => {
|
|
55
|
+
if (threadId === runtime.getState().mainThreadId) return;
|
|
56
|
+
emit(`thread.${type}`, { threadId });
|
|
57
|
+
}),
|
|
58
|
+
[runtime, emit],
|
|
59
|
+
);
|
|
60
|
+
|
|
47
61
|
const main = useClientResource(
|
|
48
62
|
ThreadClient({
|
|
49
63
|
runtime: runtime.main,
|
|
@@ -2,7 +2,13 @@ import { useSyncExternalStore } from "react";
|
|
|
2
2
|
import type { SubscribableWithState } from "../../subscribable/subscribable";
|
|
3
3
|
|
|
4
4
|
export const useSubscribable = <T>(
|
|
5
|
-
subscribable: Omit<SubscribableWithState<T, any>, "path"
|
|
5
|
+
subscribable: Omit<SubscribableWithState<T, any>, "path"> & {
|
|
6
|
+
getServerSnapshot?: () => T;
|
|
7
|
+
},
|
|
6
8
|
) => {
|
|
7
|
-
return useSyncExternalStore(
|
|
9
|
+
return useSyncExternalStore(
|
|
10
|
+
subscribable.subscribe,
|
|
11
|
+
subscribable.getState,
|
|
12
|
+
subscribable.getServerSnapshot,
|
|
13
|
+
);
|
|
8
14
|
};
|
package/src/store/scopes/part.ts
CHANGED
|
@@ -29,10 +29,13 @@ export type PartMethods = {
|
|
|
29
29
|
resumeToolCall(payload: unknown): void;
|
|
30
30
|
/**
|
|
31
31
|
* Respond to a server-side tool approval gate. The approval id is read from
|
|
32
|
-
* the part. Accepts a boolean decision
|
|
33
|
-
* options.
|
|
32
|
+
* the part. Accepts a boolean decision, the id of one of the approval's
|
|
33
|
+
* options, or a free-form answer when the request asks for one.
|
|
34
|
+
*
|
|
35
|
+
* Resolves once the runtime has accepted the response, and rejects when it
|
|
36
|
+
* could not be recorded, so a renderer can leave its controls retryable.
|
|
34
37
|
*/
|
|
35
|
-
respondToToolApproval(response: ToolApprovalResponse): void
|
|
38
|
+
respondToToolApproval(response: ToolApprovalResponse): Promise<void>;
|
|
36
39
|
__internal_getRuntime?(): MessagePartRuntime;
|
|
37
40
|
};
|
|
38
41
|
|
|
@@ -32,6 +32,30 @@ const createBinding = (initialState: TestState) => {
|
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// Mirrors runtime bindings such as `getThreadListState`, which assemble a new
|
|
36
|
+
// object on every read while the underlying values keep their identity.
|
|
37
|
+
const createRebuildingBinding = (initialState: TestState) => {
|
|
38
|
+
let state = initialState;
|
|
39
|
+
const subscribers = new Set<() => void>();
|
|
40
|
+
|
|
41
|
+
const binding: SubscribableWithState<TestState, null> = {
|
|
42
|
+
path: null,
|
|
43
|
+
getState: () => ({ ...state }),
|
|
44
|
+
subscribe: (callback) => {
|
|
45
|
+
subscribers.add(callback);
|
|
46
|
+
return () => subscribers.delete(callback);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
binding,
|
|
52
|
+
update(nextState: TestState) {
|
|
53
|
+
state = nextState;
|
|
54
|
+
for (const callback of subscribers) callback();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
35
59
|
describe("ShallowMemoizeSubject", () => {
|
|
36
60
|
it("notifies subscribers when a state key is removed", () => {
|
|
37
61
|
const source = createBinding({
|
|
@@ -131,4 +155,30 @@ describe("LazyMemoizeSubject", () => {
|
|
|
131
155
|
expect(subject.getState()).toEqual({ status: "ready" });
|
|
132
156
|
reconnect();
|
|
133
157
|
});
|
|
158
|
+
|
|
159
|
+
it("keeps one reference while unconnected and the state is unchanged", () => {
|
|
160
|
+
// A consumer that reads without subscribing (React's useSyncExternalStore
|
|
161
|
+
// calls getSnapshot outside the subscription) must not see a new object on
|
|
162
|
+
// every call: React compares snapshots with Object.is, so a fresh reference
|
|
163
|
+
// reads as a change and re-renders forever.
|
|
164
|
+
const source = createRebuildingBinding({ status: "ready" });
|
|
165
|
+
const subject = new LazyMemoizeSubject(source.binding);
|
|
166
|
+
|
|
167
|
+
const first = subject.getState();
|
|
168
|
+
|
|
169
|
+
expect(subject.getState()).toBe(first);
|
|
170
|
+
expect(subject.getState()).toBe(first);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("returns a new reference once the unchanged state actually changes", () => {
|
|
174
|
+
const source = createRebuildingBinding({ status: "ready" });
|
|
175
|
+
const subject = new LazyMemoizeSubject(source.binding);
|
|
176
|
+
|
|
177
|
+
const first = subject.getState();
|
|
178
|
+
source.update({ status: "done" });
|
|
179
|
+
|
|
180
|
+
const second = subject.getState();
|
|
181
|
+
expect(second).not.toBe(first);
|
|
182
|
+
expect(second).toEqual({ status: "done" });
|
|
183
|
+
});
|
|
134
184
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shallowEqual } from "@assistant-ui/store/client";
|
|
1
2
|
import type { Unsubscribe } from "../types/unsubscribe";
|
|
2
3
|
import { notifyEventListeners } from "../utils/notify-event-listeners";
|
|
3
4
|
|
|
@@ -57,25 +58,10 @@ export const notifySubscribers = <TArgs extends unknown[]>(
|
|
|
57
58
|
}
|
|
58
59
|
};
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
if (objA === undefined && objB === undefined) return true;
|
|
65
|
-
if (objA === undefined) return false;
|
|
66
|
-
if (objB === undefined) return false;
|
|
67
|
-
|
|
68
|
-
const keysA = Object.keys(objA);
|
|
69
|
-
if (keysA.length !== Object.keys(objB).length) return false;
|
|
70
|
-
|
|
71
|
-
for (const key of keysA) {
|
|
72
|
-
const valueA = objA[key as keyof T];
|
|
73
|
-
const valueB = objB[key as keyof T];
|
|
74
|
-
if (!Object.is(valueA, valueB)) return false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
61
|
+
const shallowEqualOrUndefined = <T extends object>(
|
|
62
|
+
a: T | undefined,
|
|
63
|
+
b: T | undefined,
|
|
64
|
+
) => (a === undefined || b === undefined ? a === b : shallowEqual(a, b));
|
|
79
65
|
|
|
80
66
|
export class BaseSubscribable {
|
|
81
67
|
private _subscribers = new Set<() => void>();
|
|
@@ -99,6 +85,32 @@ export class BaseSubscribable {
|
|
|
99
85
|
}
|
|
100
86
|
}
|
|
101
87
|
|
|
88
|
+
export class WritableSubscribable<TState> extends BaseSubscribable {
|
|
89
|
+
private _state: TState;
|
|
90
|
+
|
|
91
|
+
constructor(state: TState) {
|
|
92
|
+
super();
|
|
93
|
+
this._state = state;
|
|
94
|
+
this.subscribe = this.subscribe.bind(this);
|
|
95
|
+
this.getState = this.getState.bind(this);
|
|
96
|
+
// Hydration has to agree with what the server rendered, so the server
|
|
97
|
+
// snapshot stays at the creation-time state rather than following writes.
|
|
98
|
+
this.getServerSnapshot = () => state;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public getState(): TState {
|
|
102
|
+
return this._state;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public getServerSnapshot: () => TState;
|
|
106
|
+
|
|
107
|
+
public setState(state: TState): void {
|
|
108
|
+
if (Object.is(state, this._state)) return;
|
|
109
|
+
this._state = state;
|
|
110
|
+
this._notifySubscribers();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
// lazy connect/disconnect: only opens upstream subscription while it has subscribers
|
|
103
115
|
export abstract class BaseSubject {
|
|
104
116
|
private _subscriptions = new Set<(payload?: unknown) => void>();
|
|
@@ -168,7 +180,7 @@ export class ShallowMemoizeSubject<TState extends object, TPath>
|
|
|
168
180
|
private _syncState() {
|
|
169
181
|
const state = this.binding.getState();
|
|
170
182
|
if (state === SKIP_UPDATE) return false;
|
|
171
|
-
if (
|
|
183
|
+
if (shallowEqualOrUndefined(state, this._previousState)) return false;
|
|
172
184
|
this._previousState = state;
|
|
173
185
|
return true;
|
|
174
186
|
}
|
|
@@ -209,7 +221,7 @@ export class LazyMemoizeSubject<TState extends object, TPath>
|
|
|
209
221
|
if (
|
|
210
222
|
newState !== SKIP_UPDATE &&
|
|
211
223
|
(this._previousState === undefined ||
|
|
212
|
-
!
|
|
224
|
+
!shallowEqualOrUndefined(newState, this._previousState))
|
|
213
225
|
) {
|
|
214
226
|
this._previousState = newState;
|
|
215
227
|
}
|
|
@@ -36,6 +36,117 @@ describe("RemoteThreadListThreadListRuntimeCore load race", () => {
|
|
|
36
36
|
expect(core.threadIds[0]).toBe(newId);
|
|
37
37
|
expect(core.threadIds).toContain("t1");
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
it("keeps one slot when the list reports the thread just initialized", async () => {
|
|
41
|
+
const listDeferred = deferred<ListResult>();
|
|
42
|
+
const adapter = makeAdapter({
|
|
43
|
+
list: vi.fn(() => listDeferred.promise),
|
|
44
|
+
initialize: vi.fn(async () => ({
|
|
45
|
+
remoteId: "remote-1",
|
|
46
|
+
externalId: "remote-1",
|
|
47
|
+
})),
|
|
48
|
+
});
|
|
49
|
+
const core = createCore(adapter);
|
|
50
|
+
|
|
51
|
+
const loadPromise = core.getLoadThreadsPromise();
|
|
52
|
+
await core.switchToNewThread();
|
|
53
|
+
const localId = core.newThreadId!;
|
|
54
|
+
await core.initialize(localId);
|
|
55
|
+
|
|
56
|
+
listDeferred.resolve({
|
|
57
|
+
threads: [
|
|
58
|
+
{ status: "regular", remoteId: "remote-1", externalId: "remote-1" },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
await loadPromise;
|
|
62
|
+
|
|
63
|
+
expect(Object.keys(core.threadItems)).toEqual([localId]);
|
|
64
|
+
expect(core.threadIds).toEqual([localId]);
|
|
65
|
+
expect(core.getItemById("remote-1")?.id).toBe(localId);
|
|
66
|
+
expect(core.mainThreadId).toBe(localId);
|
|
67
|
+
|
|
68
|
+
await core.delete("remote-1");
|
|
69
|
+
|
|
70
|
+
expect(core.getItemById(localId)).toBeUndefined();
|
|
71
|
+
expect(core.getItemById("remote-1")).toBeUndefined();
|
|
72
|
+
expect(core.threadIds).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("collapses a slot the list minted while initialize was in flight", async () => {
|
|
76
|
+
const listDeferred = deferred<ListResult>();
|
|
77
|
+
const initializeDeferred = deferred<{
|
|
78
|
+
remoteId: string;
|
|
79
|
+
externalId: string;
|
|
80
|
+
}>();
|
|
81
|
+
const adapter = makeAdapter({
|
|
82
|
+
list: vi.fn(() => listDeferred.promise),
|
|
83
|
+
initialize: vi.fn(() => initializeDeferred.promise),
|
|
84
|
+
});
|
|
85
|
+
const core = createCore(adapter);
|
|
86
|
+
|
|
87
|
+
const loadPromise = core.getLoadThreadsPromise();
|
|
88
|
+
await core.switchToNewThread();
|
|
89
|
+
const localId = core.newThreadId!;
|
|
90
|
+
const initializePromise = core.initialize(localId);
|
|
91
|
+
|
|
92
|
+
listDeferred.resolve({
|
|
93
|
+
threads: [
|
|
94
|
+
{ status: "regular", remoteId: "remote-1", externalId: "remote-1" },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
await loadPromise;
|
|
98
|
+
|
|
99
|
+
initializeDeferred.resolve({
|
|
100
|
+
remoteId: "remote-1",
|
|
101
|
+
externalId: "remote-1",
|
|
102
|
+
});
|
|
103
|
+
await initializePromise;
|
|
104
|
+
|
|
105
|
+
expect(Object.keys(core.threadItems)).toEqual([localId]);
|
|
106
|
+
expect(core.threadIds).toEqual([localId]);
|
|
107
|
+
expect(core.getItemById("remote-1")?.id).toBe(localId);
|
|
108
|
+
|
|
109
|
+
await core.delete("remote-1");
|
|
110
|
+
|
|
111
|
+
expect(core.getItemById(localId)).toBeUndefined();
|
|
112
|
+
expect(core.getItemById("remote-1")).toBeUndefined();
|
|
113
|
+
expect(core.threadIds).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not leave the collapsed slot in both lists when the race reported it archived", async () => {
|
|
117
|
+
const listDeferred = deferred<ListResult>();
|
|
118
|
+
const initializeDeferred = deferred<{
|
|
119
|
+
remoteId: string;
|
|
120
|
+
externalId: string;
|
|
121
|
+
}>();
|
|
122
|
+
const adapter = makeAdapter({
|
|
123
|
+
list: vi.fn(() => listDeferred.promise),
|
|
124
|
+
initialize: vi.fn(() => initializeDeferred.promise),
|
|
125
|
+
});
|
|
126
|
+
const core = createCore(adapter);
|
|
127
|
+
|
|
128
|
+
const loadPromise = core.getLoadThreadsPromise();
|
|
129
|
+
await core.switchToNewThread();
|
|
130
|
+
const localId = core.newThreadId!;
|
|
131
|
+
const initializePromise = core.initialize(localId);
|
|
132
|
+
|
|
133
|
+
listDeferred.resolve({
|
|
134
|
+
threads: [
|
|
135
|
+
{ status: "archived", remoteId: "remote-1", externalId: "remote-1" },
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
await loadPromise;
|
|
139
|
+
|
|
140
|
+
initializeDeferred.resolve({
|
|
141
|
+
remoteId: "remote-1",
|
|
142
|
+
externalId: "remote-1",
|
|
143
|
+
});
|
|
144
|
+
await initializePromise;
|
|
145
|
+
|
|
146
|
+
expect(Object.keys(core.threadItems)).toEqual([localId]);
|
|
147
|
+
expect(core.threadIds).toEqual([localId]);
|
|
148
|
+
expect(core.archivedThreadIds).toEqual([]);
|
|
149
|
+
});
|
|
39
150
|
});
|
|
40
151
|
|
|
41
152
|
describe("preserveMidLoadTransitions", () => {
|
|
@@ -195,6 +195,30 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
195
195
|
expect(core.hasMore).toBe(false);
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
it("clears loadingMore when the superseded request never settles", async () => {
|
|
199
|
+
const listFn = vi
|
|
200
|
+
.fn<ListFn>()
|
|
201
|
+
.mockResolvedValueOnce({
|
|
202
|
+
threads: [{ status: "regular", remoteId: "a", externalId: "a" }],
|
|
203
|
+
nextCursor: "c1",
|
|
204
|
+
})
|
|
205
|
+
.mockReturnValueOnce(new Promise<never>(() => {}))
|
|
206
|
+
.mockResolvedValueOnce({
|
|
207
|
+
threads: [
|
|
208
|
+
{ status: "regular", remoteId: "fresh", externalId: "fresh" },
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
const adapter = makeAdapter({ list: listFn });
|
|
212
|
+
const core = createCore(adapter);
|
|
213
|
+
|
|
214
|
+
await core.getLoadThreadsPromise();
|
|
215
|
+
void core.loadMore();
|
|
216
|
+
await core.reload();
|
|
217
|
+
|
|
218
|
+
expect(core.threadIds).toEqual(["fresh"]);
|
|
219
|
+
expect(core.isLoadingMore).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
198
222
|
it("releases the dedup handle after a rejection so retries can proceed", async () => {
|
|
199
223
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
200
224
|
const listFn = vi
|
|
@@ -241,6 +265,60 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
241
265
|
expect(core.threadIds).toEqual(["a", "b"]);
|
|
242
266
|
});
|
|
243
267
|
|
|
268
|
+
it("refreshes a thread repeated on a later page instead of dropping it", async () => {
|
|
269
|
+
const listFn = vi
|
|
270
|
+
.fn<ListFn>()
|
|
271
|
+
.mockResolvedValueOnce({
|
|
272
|
+
threads: [
|
|
273
|
+
{ status: "regular", remoteId: "a", externalId: "a", title: "old" },
|
|
274
|
+
],
|
|
275
|
+
nextCursor: "c1",
|
|
276
|
+
})
|
|
277
|
+
.mockResolvedValueOnce({
|
|
278
|
+
threads: [
|
|
279
|
+
{
|
|
280
|
+
status: "regular",
|
|
281
|
+
remoteId: "a",
|
|
282
|
+
externalId: "a",
|
|
283
|
+
title: "updated",
|
|
284
|
+
},
|
|
285
|
+
{ status: "regular", remoteId: "b", externalId: "b", title: "New" },
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
const core = createCore(makeAdapter({ list: listFn }));
|
|
289
|
+
|
|
290
|
+
await core.getLoadThreadsPromise();
|
|
291
|
+
await core.loadMore();
|
|
292
|
+
|
|
293
|
+
expect(core.threadIds).toEqual(["a", "b"]);
|
|
294
|
+
expect(core.getItemById("a")?.title).toBe("updated");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("moves a thread whose status flips between pages", async () => {
|
|
298
|
+
const listFn = vi
|
|
299
|
+
.fn<ListFn>()
|
|
300
|
+
.mockResolvedValueOnce({
|
|
301
|
+
threads: [
|
|
302
|
+
{ status: "regular", remoteId: "a", externalId: "a", title: "A" },
|
|
303
|
+
{ status: "regular", remoteId: "b", externalId: "b", title: "B" },
|
|
304
|
+
],
|
|
305
|
+
nextCursor: "c1",
|
|
306
|
+
})
|
|
307
|
+
.mockResolvedValueOnce({
|
|
308
|
+
threads: [
|
|
309
|
+
{ status: "archived", remoteId: "a", externalId: "a", title: "A" },
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
const core = createCore(makeAdapter({ list: listFn }));
|
|
313
|
+
|
|
314
|
+
await core.getLoadThreadsPromise();
|
|
315
|
+
await core.loadMore();
|
|
316
|
+
|
|
317
|
+
expect(core.threadIds).toEqual(["b"]);
|
|
318
|
+
expect(core.archivedThreadIds).toEqual(["a"]);
|
|
319
|
+
expect(core.getItemById("a")?.status).toBe("archived");
|
|
320
|
+
});
|
|
321
|
+
|
|
244
322
|
it("__internal_setOptions clears cursor and dedup handles on adapter swap, then refetches via the new adapter", async () => {
|
|
245
323
|
const firstAdapter = makeAdapter({
|
|
246
324
|
list: vi.fn<ListFn>(async () => ({
|
|
@@ -95,6 +95,58 @@ describe("RemoteThreadListThreadListRuntimeCore.reload", () => {
|
|
|
95
95
|
expect(core.threadIds).not.toContain("stale");
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
it("clears loading when the superseded request never settles", async () => {
|
|
99
|
+
const listFn = vi
|
|
100
|
+
.fn<() => Promise<RemoteThreadListResponse>>()
|
|
101
|
+
.mockReturnValueOnce(new Promise<never>(() => {}))
|
|
102
|
+
.mockResolvedValueOnce({
|
|
103
|
+
threads: [
|
|
104
|
+
{
|
|
105
|
+
status: "regular",
|
|
106
|
+
remoteId: "fresh",
|
|
107
|
+
externalId: "fresh",
|
|
108
|
+
title: "Fresh",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
const adapter = makeAdapter({ list: listFn });
|
|
113
|
+
const core = createCore(adapter);
|
|
114
|
+
|
|
115
|
+
void core.getLoadThreadsPromise();
|
|
116
|
+
await core.reload();
|
|
117
|
+
|
|
118
|
+
expect(core.threadIds).toEqual(["fresh"]);
|
|
119
|
+
expect(core.isLoading).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("clears loading once the fresh list arrives, before the superseded request settles", async () => {
|
|
123
|
+
const first = deferred<RemoteThreadListResponse>();
|
|
124
|
+
const listFn = vi
|
|
125
|
+
.fn<() => Promise<RemoteThreadListResponse>>()
|
|
126
|
+
.mockReturnValueOnce(first.promise)
|
|
127
|
+
.mockResolvedValueOnce({
|
|
128
|
+
threads: [
|
|
129
|
+
{
|
|
130
|
+
status: "regular",
|
|
131
|
+
remoteId: "fresh",
|
|
132
|
+
externalId: "fresh",
|
|
133
|
+
title: "Fresh",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
const core = createCore(makeAdapter({ list: listFn }));
|
|
138
|
+
|
|
139
|
+
void core.getLoadThreadsPromise();
|
|
140
|
+
await core.reload();
|
|
141
|
+
|
|
142
|
+
expect(core.threadIds).toEqual(["fresh"]);
|
|
143
|
+
expect(core.isLoading).toBe(false);
|
|
144
|
+
|
|
145
|
+
first.resolve({ threads: [] });
|
|
146
|
+
await Promise.resolve();
|
|
147
|
+
expect(core.isLoading).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
98
150
|
it("recovers after a failed initial load", async () => {
|
|
99
151
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
100
152
|
const listFn = vi
|