@assistant-ui/react 0.15.8 → 0.15.9
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/LICENSE +21 -0
- package/dist/client/ExternalThread.d.ts +3 -1
- package/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +524 -335
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/legacy-runtime/cloud/auiV0.d.ts +1 -0
- package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
- package/dist/legacy-runtime/cloud/auiV0.js +2 -1
- package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +2 -0
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +32 -7
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
- package/package.json +14 -16
- package/src/client/ExternalThread.ts +172 -9
- package/src/legacy-runtime/cloud/auiV0.ts +8 -1
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +1 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +79 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +2 -0
- package/src/tests/external-thread-feedback.test.tsx +204 -0
- package/src/tests/external-thread-speech.test.tsx +328 -0
- package/src/unstable/useLiveCompletionAdapter.test.tsx +90 -0
- package/src/unstable/useLiveCompletionAdapter.ts +32 -7
|
@@ -129,6 +129,96 @@ describe("unstable_useLiveCompletionAdapter", () => {
|
|
|
129
129
|
expect(result.current.adapter.search!("ab")).toEqual([item("ab")]);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
it("allows a failed query to be retried", async () => {
|
|
133
|
+
const fetcher = vi
|
|
134
|
+
.fn<(query: string) => Promise<readonly Unstable_TriggerItem[]>>()
|
|
135
|
+
.mockRejectedValueOnce(new Error("temporarily unavailable"))
|
|
136
|
+
.mockRejectedValueOnce(new Error("still unavailable"))
|
|
137
|
+
.mockResolvedValueOnce([item("alice")]);
|
|
138
|
+
const { result } = renderHook(() =>
|
|
139
|
+
unstable_useLiveCompletionAdapter({ fetcher, debounceMs: 0 }),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
for (const attempt of [1, 2]) {
|
|
143
|
+
await act(async () => {
|
|
144
|
+
result.current.adapter.search!("alice");
|
|
145
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
146
|
+
});
|
|
147
|
+
expect(fetcher).toHaveBeenCalledTimes(attempt);
|
|
148
|
+
expect(result.current.isLoading).toBe(false);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await act(async () => {
|
|
152
|
+
result.current.adapter.search!("alice");
|
|
153
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
154
|
+
});
|
|
155
|
+
expect(fetcher).toHaveBeenCalledTimes(3);
|
|
156
|
+
expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("does not automatically retry when search runs during every render", async () => {
|
|
160
|
+
const fetcher = vi
|
|
161
|
+
.fn<(query: string) => Promise<readonly Unstable_TriggerItem[]>>()
|
|
162
|
+
.mockRejectedValueOnce(new Error("temporarily unavailable"))
|
|
163
|
+
.mockImplementation(
|
|
164
|
+
() => new Promise<readonly Unstable_TriggerItem[]>(() => {}),
|
|
165
|
+
);
|
|
166
|
+
const { result } = renderHook(() => {
|
|
167
|
+
const completion = unstable_useLiveCompletionAdapter({
|
|
168
|
+
fetcher,
|
|
169
|
+
debounceMs: 0,
|
|
170
|
+
});
|
|
171
|
+
completion.adapter.search!("alice");
|
|
172
|
+
return completion;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await act(async () => {
|
|
176
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
177
|
+
});
|
|
178
|
+
expect(result.current.isLoading).toBe(false);
|
|
179
|
+
|
|
180
|
+
await act(async () => {
|
|
181
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
182
|
+
});
|
|
183
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("re-arms a failed query when its pending retry is interrupted", async () => {
|
|
187
|
+
const fetcher = vi
|
|
188
|
+
.fn<(query: string) => Promise<readonly Unstable_TriggerItem[]>>()
|
|
189
|
+
.mockRejectedValueOnce(new Error("temporarily unavailable"))
|
|
190
|
+
.mockImplementationOnce(
|
|
191
|
+
() => new Promise<readonly Unstable_TriggerItem[]>(() => {}),
|
|
192
|
+
)
|
|
193
|
+
.mockResolvedValueOnce([item("alice")]);
|
|
194
|
+
const { result } = renderHook(() =>
|
|
195
|
+
unstable_useLiveCompletionAdapter({ fetcher, debounceMs: 0 }),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
await act(async () => {
|
|
199
|
+
result.current.adapter.search!("alice");
|
|
200
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await act(async () => {
|
|
204
|
+
result.current.adapter.search!("alice");
|
|
205
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
206
|
+
});
|
|
207
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
208
|
+
|
|
209
|
+
await act(async () => {
|
|
210
|
+
result.current.adapter.search!("alicex");
|
|
211
|
+
});
|
|
212
|
+
await act(async () => {
|
|
213
|
+
result.current.adapter.search!("alice");
|
|
214
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(fetcher).toHaveBeenCalledTimes(3);
|
|
218
|
+
expect(fetcher).toHaveBeenLastCalledWith("alice");
|
|
219
|
+
expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
|
|
220
|
+
});
|
|
221
|
+
|
|
132
222
|
it("drops an in-flight fetch when the query returns to a cached value", async () => {
|
|
133
223
|
const resolvers: Record<
|
|
134
224
|
string,
|
|
@@ -57,7 +57,8 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
57
57
|
const [state, setState] = useState<{
|
|
58
58
|
query: string;
|
|
59
59
|
items: readonly Unstable_TriggerItem[];
|
|
60
|
-
|
|
60
|
+
failed: boolean;
|
|
61
|
+
}>({ query: NO_QUERY, items: [], failed: false });
|
|
61
62
|
const [isLoading, setIsLoading] = useState(false);
|
|
62
63
|
|
|
63
64
|
const fetcherRef = useRef(fetcher);
|
|
@@ -66,6 +67,8 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
66
67
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
67
68
|
const tokenRef = useRef(0);
|
|
68
69
|
const pendingQueryRef = useRef<string | null>(null);
|
|
70
|
+
const retryableQueryRef = useRef<string | null>(null);
|
|
71
|
+
const pendingRetryQueryRef = useRef<string | null>(null);
|
|
69
72
|
|
|
70
73
|
const cancelTimer = useCallback(() => {
|
|
71
74
|
if (timerRef.current !== null) {
|
|
@@ -74,10 +77,22 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
74
77
|
}
|
|
75
78
|
}, []);
|
|
76
79
|
|
|
80
|
+
const rearmPendingRetry = useCallback(() => {
|
|
81
|
+
const query = pendingRetryQueryRef.current;
|
|
82
|
+
if (query === null) return;
|
|
83
|
+
retryableQueryRef.current = query;
|
|
84
|
+
pendingRetryQueryRef.current = null;
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
77
87
|
const scheduleFetch = useCallback(
|
|
78
88
|
(query: string) => {
|
|
79
89
|
if (!enabled) return;
|
|
80
90
|
if (pendingQueryRef.current === query) return;
|
|
91
|
+
rearmPendingRetry();
|
|
92
|
+
if (retryableQueryRef.current === query) {
|
|
93
|
+
retryableQueryRef.current = null;
|
|
94
|
+
pendingRetryQueryRef.current = query;
|
|
95
|
+
}
|
|
81
96
|
pendingQueryRef.current = query;
|
|
82
97
|
cancelTimer();
|
|
83
98
|
const token = ++tokenRef.current;
|
|
@@ -87,37 +102,47 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
87
102
|
fetcherRef.current(query).then(
|
|
88
103
|
(items) => {
|
|
89
104
|
if (token !== tokenRef.current) return;
|
|
90
|
-
|
|
105
|
+
pendingRetryQueryRef.current = null;
|
|
106
|
+
setState({ query, items, failed: false });
|
|
91
107
|
setIsLoading(false);
|
|
92
108
|
},
|
|
93
109
|
() => {
|
|
94
110
|
if (token !== tokenRef.current) return;
|
|
95
|
-
|
|
111
|
+
pendingQueryRef.current = null;
|
|
112
|
+
pendingRetryQueryRef.current = null;
|
|
113
|
+
setState({ query, items: [], failed: true });
|
|
96
114
|
setIsLoading(false);
|
|
97
115
|
},
|
|
98
116
|
);
|
|
99
117
|
}, debounceMs);
|
|
100
118
|
},
|
|
101
|
-
[enabled, debounceMs, cancelTimer],
|
|
119
|
+
[enabled, debounceMs, cancelTimer, rearmPendingRetry],
|
|
102
120
|
);
|
|
103
121
|
|
|
104
122
|
const invalidatePending = useCallback(() => {
|
|
123
|
+
rearmPendingRetry();
|
|
105
124
|
cancelTimer();
|
|
106
125
|
pendingQueryRef.current = null;
|
|
107
126
|
tokenRef.current += 1;
|
|
108
127
|
setIsLoading(false);
|
|
109
|
-
}, [cancelTimer]);
|
|
128
|
+
}, [cancelTimer, rearmPendingRetry]);
|
|
110
129
|
|
|
111
130
|
useEffect(() => {
|
|
112
131
|
if (enabled) return;
|
|
113
132
|
invalidatePending();
|
|
114
133
|
setState((s) =>
|
|
115
|
-
s.query === NO_QUERY ? s : { query: NO_QUERY, items: [] },
|
|
134
|
+
s.query === NO_QUERY ? s : { query: NO_QUERY, items: [], failed: false },
|
|
116
135
|
);
|
|
117
136
|
}, [enabled, invalidatePending]);
|
|
118
137
|
|
|
119
138
|
useEffect(() => cancelTimer, [cancelTimer]);
|
|
120
139
|
|
|
140
|
+
// Arm retries only after the failed state commits. Arming during rejection
|
|
141
|
+
// would let the failure render immediately schedule another request.
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
retryableQueryRef.current = state.failed ? state.query : null;
|
|
144
|
+
}, [state]);
|
|
145
|
+
|
|
121
146
|
const adapter = useMemo<Unstable_TriggerAdapter>(
|
|
122
147
|
() => ({
|
|
123
148
|
categories: () => [],
|
|
@@ -125,7 +150,7 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
125
150
|
search: (query: string) => {
|
|
126
151
|
// search() runs inside the popover's render; defer state updates with
|
|
127
152
|
// queueMicrotask so they are not dispatched while another component renders.
|
|
128
|
-
if (query !== state.query) {
|
|
153
|
+
if (query !== state.query || retryableQueryRef.current === query) {
|
|
129
154
|
queueMicrotask(() => scheduleFetch(query));
|
|
130
155
|
} else if (
|
|
131
156
|
pendingQueryRef.current !== null &&
|