@assistant-ui/core 0.2.21 → 0.2.23
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/adapters/attachment.d.ts.map +1 -1
- package/dist/adapters/speech.d.ts.map +1 -1
- package/dist/adapters/speech.js +9 -2
- package/dist/adapters/speech.js.map +1 -1
- package/dist/adapters/thread-history.d.ts +8 -0
- package/dist/adapters/thread-history.d.ts.map +1 -1
- package/dist/adapters/voice.d.ts.map +1 -1
- package/dist/adapters/voice.js +13 -5
- package/dist/adapters/voice.js.map +1 -1
- package/dist/internal.d.ts +5 -2
- package/dist/internal.js +5 -2
- package/dist/model-context/frame/host.d.ts +1 -0
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +41 -21
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +1 -0
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/interactable-composer-metadata.js.map +1 -1
- package/dist/model-context/types.d.ts.map +1 -1
- package/dist/model-context/types.js +13 -4
- package/dist/model-context/types.js.map +1 -1
- package/dist/react/AssistantProvider.js.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +78 -50
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.js +1 -0
- package/dist/react/client/interactable-model-context.js.map +1 -1
- package/dist/react/interactables-legacy/useInteractableState.js +1 -0
- package/dist/react/interactables-legacy/useInteractableState.js.map +1 -1
- package/dist/react/model-context/useInteractable.js.map +1 -1
- package/dist/react/model-context/useInteractableState.js +1 -0
- package/dist/react/model-context/useInteractableState.js.map +1 -1
- package/dist/react/model-context/useInteractableVersions.js +1 -0
- package/dist/react/model-context/useInteractableVersions.js.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.js +1 -0
- package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js +14 -13
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarEdit.js +1 -0
- package/dist/react/primitive-hooks/useActionBarEdit.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarFeedback.js +1 -0
- package/dist/react/primitive-hooks/useActionBarFeedback.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarReload.js +1 -0
- package/dist/react/primitive-hooks/useActionBarReload.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarSpeak.js +1 -0
- package/dist/react/primitive-hooks/useActionBarSpeak.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +1 -0
- package/dist/react/primitive-hooks/useActionBarStopSpeaking.js.map +1 -1
- package/dist/react/primitive-hooks/useBranchPickerNext.js +1 -0
- package/dist/react/primitive-hooks/useBranchPickerNext.js.map +1 -1
- package/dist/react/primitive-hooks/useBranchPickerPrevious.js +1 -0
- package/dist/react/primitive-hooks/useBranchPickerPrevious.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useComposerAddAttachment.js +1 -0
- package/dist/react/primitive-hooks/useComposerAddAttachment.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerCancel.js +1 -0
- package/dist/react/primitive-hooks/useComposerCancel.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerDictate.js +1 -0
- package/dist/react/primitive-hooks/useComposerDictate.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useComposerSend.js +1 -0
- package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
- package/dist/react/primitive-hooks/useEditComposerCancel.js +1 -0
- package/dist/react/primitive-hooks/useEditComposerCancel.js.map +1 -1
- package/dist/react/primitive-hooks/useEditComposerSend.js +1 -0
- package/dist/react/primitive-hooks/useEditComposerSend.js.map +1 -1
- package/dist/react/primitive-hooks/useMessageBranching.js +1 -0
- package/dist/react/primitive-hooks/useMessageBranching.js.map +1 -1
- package/dist/react/primitive-hooks/useMessageReload.js +1 -0
- package/dist/react/primitive-hooks/useMessageReload.js.map +1 -1
- package/dist/react/primitive-hooks/useSuggestionTrigger.js +1 -0
- package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListItemArchive.js +1 -0
- package/dist/react/primitive-hooks/useThreadListItemArchive.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListItemDelete.js +1 -0
- package/dist/react/primitive-hooks/useThreadListItemDelete.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListItemTrigger.js +1 -0
- package/dist/react/primitive-hooks/useThreadListItemTrigger.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +1 -0
- package/dist/react/primitive-hooks/useThreadListItemUnarchive.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListLoadMore.js +1 -0
- package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadListNew.js +1 -0
- package/dist/react/primitive-hooks/useThreadListNew.js.map +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.d.ts.map +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
- 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.map +1 -1
- package/dist/react/primitives/generativeUI/GenerativeUI.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts +2 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +46 -36
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/primitives/part/PartMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadSuggestions.js.map +1 -1
- package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts.map +1 -1
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +18 -30
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
- package/dist/react/providers/PartByIndexProvider.d.ts.map +1 -1
- package/dist/react/providers/PartByIndexProvider.js +18 -30
- package/dist/react/providers/PartByIndexProvider.js.map +1 -1
- package/dist/react/providers/TextMessagePartProvider.js +1 -0
- package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +9 -2
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +55 -12
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +31 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +12 -2
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +3 -2
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +12 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +2 -1
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +75 -73
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +2 -2
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.js +9 -1
- package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
- package/dist/runtime/api/assistant-runtime.d.ts +1 -1
- package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
- package/dist/runtime/api/assistant-runtime.js +1 -1
- package/dist/runtime/api/assistant-runtime.js.map +1 -1
- package/dist/runtime/api/attachment-runtime.d.ts +1 -1
- package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
- package/dist/runtime/api/attachment-runtime.js +1 -1
- package/dist/runtime/api/attachment-runtime.js.map +1 -1
- package/dist/runtime/api/composer-runtime.d.ts +2 -2
- package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
- package/dist/runtime/api/composer-runtime.js +2 -2
- package/dist/runtime/api/composer-runtime.js.map +1 -1
- package/dist/runtime/api/message-part-runtime.d.ts +4 -4
- package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.js +3 -3
- package/dist/runtime/api/message-part-runtime.js.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +6 -6
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.js +4 -3
- package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +1 -1
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +41 -14
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +3 -2
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +2 -2
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.js +6 -4
- package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.js +3 -3
- package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +4 -2
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +4 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -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 +12 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/internal.d.ts +2 -1
- package/dist/runtimes/internal.js +2 -1
- package/dist/runtimes/local/local-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +49 -15
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +4 -4
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.js +22 -9
- package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +32 -27
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +3 -2
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
- package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
- package/dist/store/clients/no-op-composer-client.js +1 -0
- package/dist/store/clients/no-op-composer-client.js.map +1 -1
- package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +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 +1 -0
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +17 -15
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scopes/thread.d.ts +1 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +4 -4
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +14 -6
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts +2 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.js +5 -2
- package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
- package/dist/types/attachment.d.ts +2 -1
- package/dist/types/attachment.d.ts.map +1 -1
- package/dist/types/attachment.js +6 -0
- package/dist/types/attachment.js.map +1 -0
- package/dist/types/message.d.ts +1 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/data-url.d.ts +9 -0
- package/dist/utils/data-url.d.ts.map +1 -0
- package/dist/utils/data-url.js +14 -0
- package/dist/utils/data-url.js.map +1 -0
- package/dist/utils/notify-event-listeners.d.ts +6 -0
- package/dist/utils/notify-event-listeners.d.ts.map +1 -0
- package/dist/utils/notify-event-listeners.js +16 -0
- package/dist/utils/notify-event-listeners.js.map +1 -0
- package/package.json +10 -9
- package/src/adapters/speech.test.ts +88 -0
- package/src/adapters/speech.ts +17 -2
- package/src/adapters/thread-history.ts +8 -0
- package/src/adapters/voice.test.ts +130 -0
- package/src/adapters/voice.ts +26 -6
- package/src/internal.ts +6 -1
- package/src/model-context/frame/host.test.ts +147 -0
- package/src/model-context/frame/host.ts +56 -19
- package/src/model-context/frame/provider.test.ts +83 -0
- package/src/model-context/frame/provider.ts +1 -0
- package/src/model-context/types.ts +16 -6
- package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +82 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +108 -58
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +79 -0
- package/src/react/primitive-hooks/useActionBarCopy.ts +4 -3
- package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -3
- package/src/react/primitives/message/MessageParts.tsx +48 -35
- package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +19 -2
- package/src/react/providers/PartByIndexProvider.tsx +16 -2
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +18 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +1 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +68 -13
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +34 -0
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +69 -0
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +27 -6
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +16 -0
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +7 -2
- package/src/react/runtimes/cloud/auiV0.ts +14 -0
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +90 -88
- package/src/react/runtimes/external-message-converter.test.ts +346 -0
- package/src/react/runtimes/external-message-converter.test.tsx +21 -0
- package/src/react/runtimes/external-message-converter.ts +2 -2
- package/src/react/runtimes/useLocalRuntime.test.tsx +99 -0
- package/src/react/runtimes/useLocalRuntime.ts +13 -1
- package/src/runtime/api/assistant-runtime.ts +4 -1
- package/src/runtime/api/attachment-runtime.ts +8 -2
- package/src/runtime/api/composer-runtime.ts +9 -5
- package/src/runtime/api/message-part-runtime.ts +10 -3
- package/src/runtime/api/message-runtime.test.ts +81 -0
- package/src/runtime/api/message-runtime.ts +11 -8
- package/src/runtime/api/thread-list-item-runtime.ts +13 -3
- package/src/runtime/api/thread-list-runtime.ts +10 -2
- package/src/runtime/base/base-composer-runtime-core.ts +77 -31
- package/src/runtime/base/base-thread-runtime-core.ts +7 -2
- package/src/runtime/base/default-edit-composer-runtime-core.ts +18 -4
- package/src/runtime/base/default-thread-composer-runtime-core.ts +13 -3
- package/src/runtime/utils/message-repository.ts +19 -13
- package/src/runtime/utils/thread-message-like.ts +1 -0
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +4 -1
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +15 -0
- package/src/runtimes/internal.ts +3 -0
- package/src/runtimes/local/local-thread-runtime-core.test.ts +746 -1
- package/src/runtimes/local/local-thread-runtime-core.ts +89 -23
- package/src/runtimes/local/should-continue.test.ts +122 -0
- package/src/runtimes/remote-thread-list/optimistic-state.test.ts +63 -0
- package/src/runtimes/remote-thread-list/optimistic-state.ts +49 -17
- package/src/runtimes/remote-thread-list/types.ts +1 -1
- package/src/runtimes/tool-invocations/EDGE_CASES.md +13 -19
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +128 -8
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +42 -34
- package/src/store/clients/model-context-client.test.ts +21 -1
- package/src/store/runtime-clients/thread-runtime-client.ts +2 -1
- package/src/store/scopes/thread.ts +1 -0
- package/src/subscribable/subscribable.test.ts +58 -0
- package/src/subscribable/subscribable.ts +29 -11
- package/src/tests/MessageRepository.test.ts +12 -0
- package/src/tests/OptimisticState-list-race.test.ts +33 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-order.test.ts +223 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-position.test.ts +121 -0
- package/src/tests/auiV0Encode.test.ts +74 -0
- package/src/tests/base-composer-runtime-core-addAttachment.test.ts +44 -0
- package/src/tests/base-composer-runtime-core-send.test.ts +433 -5
- package/src/tests/base-composer-runtime-core.test.ts +422 -0
- package/src/tests/default-edit-composer-runtime-core.test.ts +43 -5
- package/src/tests/event-subscription-listener-errors.test.ts +205 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +6 -6
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +575 -0
- package/src/tests/external-store-thread-runtime-core.test.ts +55 -1
- package/src/tests/remote-thread-list-test-helpers.ts +14 -7
- package/src/tests/thread-message-like.test.ts +57 -0
- package/src/types/attachment.ts +5 -0
- package/src/types/message.ts +1 -0
- package/src/utils/data-url.test.ts +50 -0
- package/src/utils/data-url.ts +9 -0
- package/src/utils/notify-event-listeners.ts +30 -0
- package/src/tests/OptimisticState-delete-crash.test.ts +0 -208
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { BaseComposerRuntimeCore } from "../runtime/base/base-composer-runtime-core";
|
|
3
|
+
import type { AttachmentAdapter } from "../adapters/attachment";
|
|
4
|
+
import type { DictationAdapter } from "../adapters/speech";
|
|
5
|
+
import type { CreateAttachment, PendingAttachment } from "../types/attachment";
|
|
6
|
+
import type { AppendMessage } from "../types/message";
|
|
7
|
+
import type { SendOptions } from "../runtime/interfaces/composer-runtime-core";
|
|
8
|
+
|
|
9
|
+
class TestComposerCore extends BaseComposerRuntimeCore {
|
|
10
|
+
private _attachmentAdapter: AttachmentAdapter | undefined;
|
|
11
|
+
public sentMessages: Array<Omit<AppendMessage, "parentId" | "sourceId">> = [];
|
|
12
|
+
public sentOptions: Array<SendOptions | undefined> = [];
|
|
13
|
+
|
|
14
|
+
protected getAttachmentAdapter() {
|
|
15
|
+
return this._attachmentAdapter;
|
|
16
|
+
}
|
|
17
|
+
protected getDictationAdapter(): DictationAdapter | undefined {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setAttachmentAdapter(adapter: AttachmentAdapter | undefined) {
|
|
22
|
+
this._attachmentAdapter = adapter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get canCancel() {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get canSend() {
|
|
30
|
+
return !this.isEmpty;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected handleSend(
|
|
34
|
+
message: Omit<AppendMessage, "parentId" | "sourceId">,
|
|
35
|
+
options?: SendOptions,
|
|
36
|
+
) {
|
|
37
|
+
this.sentMessages.push(message);
|
|
38
|
+
this.sentOptions.push(options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected handleCancel() {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const makePendingAttachment = (
|
|
45
|
+
id: string,
|
|
46
|
+
name = "file.txt",
|
|
47
|
+
): PendingAttachment => ({
|
|
48
|
+
id,
|
|
49
|
+
type: "file",
|
|
50
|
+
name,
|
|
51
|
+
contentType: "text/plain",
|
|
52
|
+
file: new File(["content"], name),
|
|
53
|
+
status: { type: "requires-action", reason: "composer-send" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("BaseComposerRuntimeCore", () => {
|
|
57
|
+
let composer: TestComposerCore;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
composer = new TestComposerCore();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not notify when a set value is unchanged", () => {
|
|
64
|
+
composer.setText("same");
|
|
65
|
+
const listener = vi.fn();
|
|
66
|
+
composer.subscribe(listener);
|
|
67
|
+
|
|
68
|
+
composer.setText("same");
|
|
69
|
+
expect(listener).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("isEmpty returns true for whitespace-only text", () => {
|
|
73
|
+
composer.setText(" ");
|
|
74
|
+
expect(composer.isEmpty).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("reset clears text, attachments, role, runConfig, and quote", async () => {
|
|
78
|
+
composer.setText("hello");
|
|
79
|
+
composer.setRole("assistant");
|
|
80
|
+
composer.setRunConfig({ custom: { k: "v" } });
|
|
81
|
+
composer.setQuote({ text: "q", messageId: "m1" });
|
|
82
|
+
|
|
83
|
+
await composer.reset();
|
|
84
|
+
|
|
85
|
+
expect(composer.text).toBe("");
|
|
86
|
+
expect(composer.role).toBe("user");
|
|
87
|
+
expect(composer.runConfig).toEqual({});
|
|
88
|
+
expect(composer.quote).toBeUndefined();
|
|
89
|
+
expect(composer.attachments).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("reset does not notify when already in default state", async () => {
|
|
93
|
+
const listener = vi.fn();
|
|
94
|
+
composer.subscribe(listener);
|
|
95
|
+
|
|
96
|
+
await composer.reset();
|
|
97
|
+
expect(listener).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("send includes quote in metadata and clears it", async () => {
|
|
101
|
+
const quote = { text: "quoted", messageId: "m1" };
|
|
102
|
+
composer.setText("reply");
|
|
103
|
+
composer.setQuote(quote);
|
|
104
|
+
|
|
105
|
+
await composer.send();
|
|
106
|
+
|
|
107
|
+
const msg = composer.sentMessages[0]!;
|
|
108
|
+
expect(msg.metadata).toEqual({ custom: { quote } });
|
|
109
|
+
expect(composer.quote).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("forwards send options to handleSend", async () => {
|
|
113
|
+
composer.setText("a");
|
|
114
|
+
await composer.send();
|
|
115
|
+
composer.setText("b");
|
|
116
|
+
await composer.send({ startRun: true });
|
|
117
|
+
composer.setText("c");
|
|
118
|
+
await composer.send({ startRun: false });
|
|
119
|
+
|
|
120
|
+
expect(composer.sentOptions).toEqual([
|
|
121
|
+
undefined,
|
|
122
|
+
{ startRun: true },
|
|
123
|
+
{ startRun: false },
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("removeAttachment removes via adapter", async () => {
|
|
128
|
+
const pending = makePendingAttachment("att-1");
|
|
129
|
+
const adapter: AttachmentAdapter = {
|
|
130
|
+
accept: "*",
|
|
131
|
+
add: vi.fn().mockResolvedValue(pending),
|
|
132
|
+
remove: vi.fn().mockResolvedValue(undefined),
|
|
133
|
+
send: vi.fn(),
|
|
134
|
+
};
|
|
135
|
+
composer.setAttachmentAdapter(adapter);
|
|
136
|
+
|
|
137
|
+
await composer.addAttachment(new File([""], "f.txt"));
|
|
138
|
+
expect(composer.attachments).toHaveLength(1);
|
|
139
|
+
|
|
140
|
+
await composer.removeAttachment("att-1");
|
|
141
|
+
expect(composer.attachments).toHaveLength(0);
|
|
142
|
+
expect(adapter.remove).toHaveBeenCalledWith(pending);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("removeAttachment throws for unknown id", async () => {
|
|
146
|
+
const adapter: AttachmentAdapter = {
|
|
147
|
+
accept: "*",
|
|
148
|
+
add: vi.fn(),
|
|
149
|
+
remove: vi.fn(),
|
|
150
|
+
send: vi.fn(),
|
|
151
|
+
};
|
|
152
|
+
composer.setAttachmentAdapter(adapter);
|
|
153
|
+
|
|
154
|
+
await expect(composer.removeAttachment("nonexistent")).rejects.toThrow(
|
|
155
|
+
"Attachment not found",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("unstable_on unsubscribe stops notifications", async () => {
|
|
160
|
+
const callback = vi.fn();
|
|
161
|
+
const unsub = composer.unstable_on("send", callback);
|
|
162
|
+
unsub();
|
|
163
|
+
|
|
164
|
+
composer.setText("test");
|
|
165
|
+
await composer.send();
|
|
166
|
+
|
|
167
|
+
expect(callback).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("attachmentAccept returns adapter accept or default", () => {
|
|
171
|
+
expect(composer.attachmentAccept).toBe("*");
|
|
172
|
+
|
|
173
|
+
const adapter: AttachmentAdapter = {
|
|
174
|
+
accept: "image/*",
|
|
175
|
+
add: vi.fn(),
|
|
176
|
+
remove: vi.fn(),
|
|
177
|
+
send: vi.fn(),
|
|
178
|
+
};
|
|
179
|
+
composer.setAttachmentAdapter(adapter);
|
|
180
|
+
expect(composer.attachmentAccept).toBe("image/*");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("CreateAttachment (external source)", () => {
|
|
184
|
+
const makeCreateAttachment = (
|
|
185
|
+
overrides?: Partial<CreateAttachment>,
|
|
186
|
+
): CreateAttachment => ({
|
|
187
|
+
name: "external-doc.pdf",
|
|
188
|
+
content: [{ type: "text", text: "extracted content" }],
|
|
189
|
+
...overrides,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("addAttachment with CreateAttachment adds without adapter", async () => {
|
|
193
|
+
const att = makeCreateAttachment();
|
|
194
|
+
await composer.addAttachment(att);
|
|
195
|
+
|
|
196
|
+
expect(composer.attachments).toHaveLength(1);
|
|
197
|
+
expect(composer.attachments[0]!.name).toBe("external-doc.pdf");
|
|
198
|
+
expect(composer.attachments[0]!.status).toEqual({ type: "complete" });
|
|
199
|
+
expect(composer.attachments[0]!.type).toBe("document");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("addAttachment with CreateAttachment uses provided id and type", async () => {
|
|
203
|
+
const att = makeCreateAttachment({
|
|
204
|
+
id: "custom-id",
|
|
205
|
+
type: "image",
|
|
206
|
+
contentType: "image/png",
|
|
207
|
+
});
|
|
208
|
+
await composer.addAttachment(att);
|
|
209
|
+
|
|
210
|
+
expect(composer.attachments[0]!.id).toBe("custom-id");
|
|
211
|
+
expect(composer.attachments[0]!.type).toBe("image");
|
|
212
|
+
expect(composer.attachments[0]!.contentType).toBe("image/png");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("addAttachment with CreateAttachment generates id when not provided", async () => {
|
|
216
|
+
const att = makeCreateAttachment();
|
|
217
|
+
await composer.addAttachment(att);
|
|
218
|
+
|
|
219
|
+
expect(composer.attachments[0]!.id).toBeTruthy();
|
|
220
|
+
expect(typeof composer.attachments[0]!.id).toBe("string");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("addAttachment with CreateAttachment fires attachmentAdd event", async () => {
|
|
224
|
+
const callback = vi.fn();
|
|
225
|
+
composer.unstable_on("attachmentAdd", callback);
|
|
226
|
+
|
|
227
|
+
await composer.addAttachment(makeCreateAttachment());
|
|
228
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("removeAttachment on complete attachment does not require adapter", async () => {
|
|
232
|
+
await composer.addAttachment(makeCreateAttachment({ id: "ext-1" }));
|
|
233
|
+
expect(composer.attachments).toHaveLength(1);
|
|
234
|
+
|
|
235
|
+
await composer.removeAttachment("ext-1");
|
|
236
|
+
expect(composer.attachments).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("isEmpty returns false when external attachment present", async () => {
|
|
240
|
+
expect(composer.isEmpty).toBe(true);
|
|
241
|
+
|
|
242
|
+
await composer.addAttachment(makeCreateAttachment());
|
|
243
|
+
expect(composer.isEmpty).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("send with mixed pending + complete attachments", async () => {
|
|
247
|
+
const pending = makePendingAttachment("att-pending");
|
|
248
|
+
const completedFromAdapter = {
|
|
249
|
+
id: "att-pending",
|
|
250
|
+
type: "file" as const,
|
|
251
|
+
name: "file.txt",
|
|
252
|
+
contentType: "text/plain",
|
|
253
|
+
content: [{ type: "text" as const, text: "file content" }],
|
|
254
|
+
status: { type: "complete" as const },
|
|
255
|
+
};
|
|
256
|
+
const adapter: AttachmentAdapter = {
|
|
257
|
+
accept: "*",
|
|
258
|
+
add: vi.fn().mockResolvedValue(pending),
|
|
259
|
+
remove: vi.fn(),
|
|
260
|
+
send: vi.fn().mockResolvedValue(completedFromAdapter),
|
|
261
|
+
};
|
|
262
|
+
composer.setAttachmentAdapter(adapter);
|
|
263
|
+
|
|
264
|
+
await composer.addAttachment(new File(["data"], "file.txt"));
|
|
265
|
+
await composer.addAttachment(makeCreateAttachment({ id: "ext-1" }));
|
|
266
|
+
|
|
267
|
+
expect(composer.attachments).toHaveLength(2);
|
|
268
|
+
|
|
269
|
+
composer.setText("hello");
|
|
270
|
+
await composer.send();
|
|
271
|
+
|
|
272
|
+
expect(composer.sentMessages).toHaveLength(1);
|
|
273
|
+
const msg = composer.sentMessages[0]!;
|
|
274
|
+
expect(msg.attachments).toHaveLength(2);
|
|
275
|
+
expect(adapter.send).toHaveBeenCalledTimes(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("adapter.accept enforcement", () => {
|
|
279
|
+
const makeImageAdapter = (): AttachmentAdapter => ({
|
|
280
|
+
accept: "image/*",
|
|
281
|
+
add: vi.fn(),
|
|
282
|
+
remove: vi.fn(),
|
|
283
|
+
send: vi.fn(),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("rejects external attachment with contentType not matching adapter.accept", async () => {
|
|
287
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
288
|
+
const onError = vi.fn();
|
|
289
|
+
const onAdd = vi.fn();
|
|
290
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
291
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
292
|
+
|
|
293
|
+
await expect(
|
|
294
|
+
composer.addAttachment(
|
|
295
|
+
makeCreateAttachment({
|
|
296
|
+
name: "doc.pdf",
|
|
297
|
+
contentType: "application/pdf",
|
|
298
|
+
}),
|
|
299
|
+
),
|
|
300
|
+
).rejects.toThrow(/File type application\/pdf is not accepted/);
|
|
301
|
+
|
|
302
|
+
expect(composer.attachments).toHaveLength(0);
|
|
303
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(onError).toHaveBeenCalledWith(
|
|
305
|
+
expect.objectContaining({
|
|
306
|
+
reason: "not-accepted",
|
|
307
|
+
message: expect.stringContaining(
|
|
308
|
+
"File type application/pdf is not accepted",
|
|
309
|
+
),
|
|
310
|
+
error: expect.any(Error),
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
expect(onAdd).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("accepts external attachment with contentType matching adapter.accept", async () => {
|
|
317
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
318
|
+
const onError = vi.fn();
|
|
319
|
+
|
|
320
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
321
|
+
await composer.addAttachment(
|
|
322
|
+
makeCreateAttachment({
|
|
323
|
+
name: "photo.png",
|
|
324
|
+
contentType: "image/png",
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(composer.attachments).toHaveLength(1);
|
|
329
|
+
expect(onError).not.toHaveBeenCalled();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("accepts external attachment when only filename extension matches adapter.accept", async () => {
|
|
333
|
+
composer.setAttachmentAdapter({
|
|
334
|
+
accept: ".pdf",
|
|
335
|
+
add: vi.fn(),
|
|
336
|
+
remove: vi.fn(),
|
|
337
|
+
send: vi.fn(),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await composer.addAttachment(
|
|
341
|
+
makeCreateAttachment({ name: "report.pdf" }),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(composer.attachments).toHaveLength(1);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("rejects external attachment when contentType missing and extension does not match", async () => {
|
|
348
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
349
|
+
const onError = vi.fn();
|
|
350
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
351
|
+
|
|
352
|
+
await expect(
|
|
353
|
+
composer.addAttachment(makeCreateAttachment({ name: "notes.txt" })),
|
|
354
|
+
).rejects.toThrow(/is not accepted/);
|
|
355
|
+
|
|
356
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("adds external attachment without check when no adapter is configured", async () => {
|
|
360
|
+
await composer.addAttachment(
|
|
361
|
+
makeCreateAttachment({
|
|
362
|
+
name: "anything.xyz",
|
|
363
|
+
contentType: "application/x-anything",
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
expect(composer.attachments).toHaveLength(1);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("isolates a throwing attachmentAdd subscriber without firing attachmentAddError", async () => {
|
|
371
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
372
|
+
const listenerError = new Error("add subscriber boom");
|
|
373
|
+
const consoleError = vi
|
|
374
|
+
.spyOn(console, "error")
|
|
375
|
+
.mockImplementation(() => {});
|
|
376
|
+
const onError = vi.fn();
|
|
377
|
+
composer.unstable_on("attachmentAdd", () => {
|
|
378
|
+
throw listenerError;
|
|
379
|
+
});
|
|
380
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
381
|
+
|
|
382
|
+
await expect(
|
|
383
|
+
composer.addAttachment(
|
|
384
|
+
makeCreateAttachment({
|
|
385
|
+
name: "photo.png",
|
|
386
|
+
contentType: "image/png",
|
|
387
|
+
}),
|
|
388
|
+
),
|
|
389
|
+
).resolves.toBeUndefined();
|
|
390
|
+
|
|
391
|
+
expect(composer.attachments).toHaveLength(1);
|
|
392
|
+
expect(onError).not.toHaveBeenCalled();
|
|
393
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
394
|
+
'[assistant-ui] Composer runtime "attachmentAdd" listener threw an error',
|
|
395
|
+
listenerError,
|
|
396
|
+
);
|
|
397
|
+
consoleError.mockRestore();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("does not fire attachmentAddError when a state subscriber throws on add", async () => {
|
|
401
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
402
|
+
const onError = vi.fn();
|
|
403
|
+
composer.subscribe(() => {
|
|
404
|
+
throw new Error("state subscriber boom");
|
|
405
|
+
});
|
|
406
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
407
|
+
|
|
408
|
+
await expect(
|
|
409
|
+
composer.addAttachment(
|
|
410
|
+
makeCreateAttachment({
|
|
411
|
+
name: "photo.png",
|
|
412
|
+
contentType: "image/png",
|
|
413
|
+
}),
|
|
414
|
+
),
|
|
415
|
+
).rejects.toThrow("state subscriber boom");
|
|
416
|
+
|
|
417
|
+
expect(composer.attachments).toHaveLength(1);
|
|
418
|
+
expect(onError).not.toHaveBeenCalled();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|
|
@@ -4,11 +4,13 @@ import type { AppendMessage, ThreadMessage } from "../types/message";
|
|
|
4
4
|
import type { CompleteAttachment } from "../types/attachment";
|
|
5
5
|
import type { ThreadRuntimeCore } from "../runtime/interfaces/thread-runtime-core";
|
|
6
6
|
|
|
7
|
-
const makeRuntime = (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const makeRuntime = (
|
|
8
|
+
options?: {
|
|
9
|
+
messages?: ThreadMessage[];
|
|
10
|
+
composerMetadata?: Record<string, unknown>;
|
|
11
|
+
},
|
|
12
|
+
append = vi.fn(),
|
|
13
|
+
) => {
|
|
12
14
|
const runtime = {
|
|
13
15
|
append,
|
|
14
16
|
composer: { runConfig: {} },
|
|
@@ -375,6 +377,42 @@ describe("DefaultEditComposerRuntimeCore", () => {
|
|
|
375
377
|
expect(endEdit).toHaveBeenCalledTimes(1);
|
|
376
378
|
});
|
|
377
379
|
|
|
380
|
+
it("tracks the append task while exiting edit mode immediately", async () => {
|
|
381
|
+
let resolveAppend!: () => void;
|
|
382
|
+
const appendTask = new Promise<void>((resolve) => {
|
|
383
|
+
resolveAppend = resolve;
|
|
384
|
+
});
|
|
385
|
+
const { runtime } = makeRuntime(
|
|
386
|
+
undefined,
|
|
387
|
+
vi.fn(() => appendTask),
|
|
388
|
+
);
|
|
389
|
+
const endEdit = vi.fn();
|
|
390
|
+
const composer = new DefaultEditComposerRuntimeCore(runtime, endEdit, {
|
|
391
|
+
parentId: null,
|
|
392
|
+
message: makeUserMessage(),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const sendTask = composer.handleSend({
|
|
396
|
+
createdAt: new Date(),
|
|
397
|
+
role: "user",
|
|
398
|
+
content: [{ type: "text", text: "changed" }],
|
|
399
|
+
attachments: [],
|
|
400
|
+
runConfig: {},
|
|
401
|
+
metadata: { custom: {} },
|
|
402
|
+
});
|
|
403
|
+
let settled = false;
|
|
404
|
+
void sendTask.then(() => {
|
|
405
|
+
settled = true;
|
|
406
|
+
});
|
|
407
|
+
await Promise.resolve();
|
|
408
|
+
|
|
409
|
+
expect(endEdit).toHaveBeenCalledTimes(1);
|
|
410
|
+
expect(settled).toBe(false);
|
|
411
|
+
|
|
412
|
+
resolveAppend();
|
|
413
|
+
await sendTask;
|
|
414
|
+
});
|
|
415
|
+
|
|
378
416
|
it("invokes endEditCallback on cancel", () => {
|
|
379
417
|
const endEdit = vi.fn();
|
|
380
418
|
const { runtime } = makeRuntime();
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ThreadComposerRuntimeImpl,
|
|
4
|
+
type ThreadComposerRuntimeCoreBinding,
|
|
5
|
+
} from "../runtime/api/composer-runtime";
|
|
6
|
+
import {
|
|
7
|
+
ThreadRuntimeImpl,
|
|
8
|
+
type ThreadListItemRuntimeBinding,
|
|
9
|
+
type ThreadRuntimeCoreBinding,
|
|
10
|
+
} from "../runtime/api/thread-runtime";
|
|
11
|
+
import { ThreadListItemRuntimeImpl } from "../runtime/api/thread-list-item-runtime";
|
|
12
|
+
import type { ThreadListRuntimeCoreBinding } from "../runtime/api/thread-list-runtime";
|
|
13
|
+
import { ReadonlyThreadRuntimeCore } from "../runtimes/readonly/ReadonlyThreadRuntimeCore";
|
|
14
|
+
import type { Unsubscribe } from "../types/unsubscribe";
|
|
15
|
+
|
|
16
|
+
type EventListener = (payload?: unknown) => unknown;
|
|
17
|
+
|
|
18
|
+
class GuardedEventSource {
|
|
19
|
+
private listeners = new Set<EventListener>();
|
|
20
|
+
|
|
21
|
+
public unstable_on = (
|
|
22
|
+
_event: string,
|
|
23
|
+
listener: EventListener,
|
|
24
|
+
): Unsubscribe => {
|
|
25
|
+
this.listeners.add(listener);
|
|
26
|
+
return () => this.listeners.delete(listener);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
public emit(payload?: unknown) {
|
|
30
|
+
for (const listener of this.listeners) {
|
|
31
|
+
try {
|
|
32
|
+
const result = listener(payload);
|
|
33
|
+
if (
|
|
34
|
+
typeof result === "object" &&
|
|
35
|
+
result !== null &&
|
|
36
|
+
"then" in result &&
|
|
37
|
+
typeof result.then === "function"
|
|
38
|
+
) {
|
|
39
|
+
void Promise.resolve(result).catch((error) => {
|
|
40
|
+
console.error("[test source] listener error", error);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("[test source] listener error", error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type RuntimeHarness = {
|
|
51
|
+
subscribe: (listener: EventListener) => Unsubscribe;
|
|
52
|
+
emit: () => void;
|
|
53
|
+
errorContext: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const makeComposerHarness = (): RuntimeHarness => {
|
|
57
|
+
const source = new GuardedEventSource();
|
|
58
|
+
const binding = {
|
|
59
|
+
path: {
|
|
60
|
+
ref: "test.composer",
|
|
61
|
+
threadSelector: { type: "main" },
|
|
62
|
+
composerSource: "thread",
|
|
63
|
+
},
|
|
64
|
+
getState: () => ({ unstable_on: source.unstable_on }),
|
|
65
|
+
subscribe: () => () => {},
|
|
66
|
+
} as unknown as ThreadComposerRuntimeCoreBinding;
|
|
67
|
+
const runtime = new ThreadComposerRuntimeImpl(binding);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
subscribe: (listener) =>
|
|
71
|
+
runtime.unstable_on("attachmentAdd", listener as never),
|
|
72
|
+
emit: () => source.emit({}),
|
|
73
|
+
errorContext: 'Runtime event "attachmentAdd"',
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const makeThreadHarness = (): RuntimeHarness => {
|
|
78
|
+
const source = new GuardedEventSource();
|
|
79
|
+
const core = new ReadonlyThreadRuntimeCore();
|
|
80
|
+
core.unstable_on = source.unstable_on as typeof core.unstable_on;
|
|
81
|
+
|
|
82
|
+
const path = {
|
|
83
|
+
ref: "test.thread",
|
|
84
|
+
threadSelector: { type: "main" as const },
|
|
85
|
+
};
|
|
86
|
+
const threadBinding: ThreadRuntimeCoreBinding = {
|
|
87
|
+
path,
|
|
88
|
+
getState: () => core,
|
|
89
|
+
subscribe: (listener) => core.subscribe(listener),
|
|
90
|
+
outerSubscribe: (listener) => core.subscribe(listener),
|
|
91
|
+
};
|
|
92
|
+
const threadListItemBinding: ThreadListItemRuntimeBinding = {
|
|
93
|
+
path,
|
|
94
|
+
getState: () => ({
|
|
95
|
+
id: "test",
|
|
96
|
+
remoteId: undefined,
|
|
97
|
+
externalId: undefined,
|
|
98
|
+
isMain: true,
|
|
99
|
+
status: "regular",
|
|
100
|
+
title: undefined,
|
|
101
|
+
}),
|
|
102
|
+
subscribe: () => () => {},
|
|
103
|
+
};
|
|
104
|
+
const runtime = new ThreadRuntimeImpl(threadBinding, threadListItemBinding);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
subscribe: (listener) => runtime.unstable_on("initialize", listener),
|
|
108
|
+
emit: () => source.emit({}),
|
|
109
|
+
errorContext: 'Runtime event "initialize"',
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const makeThreadListItemHarness = (): RuntimeHarness => {
|
|
114
|
+
let isMain = true;
|
|
115
|
+
const stateListeners = new Set<() => void>();
|
|
116
|
+
const binding = {
|
|
117
|
+
path: {
|
|
118
|
+
ref: "test.threadListItem",
|
|
119
|
+
threadSelector: { type: "main" as const },
|
|
120
|
+
},
|
|
121
|
+
getState: () => ({
|
|
122
|
+
id: "test",
|
|
123
|
+
remoteId: undefined,
|
|
124
|
+
externalId: undefined,
|
|
125
|
+
isMain,
|
|
126
|
+
status: "regular" as const,
|
|
127
|
+
title: undefined,
|
|
128
|
+
}),
|
|
129
|
+
subscribe: (listener: () => void) => {
|
|
130
|
+
stateListeners.add(listener);
|
|
131
|
+
return () => stateListeners.delete(listener);
|
|
132
|
+
},
|
|
133
|
+
} as unknown as ThreadListItemRuntimeBinding;
|
|
134
|
+
const runtime = new ThreadListItemRuntimeImpl(
|
|
135
|
+
binding,
|
|
136
|
+
{} as ThreadListRuntimeCoreBinding,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
subscribe: (listener) =>
|
|
141
|
+
runtime.unstable_on("switchedAway", listener as never),
|
|
142
|
+
emit: () => {
|
|
143
|
+
isMain = !isMain;
|
|
144
|
+
for (const listener of [...stateListeners]) listener();
|
|
145
|
+
},
|
|
146
|
+
errorContext: 'Thread list item "switchedAway"',
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
vi.restoreAllMocks();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe.each([
|
|
155
|
+
["composer", makeComposerHarness],
|
|
156
|
+
["thread", makeThreadHarness],
|
|
157
|
+
["thread list item", makeThreadListItemHarness],
|
|
158
|
+
] as const)("%s public unstable_on", (_name, makeHarness) => {
|
|
159
|
+
it("continues notifying sibling listeners after a synchronous error", () => {
|
|
160
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
161
|
+
const harness = makeHarness();
|
|
162
|
+
const sibling = vi.fn();
|
|
163
|
+
|
|
164
|
+
harness.subscribe(() => {
|
|
165
|
+
throw new Error("listener failed");
|
|
166
|
+
});
|
|
167
|
+
harness.subscribe(sibling);
|
|
168
|
+
|
|
169
|
+
harness.emit();
|
|
170
|
+
|
|
171
|
+
expect(sibling).toHaveBeenCalledOnce();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it.each(["object", "function"] as const)(
|
|
175
|
+
"handles rejected %s thenables returned by listeners",
|
|
176
|
+
async (thenableType) => {
|
|
177
|
+
const consoleError = vi
|
|
178
|
+
.spyOn(console, "error")
|
|
179
|
+
.mockImplementation(() => {});
|
|
180
|
+
const harness = makeHarness();
|
|
181
|
+
const listenerError = new Error("async listener failed");
|
|
182
|
+
const then = vi.fn(
|
|
183
|
+
(
|
|
184
|
+
_resolve: (value?: unknown) => void,
|
|
185
|
+
reject: (reason?: unknown) => void,
|
|
186
|
+
) => reject(listenerError),
|
|
187
|
+
);
|
|
188
|
+
const thenable =
|
|
189
|
+
thenableType === "function"
|
|
190
|
+
? Object.assign(() => {}, { then })
|
|
191
|
+
: { then };
|
|
192
|
+
|
|
193
|
+
harness.subscribe(() => thenable);
|
|
194
|
+
harness.emit();
|
|
195
|
+
|
|
196
|
+
await vi.waitFor(() => {
|
|
197
|
+
expect(then).toHaveBeenCalledOnce();
|
|
198
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
199
|
+
expect.stringContaining(harness.errorContext),
|
|
200
|
+
listenerError,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
});
|